关于破坏双亲委派机制

说明:
最近在重读《深入理解Java虚拟机》,看到破坏双亲委派这一块内容时,通过对JDBC驱动加载过程源码debug,突然茅塞顿开,收获不少,以前仅仅只是知道概念,特此记录一下

也看了一些其他博主的文章,虽然最后还是搞明白了,但是我觉得应该能更好的引入进去,而不是直接怼JDBC连接。


以下这段出自:《深入理解Java虚拟机》第三版 7.4.3节 破坏双亲委派模型

这并非是不可能出现的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务, 它的代码由启动类加载器来完成加载(在JDK 1.3时加入到rt.jar的),肯定属于Java中很基础的类型 了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程 序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码,现在问题来了,启 动类加载器是绝不可能认识、加载这些代码的,那该怎么办?

为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器 (Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方 法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内 都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

有了线程上下文类加载器,程序就可以做一些“舞弊”的事情了。JNDI服务使用这个线程上下文类 加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行 为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性 原则,但也是无可奈何的事情。Java中涉及SPI的加载基本上都采用这种方式来完成,例如JNDI、 JDBC、JCE、JAXB和JBI等。不过,当SPI的服务提供者多于一个的时候,代码就只能根据具体提供 者的类型来硬编码判断,为了消除这种极不优雅的实现方式,在JDK 6时,JDK提供了 java.util.ServiceLoader类,以M ETA-INF/services中的配置信息,辅以责任链模式,这才算是给SPI的加 载提供了一种相对合理的解决方案。

诚然,看完后并不清楚具体是怎么操作的,恰好里面涉及了SPI,兴趣就来了,想一探究竟,就拿里面列举到的我们常见的JDBC来研究一下。

首先需要了解一下JVM类加载机制,他们3个实际上是相互作用,互相影响的。我在其它博文看到有人问为什么不能让Application Class Loader直接加载第三方的driver实现类,Bootstrap Class Loader加载JDK需要加载的DriverManager类,也完全符合双亲委派,这样不就不需要TCCL(ThreadContextClassLoader)了?

其实,他这个问题的产生就是对类加载机制不太清楚,类加载机制里有一条全盘负责,如果他明白这个,可能就不会产生那个问题了。这个点到即止,后面看源码讲。

一、JVM类加载机制

1.1 全盘负责

所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

1.2 双亲委派

所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。

1.3 缓存机制

缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

二、简单认识双亲委派

要破坏它,总得要先明白他是什么吧?

2.1 三层类加载器

绝大多数Java程序都会使用到以下几个系统提供的类加载器来进行加载。

2.1.1 启动类加载器(Bootstrap Class Loader)

这个类加载器负责加载存放在 <JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够 识别的(按照文件名识别,如rt .jar、t ools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类 库加载到虚拟机的内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时, 如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可,代码清单7-9展示的就是 j a v a . l a n g. C l a s s L o a d e r . ge t C l a s s L o a d e r ( ) 方 法 的 代 码 片 段 , 其 中 的 注 释 和 代 码 实 现 都 明 确 地 说 明 了 以 n u l l 值 来代表引导类加载器的约定规则。

2.1.2 扩展类加载器(Extension Class Loader)

这个类加载器是在类sun.misc.Launcher$ExtClassLoader 中以Java代码的形式实现的。它负责加载<JAVA_HOM E>\lib\ext目录中,或者被java.ext.dirs系统变量所 指定的路径中所有的类库。根据“扩展类加载器”这个名称,就可以推断出这是一种Java系统类库的扩 展机制,JDK的开发团队允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能,在JDK 9之后,这种扩展机制被模块化带来的天然的扩展能力所取代。由于扩展类加载器是由Java代码实现 的,开发者可以直接在程序中使用扩展类加载器来加载Class文件。

2.1.3 应用程序类加载器(Application Class Loader)

这个类加载器由sun.misc.Launcher$Ap p ClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSy stem- ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径 (ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有 自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

2.2 双亲委派模型的工作过程

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加 载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的 加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请 求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zr9dZGf6-1593256026153)(evernotecid://2EC93C29-EBC0-48C7-9B33-CAA251F1491C/appyinxiangcom/29806953/ENResource/p186)]

ok,基本的知识点已经铺垫完毕,关于破坏双亲委派前文开篇就讲了,知识点着重需要了解到的就是Bootstrap Class Loader、Application Class Loader这两个类加载器的负责范围,这关系到后文中为什么要引入TCCL(ThreadContextClassLoader)的原因。以及刚才提到的加载机制。

三、创建JDBC连接

3.1 逻辑的简单介绍

	public static void main(String[] args) throws SQLException {
		String url = "jdbc:mysql://localhost:3306/base-service";
		// 通过java库获取数据库连接
		Connection conn = java.sql.DriverManager.getConnection(url, "root", "123456");
	}

从Java1.6开始自带的jdbc4.0版本已支持SPI服务加载机制,只要mysql的jar包在类路径中,就可以注册mysql驱动。所以,可以不用Class.forName() 去实例化 Driver 的实现类了。

在我们看源码前,这里需要明白一点:java.sql.DriverManager这个类是JDK自带的,它所在的位置如下
在这里插入图片描述
需要补充一下Java 的SPI机制

SPI具体约定
Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader

src.zip这个包就在JAVA_HOME/lib/中,本文2.1.1 中就提到过,Bootstrap Class Loader负责加载的区域就是<JAVA_HOME>/lib目录。

所以说,java.sql.DriverManager这个类的加载应该是由Bootstrap Class Loader类加载器负责的。同时根据JVM类加载机制的全盘负责机制,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

java.sql.DriverManager初始化的过程中,Driver的接口是JDK的,而它的实现类却是由第三方厂商提供的,通过SPI机制实现加载,注入。这个类不在JAVA_HOME/lib/中,而是位于jar包的META-INF/services/目录里,所以说这块的类加载实际上Bootstrap Class Loader是无法进行加载的,他需要Application Class Loader来完成加载,但原则上这是不被允许的,因为破坏了双亲委派机制。这里java团队就引入了线程上下文类加载器 (Thread Context ClassLoader),由他来帮助Bootstrap Class Loader完成类加载。它加载完后,因为缓存机制的存在,其他的类加载器就不需要再去加载它了。

大概逻辑就是这样,下面debug源码看一下。

3.2 JDBC连接建立的源码分析

3.2.1 JDBC DriverManager 初始化

    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

进入到loadInitialDrivers();里面

private static void loadInitialDrivers() {
        ...
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
			//SPI机制,这个Driver.class是JDK定义好的接口
			//这是要初始化ServiceLoader这个SPI服务发现
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
                    while(driversIterator.hasNext()) {
                    //实例化第三方的接口实现,这个实质上就是在执行Class.forName(),进去一看便知
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
		...
    }

看一下ServiceLoader.load(Driver.class);干了什么,这里第一次出现了TCCL,获取到了当前线程的上下文类加载器->AppClassLoader,传入的接口为:java.sql.Driver
在这里插入图片描述
之后我们进入这里,看看这个代码块做了什么

					while(driversIterator.hasNext()) {
                    //
                        driversIterator.next();
                    }

driversIterator.hasNext()里可以清晰地看到,fullName拼出了:META-INF/services/java.sql.Driver,看到这个路径应该很敏感,因为SPI的约定要求:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件,底下的loader也是AppClassLoader
在这里插入图片描述
这里补充一下,这个loader就是在SericeLoader初始化时赋值的,获取的是系统类加载器也就是AppClassLoader

回顾一下:现在初始化了ServiceLoader,当前的TCCL的类加载器是AppClassLoader,获取到了第三方服务实现的位置:META-INF/services/java.sql.Driver。那么下一步肯定是要去使用AppClassLoader加载META-INF/services/java.sql.Driver了。

接着看先前代码块里的driversIterator.next();步骤,Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootrapLoader中,其次DriverName的路径不在<JAVA_HOME>/lib中,因此传给 forName 的 loader 必然不能是BootrapLoader,通过debug看到这里的loader依然是当前的TCCL–> AppClassLoaderClass.forName(DriverName, false, loader)这是JDK1.6之前使用JDBC手动创建连接时常用的手法,现在通过java的SPI机制,实现了自动化加载,我们可以看到这里的cn的值是:com.mysql.cj.jdbc.Driver,这个路径实际上是被写在META-INF/services/java.sql.Driver里的。在这之后,DriverManager就被加载完毕了,在之后就是建立连接了。
在这里插入图片描述
整个SPI都是在破坏双亲委派,父类加载器去请求子类加载器完成类加载的行为,这种行 为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则。不过,换来了更加灵活的编码,很值得的。

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

ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作。

引用其他优秀博文里的一句话总结(我觉得写得比较通俗些):

JDK提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载,但我的ClassLoader是没法加载的,那就把它加载到当前执行线程的TCCL里,后续你想怎么操作(驱动实现类的static代码块)就是你的事了。

其实,还是不好言语出来其中的逻辑,还是需要自己去看源码,一步一步看具体实现,明白之后就会豁然开朗的

参考部分

1.https://blog.csdn.net/yangcheng33/article/details/52631940
2.《深入理解Java虚拟机》第三版

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章