Move common caching classloader functionality to the CachingClassLoader class

Use it for UberClassLoder, as a result - UberClassLoder caches all results of loadClass function
(not only findClass)
This commit is contained in:
Dmytro Ukhlov 2025-08-16 18:20:42 +03:00 committed by Dmitriy Ukhlov
parent f49b4b9418
commit 5d9b429348
2 changed files with 65 additions and 18 deletions

View File

@ -58,9 +58,9 @@ import hudson.security.ACL;
import hudson.security.ACLContext;
import hudson.security.Permission;
import hudson.security.PermissionScope;
import hudson.util.CachingClassLoader;
import hudson.util.CyclicGraphDetector;
import hudson.util.CyclicGraphDetector.CycleDetectedException;
import hudson.util.DelegatingClassLoader;
import hudson.util.ExistenceCheckingClassLoader;
import hudson.util.FormValidation;
import hudson.util.PersistedList;
@ -108,13 +108,11 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
import java.util.function.Supplier;
@ -2402,12 +2400,9 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
/**
* {@link ClassLoader} that can see all plugins.
*/
public static final class UberClassLoader extends DelegatingClassLoader {
public static final class UberClassLoader extends CachingClassLoader {
private final List<PluginWrapper> activePlugins;
/** Cache of loaded, or known to be unloadable, classes. */
private final ConcurrentMap<String, Optional<Class<?>>> loaded = new ConcurrentHashMap<>();
/**
* The servlet container's {@link ClassLoader} (the parent of Jenkins core) is
* parallel-capable and maintains its own growing {@link Map} of {@link
@ -2426,29 +2421,29 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
for (String namePrefixToSkip : CLASS_PREFIXES_TO_SKIP) {
if (name.startsWith(namePrefixToSkip)) {
throw new ClassNotFoundException("ignoring " + name);
}
}
return loaded.computeIfAbsent(name, this::computeValue).orElseThrow(() -> new ClassNotFoundException(name));
return super.loadClass(name, resolve);
}
private Optional<Class<?>> computeValue(String name) {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
for (PluginWrapper p : activePlugins) {
try {
if (FAST_LOOKUP) {
return Optional.of(ClassLoaderReflectionToolkit.loadClass(p.classLoader, name));
return ClassLoaderReflectionToolkit.loadClass(p.classLoader, name);
} else {
return Optional.of(p.classLoader.loadClass(name));
return p.classLoader.loadClass(name);
}
} catch (ClassNotFoundException e) {
// Not found. Try the next class loader.
}
}
// Not found in any of the class loaders. Delegate.
return Optional.empty();
throw new ClassNotFoundException(name);
}
@Override
@ -2480,10 +2475,6 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
return Collections.enumeration(resources);
}
void clearCacheMisses() {
loaded.values().removeIf(Optional::isEmpty);
}
@Override
public String toString() {
// only for debugging purpose

View File

@ -0,0 +1,56 @@
package hudson.util;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
*
* ClassLoader with internal caching of class loading results.
*
* <p>
* Caches both successful and failed class lookups to avoid redundant delegation
* and repeated class resolution attempts. Designed for performance optimization
* in systems that repeatedly query class presence (e.g., plugin environments,
* reflective loading, optional dependencies).
* </p>
*
* Useful for classloaders that have heavy-weight loadClass() implementations
*
* @author Dmytro Ukhlov
*/
@Restricted(NoExternalUse.class)
public class CachingClassLoader extends DelegatingClassLoader {
private final ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();
public CachingClassLoader(String name, ClassLoader parent) {
super(name, parent);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Object classOrEmpty = cache.computeIfAbsent(name, key -> {
try {
return super.loadClass(name, false);
} catch (ClassNotFoundException e) {
// Not found.
return Optional.empty();
}
});
if (classOrEmpty == Optional.empty()) {
throw new ClassNotFoundException(name);
}
Class<?> clazz = (Class<?>) classOrEmpty;
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
public void clearCacheMisses() {
cache.values().removeIf(v -> v == Optional.empty());
}
}