Skip to content

Commit

Permalink
Add SetContextClassLoader utility class (#6575)
Browse files Browse the repository at this point in the history
  • Loading branch information
basil committed Aug 2, 2022
1 parent ca261cf commit f77a28b
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 0 deletions.
96 changes: 96 additions & 0 deletions core/src/main/java/jenkins/util/SetContextClassLoader.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>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).
*
* <p>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:
*
* <pre>
* class Caller {
* void foo() {
* try (SetContextClassLoader sccl = new SetContextClassLoader()) {
* [...] // Callee uses Thread.currentThread().getContextClassLoader()
* }
* }
* }
* </pre>
*
* <p>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.
*
* <p>See <a
* href="https://www.jenkins.io/doc/developer/plugin-development/dependencies-and-class-loading/#context-class-loaders">the
* developer documentation</a> 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);
}
}
41 changes: 41 additions & 0 deletions test/src/test/java/jenkins/util/SetContextClassLoaderTest.java
Original file line number Diff line number Diff line change
@@ -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());
}
}

0 comments on commit f77a28b

Please sign in to comment.