AnthonyZero's Bolg

JVM-线程上下文类加载器

SPI机制带来的问题

Java提供了很多SPI,允许第三方为这些接口提供实现,最常见的SPI实现有JDBC、JNDI等等,根据类加载器的双亲委派模型,加载ServiceLoader的 BootstrapClassLoader 是不能加载SPI的实现类的,因为SPI的实现类是由 AppClassLoader 加载的,而 BootstrapClassLoader 是不能委派 AppClassLoader 来加载类的,那该怎么办呢?

SPI约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。

如何解决

SPI 的实现则是由各供应商来完成。我们使用时只需要将所需的实现作为 Java 应用所依赖的 jar 包包含进类路径(CLASSPATH)就可以了。

SPI的接口是Java核心库的一部分,按照双亲委派模式和类加载器搜索路径而言:它是由启动类加载器来加载的;但是SPI的实现类在我们应用引入之后在应用Classpath下,BootstrapClassLoader不认识它加载不了,只能由系统类加载器来加载的。原因在于启动类加载器是无法找到 SPI 的实现类的(因为它只加载 Java 的核心库),按照双亲委派模型,启动类加载器又无法委派系统类加载器去加载类。也就是说,类加载器的双亲委派模式无法解决这个问题

这时候线程上下文类加载器排上了用场。线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

线程上下文类加载器(TCCL)

可使用ServiceLoader的load方法获取到TCCL

1
2
3
4
5
6
7
private static final String PREFIX = "META-INF/services/";

public static <S> ServiceLoader<S> load(Class<S> service) {    
// 获取当前调用线程的类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();    
return ServiceLoader.load(service, cl);
}

Java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的线程上下文类加载器初始是AppClassLoader,在线程中运行的代码可以通过此类加载器来加载类和资源。

案例之JDBC

mysql获取数据库连接

1
Connection conn =java.sql.DriverManager.getConnection(url, "name", "password");

执行DriverManager的静态代码块

1
2
3
4
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

loadInitialDrivers方法中有这么一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 通过SPI加载驱动类
AccessController.doPrivileged(new PrivilegedAction<Void>(){
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});

分析:

  • ServiceLoader load方法中实例化了loader变量是线程上下文类加载器,并通过reload方法初始化了lookupIterator(LazyIterator类型实现了Iterator接口)
  • driversIterator.next()中就是调Class.forName(DriverName, false, loader)方法来加载,可在LazyIterator内部类的方法中看到。

Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootrapLoader中,这时候的loader已经是我们上面说的TCCL,

这里的TCCL是何值呢?通过上面ServiceLoader中Thread.currentThread().getContextClassLoader();
可知默认是AppClassLoader,所以这里就使用了AppClassLoader来加载Driver的实现类com.mysql.jdbc.Driver(META-INF/services/下定义)。
所以这里就明白了:父加载器加载不了的类 通过线程上下文类加载器拿到AppClassLoader来加载,变相违背了双亲委派模型。

总结

  • 线程上下文类加载器(它并不是一个真正的类加载器,而是通过当前线程拿到我们想要的类加载器->应用运行时它被放在了线程中,所以不管当前程序处于何处BootstrapClassLoader或ExtClassLoader等,在任何需要的时候都可以拿出去使用)。
    线程上下文类加载器打破了双亲委派机制,实现逆向调用类加载器来加载当前线程中类加载器加载不到的类
  • 当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,比如上面spi的调用者ServiceLoader所在的BootstrapClassloader无法加载的时候,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
  • 类/接口是有命名空间之分的,不同的类加载器是不同的命名空间。一个类是由A类加载器加载的,那么这个类的依赖类也会由这个A类加载器加载,但是如果所依赖的类不在A类加载器加载的范围内,那么就会找不到这个类。可以使用线程上下文类加载器进行加载使用。这种操作就是破坏了双亲委派模式