-
-
Notifications
You must be signed in to change notification settings - Fork 8.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
SetContextClassLoader
utility class (#6575)
- Loading branch information
Showing
2 changed files
with
137 additions
and
0 deletions.
There are no files selected for viewing
96 changes: 96 additions & 0 deletions
96
core/src/main/java/jenkins/util/SetContextClassLoader.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
41
test/src/test/java/jenkins/util/SetContextClassLoaderTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |