Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SetContextClassLoader utility class #6575

Merged
merged 9 commits into from
Aug 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +64 to +65
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still -0 on including the no-arg overload as in #6575 (comment): StackWalker and similar idioms should be avoided in production code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(an Object argument delegating to the Class overload would be fine, for calls from non-static contexts)

}

/**
* 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());
}
}