Poison

ContextClassLoader

关于上下文类加载器,之前一直只是了解,在开发过程中并未使用过,直到开发 Java Agent 时,才真正明白其作用,本文作简要记录。在 Java SPI 机制的核心类 ServiceLoader 中,有以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Creates a new service loader for the given service type, using the
* current thread's {@linkplain java.lang.Thread#getContextClassLoader
* context class loader}.
*
* <p> An invocation of this convenience method of the form
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>)</pre></blockquote>
*
* is equivalent to
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>,
* Thread.currentThread().getContextClassLoader())</pre></blockquote>
*
* @param <S> the class of the service type
*
* @param service
* The interface or abstract class representing the service
*
* @return A new service loader
*/
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

可以看出,在创建服务加载器时用到了线程上下文类加载器,在 DriverManagerstatic 代码块中,就用到了该方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()

AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});

println("DriverManager.initialize: jdbc.drivers = " + drivers);

if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}

关于此处为何要使用上下文类加载器,上面这个例子不能很好的解释这个原因,我只能根据开发 Java Agent 的经验做简要的解释,上下文类加载器主要是解决当前类所在的类加载器整个双亲委派层次上无法加载到类的问题。我们用 ServiceLoader 类举例,该类位于 <JAVA_HOME>/jre/lib 目录下的 rt.jar,由 Bootstrap ClassLoader 加载,如果查看 ServiceLoader 的源码,会发现最终去加载类时是使用的 LazyIterator 中的 nextService 方法去加载的类,部分源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}

其中调用的 Class.forName 方法是使用的含 ClassLoader 参数的重载版本,代码 c = Class.forName(cn, false, loader) 中的 loader 即为 Thread.currentThread().getContextClassLoader() 返回的当前线程上下文类加载器。现在,我们编写了一个简单的 Java Agent,整个 Java Agent 的源码位于 GitHub - tianshuang/agent-test at context-class-loader-not-set,该 Agent 采用了独立的类加载器,该独立的类加载器的父加载器为 Bootstrap ClassLoader,源码 AgentClassLoader.java 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package me.tianshuang.agent;

import java.net.URL;
import java.net.URLClassLoader;

public class AgentClassLoader extends URLClassLoader {

static {
ClassLoader.registerAsParallelCapable();
}

public AgentClassLoader(URL[] urls) {
// parent: Bootstrap ClassLoader
super(urls, null);
}

}

该 Agent 的入口处 premain 核心代码位于 Agent.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package me.tianshuang.agent;

import lombok.extern.log4j.Log4j2;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.CodeSource;


@Log4j2
public class Agent {

public static void premain(String agentArgs) {
try {
ClassLoader agentClassLoader = new AgentClassLoader(new URL[]{getJavaAgentFile().toURL()});
Class<?> agentClass = agentClassLoader.loadClass("me.tianshuang.agent.Agent");
Method agentStartListenerMethod = agentClass.getMethod("startAgent");

agentStartListenerMethod.invoke(null);
} catch (ClassNotFoundException | InvocationTargetException | NoSuchMethodException | IllegalAccessException | MalformedURLException e) {
log.error("Agent exception: " + e.getMessage(), e);
}
}

private static File getJavaAgentFile() {
try {
CodeSource codeSource = Agent.class.getProtectionDomain().getCodeSource();
if (codeSource != null) {
File javaagentFile = new File(codeSource.getLocation().toURI());
if (javaagentFile.isFile()) {
return javaagentFile;
}
}

throw new RuntimeException("Cannot get java agent file");
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}

public static void startAgent() {
new Thread(() -> {
while (true) {
log.info("I'm agent test thread...");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
// ignore
}
}
}, "agent-test-thread").start();
}

}

