导言
在看双亲委派机制的时候,有人提到
SPI
的概念,于是学习了一番。
SPI
,即
Service Provider Interface
,服务提供者的接口。这个
SPI
接口是由服务端提供和调用的,这个接口定义了一些规范,但不提供具体的接口功能,功能由客户端决定,客户端根据需要实现这个接口,并进行配置,将接口的实现实例和配置打包部署在服务端,服务端不会直接调用接口的实现实例,而是 调用
SPI
接口,其产生的行为取决于配置文件。
SPI
设计思想如下图所示:
SPI
接口一般由客户端和服务端共同确定(更多是服务端根据不同客户端的需求,形成一个兼容并包的接口)。客户端根据需求实现这个接口,得到一个自己的实例。服务端会有一个以这个
SPI
接口为文件名的配置文件,配置文件当中指定服务端将调用哪个客户端的接口实例。
SPI
的本质是多态,多态的好处是在解耦的同时,实现了适配。
SPI的实现之ServiceLoader
Java是通过
ServiceLoader
提供
SPI
功能的,我们来模拟服务端、客户端的角色,理解
SPI
是怎么工作的。
场景:不同的数据库想要通过Java连接,必须实现一个规范的接口,并提供相应的JDBC驱动。
一、服务端与客户端协商提供统一的SPI接口
新建一个Java项目,给出统一的
SPI
接口,代码如下,其中有一个
connect()
方法。
package com.java.db;
/**
* @ClassName DBDriver
* @Description TODO
* @Date 2021/1/11 22:19
*/
public interface DBDriver {
public String connect();
}
把这个项目打包成
jar
包,记名为
DBDriverInterface.jar
。
二、客户端提供带有配置文件的驱动类
新建一个Java项目,创建一个
MySQLDriver
类,实现
DBDriver
接口,因此这个项目中需要导入
DBDriverInterface.jar
包,目录结构如下图所示:
代码如下:
package com.java.mysql.godriver;
import com.java.db.DBDriver;
/**
* @ClassName MySQLDriver
* @Description TODO
* @Date 2021/1/11 22:21
*/
public class MySQLDriver implements DBDriver {
public String connect() {
System.out.println("MySQL连接成功");
return "一个MySQL连接实例";
}
}
在
resources
目录下,添加
META-INF\services
目录,并创建文本文件,文件名为
com.java.mysql.DBDriver
,就是
SPI
接口的
全名
,写入以下内容,这就是接口实现类的
全名
:
把这个项目打包成
jar
包,记名为
MySQLDBDriver.jar
。
三、服务端利用ServiceLoader加载驱接口
利用
ServiceLoader
加载接口的
class
对象,就可以加载配置文件当中实现了这个接口的所有驱动类,并执行相应的动作。
新建一个Java项目,导入上述两个包:
DBDriverInterface.jar
包和
MySQLDBDriver.jar
包,并利用
ServiceLoader
去加载MySQL数据库连接驱动,目录结构如下所示:
主类代码如下:
package com.java.db;
import java.util.Iterator;
import java.util.ServiceLoader;
/**
* @ClassName Main
* @Description TODO
* @Date 2021/1/11 22:43
*/
public class Main {
public static void main(String[] args) {
// 加载
ServiceLoader<DBDriver> drivers = ServiceLoader.load(DBDriver.class);
Iterator<DBDriver> iterator = drivers.iterator();
// 加载了哪些内容?
while (iterator.hasNext()){
DBDriver next = iterator.next();
String connect = next.connect();
System.out.println(connect);
}
}
}
结果如下所示:
MySQL连接成功
一个MySQL连接实例
Process finished with exit code 0
主方法当中,通过
ServiceLoader
加载的
class
对象是
DBDriver.class
,没有指明是哪个实现类,但在配置文件当中指明了。
如果你想实现其他数据库,例如
Oracle
、
redis
等,可以按照
第二步
的方法操作。
JDBC4.0之ServiceLoader
MySQL驱动源码
JDBC4.0开始使用ServiceLoader的方式加载数据库连接的驱动,导入
mysql-connector-java-xxxx.jar
包之后,连接
MySQL
的代码如下:
package com.java.www.day20210111.dbconnection;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
* @ClassName MySQLConnectionTest
* @Description TODO
* @Date 2021/1/11 19:48
*/
public class MySQLConnectionTest {
private final static String DB_URL = "jdbc:mysql://ip:3306/dbname?useSSL=true&useUnicode=true&characterEncoding=utf-8";
private final static String DB_USER = "root";
private final static String DB_PASSWD = "123456";
public static void main(String[] args) {
try {
// Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWD);
System.out.println(connection);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
JDBC4.0
之后就不需要
Class.forName("com.mysql.jdbc.Driver");
这句代码了,因为已经通过
ServiceLoader
的方式进行加载了。我们既然已经知道了
ServiceLoader
可以通过配置文件加载类,去查找一下
mysql-connector-java-xxxx.jar
包,展开如下图所示:
META-INF\services
目录下的文件名为
java.sql.Driver
,根据前面的讨论可知,这是
SPI
接口名,说明在
DriverManager
类当中通过
SeriviceLoader
加载了
java.sql.Driver
这个接口,这是
JDK
的核心类库,点进源码看一下:
package java.sql;
import java.util.logging.Logger;
public interface Driver {
Connection connect(String url, java.util.Properties info)
throws SQLException;
boolean acceptsURL(String url) throws SQLException;
DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
throws SQLException;
// 省略代码
}
java.sql.Driver
接口当中,有很多方法。
DriverManager
获取连接对象调用的是
connect()
方法,因此可以推断:MySQL驱动对这个接口的实现类当中一定有这个方法的实现,因此再看一下
mysql-connector-java-xxxx.jar
包的配置文件的内容:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
对于
com.mysql.jdbc.Driver
而言,加载的类是
com.mysql.jdbc
包下的
Driver.java
的字节码文件,点进字节码看一下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.mysql.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
这个类名和其实现的
java.sql.Driver
同名(没关系),但没有实现
java.sql.Driver
的
connect()
等方法,所以应该是在其父类
NonRegisteringDriver
当中有实现,点开源码可以看到有
SPI
接口
java.sql.Driver
中的方法:
看一下
connect()
方法源码:
鉴于我们讨论的是
ServiceLoader
,这个源码先不看。
回到
com.mysql.jdbc.Driver.class
字节码中,静态代码块里面调用了一句代码:
即注册了一个
Driver
实例,点进
registerDriver()
源码:
registerDriver()
方法中,将当前的这个
Driver()
实例添加到
registeredDrivers
中,
registeredDrivers
是一个全局的集合对象:
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers =
new CopyOnWriteArrayList<>();
源码看到这里,居然还没谈到
ServiceLoader
。再看看
DriverManager.java
,代码中搜索一下
ServiceLoader.load
或者
Driver.class
,发现:
这是明显在加载
SPI
接口的
class
对象,这个
loadInitialDrivers()
是在静态代码块中执行的:
打破双亲委派之线程上下文加载器
在上面的代码中,
Driver.class
是一个
class
对象被
ServiceLoader.load()
加载了,说明
ServiceLoader.load()
应该有一个类加载器,点进
load()
源码:
可以看到关键代码:
Thread.currentThread().getContextClassLoader();
,下面捋一捋:
-
是DriverManager.java
下的类,属于JDK核心类库,只能由根加载器加载:java.sql
-
是第三方类,只能由应用加载器加载。com.mysql.jdbc.Driver.class
我们看到的是在
DriverManager.java
当中加载了
java.sql.Driver
,虽然
java.sql.Driver
也是JDK核心类库,但我们知道本质上是加载的第三方类
com.mysql.jdbc.Driver.class
,问题是
DriverManager.java
是只能由根加载器加载,那么它为什么可以去加载第三方类库呢?
以前,我们需要通过
Class.forName("com.mysql.jdbc.Driver");
加载,这是通过应用加载器加载的。
JDBC4.0
之后就不需要这行代码了,因为
DriverManager.java
中有这句代码:
Thread.currentThread().getContextClassLoader();
测一下这句代码可以得到什么。
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println("loader = " + loader);
这句代码获取的是应用加载器:
loader = sun.misc.Launcher$AppClassLoader@18b4aac2
Process finished with exit code 0
这就是
线程上下文类加载器
,虽然只有一句代码,但是这种办法实现了在父加载器中加载了子加载器,弥补了双亲委派机制的缺陷。使用
线程上下文类加载器
是打破双亲委派机制的两种方式之一。
至于接下来,当前线程的应用加载器如何加载第三方的类,可以再继续看
load()
源码,发现是通过
LazyIterator
进行懒加载的。