类加载机制(七):线程上下文类加载器


title: 类加载机制(七):线程上下文类加载器
date: 2019-03-21 20:14:06
categories:

  • Java虚拟机
    tags:
  • 类加载机制
  • 线程上下文类加载器

双亲委托机制的破坏

我们知道,class文件的加载是按照双亲委托机制完成的,这个机制解决了各个类加载器的基础类的统一问题,因为上层类加载器加载的类对下层加载的类是可见的,所以这些基础类可以被Java程序所调用,但是如果这些基础类需要调用用户所写的类呢,可下层类加载器加载的类不是对上层类加载器加载的类是透明的吗?这种情况不是不可能的,比如JDBC,它位于rt.jar包下,是由启动类加载器加载,而它却需要classPath下的接口提供者(Service Provider Interface)的代码,但是启动类不可能去加载这些代码,那怎么办呢?

SPI(Service Provider Interface)
只提供了接口的声明,具体实现由厂商完成。某些SPI需要调用由厂商实现并部署在classPath下的接口实现代码。这些接口由启动类加载器去加载,但启动类加载器不认识classPath下的代码。

这时候就需要对双亲委托机制进行“破坏”了,Java设计者设计了一种叫做“线程上下文类加载器”的机制,当这些接口需要实现的代码时,就去使用这个线程上下文类加载器完成这些接口实现代码的加载。

线程上下文类加载器

线程上下文类加载器contextClassLoader,其实,我们在分析Launcher类的源码时,已经遇到过了:

Thread.currentThread().setContextClassLoader(this.loader);

Launcher类的构造方法中,执行了这条语句,并将系统类加载器传进去,我们跟着去看看Thread的源码。

  private ClassLoader contextClassLoader;

内部维护了一个类加载器,也就是线程上下文类加载器。

 if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;

Threadinit方法中,线程继承其父线程的上下文类加载器。

public void setContextClassLoader(ClassLoader cl) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("setContextClassLoader"));
        }
        contextClassLoader = cl;
    }

Thread中,可以设置线程类加载器,也就是说,若如果没有通过setContextClassLoader进行设置的话,线程将继承其父线程的上下文类加载器。前面说过,在Launcher类中设置了线程上下文类加载为系统类加载器,即在Java程序中未设置线程上下文类加载器的话,线程上下文类加载器就为系统类加载器。

深入理解线程上下文类加载器

线程上下文类加载器的一般使用模式

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try{
	Thread.currentThread().setContextClassLoader(targetccl);
	doMethod();
}finally{
	Thread.currentThread().setContextClassLoader(classLoader);
}

线程上下文类加载器的一般使用模式(获取-使用-还原)

doMethod()里面调用了Thread.currentThread().setContextClassLoader()来获取当前线程的上下文类加载器做某些事情。

ServiceLoader类

在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI来说。有些接口是Java核心库所提供的,而Java核心库是由启动类加载器来加载的,而这些接口的实现却是来自于不同的jar包(由各独立厂商实现),Java的启动类加载器是不会加载其他来源的jar包,这样就导致一些接口由启动类加载加载,实现由其他加载器加载,传统的双亲委托机制就会无法满足SPI的要求。而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载 。就比如JDBC驱动,如下图所示。

类加载机制(七):线程上下文类加载器_第1张图片

首先,在pom文件中添加了关于MySql的依赖,我们通过代码找到项目中的驱动。

public class MyTest17 {
    public static void main(String[] args){
        //通过ServiceLoader去加载驱动
        ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);
        Iterator<Driver> iterator = drivers.iterator();
        //打印出每个驱动以及驱动的类加载器
        while (iterator.hasNext()){
            Driver next = iterator.next();
            System.out.println("driver:" + next.getClass());
            System.out.println("loader:" + next.getClass().getClassLoader());
            System.out.println("-------------");
        }
        //打印出当前线程上下文类加载器
        System.out.println("当前线程上下文类加载器:" + 				Thread.currentThread().getContextClassLoader());
        //打印出ServiceLoader的类加载器
        System.out.println("ServiceLoader的类加载器:" + ServiceLoader.class.getClassLoader());
    }
}

输出结果:

driver:class com.mysql.jdbc.Driver
loader:sun.misc.Launcher$AppClassLoader@18b4aac2
-------------
driver:class com.mysql.fabric.jdbc.FabricMySQLDriver
loader:sun.misc.Launcher$AppClassLoader@18b4aac2
-------------
当前线程上下文类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
ServiceLoader的类加载器:null

