$1.6、线程上下文类加载器揭秘及作用

在前面5篇文章介绍了类加载器和对应的双亲委托模式后,我们还需要了解“破坏”双亲模式的特例,而这种特例在很多SPI(服务提供接口)架构中有着广泛的应用,甚至说 ,如果不破坏双亲模式,Java的很多功能都是无法实现的。

以jdbc的模式进行举例:java对jdbc的规范中,只是定义了Driver(驱动)、Connection(连接)等接口,具体的实现是由各自的数据库厂商决定的,比如mysql厂商的驱动是com.jdbc.mysql.driver,oracel的驱动为com.jdbc.oracle.driver,显然不一样。在jvm启动是,因为java.util.sql包的加载是由根类加载器进行加载的,但实际使用的具体实现对应的加载不可能是根类加载器(具体实现的类是位于classpath中,肯定不在rt.jar中,因此根类加载器加载不了),是由系统类加载器加载的,那么问题就出现了:

按照双亲委托模式的原则,父类加载器是看不到子类加载器所加载的类,换言之,在实际应用中,真正的数据库却动或者连接就找不到真正的实现类了。这就是SPI的通病,因此引入了线程上下文类加载器(ThreadContent ClassLoader)

线程上下文加载器的作用:在当前线程中,可以使用设定的加载器进行加载;默认的线程上下文加载器是系统类加载器,在虚拟机启动时,机器码指定会进行加载根类加载器,继而在根类加载器里面创建对应的应用类加载器和扩展类加载器;这个类就是Launcher类

这块指定了根类加载器的加载路径,我们查看Launcher的构造方法

public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
                //见名知意,获取扩展类加载器
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            //获取应用类加载器
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
        //这就是关键处:将获取到的应用类加载器加载放到线程上下文中,包括了默认和自定义的
        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
        if (var2 != null) {
            SecurityManager var3 = null;
            if (!"".equals(var2) && !"default".equals(var2)) {
                try {
                    var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                } catch (IllegalAccessException var5) {
                } catch (InstantiationException var6) {
                } catch (ClassNotFoundException var7) {
                } catch (ClassCastException var8) {
                }
            } else {
                var3 = new SecurityManager();
            }

            if (var3 == null) {
                throw new InternalError("Could not create SecurityManager: " + var2);
            }

            System.setSecurityManager(var3);
        }

    }

细节处:根类加载器和应用加载器的加载路径,我们都从Launcher中得知:

在产生应用类加载器的代码中,Luncher调用了ClassLoader中privilegedAction中的run方法,我们查询run方法,里面的秘密就出来了:

run方法里面揭秘

因此从上述总结出,默认的或者自定义的系统类加载器都会放到线程上下文中。

下面重点详解:线程上下文加载器:

(双亲模式是使用加载调用该类的加载器进行加载:A是使用了B,那么加载A的加载器也会尝试去加载B,以B为第一层子类)

一般的使用流程是:取出->使用->放回

取出:

ClassLader cl= Thread.currentThread().getContentClassLoader();

try{

Thread.currentThread().setContentClassLoader(自己指定的加载器);

//method()方法

}finally{

//设定的用完之后,将原线程的加载器放回

Thread.currentThread().setContentClassLoader(cl);

}

要了解上下文加载器,首先需要引入一个核心类:ServiceLoader我们以mysql实现JDBC的标准为例

先看示例代码:

public static void main(String[] args) throws Exception {

        //java.sql.Driver jdbc标准对应的二进制名称
        Class<Driver> driverClazz = (Class<Driver>) Class.forName("java.sql.Driver");
        //查看具体被加载出来的实现类
        Iterator<Driver> iterator = ServiceLoader.load(driverClazz).iterator();
        while (iterator.hasNext()) {
            Driver next = iterator.next();
            System.out.println("加载到的具体实现驱动" + next);
        }
        System.out.println("加载mysql驱动的加载器:" + driverClazz.getClassLoader());

        System.out.println("当前线程的上下文类加载器:" +     Thread.currentThread().getContextClassLoader());

        //查看Launcher类的加载器
        System.out.println("加载jdbc标准规范的加载器:" + Launcher.class.getClassLoader());
    }

得出的结果为:

从结果我们可知:1、加载标准规范的驱动是由根类加载器;2、从classpath中的路径中加载出来了具体的实现驱动(包含了msql,alibaba的)3、当前线程的上下文加载器是应用类加载器。这三点就完全验证了我们前面所讲述的内容。

最后还有一点,这些具体实现厂商的驱动是如何被发现和加载的呢?查看ServiceLoader的文档:

是不是豁然开朗,一切的一切的都已经在doc文档中说明(看资料还是看源码,看官方注解文档最地道),原来是需要一个配置文件,名称和对应的目录位置都已明确,按照标准服务的二进制名称进行命名,具体的实现按照每行给出,我们找到jar包中的路径:

这样就解释了我们可以找到具体实现的根本原因了,下面为什么这些是由应用类加载器加载的,继续跟代码

在刚刚调用的load方法的实现里,第一行就是我们今天的主角:线程上下文加载器,然后传递下去进行加载动作

最后就顺其自然了,在加载的过程中,我拿到可以加载classpath路径中的加载器后,那么相对应的实现类也就加载成功了,从标准到具体实现,全部加载成功,而且根据配置文件里面内容的二进制名称,顺利找到对应的类,一切和谐,完美~

发布了12 篇原创文章 · 获赞 1 · 访问量 368

猜你喜欢

转载自blog.csdn.net/weixin_39800596/article/details/103592330