Context ClassLoader的出现
JVM既然有了双亲委派模型来加载类,为什么又出现了上下文类加载器,去打破双亲委派模型呢。
java双亲委派类加载模型
举例说明应用场景:java中的SPI机制是扩展java功能的扩展点。比如JDBC驱动的实现,java只实现接口定义,定义的类当然是由能加载java平台api的Platform class loader类加载器加载(java17环境下,本博文)。
而第三方实现的驱动,则是在我们的class path目录下,由System class loader即application class loader 加载。
目前java17内置的类有:
接口与实现类如果是由不同的类加载器加载,在运行时,由于双亲委派模型,父类加载器加载的类是找不到子类加载器加载的类,导致实现类是找不到的。
我们可以验证一下:
System.out.println("java.sql.Connection :==> " + Connection.class.getClassLoader());
System.out.println("java.sql.DriverManager :==> " + DriverManager.class.getClassLoader());
System.out.println("java.sql.Driver :==> " + Driver.class.getClassLoader());
System.out.println("com.mysql.cj.jdbc.ConnectionImpl :==> " + com.mysql.cj.jdbc.ConnectionImpl.class.getClassLoader());
运行结果:
java平台定义的JDBC接口是由Platform class loader类加载器加载,而驱动的实现由由System class loader即application class loader 加载。
所以运行时动态加载JDBC实现类时,双亲委派机制就不行了,Platform class loader类加载器此时加载不到驱动的实现类,此时Context ClassLoader就派上用场了。
Context ClassLoader的加载机制
类加载时,我们可以指定类加载器,如下方法:
java.lang.Class#forName(java.lang.String, boolean, java.lang.ClassLoader)
而当前上下文的类加载器我们可以从当前线程获取:
java.lang.Thread#getContextClassLoader
我们以ServiceLoader示例,JDBC驱动实现类的加载是ServiceLoader实现的。
java.sql.DriverManager#ensureDriversInitialized
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;
ServiceLoader加载类:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
类加载时使用的类加载器是当前线程上下文类加载器:
Thread.currentThread().getContextClassLoader();
用当前上下文类加载器打破双亲委派模型,最终类加载的方法:
java.lang.Class#forName(java.lang.String, boolean, java.lang.ClassLoader)
用的是同一个线程上下文类加载器去加载和查找类。
spring boot不打包运行与打包运行jar的区别
spring boot 不打包,即开发模式IDE直接运行,应用中的类是由application class loader 加载的,线程上下文类加载器默认也是application class loader 。
我们来验证一下:
package com.example.demo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.*;
/**
* @author 认知科技技术团队
* 微信公众号:认知科技技术团队
*/
@Component
@Slf4j
public class ThreadPoolConfig {
@PostConstruct
void init() throws ExecutionException, InterruptedException {
log.info("当前类加载器:{}", getClass().getClassLoader());
log.info("启动类加载器:{}", Demo1Application.class.getClassLoader());
log.info("{} ===> {}", "Thread.currentThread().getContextClassLoader", Thread.currentThread().getContextClassLoader());
Thread thread = new Thread(
() ->
log.info("Thread: {} ===> {}", "Thread.currentThread().getContextClassLoader",
Thread.currentThread().getContextClassLoader())
);
thread.start();
thread.join();
//CompletableFuture 默认线程池注意上下文类加载器
CompletableFuture<Void> future = CompletableFuture.runAsync(
() ->
log.info("CompletableFuture: {} ===> {}", "Thread.currentThread().getContextClassLoader",
Thread.currentThread().getContextClassLoader())
);
future.get();
}
}
本地不打包运行结果:
而spring boot打包运行,即java -jar demo.jar
spring boot 打包运行所用的类加载器是
org.springframework.boot.loader.LaunchedURLClassLoader
spring boot打包运行,使用了自己实现的类加载器。
spring boot的LaunchedURLClassLoader有什么坑
来源 https://docs.spring.io/spring-boot/docs/2.3.12.RELEASE/reference/htmlsingle/#executable-jar-restrictions
第二点,大部分第三方jar包的类加载器使用线程上下文类加载器(Thread.getContextClassLoader()),少数使用系统类加载器即应用类加载器ClassLoader.getSystemClassLoader()(即application class loader),此时类加载就会失败。
java.util.Logging上面提到了总是使用系统类加载器。
而且细心的读者,可以看到上面的示例中
CompletableFuture异步提交任务,使用默认的ForkJoinPool线程池时,会使系统类加载器即应用类加载器,成为了当前线程上下文加载器。此时遇到第三方jar包,在CompletableFuture提交的异步任务内加载时,同时在spring jar包运行下,使用线程上下文类加载器加载类导致失败。
运行环境:
java version "17.0.2" 2022-01-18 LTS
Java(TM) SE Runtime Environment (build 17.0.2+8-LTS-86)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.2+8-LTS-86, mixed mode, sharing)
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
小结
线程上下文类加载器打破了双亲委派类加载机制,这个在模块化框架中会经常遇到。
线程上下文类加载器使得SPI机制顺利加载到第三方jar包的类。
spring boot 以jar包运行环境下,使用的是spring自己实现的类类加载器LaunchedURLClassLoader,并且存在一下类加载坑(第三方jar包不是以当前线程上下文类加载器加载,或者使用了类ForkJoinPool的线程池,使得当前线程上下文类加载器改变)。