SPI如何破坏双亲委派机制?可根据以下概念一步步深入
什么是双亲委派机制?
双亲委派机制是Java类加载器体系中采用的一种类加载策略,旨在保证类加载的安全性和稳定性。
这一机制规定了类加载的顺序和规则,即当一个类加载器收到类加载请求时,首先不会自己去尝试加载这个类,而是将这个任务委托给它的父类加载器去完成,只有当父加载器无法加载该类时(表现为抛出ClassNotFoundException),才会尝试自己加载。
优点:核心的包,谁也改变不了,保证了安全性。
缺点:在于它的隔离性,Bootstrap加载器无法去加载应用程序的类。(后面就引出SPI的概念)
1 先去自定义加载器加载,自定义加载器没有加载过这个类,委派给应用类加载器
2 应用类加载器也没有加载过这个类,委托给扩展类加载器
3 扩展类加载器没有加载过这个类,委托给根加载器,根加载器去rt.jar的包里去找需要加载的类,rt.jar包里没有,就让扩展类加载器去ext路径下去找
4 扩展类加载器也没有找到,就让应用类加载器去CLASSPATH下面找…
举例验证:当你自定义一个java.lang.String的类,去new这个对象时,加载的仍然是Java自带的String对象,而不是你自定义的String对象。(详细如下图)
什么是SPI?
SPI全称ServiceProviderInterface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。SPI的作用就是为这些被扩展的API寻找服务实现。
SPI使用方式?
1 定义一个接口,并创建它的实现类
2 然后在项目的src/main/resource/META-INF/services目录下创建一个与上述接口的全限定名一致的文件,在这个文件中写入接口的实现类的全限定名
3 运行时,会查找加载这些实现类。(比如下面举例的加载mysql驱动程序)
为什么你在自己的JDBC项目中没有找到META-INF/services/java.sql.Driver文件?
如果你的项目是直接使用像Spring Boot这样的现代框架,它们通常有自动配置或者依赖管理特性,能自动处理JDBC驱动的加载,而不需要你手动放置这个文件。特别是当你通过Maven或Gradle等构建工具管理依赖时,驱动的SPI配置已经是驱动包内部的事情,你的项目不需要直接包含这个文件。
什么是rt.jar?
rt.jar 是 Java Runtime Environment (JRE) 的一个核心组件,全称为 Runtime JAR
如何“破坏”?
SPI核心类在rt.jar 位于Bootstrap ClassLoader启动类加载器中,但是SPI需要实现来自不同厂商提供的数据库Driver实现类,而这些来自不同厂商的SPI接口实现类(如com.mysql.cj.jdbc.Driver)位于Application ClassLoader应用类加载器中
为何说“破坏”?
尽管SPI本身是由启动类加载器加载,但是它间接的通过应用类加载器加载第三方驱动类,绕过了严格的双亲委派机制
为什么要破坏?
因为使用双亲委派有一定的局限性,在正常情况下,用户代码一定是依赖核心类库的,所以双亲委派加载机制是没有问题的,但是在加载核心类库时,又要反过来使用用户代码,双亲委派就无法满足。这是什么样的一个场景呢?比如jdbc利用Driver.Manager.getConnection获取连接时,DriverManager是由根类加载器Bootstrap ClassLoader加载的,在加载DriverManager时,会执行其静态方法,加载驱动程序(也就是Driver接口的实现类),这些实现类都是由第三方数据库厂商提供,根据双亲委派原则,第三方类不可能被根类加载器加载
双亲委派机制源码解析?
这里forName进行加载类,forName方法的getClassLoader是获取caller对应的ClassLoader,caller就是指当前调用的forName方法的那个类,即ReflectionTest这个类,因为这个类是自己写的,所以在CLASSPATH路径下,获取到的加载器应该是应用类加载器(App ClassLoader),解析去会尝试用应用类加载器加载Persion类
通过findLoadedClass(name),去判断当前类加载器有没有加载过,没有加载过,并且parent加载器不为空,就调用parent加载器的loadClass方法…(由此递归)
当到根加载器调用loadClass方法的时候,由于根加载器没有parent,所以就执行findBootstrapClassOrNull(name)去尝试加载类,如果加载不到再让子类去加载
SPI破坏双亲委派机制- JDBC应用场景源码解析?
补充说明:Class.forName("com.ibm.db2.jcc.DB2Driver");这种写法源码里是获取的应用类加载器,这里没有问题,正常的做法。
但是java1.6开始,后期的SPI版本是不需要写这句代码的,即不需要显示的指定驱动程序,DriverManager会自动查找合适的驱动,从而引出SPI为什么会破坏双亲委派机制。
1 假设这里按照双亲委派的加载思想,这里ServiceLoader是属于rt包,属于根加载器,根加载器去加载mysql的驱动,肯定是找不到的,而且你也没有向下去委托扩展类加载器的路,因为你起步就是根加载器,没法往下递归了,根加载器加载不到,就结束了
2 但是,SPI这里并没有这样去做,而是从Thread.currentThread().getContextClassLoader()获取类加载器,这里获取到的是应用类加载器(为什么获取到应用类加载器?下面进行了详细解释),然后由应用类加载器完成加载
ClassLoader构造函数
获取系统的classLoader
初始化classLoader,从Launcher中获取ClassLoader
Launcher是干嘛的?Launcher是一个启动类,Lanucher创建了扩展类加载器,创建了应用类加载器,并把扩展类加载器添加成父类,并且把应用类加载器添加到当前线程的ContextClassLoader 即Thread.currentThread().SetContextClassLoader()
但是,上图中并没有创建bootstrap加载器,为什么呢?因为bootstarp加载器是jvm内部创建,默认是所有机载器的父类。
总结:
1 SPI加载方式实际上是通过获取当前线程的应用类加载器加载的rt包的接口实现类,这些实现类是存在于CLASSPATH中
2 而双亲委派机制,是根据你当前调用forName的那类,来决定起点加载器是哪一个,再向上委派,向下加载。