SPI是否打破了双亲委派机制?
点击蓝字
关注我们
1.什么是双亲委派?
双亲委派机制是指一个类在收到类加载请求后不会尝试自己加载这个类,而是把该类加载请求向上委派给其父类加载器去完成,其父类加载器在接收到该类加载请求后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动类加载器中。若父类加载器在接收到类加载请求后发现自己也无法加载该类(通常原因是该类的Class文件在父类的类加载路径中不存在),则父类会将该信息反馈给子类并向下委派子类加载器加载该类,直到该类被成功加载,若找不到该类,则JVM会抛出ClassNotFoud异常。
为什么需要双亲委派模型?
避免类的重复加载 保证程序安全,避免核心API被修改
详情可参考《深入理解Java虚拟机》,此书也是Java程序员必读书籍
2.什么是SPI
2.1 定义
SPI(Service provide interface),直译过来是服务提供接口,在这里指的是厂商负责定义一个接口但不负责提供实现类,定义完接口后厂商直接使用这个接口的方法,但是如果不给此接口提供实现肯定运行要报错的,所以谁要想用厂商这个接口,谁负责实现。最典型的是jdbc,java可以连接各种数据库,比如mysql、oracle、h2……若是让各个数据库厂商都去实现自己的数据库连接方式,那么非常不利于统一管理,所以sun公司为了避免这种各自为战的乱象,他们就规定了一个规范,这就是jdbc了,在java.sql包下,sun指定一个接口叫做Driver,各大厂商负责实现这个Driver就可以了,只要你实现按要求这个接口的方法,那么你就可以直接连接到你的数据库。此处不得不说一句“一流的公司卖标准,二流公司卖实物,三流公司卖服务”
2.2 使用场景
jdbc4(jdbc4是随着jdk1.6发布的,此版本才开始支持SPI) springboot的自动装配也是同样的原理 阿里的dubbo 其他
2.3 自己写一个SPI模拟jdbc的spi
2.3.1 定义规范(sun公司定义的jdbc规范在java.sql包)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>JdbcSPI</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
//定义驱动规范,各数据库厂商自行实现
public interface Driver {
String getDriver();
}
public class DriverManager {
//使用厂商是实现的驱动连接他的数据库
public void connect(){
ServiceLoader<Driver> load = ServiceLoader.load(Driver.class);
Iterator<Driver> iterator = load.iterator();
while (iterator.hasNext()) {
Driver next = iterator.next();
String driver = next.getDriver();
//假装业务处理
System.out.println("我拿到了用户实现的driver,可以进行连接数据库了,用户用的driver是:" + driver);
}
}
}
2.3.2 厂商实现
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>MysqlDriver</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>JdbcSPI</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
public class MysqlDriver implements Driver {
@Override
public String getDriver() {
return "MysqlDriver";
}
}
按照SPI规范配置好具体实现类
public class Client {
//客户端使用
public static void main(String[] args) {
new DriverManager().connect();
}
}
输出:
3.为什么SPI打破了双亲委派
3.1 ContextClassLoader
Thread context class loader存在的目的主要是为了解决parent delegation机制下无法干净的解决的问题。假如有下述委派链:
那么委派链左边的ClassLoader就可以很自然的使用右边的ClassLoader所加载的类。
但如果情况要反过来,是右边的ClassLoader所加载的代码需要反过来去找委派链靠左边的ClassLoader去加载东西怎么办呢?没辙,parent delegation是单向的,没办法反过来从右边找左边.
就是说当我们this.getClass().getClassLoader();可以获取到所有已经加载过的文件,
但是Application class loader -> Extension class loader -> Bootstrap class loader
就获取不到Custom ClassLoader 能加载到的信息,那么怎么办呢? 于是,Thread就把当前的类加载器,给保存下来了,其他加载器,需要的时候,就把当前线程的加载器,获取到.
4.从源码来分析jdbc的SPI
4.1 jdbc介绍
jdbc是java标准的一部分,并不是一开始就支持SPI的,是从JDBC4开始支持,Jdbc4是随着jdk1.6发布的,目前最新的也就是jdbc4.3,随着jdk9发布的, jdbc规范从4.0开始支持SPI,如果要使用spi连接mysql的数据库,那么需要mysql驱动版本至少为5.1.6,之前版本是适配jdbc4.0之前的规范的.
5.1.6版本的mysql驱动,可以看到有一个META-INF/services/java.sql.Driver 就是SPI规范要求的文件
5.1.5版本打开看看,就没有了META-INF/services/java.sql.Driver
jdbc4.0规范说了,可以自动加载驱动,就是因为用了这个SPI,当然你的驱动必须是>=5.1.6版本
4.2 jdbc一定打破双亲委派吗?
在(4.1 jdbc介绍)介绍了很多jdbc的东西,这些东西在我们实际开发中其实并没人关注,说了这么多主要是为了搞清楚jdbc打破双亲委派机制问题.
我在看了很多博客包括周志明老师的《深入理解java虚拟机》都说了jdbc就打破双亲委派,其实这种说法不严谨,我在测试时用的mysql驱动时5.1.5版本,此版本还没支持SPI,只能用Class.forname(“com.msyql.java.Driver”)来加载驱动,使用这种方式其实并没有打破双亲委派。
现在很多新手刚使用jdbc时,随笔一搜《jdbc连接过程xxx》基本上出来的结果第一步都是让你Class.forName("com.mysql.jdbc.Driver")
,其时压根不用写这一行,直接DriverManager.getConnection("jdbc:mysqlxxxx")
就可以了(前提是你的jdk1.6+,mysql驱动5.1.6+,现在很少有jdk1.6以下的了吧)
public static void main(String[] args) {
try {
Class.forName("com.mysql.jdbc.Driver");
// Connection connection = DriverManager.getConnection("jdbc:mysql://10.10.102.105:3306/abc123", "root", "sonoscape");
// Statement statement = connection.createStatement();
// ResultSet resultSet = statement.executeQuery("select * from users");
// while (resultSet.next()) {
// String string = resultSet.getString(7);
// System.out.println(string);
// }
} catch (Exception e) {
e.printStackTrace();
}
}
真正的打破双亲委派是在jdbc4.0+,并且mysql驱动在5.1.6+才会使用SPI打破双亲委派
关于Class.forName("com.mysql.jdbc.Driver")
;这里调试类加载过程不再分析,调试中使用-verbose:class可以看到类加载过程,Class.forName("com.mysql.jdbc.Driver")
;执行完后,com.mysql.jdbc.Driver
类就会被加载。
4.3 调试jdbc4.0+、mysql5.1.6+版本的spi打破双亲委派
测试环境:
linux 、jdk8(jdbc4.2)、mysql驱动:5.1.6
public static void main(String[] args) throws SQLException {
//测试代码就这一行,jvm参数:-verbose:class
Connection connection = DriverManager.getConnection("jdbc:mysql:///abc123", "root", "123");
}
记得打上断点开启debug之路,第一次进入断点输出的类加载信息如下,可以看到我们的Client类被加载了,看完了日志后清理,防止太多看起来烟花缭乱
接下来肯用到DriverManager,肯定要触发加载,在日志中可以看到
提前到java.sql.DriverManager#loadInitialDrivers打好断点,586行看到了熟悉的ServiceLoader,这就是SPI的核心,它要触发Driver.class的加载了。
注意:此时我们还在DriverManager中这个类在jdk的核心包中lib下,也就是rt.jar中,在第一节就说了,此包中的类是由启动类加载器BootStrapClassLoader负责的,这是由C++写的,java中看不到,这个类加载器就要委托其子孙加载器来加载Driver,先到
java.util.ServiceLoader#load(java.lang.Class<S>)
提前打好断点继续调试,来证明
先加载了java.sql.Driver,这个肯定也是启动类加载器加载的,然后注意看ClassLoader cl = Thread.currentThread().getContextClassLoader();这里获取线程上下文加载器,默认就是AppClassLoader,然后用获取到的类加载器来加载Driver==>ServiceLoader.load(service, cl);
跟踪进入另一个load方法
继续跟踪, java.util.ServiceLoader#load(java.lang.Class<S>)
执行完了回到此处
这里就是真正要加载com.mysql.jdbc.Driver了
跟踪进去,最后会进入 java.util.ServiceLoader.LazyIterator#nextService
,可以到这里用AppClassLoader加载了com.mysql.jdbc.Driver,这个类就是之前在java.sql.DriverManager的静态代码快中受到BootStrapClassLoader的委托而加载的。这就证明了父加载器委托子加载器加载,从而证明了spi打破了双亲委派机制
来源:https://blog.csdn.net/chen462488588/
THE END
推荐阅读
掌握这些 Spring Boot 启动扩展点,已经超过 90% 的人了!
点赞+在看,关注公众号回复“666”领取福利