SPI机制是如何规避双亲委派机制的?

发布时间 2023-08-29 22:07:26作者: 苦行僧DH

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机制存在一些约定,这些约束如下:

  1. 三方接口需在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

image

​ 注意这里的loader是我们前面获取到的应用类加载器,这个方法中是获取到了具体需要实例化的实现类,即将对其进行实例化, 在这之前需要先获取到Class,这里使用Class.forName(class, false, ClassLoader)方法,这个方法的含义是使用指定的类加载器去加载指定的类。既然这里的类加载器是应用类加载器,那么类加载顺序自然就又回到了应用类加载器-->扩展类加载器-->BootStrap类加载器-->扩展类加载器-->应用类加载器,能加载到我们想要的类也就不奇怪了。

​ 看到这里也就明白了为什么使用SPI仍然能正常加载类了。

​ SPI的加载机制看起来虽然方面,但仍然有缺点:

1. 无法实现动态加载、卸载的效果,只有最简单的加载三方类的实现。
1. 由于实现原因,实现类必须提供无参构造器,局限性和扩展性很低

​ 综合来说,SPI简单但局限性大,项目中能接受这些缺点就可以放心使用,如接受不了则可以模拟SPI机制自行实现一套加载机制,自己实现起来扩展性和局限性肯定是原生SPI不能比的。

​ 本次内容结束,如发现内容错误请留言,会尽快改正。