java类加载器—ContextClassLoader类加载器

心已赠人 2022-09-03 15:12 484阅读 0赞

ContextClassLoader是一种与线程相关的类加载器,类似ThreadLocal,每个线程对应一个上下文类加载器.在实际使用时一般都用下面的经典结构:

  1. ClassLoader targetClassLoader = null;// 外部参数
  2. ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
  3. try {
  4. Thread.currentThread().setContextClassLoader(targetClassLoader);
  5. // TODO
  6. } catch (Exception e) {
  7. e.printStackTrace();
  8. } finally {
  9. Thread.currentThread().setContextClassLoader(contextClassLoader);
  10. }
  1. 首先获取当前线程的线程上下文类加载器并保存到方法栈,然后将外部传递的类加载器设置为当前线程上下文类加载器
  2. doSomething则可以利用新设置的类加载器做一些事情
  3. 最后在设置当前线程上下文类加载器为老的类加载器

    上面的使用场景是什么?Java默认的类加载机制是委托机制,但是有些时候需要破坏这种固定的机制

具体来说,比如Java中的SPI(Service Provider Interface)是面向接口编程的,服务规则提供者会在JRE的核心API里面提供服务访问接口,而具体的实现则由其他开发商提供.我们知道Java核心API,比如rt.jar包,是使用Bootstrap ClassLoader加载的,而用户提供的jar包再由AppClassLoader加载.并且我们知道一个类由类加载器A加载,那么这个类依赖类也应该由相同的类加载器加载.那么Bootstrap ClassLoader加载了服务提供者在rt.jar里面提供的搜索开发上提供的实现类的API类(ServiceLoader),那么这些API类里面依赖的类应该也是有Bootstrap ClassLoader来加载.而上面说了用户提供的Jar包有AppClassLoader加载,所以需要一种违反双亲委派模型的方法,线程上下文类加载器ContextClassLoader就是为了解决这个问题。

下面使用JDBC来具体说明,JDBC是基于SPI机制来发现驱动提供商提供的实现类,提供者只需在JDBC实现的jar的META-INF/services/java.sql.Driver文件里指定实现类的方式暴露驱动提供者.例如:MYSQL实现的jar如下:

è¿éåå¾çæè¿°

其中MYSQL的驱动如下实现了java.sql.Driver

  1. public class Driver extends NonRegisteringDriver implements java.sql.Driver

引入MySQL驱动的jar包,测试类如下:

  1. import java.sql.Driver;
  2. import java.util.Iterator;
  3. import java.util.ServiceLoader;
  4. public class MySQLClassLoader {
  5. public static void main(String[] args) {
  6. ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
  7. Iterator<Driver> iterator = loader.iterator();
  8. while (iterator.hasNext()) {
  9. Driver driver = (Driver) iterator.next();
  10. System.out.println("driver:" + driver.getClass() + ",loader:" + driver.getClass().getClassLoader());
  11. }
  12. System.out.println("current thread contxtloader:" + Thread.currentThread().getContextClassLoader());
  13. System.out.println("ServiceLoader loader:" + ServiceLoader.class.getClassLoader());
  14. }
  15. }

执行结果如下:

  1. driver:class com.mysql.jdbc.Driver,loader:sun.misc.Launcher$AppClassLoader@2a139a55
  2. driver:class com.mysql.fabric.jdbc.FabricMySQLDriver,loader:sun.misc.Launcher$AppClassLoader@2a139a55
  3. driver:class com.alibaba.druid.proxy.DruidDriver,loader:sun.misc.Launcher$AppClassLoader@2a139a55
  4. driver:class com.alibaba.druid.mock.MockDriver,loader:sun.misc.Launcher$AppClassLoader@2a139a55
  5. current thread contxtloader:sun.misc.Launcher$AppClassLoader@2a139a55
  6. ServiceLoader loader:null

从执行结果中可以知道ServiceLoader的加载器为Bootstrap,因为这里输出了null,并且从该类在rt.jar里面,也可以证明.

