Skip to content

Commit 42ae590

Browse files
committed
refactoring
1 parent b744739 commit 42ae590

File tree

4 files changed

+186
-143
lines changed

4 files changed

+186
-143
lines changed

core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/ClassLoadingChainAnalyzer.java

Lines changed: 112 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@
3838
* <h3>Phase 1 -- Bytecode analysis (seed propagation)</h3>
3939
* <p>
4040
* Scans all reachable classes and builds a method-level call graph. Starting from seed
41-
* methods ({@code ClassLoader.loadClass(String)}, {@code Class.forName(String)}, and
42-
* {@code Class.forName(String, boolean, ClassLoader)}), propagates backwards through the
41+
* methods ({@code ClassLoader.loadClass(String)}, {@code Class.forName(String)},
42+
* {@code Class.forName(String, boolean, ClassLoader)}, and
43+
* {@code MethodHandles.Lookup.findClass(String)}), propagates backwards through the
4344
* call graph using a fixed-point algorithm to identify every application method that
4445
* transitively calls a class-loading seed. JDK and infrastructure classes
4546
* ({@code java/}, {@code javax/}, {@code jakarta/}, {@code sun/}, {@code org/objectweb/})
@@ -96,9 +97,12 @@ class ClassLoadingChainAnalyzer {
9697

9798
private static final Logger log = Logger.getLogger(ClassLoadingChainAnalyzer.class.getName());
9899

99-
private static final String CLASSLOADER_LOAD_CLASS = "java/lang/ClassLoader.loadClass(Ljava/lang/String;)Ljava/lang/Class;";
100-
private static final String CLASS_FOR_NAME_1 = "java/lang/Class.forName(Ljava/lang/String;)Ljava/lang/Class;";
101-
private static final String CLASS_FOR_NAME_3 = "java/lang/Class.forName(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;";
100+
/** JDK methods that load classes by name — the seeds for call chain propagation. */
101+
private static final Set<String> SEED_METHODS = Set.of(
102+
"java/lang/ClassLoader.loadClass(Ljava/lang/String;)Ljava/lang/Class;",
103+
"java/lang/Class.forName(Ljava/lang/String;)Ljava/lang/Class;",
104+
"java/lang/Class.forName(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;",
105+
"java/lang/invoke/MethodHandles$Lookup.findClass(Ljava/lang/String;)Ljava/lang/Class;");
102106

103107
private static final int MAX_CALLER_DEPTH = 5;
104108

@@ -121,16 +125,14 @@ static Set<String> analyze(
121125
Map<String, Set<String>> callerIndex = new HashMap<>();
122126
Set<String> classLoadingMethods = findClassLoadingMethods(reachableClasses, allBytecode, callerIndex);
123127
if (classLoadingMethods.isEmpty()) {
124-
log.debug("No class-loading methods found in reachable classes");
125-
return new HashSet<>();
128+
return Set.of();
126129
}
127130
log.debugf("Found %d class-loading methods", classLoadingMethods.size());
128131

129132
// Phase 2: Find entry point classes
130133
Set<String> entryPointClasses = findEntryPointClasses(classLoadingMethods, callerIndex);
131134
if (entryPointClasses.isEmpty()) {
132-
log.debug("No entry point classes found for class-loading chains");
133-
return new HashSet<>();
135+
return Set.of();
134136
}
135137
log.debugf("Found %d entry point classes for class-loading chains", entryPointClasses.size());
136138

@@ -154,14 +156,22 @@ private static Set<String> findClassLoadingMethods(
154156
Map<String, Supplier<byte[]>> allBytecode,
155157
Map<String, Set<String>> callerIndex) {
156158

157-
// Seed set
158-
Set<String> classLoadingMethods = new HashSet<>();
159-
classLoadingMethods.add(CLASSLOADER_LOAD_CLASS);
160-
classLoadingMethods.add(CLASS_FOR_NAME_1);
161-
classLoadingMethods.add(CLASS_FOR_NAME_3);
159+
Map<String, Set<String>> methodCallees = buildCallGraph(reachableClasses, allBytecode, callerIndex);
160+
return propagateFromSeeds(methodCallees);
161+
}
162+
163+
/**
164+
* Scans reachable bytecode and builds two maps in a single pass:
165+
* <ul>
166+
* <li>{@code methodCallees}: method → set of methods it calls</li>
167+
* <li>{@code callerIndex}: method → set of methods that call it (reverse index)</li>
168+
* </ul>
169+
*/
170+
private static Map<String, Set<String>> buildCallGraph(
171+
Set<String> reachableClasses,
172+
Map<String, Supplier<byte[]>> allBytecode,
173+
Map<String, Set<String>> callerIndex) {
162174

163-
// Build method call graph and caller index in one pass
164-
// methodCallees: method -> set of methods it calls
165175
Map<String, Set<String>> methodCallees = new HashMap<>();
166176

167177
for (String className : reachableClasses) {
@@ -195,8 +205,17 @@ public void visitMethodInsn(int opcode, String owner, String mname,
195205
}, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
196206
}
197207

198-
// Propagate to fixed point: if method A calls method B which is a class-loading method
199-
// (and B is not a JDK/infra class), then A is also a class-loading method
208+
return methodCallees;
209+
}
210+
211+
/**
212+
* Starting from {@link #SEED_METHODS}, propagates through the call graph to find all
213+
* application methods that transitively call a class-loading seed.
214+
* JDK/infrastructure methods are excluded from propagation.
215+
*/
216+
private static Set<String> propagateFromSeeds(Map<String, Set<String>> methodCallees) {
217+
Set<String> classLoadingMethods = new HashSet<>(SEED_METHODS);
218+
200219
boolean changed = true;
201220
while (changed) {
202221
changed = false;
@@ -217,11 +236,7 @@ public void visitMethodInsn(int opcode, String owner, String mname,
217236
}
218237
}
219238

220-
// Remove the seed JDK methods
221-
classLoadingMethods.remove(CLASSLOADER_LOAD_CLASS);
222-
classLoadingMethods.remove(CLASS_FOR_NAME_1);
223-
classLoadingMethods.remove(CLASS_FOR_NAME_3);
224-
239+
classLoadingMethods.removeAll(SEED_METHODS);
225240
return classLoadingMethods;
226241
}
227242

@@ -280,33 +295,26 @@ private static Set<String> findEntryPointClasses(
280295
}
281296

282297
/**
283-
* Extracts the method name from a method key like "com/example/Foo.methodName(Ljava/lang/String;)V".
298+
* Finds the dot+paren boundary in a method key like "com/example/Foo.methodName(...)V".
299+
* Returns the dot index, or -1 if the key is malformed.
284300
*/
285-
private static String extractMethodName(String methodKey) {
301+
private static int findMethodSeparator(String methodKey) {
286302
int parenIdx = methodKey.indexOf('(');
287-
if (parenIdx < 0) {
288-
return null;
289-
}
290-
int dotIdx = methodKey.lastIndexOf('.', parenIdx);
291-
if (dotIdx < 0) {
292-
return null;
293-
}
294-
return methodKey.substring(dotIdx + 1, parenIdx);
303+
return parenIdx < 0 ? -1 : methodKey.lastIndexOf('.', parenIdx);
304+
}
305+
306+
/** Extracts the method name from a key like {@code "com/example/Foo.bar(I)V"} → {@code "bar"}. */
307+
private static String extractMethodName(String methodKey) {
308+
int dotIdx = findMethodSeparator(methodKey);
309+
return dotIdx < 0 ? null : methodKey.substring(dotIdx + 1, methodKey.indexOf('('));
295310
}
296311

297312
/**
298-
* Extracts the class name (internal form) from a method key.
313+
* Extracts the class name (internal form) from a key like {@code "com/example/Foo.bar(I)V"} → {@code "com/example/Foo"}.
299314
*/
300315
private static String extractClassName(String methodKey) {
301-
int parenIdx = methodKey.indexOf('(');
302-
if (parenIdx < 0) {
303-
return null;
304-
}
305-
int dotIdx = methodKey.lastIndexOf('.', parenIdx);
306-
if (dotIdx < 0) {
307-
return null;
308-
}
309-
return methodKey.substring(0, dotIdx);
316+
int dotIdx = findMethodSeparator(methodKey);
317+
return dotIdx < 0 ? null : methodKey.substring(0, dotIdx);
310318
}
311319

312320
/**
@@ -318,9 +326,29 @@ private static Set<String> executeWithRecordingClassLoader(
318326
Map<String, Supplier<byte[]>> allBytecode,
319327
Set<String> allKnownClasses) {
320328

329+
Map<String, byte[]> bytecodeMap = resolveBytecodeMap(allBytecode);
321330
Set<String> allDiscovered = new HashSet<>();
322331

323-
// Resolve bytecode once for all entry points
332+
// Suppress stdout/stderr: loaded classes may print warnings or stack traces
333+
// during their static initialization. These are expected and harmless.
334+
java.io.PrintStream originalOut = System.out;
335+
java.io.PrintStream originalErr = System.err;
336+
try {
337+
System.setOut(new java.io.PrintStream(java.io.OutputStream.nullOutputStream()));
338+
System.setErr(new java.io.PrintStream(java.io.OutputStream.nullOutputStream()));
339+
for (String entryPoint : entryPointClasses) {
340+
executeEntryPoint(entryPoint, bytecodeMap, allKnownClasses, allDiscovered);
341+
}
342+
} finally {
343+
System.setOut(originalOut);
344+
System.setErr(originalErr);
345+
}
346+
347+
return allDiscovered;
348+
}
349+
350+
/** Resolves all bytecode suppliers into a concrete map for use by the RecordingClassLoader. */
351+
private static Map<String, byte[]> resolveBytecodeMap(Map<String, Supplier<byte[]>> allBytecode) {
324352
Map<String, byte[]> bytecodeMap = new HashMap<>();
325353
for (Map.Entry<String, Supplier<byte[]>> entry : allBytecode.entrySet()) {
326354
try {
@@ -329,57 +357,51 @@ private static Set<String> executeWithRecordingClassLoader(
329357
// skip unreadable classes
330358
}
331359
}
360+
return bytecodeMap;
361+
}
332362

333-
// Suppress stdout/stderr during recording: loaded classes (BouncyCastle, SLF4J, etc.)
334-
// may print warnings, stack traces, or status messages during their static initialization.
335-
// These are expected and harmless — redirect to a null stream to keep the build log clean.
336-
java.io.PrintStream originalOut = System.out;
337-
java.io.PrintStream originalErr = System.err;
338-
java.io.PrintStream nullStream = new java.io.PrintStream(java.io.OutputStream.nullOutputStream());
339-
try {
340-
System.setOut(nullStream);
341-
System.setErr(nullStream);
342-
for (String entryPoint : entryPointClasses) {
343-
log.debugf("Executing entry point class: %s", entryPoint);
344-
345-
RecordingClassLoader loader = new RecordingClassLoader(bytecodeMap);
346-
347-
try {
348-
Class<?> clazz = Class.forName(entryPoint, true, loader);
349-
try {
350-
Object instance = clazz.getConstructor().newInstance();
351-
// If the instance is a Map (like BouncyCastle's Provider which extends Properties),
352-
// iterate values and record strings that match known classes
353-
if (instance instanceof java.util.Map) {
354-
java.util.Map<?, ?> map = (java.util.Map<?, ?>) instance;
355-
for (Object value : map.values()) {
356-
if (value instanceof String) {
357-
String strValue = (String) value;
358-
if (allKnownClasses.contains(strValue)) {
359-
allDiscovered.add(strValue);
360-
}
361-
}
362-
}
363-
}
364-
} catch (Exception e) {
365-
log.debugf("Could not instantiate entry point %s: %s", entryPoint, e.getMessage());
366-
} catch (LinkageError e) {
367-
log.debugf("LinkageError instantiating entry point %s: %s", entryPoint, e.getMessage());
368-
}
369-
} catch (Exception e) {
370-
log.debugf("Could not load entry point %s: %s", entryPoint, e.getMessage());
371-
} catch (LinkageError e) {
372-
log.debugf("LinkageError loading entry point %s: %s", entryPoint, e.getMessage());
373-
}
363+
/**
364+
* Loads and instantiates a single entry point class in a fresh RecordingClassLoader.
365+
* Records all class load attempts, plus class name strings from Map values.
366+
*/
367+
private static void executeEntryPoint(String entryPoint, Map<String, byte[]> bytecodeMap,
368+
Set<String> allKnownClasses, Set<String> discovered) {
369+
log.debugf("Executing entry point class: %s", entryPoint);
370+
RecordingClassLoader loader = new RecordingClassLoader(bytecodeMap);
374371

375-
allDiscovered.addAll(loader.getLoadedClassNames());
372+
try {
373+
Class<?> clazz = Class.forName(entryPoint, true, loader);
374+
try {
375+
Object instance = clazz.getConstructor().newInstance();
376+
collectClassNamesFromMapValues(instance, allKnownClasses, discovered);
377+
} catch (Exception e) {
378+
log.debugf("Could not instantiate entry point %s: %s", entryPoint, e.getMessage());
379+
} catch (LinkageError e) {
380+
log.debugf("LinkageError instantiating entry point %s: %s", entryPoint, e.getMessage());
376381
}
377-
} finally {
378-
System.setOut(originalOut);
379-
System.setErr(originalErr);
382+
} catch (Exception e) {
383+
log.debugf("Could not load entry point %s: %s", entryPoint, e.getMessage());
384+
} catch (LinkageError e) {
385+
log.debugf("LinkageError loading entry point %s: %s", entryPoint, e.getMessage());
380386
}
381387

382-
return allDiscovered;
388+
discovered.addAll(loader.getLoadedClassNames());
389+
}
390+
391+
/**
392+
* If the object is a Map (e.g., BouncyCastle's Provider extends Properties),
393+
* extracts String values that match known class names. These are class names stored
394+
* via methods like {@code addAlgorithm(key, className)} for deferred loading.
395+
*/
396+
private static void collectClassNamesFromMapValues(Object instance,
397+
Set<String> allKnownClasses, Set<String> discovered) {
398+
if (instance instanceof java.util.Map<?, ?> map) {
399+
for (Object value : map.values()) {
400+
if (value instanceof String strValue && allKnownClasses.contains(strValue)) {
401+
discovered.add(strValue);
402+
}
403+
}
404+
}
383405
}
384406

385407
/**

core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarTreeShaker.java

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,6 @@ private Set<String> traceReachableClasses(Set<String> startingRoots, Set<String>
177177
}
178178

179179
enqueueBytecodeReferences(name, bytecode, allKnownClasses, visited, queue);
180-
includeAllClassesIfDynamicLoading(name, bytecode, visited, queue);
181180
includeDeserializedClasses(name, bytecode, depDeserializationFlags, visited, queue);
182181
}
183182
return visited;
@@ -330,25 +329,6 @@ private void enqueueBytecodeReferences(String name, byte[] bytecode, Set<String>
330329
}
331330
}
332331

333-
/**
334-
* If this class uses dynamic class loading (MethodHandles.Lookup.findClass),
335-
* include all classes from the same dependency since the loaded class names
336-
* are constructed at runtime and can't be statically determined.
337-
*/
338-
private void includeAllClassesIfDynamicLoading(String name, byte[] bytecode, Set<String> visited,
339-
Queue<String> queue) {
340-
ArtifactKey depKey = input.classToDep.get(name);
341-
if (depKey != null && usesDynamicClassLoading(bytecode)) {
342-
log.debugf("Dynamic class loading detected in %s, keeping all classes from %s", name, depKey);
343-
for (var entry : input.classToDep.entrySet()) {
344-
if (depKey.equals(entry.getValue()) && visited.add(entry.getKey())) {
345-
queue.add(entry.getKey());
346-
}
347-
}
348-
}
349-
350-
}
351-
352332
private static final int FLAG_RESOURCE_ACCESS = 1;
353333
private static final int FLAG_OBJECT_INPUT_STREAM = 2;
354334
private static final int FLAG_RESOURCE_DESERIALIZATION = FLAG_RESOURCE_ACCESS | FLAG_OBJECT_INPUT_STREAM;
@@ -925,37 +905,6 @@ private static String formatSize(long bytes) {
925905
return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
926906
}
927907

928-
/**
929-
* Detects whether bytecode uses dynamic class loading patterns where class names
930-
* are constructed at runtime (e.g. MethodHandles.Lookup.findClass). When detected,
931-
* all classes from the same dependency must be preserved since the loaded class names
932-
* can't be statically determined.
933-
*/
934-
private static boolean usesDynamicClassLoading(byte[] bytecode) {
935-
boolean[] found = new boolean[1];
936-
ClassReader reader = new ClassReader(bytecode);
937-
reader.accept(new ClassVisitor(Opcodes.ASM9) {
938-
@Override
939-
public MethodVisitor visitMethod(int access, String name, String descriptor,
940-
String signature, String[] exceptions) {
941-
if (found[0]) {
942-
return null;
943-
}
944-
return new MethodVisitor(Opcodes.ASM9) {
945-
@Override
946-
public void visitMethodInsn(int opcode, String owner, String mname,
947-
String mdescriptor, boolean isInterface) {
948-
if ("findClass".equals(mname)
949-
&& "java/lang/invoke/MethodHandles$Lookup".equals(owner)) {
950-
found[0] = true;
951-
}
952-
}
953-
};
954-
}
955-
}, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
956-
return found[0];
957-
}
958-
959908
/**
960909
* Checks whether bytecode contains both an LDC string constant for the sisu named resource path
961910
* and a ClassLoader.getResources()/getResource() call. These may be in different methods

0 commit comments

Comments
 (0)