该 Java Agent 依赖了 log4j2 2.11.1 版本的依赖,此时,使用该 Java Agent 的应用也依赖了 log4j2 2.11.1 版本的依赖,AgentClassLoader 的父加载器为 Bootstrap ClassLoader,因为我们未设置过上下文类加载器,根据 Oracle 文档 Class Loading 中的描述,默认为加载应用程序的类加载器,即系统类加载器,那么,当执行到 nextService 方法时,从当前线程中获取到的上下文类加载器为 Launcher.AppClassLoader,即系统类加载器,而此时我们需要加载服务实现的接口 org.apache.logging.log4j.util.PropertySource 又是由 AgentClassLoader 加载的,我们使用了上下文类加载器 Launcher.AppClassLoader 去加载了服务的实现 org.apache.logging.log4j.util.EnvironmentPropertySource 且成功加载了该类,但是,在随后的 service.isAssignableFrom(c) 判断中,返回了 false,随即触发了 ServiceConfigurationError,即 ServiceLoader 不允许接口与实现类由不同的类加载器加载,此时会认为 EnvironmentPropertySource 不是 PropertySource 的子类型,异常信息如下:

1
org.apache.logging.log4j.util.PropertySource: Provider org.apache.logging.log4j.util.EnvironmentPropertySource not a subtype

虽然 EnvironmentPropertySource 实现了 PropertySource 接口,但是它们由不同的类加载器加载,那么就不符合 ServiceLoader 的规定,如果我们参考 OpenTelemetry 的 Java Agent 实现,我们会发现,它们在使用 Agent 独立的类加载器时,即将去初始化 Agent 的相关逻辑时,设置了上下文类加载器,最后在 finally 中恢复了当前线程之前的上下文类加载器,这就是上下文类加载器的作用,其源码位于 AgentInitializer.java,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// called via reflection in the OpenTelemetryAgent class
public static void initialize(Instrumentation inst, File javaagentFile) throws Exception {
if (agentClassLoader == null) {
agentClassLoader = createAgentClassLoader("inst", javaagentFile);

Class<?> agentInstallerClass =
agentClassLoader.loadClass("io.opentelemetry.javaagent.tooling.AgentInstaller");
Method agentInstallerMethod =
agentInstallerClass.getMethod("installBytebuddyAgent", Instrumentation.class);
ClassLoader savedContextClassLoader = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(agentClassLoader);
agentInstallerMethod.invoke(null, inst);
} finally {
Thread.currentThread().setContextClassLoader(savedContextClassLoader);
}
}
}

简单来说,上下文类加载器在 ServiceLoader 中主要用于打破双亲委派模型,如果不使用上下文类加载器,那么默认 Class.forName 实现采用的调用方的类加载器,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* Returns the {@code Class} object associated with the class or
* interface with the given string name. Invoking this method is
* equivalent to:
*
* <blockquote>
* {@code Class.forName(className, true, currentLoader)}
* </blockquote>
*
* where {@code currentLoader} denotes the defining class loader of
* the current class.
*
* <p> For example, the following code fragment returns the
* runtime {@code Class} descriptor for the class named
* {@code java.lang.Thread}:
*
* <blockquote>
* {@code Class t = Class.forName("java.lang.Thread")}
* </blockquote>
* <p>
* A call to {@code forName("X")} causes the class named
* {@code X} to be initialized.
*
* @param className the fully qualified name of the desired class.
* @return the {@code Class} object for the class with the
* specified name.
* @exception LinkageError if the linkage fails
* @exception ExceptionInInitializerError if the initialization provoked
* by this method fails
* @exception ClassNotFoundException if the class cannot be located
*/
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

而此时调用方 ServiceLoader 由 Bootstrap ClassLoader 类加载器加载,其对应的目录为 <JAVA_HOME>/jre/lib,此时就无法加载位于应用程序类路径中的类了,所以,上下文类加载器就是为了解决该问题,支持根据当前线程的上下文类加载器去进行服务实现的加载,以规避双亲委派模型无法加载实现类的问题。

Reference

ServiceLoader (Java Platform SE 8 )
Java Classloader - Wikipedia