|
58 | 58 | import hudson.security.ACLContext; |
59 | 59 | import hudson.security.Permission; |
60 | 60 | import hudson.security.PermissionScope; |
| 61 | +import hudson.util.CachingClassLoader; |
61 | 62 | import hudson.util.CyclicGraphDetector; |
62 | 63 | import hudson.util.CyclicGraphDetector.CycleDetectedException; |
| 64 | +import hudson.util.DelegatingClassLoader; |
63 | 65 | import hudson.util.FormValidation; |
64 | 66 | import hudson.util.PersistedList; |
65 | 67 | import hudson.util.Retrier; |
|
106 | 108 | import java.util.Locale; |
107 | 109 | import java.util.Map; |
108 | 110 | import java.util.Objects; |
109 | | -import java.util.Optional; |
110 | 111 | import java.util.ServiceLoader; |
111 | 112 | import java.util.Set; |
112 | 113 | import java.util.TreeMap; |
113 | 114 | import java.util.UUID; |
114 | 115 | import java.util.concurrent.ConcurrentHashMap; |
115 | | -import java.util.concurrent.ConcurrentMap; |
116 | 116 | import java.util.concurrent.CopyOnWriteArrayList; |
117 | 117 | import java.util.concurrent.Future; |
118 | 118 | import java.util.function.Supplier; |
@@ -356,7 +356,19 @@ PluginManager doCreate(@NonNull Class<? extends PluginManager> klass, |
356 | 356 | */ |
357 | 357 | // implementation is minimal --- just enough to run XStream |
358 | 358 | // and load plugin-contributed classes. |
359 | | - public final ClassLoader uberClassLoader = new UberClassLoader(activePlugins); |
| 359 | + public final ClassLoader uberClassLoader = new CachingClassLoader("Caching UberClassLoader", |
| 360 | + new UberClassLoader(activePlugins)) { |
| 361 | + @Override |
| 362 | + protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { |
| 363 | + // basic check before use cache-based loading |
| 364 | + if (name.startsWith("SimpleTemplateScript") || // cf. groovy.text.SimpleTemplateEngine |
| 365 | + name.startsWith("groovy.tmp.")) { |
| 366 | + throw new ClassNotFoundException("ignoring " + name); |
| 367 | + } |
| 368 | + |
| 369 | + return super.loadClass(name, resolve); |
| 370 | + } |
| 371 | + }; |
360 | 372 |
|
361 | 373 | /** |
362 | 374 | * Once plugin is uploaded, this flag becomes true. |
@@ -538,7 +550,7 @@ protected void reactOnCycle(PluginWrapper q, List<PluginWrapper> cycle) { |
538 | 550 | for (PluginWrapper p : cgd.getSorted()) { |
539 | 551 | if (p.isActive()) { |
540 | 552 | activePlugins.add(p); |
541 | | - ((UberClassLoader) uberClassLoader).clearCacheMisses(); |
| 553 | + ((CachingClassLoader) uberClassLoader).clearCacheMisses(); |
542 | 554 | } |
543 | 555 | } |
544 | 556 | } catch (CycleDetectedException e) { // TODO this should be impossible, since we override reactOnCycle to not throw the exception |
@@ -660,7 +672,6 @@ void considerDetachedPlugin(String shortName, String source) { |
660 | 672 | } catch (IOException e) { |
661 | 673 | failedPlugins.add(new FailedPlugin(arc.getName(), e)); |
662 | 674 | } |
663 | | - |
664 | 675 | } |
665 | 676 | } |
666 | 677 |
|
@@ -973,7 +984,7 @@ public void dynamicLoad(File arc, boolean removeExisting, @CheckForNull List<Plu |
973 | 984 | plugins.add(p); |
974 | 985 | if (p.isActive()) { |
975 | 986 | activePlugins.add(p); |
976 | | - ((UberClassLoader) uberClassLoader).clearCacheMisses(); |
| 987 | + ((CachingClassLoader) uberClassLoader).clearCacheMisses(); |
977 | 988 | } |
978 | 989 |
|
979 | 990 | // TODO antimodular; perhaps should have a PluginListener to complement ExtensionListListener? |
@@ -2396,43 +2407,39 @@ public <T> T of(String key, Class<T> type, Supplier<T> func) { |
2396 | 2407 | /** |
2397 | 2408 | * {@link ClassLoader} that can see all plugins. |
2398 | 2409 | */ |
2399 | | - public static final class UberClassLoader extends ClassLoader { |
| 2410 | + public static final class UberClassLoader extends DelegatingClassLoader { |
2400 | 2411 | private final List<PluginWrapper> activePlugins; |
2401 | 2412 |
|
2402 | | - /** Cache of loaded, or known to be unloadable, classes. */ |
2403 | | - private final ConcurrentMap<String, Optional<Class<?>>> loaded = new ConcurrentHashMap<>(); |
2404 | | - |
2405 | | - static { |
2406 | | - registerAsParallelCapable(); |
2407 | | - } |
2408 | | - |
2409 | 2413 | public UberClassLoader(List<PluginWrapper> activePlugins) { |
2410 | 2414 | super("UberClassLoader", PluginManager.class.getClassLoader()); |
2411 | 2415 | this.activePlugins = activePlugins; |
2412 | 2416 | } |
2413 | 2417 |
|
2414 | 2418 | @Override |
2415 | | - protected Class<?> findClass(String name) throws ClassNotFoundException { |
2416 | | - if (name.startsWith("SimpleTemplateScript")) { // cf. groovy.text.SimpleTemplateEngine |
2417 | | - throw new ClassNotFoundException("ignoring " + name); |
| 2419 | + protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { |
| 2420 | + if (getResource(name.replace('.', '/') + ".class") == null && |
| 2421 | + !name.equals("java.lang.$JaCoCo")) { // Add support for loading of JaCoCo dynamic instrumentation classes |
| 2422 | + throw new ClassNotFoundException(name); |
2418 | 2423 | } |
2419 | | - return loaded.computeIfAbsent(name, this::computeValue).orElseThrow(() -> new ClassNotFoundException(name)); |
| 2424 | + |
| 2425 | + return super.loadClass(name, resolve); |
2420 | 2426 | } |
2421 | 2427 |
|
2422 | | - private Optional<Class<?>> computeValue(String name) { |
| 2428 | + @Override |
| 2429 | + protected Class<?> findClass(String name) throws ClassNotFoundException { |
2423 | 2430 | for (PluginWrapper p : activePlugins) { |
2424 | 2431 | try { |
2425 | 2432 | if (FAST_LOOKUP) { |
2426 | | - return Optional.of(ClassLoaderReflectionToolkit.loadClass(p.classLoader, name)); |
| 2433 | + return ClassLoaderReflectionToolkit.loadClass(p.classLoader, name); |
2427 | 2434 | } else { |
2428 | | - return Optional.of(p.classLoader.loadClass(name)); |
| 2435 | + return p.classLoader.loadClass(name); |
2429 | 2436 | } |
2430 | 2437 | } catch (ClassNotFoundException e) { |
2431 | 2438 | // Not found. Try the next class loader. |
2432 | 2439 | } |
2433 | 2440 | } |
2434 | | - // Not found in any of the class loaders. Delegate. |
2435 | | - return Optional.empty(); |
| 2441 | + |
| 2442 | + throw new ClassNotFoundException(name); |
2436 | 2443 | } |
2437 | 2444 |
|
2438 | 2445 | @Override |
@@ -2464,31 +2471,28 @@ protected Enumeration<URL> findResources(String name) throws IOException { |
2464 | 2471 | return Collections.enumeration(resources); |
2465 | 2472 | } |
2466 | 2473 |
|
2467 | | - void clearCacheMisses() { |
2468 | | - loaded.values().removeIf(Optional::isEmpty); |
2469 | | - } |
2470 | 2474 |
|
2471 | 2475 | @Override |
2472 | 2476 | public String toString() { |
2473 | 2477 | // only for debugging purpose |
2474 | 2478 | return "classLoader " + getClass().getName(); |
2475 | 2479 | } |
| 2480 | + } |
2476 | 2481 |
|
2477 | | - // TODO Remove this once we require post 2024-07 remoting minimum version and deleted ClassLoaderProxy#fetchJar(URL) |
2478 | | - @SuppressFBWarnings( |
2479 | | - value = "DMI_COLLECTION_OF_URLS", |
2480 | | - justification = "All URLs point to local files, so no DNS lookup.") |
2481 | | - @Restricted(NoExternalUse.class) |
2482 | | - public boolean isPluginJar(URL jarUrl) { |
2483 | | - for (PluginWrapper plugin : activePlugins) { |
2484 | | - if (plugin.classLoader instanceof URLClassLoader) { |
2485 | | - if (Set.of(((URLClassLoader) plugin.classLoader).getURLs()).contains(jarUrl)) { |
2486 | | - return true; |
2487 | | - } |
| 2482 | + // TODO Remove this once we require post 2024-07 remoting minimum version and deleted ClassLoaderProxy#fetchJar(URL) |
| 2483 | + @SuppressFBWarnings( |
| 2484 | + value = "DMI_COLLECTION_OF_URLS", |
| 2485 | + justification = "All URLs point to local files, so no DNS lookup.") |
| 2486 | + @Restricted(NoExternalUse.class) |
| 2487 | + public boolean isPluginJar(URL jarUrl) { |
| 2488 | + for (PluginWrapper plugin : activePlugins) { |
| 2489 | + if (plugin.classLoader instanceof URLClassLoader) { |
| 2490 | + if (Set.of(((URLClassLoader) plugin.classLoader).getURLs()).contains(jarUrl)) { |
| 2491 | + return true; |
2488 | 2492 | } |
2489 | 2493 | } |
2490 | | - return false; |
2491 | 2494 | } |
| 2495 | + return false; |
2492 | 2496 | } |
2493 | 2497 |
|
2494 | 2498 | @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "for script console") |
|
0 commit comments