文章内容引用自 咕泡科技 咕泡出品,必属精品
首先我们要知道双亲委派机制是为了解决什么问题?
双亲委派机制的目的: 为了安全,保证核心类库的安全性,防止被篡改,以及保证类的唯一 怎么保证类的唯一呢? 默认的情况下,一个限定名的类只会被一个类加载器加载解析并使用,这样在程序中,它就是唯一的,不会产生歧义。
有关类加载器,可以参考我的这篇博客:
双亲委派
所谓的双亲委派,就是先让父亲加载器试图加载该Class,只有在父亲加载器无法加载该类时才尝试从自己的类路径中加载该类。 通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父亲加载器,依次递归,如果父亲加载器可以完成类加载任务,就成功返回; 只有父亲加载器无法完成此加载任务时,才自己去加载。
我们通过这张图来理解一下:
在被动的情况下,当一个类加载器收到加载请求,他不会首先自己去加载,而是传递给自己的父亲加载器。
这样所有的类都会首先传递到最上层的Bootstrap ClassLoader,只有父亲加载器无法完成加载,那么此时儿子加载器才会自己去尝试加载。
代码语言:javascript复制什么叫无法父亲加载器加载呢? 就是根据类的限定名,类加载器没有在自己负责的加载路径中找到该类。
这里注意,我没有用父类加载器、子类加载器这样的语句,而是使用了**父亲加载器**,因为上图中这些箭头并**不表示继承关系**,而是一种**逻辑关系**,
实际上是通过组合的方式来实现的,这也是很多博客上没有写清楚,容易误导人的一点。
通过源码来看一下双亲委派具体是怎么实现的
代码很简单,来看java.lang.Class Loader中的load class方法
首先呢检查该类是否已经被加载了,如果没有,则开启加载流程,如果有,那么就直接读取缓存。
缓存机制 缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。 这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
parent变量代表了当前class loader 的父亲加载器,这里就体现了不是通过继承,而是通过组合的方式实现类加载器之间的父子关系。
如果parent等于null,那这里要说一下,当parent等于null 代表parent 为bootstrap classloader 。我们开头也讲过,bootstrap classloader是由jvm内部实现的,没有办法被程序引用,所以这里约定为null。当parent为null,就调用findBootstrapClassOrNull这个方法,让BootstrapClassLoader尝试进行加载,如果parent不为null 就让parent根据类的全限定名尝试加载,并返回该类。如果返回的class为null 说明parent加载不了,使用当前的loader的findclass方法尝试加载这个全限定名的class,需要类加载器自己去实现。
比如Extension ClassLoader 和 Application ClassLoader都使用以下这段逻辑来实现findClass
这里呢可以看到,通过类的全限定名定位到class文件,然后通过ucp这个对象去查找,找到文件的资源后,再调用 defineClass去进行类加载的后续流程。defineClass方法是ClassLoader中一个被final修饰的方法,意味着获取到class文件的二进制流后,最终会由java.lang.classloader来进行操作,因为它是被final修饰的,意味着是不能够被外部重写的。这个符合了我们最开始所说的类的加载过程中除了读取二进制流憎操作外,剩余的逻辑都是由jvm内部实现的,双亲委派机制就是这么简单。
打破双亲委派机制
在大部分情况下,双亲委派机制是能够生效并且是能按预期执行的。
1第一次被破坏
在一些情况下,双亲委派机制是可以主动破坏的,细心的同学可以发现,我自己通过匿名内部类直接重写了java.lang.classloader的load class方法,而我们的双亲委派机制是存在于这个方法内的。那么我们这次重写就是对原有的双亲委派机制的逻辑破坏,所以也出现同一个限定名出现了两个不同classloader进行load的情况。
代码语言:javascript复制本图取自咕泡学院,如有侵权,联系速删
除非是有特殊的业务场景,一般来说不要主动去破坏双亲委派模型
那有的人可能会有疑问啦,既然jvm推荐并希望开发者遵循双亲尾派模型,那么为什么不把load class方法像defineClass设定成final来修饰?
那这样的话我这边就没有办法重写,也就代表着上层开发者需要尽可能的遵循双亲委派的逻辑了。 这是JVM无法解决的问题
java.lang.ClassLoader 的load class方法呢在java很早的版本就有了,而双亲委派模型是在jdk1.2引入的特性。
java是向下兼容的,也就是说引入双擎委派机制时呢,世界上已经存在了很多像我上面一样的代码,那么jvm只能向下兼容的,只能提出折中的解决方式。
这个解决的措施就是在Jdk1.2后,引入了findclass方法,推荐用户去重写该方法,而不是直接重写 Load class方法,这样呢就依然能够符合双亲委派模型。
这就是史上第一次的双亲委派模型被破坏了,像很多事情(*装)只有零次和N次,双亲委派模型第二次被破坏,是由于这个模型自身的缺陷导致,双亲委派能很好的解决了各个类加载器协作时基础类型的一致性问题,但是如果有基础类型要调用用户的代码,这又该怎么办呢?
第二次被破坏
比如说JDK想要提供操作数据库的功能,那么数据库有很多种,并且随着时间的推移将会出现各种品类的数据库,想要JDK需要针对不同的数据库和具体代码都一一实现,这不现实,jdk也不知道各种品类数据里具体的实现方式,那么比较合理的方式就是JDK提供一组规范和接口,各个不同数据库厂商按照这个接口去实现自己的类库。
好,那么问题就出现了。JDK代码包中的加载肯定是使用了上层的类加载器的情况,二具体实现是由第三方厂商来实现的,加载第三方厂商的类加载器肯定不是bootstrap classloader,但是当你去调用jdk中的接口时呢,接口的所在类必然会引起第三方类库的加载,这就不符合自下而上的委派加载顺序了。出现了了上层加载器调用下层类加载器的情况,就产生了双亲委派模型的破坏。
我们来看一个具体的例子,在这个DriverManager这个类的类加载器为null,我之前说类加载器为null就是Bootstrap Classloader,因为它不可以被Java程序调用(C,C )编写,而Driver Manager内部加载了两个driver,他们的类加载器都是Application ClassLoader,这说明不是Bootstrap 进行加载的,而是委托了Application ClassLoader去加载来自第三方的类,这个其实就是Java的SPI。
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类,以META-INF/services中的配置信息,辅以责任链模式,这才算是给SPI的加 载提供了一种相对合理的解决方案。
Spring Boot 当中的SPI机制也是在JDK的机制之上去完成的,实现的思路是一样的,只不过会比JDK的实现更加优雅。