从输出结果,我们可以看到,MySql的两个驱动都是由系统类加载器所加载的,按常理来说,这些驱动位于classPath下,也该由系统类记载器去加载;前面也说了,当前线程上下文类加载器默认是被设置为系统类加载器;而ServiceLoader位于java.util下,属于核心类库,是由启动类加载器加载的。

ServceLoader是对加载驱动很重要的一个类,那它是如何找到这些驱动的呢?

详解ServiceLoader

ServiceLoader是从JDK1.6才开始出现的,它的作用就是定位加载这些服务的具体实现。从JavaDoc中,我们可以看出ServiceLoader是从服务实现的jar包中的资源目录META-INF/services下去读取驱动名的,并且要求包含这些驱动的文件名必须是服务类型的完全限定的二进制名字,且文件中包含的也是服务实现类的完全限定二进制名,如下图所示。

类加载机制(七):线程上下文类加载器_第2张图片

META-INF/services下的java.sql.Driver文件里的内容。

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
ServiceLoader源码
NO.1

首先,看看ServiceLoader类内部维护的重要字段。

 private static final String PREFIX = "META-INF/services/";
 private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

直接将资源目录META-INF/services/写死在类中,因为这个资源目录是定好了的,写死也无所谓的。

第二个字段是用做缓存的,ServiceLoader维护到目前为止加载的服务SPI的缓存

NO.2

然后,从load方法说起。

 public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

将服务类型与当前系统类加载器传给另一个load方法。

这里为什么要获取线程上下文类加载器呢?分析一下:ServiceLoader是在普通的Java程序中被引用到的,我们知道,在一个类X中引用另一个类Y,是使用X的类加载器去尝试加载类Y

  • 系统类加载器会去尝试加载ServiceLoader

  • 最终启动类加载器将其加载

  • ServiceLoader中引用到的类,会由启动类加载器去加载

  • 但启动类加载器是无法加载到classPath下的类的,所以就通过线程上下文类加载器去加载

NO.3

接着,调用私有的构造方法。

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        //要是传进来的线程类加载器为空,就将cl设置为系统类加载器
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        //调用reload
        reload();
    }

 public void reload() {
     	//将上文提到的缓存清空
        providers.clear();
     	//使用一个内部类进行懒加载
        lookupIterator = new LazyIterator(service, loader);
    }

ServiceLoader内部加载这些服务实现类,是通过按需加载的方式。

NO.4

只贴出这个内部类LazyIterator的主要代码。

private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

        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
        }

在这个内部类内部最重要的两个方法,hasNextService判断是否还有下一个服务实现类,nextService去加载服务实现类。

改变线程类加载器
public class MyTest17 {
    public static void main(String[] args){   Thread.currentThread().setContextClassLoader(MyTest17.class.getClassLoader().getParent());
        ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);
        Iterator<Driver> iterator = drivers.iterator();
        while (iterator.hasNext()){
            Driver next = iterator.next();
            System.out.println("driver:" + next.getClass());
            System.out.println("loader:" + next.getClass().getClassLoader());
            System.out.println("-------------");
        }
        System.out.println("当前线程上下文类加载器:" + Thread.currentThread().getContextClassLoader());
        System.out.println("ServiceLoader的类加载器:" + ServiceLoader.class.getClassLoader());
    }
}

输出结果:

当前线程上下文类加载器:sun.misc.Launcher$ExtClassLoader@1540e19d
ServiceLoader的类加载器:null

可以看到,我们将线程类加载器设置为扩展类加载器后,确实是无法将classPath下的服务实现类加载成功的。

我们将线程类加载器设置为我们的自定义类加载器。

Thread.currentThread().setContextClassLoader(new MyClassLoader("loader"));

输出结果:

driver:class com.mysql.jdbc.Driver
loader:sun.misc.Launcher$AppClassLoader@18b4aac2
-------------
driver:class com.mysql.fabric.jdbc.FabricMySQLDriver
loader:sun.misc.Launcher$AppClassLoader@18b4aac2
-------------
当前线程上下文类加载器:classLoader.MyClassLoader@6d6f6e28
ServiceLoader的类加载器:null

你可能感兴趣的:(Java虚拟机)