-
-
Notifications
You must be signed in to change notification settings - Fork 8.8k
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
Changes from all commits
a7a66a0
ee5cf6e
80c7b29
e8b04d1
22f9c02
752dc64
d531df7
b13e3a8
8d38b48
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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()); | ||
Comment on lines
+64
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still -0 on including the no-arg overload as in #6575 (comment): There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (an |
||
} | ||
|
||
/** | ||
* 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); | ||
} | ||
} |
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()); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool can hopefully simplify a few APIs with this, e.g. https://github.com/jenkinsci/configuration-as-code-plugin/blob/e345fd8df353db1a9fbd930968b87fce30548438/test-harness/src/main/java/io/jenkins/plugins/casc/misc/Util.java#L172