【JVM】线程上下文类加载器详解

引子

有一个问题值得思考,双亲委派模型真的是适用与一切场景么?它有没有弊端呢?
在我们使用JDBC的时候,JDBC有些接口在java核心库中,由启动类加载器去加载,但是接口的实现类却是不同厂商所提供的,例如MySQl和Oracle,这两个jar包被下载下来直接就放入Classpath中使用,这会导致启动类加载器根本找不到实现类jar包,自然也就无法加载了,这就体现出线程上下文类加载器的作用了

线程上下文类加载器(Context Classloader)

线程上下文类加载器是从JDK 1.2开始引入的,类Thread中的getContextClassLooder()与 setContextClaseLoader(ClasLoader c1)
分别用来获取和设置上下文类加载器。如果没有通过setContextClassLoader(Classtoader cl )进行设置的话.线程将继承其父线程的上下文类加载器。

Java应用运行时的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过该类加载器来加载类与资源。
这一点在Launcher的构造方法可以体现

public Launcher() {  
          
        ClassLoader extcl;  
        try {  
            extcl = ExtClassLoader.getExtClassLoader();  //得到扩展类加载器
        } catch (IOException e) {  
            throw new InternalError(  
                "Could not create extension class loader");  
        }  
  
        try {  
            loader = AppClassLoader.getAppClassLoader(extcl); //将扩展类加载器作为参数(父)得到默认系统类加载器 
        } catch (IOException e) {  
            throw new InternalError(  
                "Could not create application class loader");  
        }  
  
        //  将得到的默认AppClassLoader设置进线程上下文类加载器
        Thread.currentThread().setContextClassLoader(loader);  
        //...  
    } 

线程上下文类加载器的重要性

SPI

这里引用一段关于SPI的介绍:

SPI (Service ProviderInterface),主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java
SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

父ClassLoader可以使用当前线程Thread.currentThread().getContextClassLoader( )所指定的classloader加载的类。这就改变了父ClassLoader不能使用子ClassLoader或是其他没有直接父子关系的ClassLoader加载的类的情况,即改变了双亲委托模型。

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

实例分析

import java.sql.Driver;
import java.util.Iterator;
import java.util.ServiceLoader;

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

输出结果:

driver:class com.mysql.cj.jdbc.Driver, loader:sun.misc.Launcher$AppClassLoader@73d16e93
线程上下文类加载器:sun.misc.Launcher$AppClassLoader@73d16e93
ServiceLoader的类加载器:null

但看输出结果就已经产生了疑惑,ServiceLoader是java.util包里的即是java核心库里的,故其被启动类加载器来加载(输出结果null已经再次证实了),调用load方法给的参数只是一个接口,而输出的driver是在我导入的jar包里的类,该jar包位于Classpath,并不在启动类的查找路径范围,那启动类加载器又是如何找到他并加载的?接下来通过分析来解释。

ServiceLoader源码解析

通过阅读ServiceLoader源码的Doc文档我们可以得到以下信息:
1.ServiceLoader是一个简单地服务提供者(实现类,例如mysql的jar包)的加载设施
2.服务提供者会去实现服务接口,并提供自己的代码逻辑,且实现类中必须有一个无参构造方法用于之后的实例化
3.服务提供者将提供的配置文件放入"META-INF/services/"下,且文件名必须为服务类型的完全限定名,文件里面每一行就是一个服务提供者的相应的类名
对第三点进行一下详细说明:

private static final String PREFIX = "META-INF/services/";  
//源码中已将目录名限定死了,这是合理的,规范,ServiceLoader就会去查找META-INF/services/目录下的文件,在通过类型对应名去加载对应的提供者配置文件

这是下载的mysql的jar包,里面就有一个该目录

在这里插入图片描述

文件名与类型名相同,文件里的内容就是真正的服务提供者类:

在这里插入图片描述
与上面被加载的driver输出结果driver:class com.mysql.cj.jdbc.Driver,完全一致,故其实ServiceLoader.load加载不是接口,而是实现类。

4.ServiceLoader中有一个缓存列表,缓存了已经被加载过的服务提供者

