diff --git a/core/src/main/java/jenkins/util/SetContextClassLoader.java b/core/src/main/java/jenkins/util/SetContextClassLoader.java new file mode 100644 index 000000000000..e0932ca3041c --- /dev/null +++ b/core/src/main/java/jenkins/util/SetContextClassLoader.java @@ -0,0 +1,96 @@ +package jenkins.util; + +import hudson.PluginManager; +import hudson.remoting.ObjectInputStreamEx; +import java.io.ObjectInputStream; + +/** + * Java defines a {@link Thread#getContextClassLoader}. Jenkins does not use this much; it will + * normally be set by the servlet container to the Jenkins core class loader. + * + *

Some Java libraries have a fundamental design flaw, originating in premodular systems with a + * "flat classpath", whereby they expect {@link Thread#getContextClassLoader} to have access to the + * same classes as the class loader of the calling class. This fails in Jenkins, because {@link + * Thread#getContextClassLoader} can only see Jenkins core, not plugins. + * + *

It is a design flaw in the library if it fails to allow clients to directly specify a {@link + * ClassLoader} to use for lookups (or preregister {@link Class} instances for particular names). + * Consider patching the library or looking harder for appropriate APIs that already exist. As an + * example, {@link ObjectInputStream} (used for deserializing Java objects) by default uses a + * complicated algorithm to guess at a {@link ClassLoader}, but you can override {@link + * ObjectInputStream#resolveClass} to remove the need for guessing (as {@link ObjectInputStreamEx} + * in fact does). + * + *

Alternatively, work around the problem by applying {@link SetContextClassLoader} liberally in + * a {@code try}-with-resources block wherever we might be calling into such a library: + * + *

+ * class Caller {
+ *     void foo() {
+ *         try (SetContextClassLoader sccl = new SetContextClassLoader()) {
+ *             [...] // Callee uses Thread.currentThread().getContextClassLoader()
+ *         }
+ *     }
+ * }
+ * 
+ * + *

When called from a plugin, {@link #SetContextClassLoader()} should typically be used. This + * implicitly uses the class loader of the calling class, which has access to all the plugin's + * direct and transitive dependencies. Alternatively, the class loader of a specific class can be + * used via {@link #SetContextClassLoader(Class)}. When the particular class loader needed is + * unclear, {@link #SetContextClassLoader(ClassLoader)} can be used as a fallback with {@link + * PluginManager.UberClassLoader} as the argument, though this is not as safe since lookups could be + * ambiguous in case two unrelated plugins both bundle the same library. In functional tests, {@code + * RealJenkinsRule.Endpoint} can be used to reference a class loader that has access to the plugins + * defined in the test scenario. + * + *

See the + * developer documentation for more information. + * + * @since TODO + */ +public final class SetContextClassLoader implements AutoCloseable { + + private final Thread t; + private final ClassLoader orig; + + /** + * Change the {@link Thread#getContextClassLoader} associated with the current thread to that of + * the calling class. + * + * @since TODO + */ + public SetContextClassLoader() { + this(StackWalker.getInstance().getCallerClass()); + } + + /** + * Change the {@link Thread#getContextClassLoader} associated with the current thread to that of + * the specified class. + * + * @param clazz The {@link Class} whose {@link ClassLoader} to use. + * @since TODO + */ + public SetContextClassLoader(Class clazz) { + this(clazz.getClassLoader()); + } + + /** + * Change the {@link Thread#getContextClassLoader} associated with the current thread to the + * specified {@link ClassLoader}. + * + * @param cl The {@link ClassLoader} to use. + * @since TODO + */ + public SetContextClassLoader(ClassLoader cl) { + t = Thread.currentThread(); + orig = t.getContextClassLoader(); + t.setContextClassLoader(cl); + } + + @Override + public void close() { + t.setContextClassLoader(orig); + } +} diff --git a/test/src/test/java/jenkins/util/SetContextClassLoaderTest.java b/test/src/test/java/jenkins/util/SetContextClassLoaderTest.java new file mode 100644 index 000000000000..71cd6e9354d3 --- /dev/null +++ b/test/src/test/java/jenkins/util/SetContextClassLoaderTest.java @@ -0,0 +1,41 @@ +package jenkins.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.RealJenkinsRule; + +public class SetContextClassLoaderTest { + + @Rule public RealJenkinsRule rr = new RealJenkinsRule(); + + @Test + public void positive() throws Throwable { + rr.then(SetContextClassLoaderTest::_positive); + } + + private static void _positive(JenkinsRule r) throws ClassNotFoundException { + try (SetContextClassLoader sccl = new SetContextClassLoader(RealJenkinsRule.Endpoint.class)) { + assertEquals("hudson.tasks.Mailer$UserProperty", getUserPropertyClass().getName()); + } + } + + @Test + public void negative() throws Throwable { + rr.then(SetContextClassLoaderTest::_negative); + } + + private static void _negative(JenkinsRule r) { + assertThrows(ClassNotFoundException.class, SetContextClassLoaderTest::getUserPropertyClass); + } + + private static Class getUserPropertyClass() throws ClassNotFoundException { + return Class.forName( + "hudson.tasks.Mailer$UserProperty", + true, + Thread.currentThread().getContextClassLoader()); + } +}