SPI是如何规避双亲委派机制的?
1、何为双亲委派机制?
双亲委派机制是什么?
双亲委派机制指的是Java中类加载机制的特性。
双亲委派机制是作用于什么地方?
双亲委派机制主要作用于类加载的时候。
类加载器
首先需要清晰的知道,双亲委派机制指的是类加载的特性。在了解其特性之前,我们需要先了解类加载器有哪些(不考虑自定义加载器的情况)。
加载器 | 解释 |
---|---|
BootStrap加载器 | 最为顶层的加载器,负责加载System.getProperty("sun.boot.class.path")下的Jar包,主要是jre\lib目录下的内容。该类加载器为C实现,在Java中无法获取 |
Ext类加载器 | 扩展类加载器,负责加载System.getProperty("java.ext.dirs")下的Jar包,主要是jre\lib\ext下的内容。在Java中对应ExtClassLoader(注意此处以jdk8为例,jdk11中有所改变)。 |
App类加载器 | 应用类加载器,负责加载System.getProperty("java.class.path")下的Jar包,主要是自身程序加载的包。在Java中对应AppClassLoader(注意此处以jdk8为例,jdk11中有所改变)。 |
类加载器之间的结构如何:
可以看出来,App类加载器是最小的一层,也是我们开发用户接触最多的一层,越往上加载的类就越核心。
双亲委派机制是什么样的结构?
双亲委派机制其实就是描述类加载器加载类的顺序及其特点。
我们开发者需要去加载类的场景每天都在接触,例如在代码中new Car(我们自己的类),此时就是需要去加载这个类。在触发加载类的时候,开发者处于加载器的最低层。那么就可以看作成:App类加载器去加载Car这个类。
而实际上的加载顺序是这样的:
App类加载器--通知-->Ext类加载器--通知-->BootStrap类加载器
BootStrap类加载器--发现找不到该类,则向下返回-->Ext类加载器--发现找不到该类,继续向下返回-->App类加载器(当前类加载器如果找不到该类则抛出异常,否则加载成功)
上述为双亲委派机制加载类时的顺序,其特点为先向上通知到最顶层,再由最顶层往下尝试,直到成功加载或到达发送加载类请求的加载器。
这种加载特点最大的作用如下:
安全性:由于Java核心类均有BootStrap加载器、Ext加载器去加载,再加上这种加载类的特性,可以有效防止Java核心类被篡改,正常的Java应用无法修改核心类实现。不仅可以应用在Java核心类中,当我们的应用是插件式时,此方式也可以防止插件中篡改主程序的代码。
2、SPI是什么?
上面我们讲述了双亲委派机制,现在要讲述SPI。
SPI是什么?
SPI(Service Provider Interface)是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用。
例如数据库驱动中java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将功能实现剔除到程序之外,这针对与模块化解耦有很大的作用。
例如下图:
除数据库驱动以外,例如日志框架、Dubbo等也涉及到SPI机制。
在上图中,例如当我们需要具体Driver实现的时候,直接通过JDK的API:
ServiceLoader<java.sql.Driver> serviceLoader = ServiceLoader.load(java.sql.Driver.class);
for (java.sql.Driver driver : serviceLoader) {
// mysql、pg、oracle、db2等
}
注意,SPI机制存在一些约定,这些约束如下:
-
三方接口需在META-INF/services/${interface_name}文件中列举实现类,每一个实现类为一行。例如数据库这,那么示例如下:
META-INF/services/java.sql.Driver
com.mysql.cj.jdbc.Driver
org.postgresql.Driver
oracle.jdbc.OracleDriver
com.ibm.db2.jcc.DB2Driver
2.定义的实现类必须实现对应接口
3.实现类必须提供无参构造器
3、为什么说SPI规避了双亲委派机制?
注意,我们前面说了双亲委派机制中,加载器会往上层加载器递交加载请求,我们已知java.util.ServiceLoader的类加载器为BootStrap加载器。此加载器已经是最顶层,无更加上层的加载器。而按照加载器职责的约定,ServiceLoader所属类加载器的职责是加载jdk核心类,其是无法加载到用户的类。例如下图:
现在的问题是:既然ServiceLoader的类加载器是最顶层的,其加载职责不负责我们自己的类,那么它是如何加载到类似JDBC这种实现类的呢?
附:ServiceLoader的类加载器是BootStrap类加载器,在程序中是无法获取到该类的类加载器的。
4、SPI是如何规避双亲委派机制的?
要搞清楚这个问题的原因,得先确认我们使用SPI的入口:
ServiceLoader<Xxxx> serviceLoader = ServiceLoader.load(Xxxx.class);
进入该方法,寻找其实现的方式:
java.util.ServiceLoader#load(java.lang.Class)
注意此处获取了当前线程的类加载器,而在线程中调用该类方法的是我们用户自己。那么这里就理解为获取到了用户的类加载器。
再往该方法中查找,找到该段代码:
java.util.ServiceLoader#ServiceLoader
注意该段代码中,cl为上一步获取到的类加载器,如果发现类加载器不存在,会再次获取系统默认加载器,这个系统默认加载器在常规情况下是用于加载启动类的加载器(jdk注释中解释),而启动类则是我们用户自己定义的类,这里毋庸置疑也会是应用类加载器。
从上面的代码中我们总结出来,ServiceLoader获取了我们的应用类加载器,至此load方法入口基本上没有其他内容可以细看。
为减轻文章阅读压力,直接跳转到该方法
java.util.ServiceLoader.LazyIterator#nextService
注意这里的loader是我们前面获取到的应用类加载器,这个方法中是获取到了具体需要实例化的实现类,即将对其进行实例化, 在这之前需要先获取到Class,这里使用Class.forName(class, false, ClassLoader)方法,这个方法的含义是使用指定的类加载器去加载指定的类。既然这里的类加载器是应用类加载器,那么类加载顺序自然就又回到了应用类加载器-->扩展类加载器-->BootStrap类加载器-->扩展类加载器-->应用类加载器,能加载到我们想要的类也就不奇怪了。
看到这里也就明白了为什么使用SPI仍然能正常加载类了。
SPI的加载机制看起来虽然方面,但仍然有缺点:
1. 无法实现动态加载、卸载的效果,只有最简单的加载三方类的实现。
1. 由于实现原因,实现类必须提供无参构造器,局限性和扩展性很低
综合来说,SPI简单但局限性大,项目中能接受这些缺点就可以放心使用,如接受不了则可以模拟SPI机制自行实现一套加载机制,自己实现起来扩展性和局限性肯定是原生SPI不能比的。
本次内容结束,如发现内容错误请留言,会尽快改正。