//缓存服务提供者,按服务提供者实例化顺序  
    private LinkedHashMap<String, S> providers = new LinkedHashMap<String, S>(); 

以下是load部分源码详解:

public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
    return new ServiceLoader<>(service, loader);//实例化执行构造
}

public static <S> ServiceLoader<S> load(Class<S> service) {
	//这里其实已经看到取出并使用线程上下文加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

这里设置线程上线文类加载器就体现出来了。首先,我定义的Test类位于Classpath下会由AppClassloader去加载,Test类里的main方法里的ServiceLoader.load调用了ServiceLoader的静态方法,则视为对其主动使用,就会导致该类的初始化,则其必然会先被加载。所以AppClassloader会去尝试加载ServiceLoader,由于双亲委派机制,最终ServiceLoader由启动类加载器去加载,但是启动类加载器根本无法加载到Classpath下的jar包,而且他没有父加载器,也不认识下面的其他加载器(其实站在启动类加载器的角度看,在双亲委派机制中他作为所有加载器的最高层,也是最早诞生的,其他加载器对他来说就是未来的东西,他根本无法认识其他加载器)怎么办呢?

只能通过另一种手段来强行打破双亲委派机制,让他去认识并委托其他加载器由其他加载器去完成加载。那又在哪里去设置一个时时刻刻都可以提供服务的加载器来满足他人使用呢?这时就巧用了线程,因为程序运行时是在线程里运行的,只要当前线程里保存了这么一个加载器,那就可以很容易的给其他人取出使用,这就诞生了线程上下文类加载器。

而且给了规定,要是当前线程没有设置线程上下文加载器,那就继承他父线程的上下文加载器,一路往上最初始的那个线程一定是执行了Luancher类的构造方法将默认的系统类加载器设置为线程上线文加载器(上面Luancher的源码已经体现了),这样就更加安全了.
所以ServiceLoader.load方法通过线程上下文加载器将默认设置的AppClassloader取出,用他去加载。

private static final String PREFIX = "META-INF/services/";  
   
    private Class<S> service;  
    //加载服务服务提供者的ClassLoader  
    private ClassLoader loader;  
    //缓存服务提供者,按服务提供者实例化顺序  
    private LinkedHashMap<String, S> providers = new LinkedHashMap<String, S>();  
    private LazyIterator lookupIterator;  
  
private ServiceLoader(Class<S> svc, ClassLoader cl) {  //双参构造
        service = svc;  
        loader = cl;  
        reload();  
    }  
  
    public void reload() {  
        providers.clear();  //清空缓存列表
        lookupIterator = new LazyIterator(service, loader);  
    }  
    
	// LazyIterator 是一个私有的内部类,延迟查找提供者
    private class LazyIterator implements Iterator<S> {  
        Class<S> service;  
        ClassLoader loader;  
        Enumeration<URL> configs = null;//文件对应路径URL  
        Iterator<String> pending = null;//文件内容,一行一行存放  
        String nextName = null;//当前调用hashNext方法时,就得到下一个文件内容。  
  
        private LazyIterator(Class<S> service, ClassLoader loader) {  //构造
            this.service = service;  
            this.loader = loader;  
        }  
  
        public boolean hasNext() {  //取出并解析资源,接下来用
            if (nextName != null) {  
                return true;  
            }  
            if (configs == null) {  
                try {  
                    String fullName = PREFIX + service.getName();  //目录名加包名
                    if (loader == null)  //如果loader为null则直接用getSystemResources得到完整的文件资源
                        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();  //调用底下的next方法
            return true;  
        }  
        
		 public S next() {  // 将解析好的资源进行加载和实例化,下来会用
            if (!hasNext()) {  
                throw new NoSuchElementException();  
            }  
            String cn = nextName;  //这就是文件每一行对应的一个类
            nextName = null;  
            try {  
                //反射Class.forName上篇博文做了介绍,这里就是用loader去加载cn表示的类,并且初始化该类,最后在通过newInstance()得到该类实例化对象
                S p = service.cast(Class.forName(cn, true, loader)  
                        .newInstance());  
                //缓存已经实例化的服务提供者  
                providers.put(cn, p);  //将类名为键,该类的实例化对象为值保存到缓存列表中
                return p;  
            } catch (ClassNotFoundException x) {  
                fail(service, "Provider " + cn + " not found");  
            } catch (Throwable x) {  
                fail(service, "Provider " + cn + " could not be instantiated: "  
                        + x, x);  
            }  
            throw new Error(); // This cannot happen  
        }  
  
        public void remove() {  
            throw new UnsupportedOperationException();  
        }  
  
    }  
      

至此Test代码ServiceLoader loader = ServiceLoader.load(Driver.class);这一行已经解释完了,下来是Iterator iterator = loader.iterator();生成一个迭代器

   
    public Iterator<S> iterator() {  
        return new Iterator<S>() {  
            //迭代被缓存的服务提供者
            Iterator<Map.Entry<String, S>> knownProviders = providers  
                    .entrySet().iterator();  
  
            //先去缓存服务提供者里面找  ,找到了说明加载过了,没找到就去用上面lookupIterator去加载
            public boolean hasNext() {  
                if (knownProviders.hasNext())  
                    return true;  
                return lookupIterator.hasNext();  
            }  
  
            public S next() {  
                if (knownProviders.hasNext())  
                    return knownProviders.next().getValue();  
                return lookupIterator.next();  
            }  
  
            public void remove() {  
                throw new UnsupportedOperationException();  
            }  
  
        };  
    }  

然后iterator.hasNext()就是上面内部类里的方法,去取出提供者类对应的文件,并解析文件资源
最后Driver driver = iterator.next(); 也是上面源码的方法,调用该方法将解析好的类进行加载,初始化,实例化,再将实例化的对象返回,并以类名称为键,实例化的结果为值存入缓存列表中去,方便之后使用。

至此我们Test类里的所有代码都解释完毕了,线程上线文加载器的由来也做了说明
接下来对遇到的问题做相关讨论;

1.为什么说线程上线文类加载打破了双亲委派机制?双亲委派本来就是父加载不了的让子去加载么?那不就没有打破么?
答:这个问题其实是我再开始学类加载器的时候看到的别人的问题,我当时还想了想觉得这个很有道理,现在系统的学完了类加载器后就发现这句话本质的问题
在双亲委派中,当一个类要被加载时会先向上抛,最终由根加载器开始自上而下的尝试加载,这个在上篇文章的load方法源码中深有体现,他是由一种循环的方式去尝试加载,然后在一个个往回返,也就是说父加载不了他就不管了,循环继续向下进行。而不是字面理解的父加载不了了让子去加载,父根本就没有这个权利,从他的角度来说子就是未来,他根本不知道未来会有多少个子,他的类里也肯定不可能保存有子的对象。
而子不同,从子的角度来看,他保存有他父的对象所以他认识他的父,他可以去访问父的命名空间(这一点上篇已经证实了),因此可以将任务抛给其父。
所以说通过线程上线文加载器,可以让父委托他的子,也就是上面的例子:根加载器最为最高层,他不认识任何人,他无法去加载其他路径下的文件,那怎么办呢?于是他就说:要想让我加载我能力范围之外的,请先设置好一个能加载该类的加载器,然后由他来加载。

2.为什么这么麻烦设置线程上线文加载器?反正都是让默认的系统类加载器来加载,直接用getSystemClassloader不是更好?
答:这样做就会丧失灵活性,上面的例子我们并没有set新的加载器,所以就用了从父线程所继承的默认的线程上下文类加载(Appclassloader),而且要加载的类刚好在Classpath下,所以能够成功,那要是要加载的类不在Classpath下,但又没有设置线程上线文加载器仍然用默认的Appclassloader去加载,很明显会加载失败。所以通过线程上下文加载器就可以灵活的用任意的加载器去加载类,只要在使用前将其set进去就行。这样也使得其适用场景变得更加丰富。

以上便是我对线程上下文类加载器的理解,其中有部分内容掺杂个人理解,如果有问题欢迎指出,相互交流共勉互进!

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值