Skip to content

Commit 64628eb

Browse files
committed
Experimental: runtime usage report + /actuator/bootusage endpoint
Adds usage analysis auto-config, endpoint, SPIs, policies, docs, schema, tests. Signed-off-by: dhruv-15-03 <[email protected]>
1 parent e70edce commit 64628eb

26 files changed

+2049
-2
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.usage;
18+
19+
import java.net.URL;
20+
import java.security.CodeSource;
21+
import java.security.ProtectionDomain;
22+
import java.util.Collections;
23+
import java.util.Map;
24+
import java.util.concurrent.ConcurrentHashMap;
25+
26+
import org.springframework.beans.BeansException;
27+
import org.springframework.beans.factory.config.BeanPostProcessor;
28+
29+
/**
30+
* Simple {@link BeanPostProcessor} capturing bean class -> code source locations.
31+
* This is intentionally lightweight; it ignores infrastructure beans.
32+
*/
33+
/**
34+
* Experimental – captures bean -> code source (jar/location) mapping for enrichment.
35+
*/
36+
public class BeanOriginTrackingPostProcessor implements BeanPostProcessor {
37+
38+
private final Map<String, String> beanOrigins = new ConcurrentHashMap<>();
39+
40+
@Override
41+
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
42+
Class<?> type = bean.getClass();
43+
if (type.getName().startsWith("org.springframework.")) {
44+
return bean; // skip core Spring infrastructure for clarity
45+
}
46+
String location = findLocation(type);
47+
if (location != null) {
48+
this.beanOrigins.put(beanName, location);
49+
}
50+
return bean;
51+
}
52+
53+
private String findLocation(Class<?> type) {
54+
try {
55+
ProtectionDomain pd = type.getProtectionDomain();
56+
if (pd == null) {
57+
return null;
58+
}
59+
CodeSource cs = pd.getCodeSource();
60+
if (cs == null) {
61+
return null;
62+
}
63+
URL url = cs.getLocation();
64+
return (url != null ? url.toString() : null);
65+
}
66+
catch (Throwable ex) {
67+
return null;
68+
}
69+
}
70+
71+
public Map<String, String> getBeanOrigins() {
72+
return Collections.unmodifiableMap(this.beanOrigins);
73+
}
74+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package org.springframework.boot.autoconfigure.usage;
2+
3+
import java.io.InputStream;
4+
import java.util.Collections;
5+
import java.util.HashMap;
6+
import java.util.HashSet;
7+
import java.util.List;
8+
import java.util.Map;
9+
import java.util.Properties;
10+
import java.util.Set;
11+
import java.util.concurrent.ConcurrentHashMap;
12+
import java.util.jar.JarEntry;
13+
import java.util.jar.JarFile;
14+
import java.util.regex.Pattern;
15+
import java.util.stream.Collectors;
16+
17+
/**
18+
* Heuristic starter usage analyzer (experimental).
19+
*/
20+
class StarterUsageAnalyzer {
21+
22+
private static final Pattern STARTER_PATTERN = Pattern.compile("spring-boot-starter-.*");
23+
24+
private final Map<String, StarterInfo> starters = new ConcurrentHashMap<>();
25+
26+
private static final Set<String> PROTECTED_STARTERS = Set.of(
27+
"spring-boot-starter",
28+
"spring-boot-starter-logging",
29+
"spring-boot-starter-json"
30+
);
31+
32+
private final Map<String, String> keywordToAutoConfigToken;
33+
34+
StarterUsageAnalyzer() {
35+
Map<String, String> map = new HashMap<>();
36+
map.put("web", "WebMvc");
37+
map.put("webflux", "WebFlux");
38+
map.put("actuator", "Actuator");
39+
map.put("data-jpa", "Jpa");
40+
map.put("data-jdbc", "Jdbc");
41+
map.put("data-redis", "Redis");
42+
map.put("data-mongodb", "Mongo");
43+
map.put("data-cassandra", "Cassandra");
44+
map.put("security", "SecurityAutoConfiguration");
45+
map.put("oauth2-client", "OAuth2Client");
46+
map.put("oauth2-resource-server", "OAuth2ResourceServer");
47+
map.put("oauth2-authorization-server", "AuthorizationServer");
48+
map.put("graphql", "GraphQl");
49+
map.put("kafka", "Kafka");
50+
map.put("amqp", "Rabbit");
51+
map.put("batch", "Batch");
52+
map.put("integration", "IntegrationAutoConfiguration");
53+
map.put("mail", "MailSender");
54+
map.put("thymeleaf", "Thymeleaf");
55+
map.put("mustache", "Mustache");
56+
map.put("freemarker", "FreeMarker");
57+
map.put("quartz", "Quartz");
58+
map.put("rsocket", "RSocket");
59+
this.keywordToAutoConfigToken = Collections.unmodifiableMap(map);
60+
}
61+
62+
void scanClasspath() {
63+
String cp = System.getProperty("java.class.path", "");
64+
if (cp.isEmpty()) return;
65+
String[] entries = cp.split(java.io.File.pathSeparator);
66+
for (String entry : entries) {
67+
if (entry.endsWith(".jar")) {
68+
try (JarFile jar = new JarFile(entry)) {
69+
JarEntry pomProps = findPomProperties(jar);
70+
if (pomProps != null) {
71+
Properties props = new Properties();
72+
try (InputStream is = jar.getInputStream(pomProps)) { props.load(is); }
73+
String artifactId = props.getProperty("artifactId");
74+
if (artifactId != null && STARTER_PATTERN.matcher(artifactId).matches()
75+
&& !"spring-boot-starter".equals(artifactId)) {
76+
String groupId = props.getProperty("groupId", "");
77+
String version = props.getProperty("version", "");
78+
starters.putIfAbsent(artifactId, new StarterInfo(groupId + ":" + artifactId + ":" + version));
79+
}
80+
}
81+
} catch (Exception ignored) { }
82+
}
83+
}
84+
}
85+
86+
private JarEntry findPomProperties(JarFile jar) {
87+
return jar.stream().filter(e -> e.getName().startsWith("META-INF/maven/") && e.getName().endsWith("/pom.properties")).findFirst().orElse(null);
88+
}
89+
90+
StarterUsageResult classify(List<String> appliedAutoConfigurations) {
91+
if (starters.isEmpty()) {
92+
scanClasspath();
93+
}
94+
Set<String> applied = new HashSet<>(appliedAutoConfigurations);
95+
starters.forEach((starter, info) -> {
96+
String keyword = starter.substring("spring-boot-starter-".length());
97+
String token = keywordToAutoConfigToken.get(keyword);
98+
boolean used;
99+
if (token != null) {
100+
used = applied.stream().anyMatch(ac -> ac.contains(token));
101+
} else {
102+
used = applied.stream().anyMatch(ac -> simpleName(ac).toLowerCase().contains(keyword.replace("-", "")));
103+
}
104+
info.used = used;
105+
});
106+
List<String> declared = starters.keySet().stream().sorted().collect(Collectors.toList());
107+
List<String> used = starters.entrySet().stream().filter(en -> en.getValue().used).map(Map.Entry::getKey).sorted().collect(Collectors.toList());
108+
List<String> unused = starters.entrySet().stream()
109+
.filter(en -> !en.getValue().used && !PROTECTED_STARTERS.contains(en.getKey()))
110+
.map(Map.Entry::getKey)
111+
.sorted()
112+
.collect(Collectors.toList());
113+
return new StarterUsageResult(declared, used, unused);
114+
}
115+
116+
private String simpleName(String fqcn) {
117+
int dot = fqcn.lastIndexOf('.');
118+
return dot > -1 ? fqcn.substring(dot + 1) : fqcn;
119+
}
120+
121+
private static class StarterInfo {
122+
final String coordinate;
123+
boolean used;
124+
StarterInfo(String coordinate) { this.coordinate = coordinate; }
125+
}
126+
127+
static class StarterUsageResult {
128+
final List<String> declaredStarters;
129+
final List<String> usedStarters;
130+
final List<String> unusedStarters;
131+
StarterUsageResult(List<String> d, List<String> u, List<String> un) {
132+
this.declaredStarters = d; this.usedStarters = u; this.unusedStarters = un;
133+
}
134+
}
135+
}

0 commit comments

Comments
 (0)