当前线程上下文类加载器为AppClassLoader.而com.mysql.jdbc.Driver则使用AppClassLoader加载.我们知道如果一个类中引用了另外一个类,那么被引用的类也应该由引用方类加载器来加载,而现在则是引用方ServiceLoader使用BootStartClassLoader加载,被引用方则使用子加载器APPClassLoader来加载了.是不是很诡异.

下面来看一下ServiceLoader的load方法源码:

  1. public static <S> ServiceLoader<S> load(Class<S> service,
  2. ClassLoader loader)
  3. {
  4. return new ServiceLoader<>(service, loader);
  5. }
  6. public static <S> ServiceLoader<S> load(Class<S> service) {
  7. // 获取当前线程上下文加载器,这里是APPClassLoader
  8. ClassLoader cl = Thread.currentThread().getContextClassLoader();
  9. return ServiceLoader.load(service, cl);
  10. }

上述代码获得了线程上下文加载器(其实就是AppClassLoader),并将该类加载器传递到下面的ServiceLoader类的构造方法loader成员变量中:

  1. private ServiceLoader(Class<S> svc, ClassLoader cl) {
  2. service = Objects.requireNonNull(svc, "Service interface cannot be null");
  3. loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
  4. acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
  5. reload();
  6. }

上述loader变量什么时候使用的?看一下下面的代码:

  1. public S next() {
  2. if (acc == null) {
  3. return nextService();
  4. } else {
  5. PrivilegedAction<S> action = new PrivilegedAction<S>() {
  6. public S run() { return nextService(); }
  7. };
  8. return AccessController.doPrivileged(action, acc);
  9. }
  10. }
  11. private S nextService() {
  12. if (!hasNextService())
  13. throw new NoSuchElementException();
  14. String cn = nextName;
  15. nextName = null;
  16. Class<?> c = null;
  17. try {
  18. // 使用loader类加载器加载
  19. // 至于cn怎么来的,可以参照next()方法
  20. c = Class.forName(cn, false, loader);
  21. } catch (ClassNotFoundException x) {
  22. fail(service,
  23. "Provider " + cn + " not found");
  24. }
  25. if (!service.isAssignableFrom(c)) {
  26. fail(service,
  27. "Provider " + cn + " not a subtype");
  28. }
  29. try {
  30. S p = service.cast(c.newInstance());
  31. providers.put(cn, p);
  32. return p;
  33. } catch (Throwable x) {
  34. fail(service,
  35. "Provider " + cn + " could not be instantiated",
  36. x);
  37. }
  38. throw new Error(); // This cannot happen
  39. }

到目前为止:ContextClassLoader的作用都是为了破坏Java类加载委托机制,JDBC规范定义了一个JDBC接口,然后使用SPI机制提供的一个叫做ServiceLoader的Java核心API(rt.jar里面提供)用来扫描服务实现类,服务实现者提供的jar,比如MySQL驱动则是放到我们的classpath下面.从上文知道默认线程上下文类加载器就是AppClassLoader,所以例子里面没有显示在调用ServiceLoader前设置线程上下文类加载器为AppClassLoader,ServiceLoader内部则获取当前线程上下文类加载器(这里为AppClassLoader)来加载服务实现者的类,这里加载了classpath下的MySQL的驱动实现.

可以尝试在调用ServiceLoader的load方法前设置线程上下文类加载器为ExtClassLoader,代码如下:

  1. Thread.currentThread().setContextClassLoader(ContextClassLoaderTest.class.getClassLoader().getParent());

然后运行本例子,设置后ServiceLoader内部则获取当前线程上下文类加载器为ExtClassLoader,然后会尝试使用ExtClassLoader去查找JDBC驱动实现,而ExtClassLoader扫描类的路径为:JAVA_HOME/jre/lib/ext/,而这下面没有驱动实现的Jar,所以不会查找到驱动.

总结下,当父类加载器需要加载子类加载器中的资源时,可以通过设置和获取线程上下文类加载器来实现.

发表评论

表情:
评论列表 (有 0 条评论,484人围观)

还没有评论,来说两句吧...

相关阅读

    相关 ——

    虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作

    相关

    类的加载 什么是类的加载? 当程序需要使用某个类的时候,如果该类还未被加载到内存中,则系统会通过加载,连接,初始化三步来实现对这个类的初始化。 加载