From bccbc865e4e1c8ba547176d1476efbd96cdf6b08 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Fri, 10 Dec 2021 09:51:57 -0800 Subject: [PATCH] `URLClassLoader` support for `PluginFirstClassLoader` (#5970) Co-authored-by: Jesse Glick --- .../java/hudson/ClassicPluginStrategy.java | 29 ++-- .../java/hudson/PluginFirstClassLoader2.java | 135 ++++++++++++++++++ .../java/hudson/util/CompoundEnumeration.java | 6 + .../hudson/util/CompoundEnumerationTest.java | 74 ++++++++++ 4 files changed, 232 insertions(+), 12 deletions(-) create mode 100644 core/src/main/java/hudson/PluginFirstClassLoader2.java create mode 100644 core/src/test/java/hudson/util/CompoundEnumerationTest.java diff --git a/core/src/main/java/hudson/ClassicPluginStrategy.java b/core/src/main/java/hudson/ClassicPluginStrategy.java index a7f3259bb5..54d8e6b510 100644 --- a/core/src/main/java/hudson/ClassicPluginStrategy.java +++ b/core/src/main/java/hudson/ClassicPluginStrategy.java @@ -285,19 +285,18 @@ public class ClassicPluginStrategy implements PluginStrategy { * Creates the classloader that can load all the specified jar files and delegate to the given parent. */ protected ClassLoader createClassLoader(List paths, ClassLoader parent, Attributes atts) throws IOException { - if (atts != null) { - String usePluginFirstClassLoader = atts.getValue( "PluginFirstClassLoader" ); - if (Boolean.parseBoolean( usePluginFirstClassLoader )) { - PluginFirstClassLoader classLoader = new PluginFirstClassLoader(); - classLoader.setParentFirst( false ); - classLoader.setParent( parent ); - classLoader.addPathFiles( paths ); - return classLoader; - } - } + boolean usePluginFirstClassLoader = + atts != null && Boolean.parseBoolean(atts.getValue("PluginFirstClassLoader")); if (useAntClassLoader) { - AntClassLoader classLoader = new AntClassLoader(parent, true); + AntClassLoader classLoader; + if (usePluginFirstClassLoader) { + classLoader = new PluginFirstClassLoader(); + classLoader.setParentFirst(false); + classLoader.setParent(parent); + } else { + classLoader = new AntClassLoader(parent, true); + } classLoader.addPathFiles(paths); return classLoader; } else { @@ -305,7 +304,13 @@ public class ClassicPluginStrategy implements PluginStrategy { for (File path : paths) { urls.add(path.toURI().toURL()); } - return new URLClassLoader2(urls.toArray(new URL[0]), parent); + URLClassLoader2 classLoader; + if (usePluginFirstClassLoader) { + classLoader = new PluginFirstClassLoader2(urls.toArray(new URL[0]), parent); + } else { + classLoader = new URLClassLoader2(urls.toArray(new URL[0]), parent); + } + return classLoader; } } diff --git a/core/src/main/java/hudson/PluginFirstClassLoader2.java b/core/src/main/java/hudson/PluginFirstClassLoader2.java new file mode 100644 index 0000000000..974939d53a --- /dev/null +++ b/core/src/main/java/hudson/PluginFirstClassLoader2.java @@ -0,0 +1,135 @@ +package hudson; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.util.CompoundEnumeration; +import java.io.IOException; +import java.net.URL; +import java.util.Enumeration; +import java.util.Objects; +import jenkins.util.URLClassLoader2; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +/** + * Class loader that consults the plugin's {@code WEB-INF/lib/*.jar} and {@code WEB-INF/classes} + * directories and the Jenkins core class loader (in that order). + * + *

To use this class loader, set {@code pluginFirstClassLoader} to {@code true} in the {@code + * maven-hpi-plugin} configuration. + * + * @author Basil Crow + */ +@Restricted(NoExternalUse.class) +public class PluginFirstClassLoader2 extends URLClassLoader2 { + static { + registerAsParallelCapable(); + } + + public PluginFirstClassLoader2(@NonNull URL[] urls, @NonNull ClassLoader parent) { + super(Objects.requireNonNull(urls), Objects.requireNonNull(parent)); + } + + /** + * Load the class with the specified binary name. This method searches for classes in the + * following order: + * + *

    + *
  1. + *

    Invoke {@link #findLoadedClass(String)} to check if the class has already been + * loaded. + *

  2. + *

    Invoke {@link #findClass(String)} to find the class. + *

  3. + *

    Invoke {@link #loadClass(String)} on the parent class loader. + *

+ * + *

If the class was found using the above steps and the {@code resolve} flag is true, this + * method will then invoke {@link #resolveClass(Class)} on the resulting {@link Class} object. + * + *

This method synchronizes on the result of {@link #getClassLoadingLock(String)} during the + * entire class loading process. + * + * @param name The binary name of the class + * @param resolve If {@code true} then resolve the class + * @return The resulting {@link Class} object + * @throws ClassNotFoundException If the class could not be found + */ + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + Class c = findLoadedClass(name); + if (c == null) { + try { + c = findClass(name); + } catch (ClassNotFoundException e) { + // ignore + } + } + if (c == null) { + c = getParent().loadClass(name); + } + if (resolve) { + resolveClass(c); + } + return c; + } + } + + /** + * Find the resource with the given name. A resource is some data (images, audio, text, etc) + * that can be accessed by class code in a way that is independent of the location of the code. + * + *

The name of a resource is a '{@code /}'-separated path name that identifies the resource. + * This method searches for resources in the following order: + * + *

    + *
  1. + *

    Invoke {@link #findResource(String)} to find the resource. + *

  2. + *

    Invoke {@link #getResource(String)} on the parent class loader. + *

+ * + * @param name The resource name + * @return {@link URL} object for reading the resource; {@code null} if the resource could not + * be found, a {@link URL} could not be constructed to locate the resource, the resource is + * in a package that is not opened unconditionally, or access to the resource is denied by + * the security manager. + * @throws NullPointerException If {@code name} is {@code null} + */ + @Override + public URL getResource(String name) { + Objects.requireNonNull(name); + URL url = findResource(name); + if (url == null) { + url = getParent().getResource(name); + } + return url; + } + + /** + * Find all the resources with the given name. A resource is some data (images, audio, text, + * etc) that can be accessed by class code in a way that is independent of the location of the + * code. + * + *

The name of a resource is a {@code /}-separated path name that identifies the resource. + * This method first invokes {@link #findResources(String)} to find the resources with the name + * in this class loader. Finally, it invokes {@link #getResources(String)} on the parent class + * loader. It returns an enumeration whose elements are the {@link URL}s found by searching the + * {@link URL}s found with {@link #findResources(String)}, followed by the {@link URL}s found by + * searching the parent class loader. + * + * @param name The resource name + * @return An enumeration of {@link URL} objects for the resource. If no resources could be + * found, the enumeration will be empty. Resources for which a {@link URL} cannot be + * constructed, which are in a package that is not opened unconditionally, or for which + * access to the resource is denied by the security manager, are not returned in the + * enumeration. + * @throws IOException If I/O errors occur + * @throws NullPointerException If {@code name} is {@code null} + */ + @Override + public Enumeration getResources(String name) throws IOException { + Objects.requireNonNull(name); + return new CompoundEnumeration<>(findResources(name), getParent().getResources(name)); + } +} diff --git a/core/src/main/java/hudson/util/CompoundEnumeration.java b/core/src/main/java/hudson/util/CompoundEnumeration.java index 6de29d96e5..c1679f7a26 100644 --- a/core/src/main/java/hudson/util/CompoundEnumeration.java +++ b/core/src/main/java/hudson/util/CompoundEnumeration.java @@ -1,6 +1,7 @@ package hudson.util; import java.util.Arrays; +import java.util.Collections; import java.util.Enumeration; import java.util.Iterator; import java.util.NoSuchElementException; @@ -21,6 +22,11 @@ public class CompoundEnumeration implements Enumeration { public CompoundEnumeration(Iterable> e) { this.base = e.iterator(); + if (this.base.hasNext()) { + this.cur = this.base.next(); + } else { + this.cur = Collections.emptyEnumeration(); + } } @Override diff --git a/core/src/test/java/hudson/util/CompoundEnumerationTest.java b/core/src/test/java/hudson/util/CompoundEnumerationTest.java new file mode 100644 index 0000000000..13335ab1d1 --- /dev/null +++ b/core/src/test/java/hudson/util/CompoundEnumerationTest.java @@ -0,0 +1,74 @@ +package hudson.util; + +import static org.junit.Assert.assertEquals; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.Test; + +public class CompoundEnumerationTest { + + @Test + public void smokes() { + assertEquals( + rangeClosed(1, 12), + Collections.list( + new CompoundEnumeration<>( + Collections.enumeration(rangeClosed(1, 4)), + Collections.enumeration(rangeClosed(5, 8)), + Collections.enumeration(rangeClosed(9, 12))))); + } + + @Test + public void empty() { + assertEquals(Collections.emptyList(), Collections.list(new CompoundEnumeration<>())); + assertEquals( + Collections.emptyList(), + Collections.list(new CompoundEnumeration<>(Collections.emptyEnumeration()))); + assertEquals( + Collections.emptyList(), + Collections.list( + new CompoundEnumeration<>( + Collections.emptyEnumeration(), Collections.emptyEnumeration()))); + assertEquals( + Collections.emptyList(), + Collections.list( + new CompoundEnumeration<>( + Collections.emptyEnumeration(), + Collections.emptyEnumeration(), + Collections.emptyEnumeration()))); + } + + @Test + public void gaps() { + assertEquals( + rangeClosed(1, 8), + Collections.list( + new CompoundEnumeration<>( + Collections.emptyEnumeration(), + Collections.enumeration(rangeClosed(1, 4)), + Collections.enumeration(rangeClosed(5, 8))))); + assertEquals( + rangeClosed(1, 8), + Collections.list( + new CompoundEnumeration<>( + Collections.enumeration(rangeClosed(1, 4)), + Collections.emptyEnumeration(), + Collections.enumeration(rangeClosed(5, 8))))); + assertEquals( + rangeClosed(1, 8), + Collections.list( + new CompoundEnumeration<>( + Collections.enumeration(rangeClosed(1, 4)), + Collections.enumeration(rangeClosed(5, 8)), + Collections.emptyEnumeration()))); + } + + private static List rangeClosed(int startInclusive, int endInclusive) { + return IntStream.rangeClosed(startInclusive, endInclusive) + .boxed() + .collect(Collectors.toList()); + } +}