深入理解JVM 线程上下文加载器及JDBC例子
在这之前,我们先讨论一下,类的加载为什么要遵循双亲委派模型呢,当然因素是多方面的,最主要的考虑应该是为了安全。java的基础类库和一些规范都是由根加载器加载的,双亲委派模型能很大程度上杜绝了自定义的一些类覆盖基础类库及基础规范的行为。
举个例子:如果在工程内自定义java.lang.String 或者 java.lang.Object,而类加载器覆盖掉基础类库的话,会对运行的基础环境造成破坏。
那为什么要打破双亲委派呢,拿JDBC为例子,rt.jar内包含很多只有作为规范的接口,而具体实现需要由厂商来提供的SPI 。SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。
下面我们分析一下jdbc使用时,调用的全过程,在过程中体会线程上下文加载器在其中的使用。
老规矩,show the code
看这段源码很简单,但是包含了前面两章的内容,看源码时要顺着一个主线去看,除了跟踪逻辑之外,还要考虑类是什么时候初始化的,初始化做了什么(这是先决条件),然后才是具体的逻辑做了什么。
源码1 :代码一共两句,我们先看第一句,跟进去看看:
直接加载Driver调用了本地方法,是否初始化选择了true,使用了调用者的classLoader,将调用者也一并传入。
其中@CallSensitive注解的作用是为了堵住漏洞用的。曾经有黑客通过构造双重反射来提升权限,原理是当时反射只检查固定深度的调用者的类,看它有没有特权,例如固定看两层的调用者(getCallerClass(2))。如果我的类本来没足够权限群访问某些信息,那我就可以通过双重反射去达到目的:反射相关的类是有很高权限的,而在 我->反射1->反射2这样的调用链上,反射2检查权限时看到的是反射1的类,这就被欺骗了,导致安全漏洞。使用CallerSensitive后,getCallerClass不再用固定深度去寻找actual caller(“我”),而是把所有跟反射相关的接口方法都标注上CallerSensitive,搜索时凡看到该注解都直接跳过,这样就有效解决了前面举例的问题
第一句就是加载了Driver的实现类。
我们继续看第二句干了啥。
源码2-1:对DriverManager初始化,初始化过程中,将Drivers加载并初始化。
DriverManager.getConnection使用了DriverManager静态方法,所以需要先对DriverManager进行初始化,所以先寻找初始化的代码:
这是存放注册了的Drivers信息的列表,我们知道会有个注册的过程,关于copyOnWrite不是本文重点,先忽略:
源码2-1-1:
注册列表源码2-1-2:
初始化Drivers重点看loadInitialDrivers()方法干了啥,这段代码比较长,符合构造方法尽量少,初始化动作放init内的原则,我们分段看:
第一段:看是否设置了环境变量,从环境变量里取一下试试。
源码2-1-3:
先检查有没有环境变量第二段:这段代码有一段很详细的注释,大体意思是说,为了把Driver实例化,把所有能找到的Driver都加载进来。有可能在配置文件里有,但是实际的文件缺失,所以下面弄了个异常机制。
这都不重要,重要的是load这个动作。
源码2-1-4:
调用load并调用迭代器内的初始化第三段:这段是为第一段服务的,把第一段内拿到的Drivers加载并初始化。
源码2-1-5:
环境变量内的Driver初始化咱们重点看第二段的(在源码2-1-4之内)
源码2-1-6:
ServiceLoader.load从这两段代码再展开:
第一句使用了ServiceLoader的静态方法load,所以要对ServiceLoader初始化:
源码2-1-7:
关键参数及变量先看一下变量,发现三个重要的变量,查找路径、提供者map、迭代器
源码2-1-8:
使用线程上下文加载器load方法内,获取了线程上下文加载器,并使用线程上下文加载器加载Driver。
源码2-1-9:
调用构造函数执行了构造方法:
源码2-1-10:
变量检查及赋值构造器内做了一些校验动作,并给变量赋值,然后调用reload。reload清理了提供者map,然后初始化迭代器。迭代器内方法内第一步有效动作,是遍历配置文件内的drivers。
源码2-1-11:
获取各个Driver的Binaryname这个是driver配置文件的路径
源码2-1-12:
配置文件这是其中的内容,可以是多行
源码2-1-13:
配置文件内容这一步其实是提供了Binaryname
第二步就是加载和初始化了:
源码2-1-14:
加载并实例化加载、实例化。
到此为止DriverManager初始化结束。
源码2-2:c.newInstance()触发的com.mysql.cj.jdbc.Driver初始化
初始化:
注册到Driver列表将实例注册到这个里面去了。
Driver注册列表我们可以确定,调用的实际实现,都是通过具体的实现类来做的。
好了,必要的初始化过程结束了,开始调用具体的方法:
源码2-3-1:调用getConnection方法
传入了必要的连接参数,和一个调用类
源码2-3-2:
如果调用类加载器为null,使用线程上下文加载器参数校验阶段,其中判断了调用类的类加载器是否是null,如果是null,那就使用线程上线文加载器。
源码2-3-3:
检查是否合法参数校验完成后,遍历注册list,循环中判断可否使用这个Driver
源码2-3-4:
判断命名空间是否一致判断的逻辑内,用调用类或者是线程上线文的类加载器又加载了一遍这个类,判断list内的Driver和这个新加载的类是不是同一个。我们回忆一下,list内的Driver使用什么类加载器加载的
源码2-3-5:
使用的线程上线文加载器,那这个校验的意义何在呢,意义在于,要求调用类和加载的Driver在一个命名空间内,补偿双亲委派机制打破造成的风险。
回过头来,我们看源码1,
冗余代码,可能是版本兼容性考虑没有这一句,代码不会任何影响。
总结:
到此为止,我们以JDBC为例子,过了一遍Driver加载使用的源码。我们注意到,实际上jdk在对调用SPI服务时的关键代码是ServiceLoader类的迭代器实例化。
首先是制定规范,规定扫描"META-INF/services/"下的配置文件内容。
然后根据serviceName加载相应的配置文件。然后根据配置文件内的Binaryname使用线程上下文加载器加载并初始化。初始化过程中,具体的实现会将自己的引用,放入对应的Manager的集合容器内。
最后调用者遍历集合容器,调用合适的实现。
这个过程中,Manager是由根加载器加载的,那么他想加载具体的实现类,默认是用根加载器加载的,根加载器加载不到classpath内的各厂商的具体实现类,jdk的做法是用线程上下文加载器加载,并要求调用者和加载到的类对象在一个命名空间内,保证调用的正确执行。
类加载相关的内容暂时告一段落,后面还会详细分析tomcat的类加载机制,以及一些开发中具体的问题,比如说空间隔离、热部署、jsp、jar hell问题等等,在分析tomcat源码的时候,再另开一篇详细分析。