Skip to content

Commit 34345df

Browse files
authored
Move common caching class loading functionality to the separate class (#10979)
2 parents 3a6949d + 5d9b429 commit 34345df

File tree

2 files changed

+65
-18
lines changed

2 files changed

+65
-18
lines changed

core/src/main/java/hudson/PluginManager.java

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@
5858
import hudson.security.ACLContext;
5959
import hudson.security.Permission;
6060
import hudson.security.PermissionScope;
61+
import hudson.util.CachingClassLoader;
6162
import hudson.util.CyclicGraphDetector;
6263
import hudson.util.CyclicGraphDetector.CycleDetectedException;
63-
import hudson.util.DelegatingClassLoader;
6464
import hudson.util.ExistenceCheckingClassLoader;
6565
import hudson.util.FormValidation;
6666
import hudson.util.PersistedList;
@@ -108,13 +108,11 @@
108108
import java.util.Locale;
109109
import java.util.Map;
110110
import java.util.Objects;
111-
import java.util.Optional;
112111
import java.util.ServiceLoader;
113112
import java.util.Set;
114113
import java.util.TreeMap;
115114
import java.util.UUID;
116115
import java.util.concurrent.ConcurrentHashMap;
117-
import java.util.concurrent.ConcurrentMap;
118116
import java.util.concurrent.CopyOnWriteArrayList;
119117
import java.util.concurrent.Future;
120118
import java.util.function.Supplier;
@@ -2402,12 +2400,9 @@ public <T> T of(String key, Class<T> type, Supplier<T> func) {
24022400
/**
24032401
* {@link ClassLoader} that can see all plugins.
24042402
*/
2405-
public static final class UberClassLoader extends DelegatingClassLoader {
2403+
public static final class UberClassLoader extends CachingClassLoader {
24062404
private final List<PluginWrapper> activePlugins;
24072405

2408-
/** Cache of loaded, or known to be unloadable, classes. */
2409-
private final ConcurrentMap<String, Optional<Class<?>>> loaded = new ConcurrentHashMap<>();
2410-
24112406
/**
24122407
* The servlet container's {@link ClassLoader} (the parent of Jenkins core) is
24132408
* parallel-capable and maintains its own growing {@link Map} of {@link
@@ -2426,29 +2421,29 @@ public UberClassLoader(List<PluginWrapper> activePlugins) {
24262421
}
24272422

24282423
@Override
2429-
protected Class<?> findClass(String name) throws ClassNotFoundException {
2424+
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
24302425
for (String namePrefixToSkip : CLASS_PREFIXES_TO_SKIP) {
24312426
if (name.startsWith(namePrefixToSkip)) {
24322427
throw new ClassNotFoundException("ignoring " + name);
24332428
}
24342429
}
2435-
return loaded.computeIfAbsent(name, this::computeValue).orElseThrow(() -> new ClassNotFoundException(name));
2430+
return super.loadClass(name, resolve);
24362431
}
24372432

2438-
private Optional<Class<?>> computeValue(String name) {
2433+
@Override
2434+
protected Class<?> findClass(String name) throws ClassNotFoundException {
24392435
for (PluginWrapper p : activePlugins) {
24402436
try {
24412437
if (FAST_LOOKUP) {
2442-
return Optional.of(ClassLoaderReflectionToolkit.loadClass(p.classLoader, name));
2438+
return ClassLoaderReflectionToolkit.loadClass(p.classLoader, name);
24432439
} else {
2444-
return Optional.of(p.classLoader.loadClass(name));
2440+
return p.classLoader.loadClass(name);
24452441
}
24462442
} catch (ClassNotFoundException e) {
24472443
// Not found. Try the next class loader.
24482444
}
24492445
}
2450-
// Not found in any of the class loaders. Delegate.
2451-
return Optional.empty();
2446+
throw new ClassNotFoundException(name);
24522447
}
24532448

24542449
@Override
@@ -2480,10 +2475,6 @@ protected Enumeration<URL> findResources(String name) throws IOException {
24802475
return Collections.enumeration(resources);
24812476
}
24822477

2483-
void clearCacheMisses() {
2484-
loaded.values().removeIf(Optional::isEmpty);
2485-
}
2486-
24872478
@Override
24882479
public String toString() {
24892480
// only for debugging purpose
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package hudson.util;
2+
3+
import java.util.Optional;
4+
import java.util.concurrent.ConcurrentHashMap;
5+
import org.kohsuke.accmod.Restricted;
6+
import org.kohsuke.accmod.restrictions.NoExternalUse;
7+
8+
/**
9+
*
10+
* ClassLoader with internal caching of class loading results.
11+
*
12+
* <p>
13+
* Caches both successful and failed class lookups to avoid redundant delegation
14+
* and repeated class resolution attempts. Designed for performance optimization
15+
* in systems that repeatedly query class presence (e.g., plugin environments,
16+
* reflective loading, optional dependencies).
17+
* </p>
18+
*
19+
* Useful for classloaders that have heavy-weight loadClass() implementations
20+
*
21+
* @author Dmytro Ukhlov
22+
*/
23+
@Restricted(NoExternalUse.class)
24+
public class CachingClassLoader extends DelegatingClassLoader {
25+
private final ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();
26+
27+
public CachingClassLoader(String name, ClassLoader parent) {
28+
super(name, parent);
29+
}
30+
31+
@Override
32+
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
33+
Object classOrEmpty = cache.computeIfAbsent(name, key -> {
34+
try {
35+
return super.loadClass(name, false);
36+
} catch (ClassNotFoundException e) {
37+
// Not found.
38+
return Optional.empty();
39+
}
40+
});
41+
42+
if (classOrEmpty == Optional.empty()) {
43+
throw new ClassNotFoundException(name);
44+
}
45+
46+
Class<?> clazz = (Class<?>) classOrEmpty;
47+
if (resolve) {
48+
resolveClass(clazz);
49+
}
50+
return clazz;
51+
}
52+
53+
public void clearCacheMisses() {
54+
cache.values().removeIf(v -> v == Optional.empty());
55+
}
56+
}

0 commit comments

Comments
 (0)