本文实践环境:
Operating System: macOS 14.4.1
Kernel: Darwin 23.4.0
Architecture: arm64Java version: 21
听某个 Java 专栏时,无意中知道 Java 里面还有一个 SPI 机制,想着抽空总结一下,可一转眼几个月就过去了。这也许就是我与优秀者之间的差距,行动上的矮子,没有执行力。最近在看《Effective Java 中文版(第3版) (豆瓣) 》,里面又提到了这个机制,于是便有了这篇文章。
什么是 SPI
SPI 的全称是 Service Provider Interface 的缩写,即服务提供者接口。得益于 AI 的发展,想了解某个知识点要比之前方便快捷的多,我们先来问问:
可以看到,SPI 是一种 JDK 内置的服务发现机制,使用方定义接口,不同的提供方给出各自的实现。与日常开发有所不同的是,这里的接口与实现类不在一个代码工程(即传统意义上一个 jar 包)中,从而实现了解耦。编译时,即使实现类没有,也不会有影响。等运行时,通过动态加载,我们便可以在不同的实现类中切换或选择。
这么说,可能还比较抽象,结合前面提供的实现步骤,我们通过具体的代码案例进行理解。
SPI 实战
笔者创建了一个 Java 项目工程 java-spi-practice
,下面有 4 个子项目:
├── README.md
├── build.gradle
├── java-spi-chinese-greeter/
│ ├── build.gradle
│ └── src/
├── java-spi-english-greeter/
│ ├── build.gradle
│ └── src/
├── java-spi-greeter-api/
│ ├── build.gradle
│ └── src/
├── java-spi-main/
│ ├── build.gradle
│ └── src/
└── settings.gradle
1)根项目 java-spi-practice
的 build.gradle
配置如下:
plugins {
id 'java'
id 'maven-publish'
}
allprojects {
apply plugin: 'java'
}
subprojects {
apply plugin: 'maven-publish'
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
}
}
}
group = 'tech.demonlee'
version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
testImplementation platform('org.junit:junit-bom:5.9.1')
testImplementation 'org.junit.jupiter:junit-jupiter'
}
test {
useJUnitPlatform()
}
}
2)java-spi-greeter-api
项目用于定义服务接口,对应 build.gradle
没有配置,即完全使用父项目的配置。
3)java-spi-chinese-greeter
项目是一种 Greeter
的实现,即中文问候,对应的 build.gradle
配置如下:
dependencies {
implementation('tech.demonlee:java-spi-greeter-api:1.0-SNAPSHOT')
}
4)java-spi-english-greeter
项目是另一种 Greeter
的实现,即英文问候,对应的 build.gradle
配置同上。
5)java-spi-main
项目用于模拟使用者主程序,对应的 build.gradle
配置如下:
dependencies {
implementation('tech.demonlee:java-spi-greeter-api:1.0-SNAPSHOT')
implementation('tech.demonlee:java-spi-chinese-greeter:1.0-SNAPSHOT')
implementation('tech.demonlee:java-spi-english-greeter:1.0-SNAPSHOT')
}
创建好工程之后,我们开始写程序。
第 1 步:定义服务接口,对应工程为 java-spi-greeter-api
, 比如笔者这里定义的 Greeter
接口:
package tech.demonlee.greeter;
public interface Greeter {
void greet(String name);
}
第 2 步:服务提供者实现接口,这里以 java-spi-english-greeter
为例,对应代码为:
package tech.demonlee.greeter.english;
import tech.demonlee.greeter.Greeter;
public class EnglishGreeter implements Greeter {
@Override
public void greet(String name) {
System.out.println("Hi, " + name);
}
}
第 3 步:为方便 SPI 查找实现类,在 META-INF/services
目录下配置一个文件,文件名为接口全限定名,这里就是 tech.demonlee.greeter.Greeter
,文件中的内容是实现类的全限定名,比如这里的 tech.demonlee.greeter.english.EnglishGreeter
:
$ tree
.
├── java
│ └── tech
│ └── demonlee
│ └── greeter
│ └── english
│ └── EnglishGreeter.java
└── resources
└── META-INF
└── services
└── tech.demonlee.greeter.Greeter
9 directories, 2 files
$ cat resources/META-INF/services/tech.demonlee.greeter.Greeter
tech.demonlee.greeter.english.EnglishGreeter
第 4 步:测试验证,在 java-spi-main
工程中编写如下代码:
package tech.demonlee;
import tech.demonlee.greeter.Greeter;
import java.util.ServiceLoader;
public class Main {
public static void main(String[] args) {
greet();
}
private static void greet() {
ServiceLoader<Greeter> greeterServiceLoader = ServiceLoader.load(Greeter.class);
for (Greeter greeter : greeterServiceLoader) {
System.out.println("greeter is: " + greeter.getClass().getCanonicalName());
greeter.greet("Jack.Ma");
}
}
}
输出日志如下:
greeter is: tech.demonlee.greeter.chinese.ChineseGreeter
你好,Jack.Ma
greeter is: tech.demonlee.greeter.english.EnglishGreeter
Hi, Jack.Ma
完整的代码,笔者已上传到 GitHub,可以点击这里获取。
根据上面的代码实战,我们也可以看出一些端倪,那就是 SPI 机制的一些默认约定,主要有三点:
- 实现类需要有一个无参构造函数;
- 实现方在
META-INF/services
下配置对应接口文件; - 实现类对应 jar 文件需要放到 classpath 中,因为这还是一个进程内部的服务发现。
这里补充说明一下 ServiceLoader
加载 SPI 接口及其实现类的类加载器,我们先看一看 ServiceLoader#load(Class<S> service)
方法的源码:
/**
* Creates a new service loader for the given service type, using the
* current thread's {@linkplain java.lang.Thread#getContextClassLoader
* context class loader}.
*
* @apiNote Service loader objects obtained with this method should not be
* cached VM-wide. For example, different applications in the same VM may
* have different thread context class loaders. A lookup by one application
* may locate a service provider that is only visible via its thread
* context class loader and so is not suitable to be located by the other
* application. Memory leaks can also arise. A thread local may be suited
* to some applications.
*
* ...
* ...
*
* @revised 9
*/
@CallerSensitive
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
SPI 默认使用线程上下文加载器来加载对应的接口实现类,而默认情况下,线程上下文加载器是应用类加载器(AppClassLoader)。而我们知道,JVM 主要有三类加载器:
1)BootClassLoader
:启动类加载器,Java 核心类由该加载器进行加载,比如 java.base
等。启动类加载器由 C++ 实现,没有对应的 Java 对象,所以在 Java 中就使用 null
表示。
2)PlatformClassLoader
:平台类加载器,对应 Java 9 之前的扩展类加载器,Java SE 中除了几个关键的模块由启动类加载器加载外,其他都由平台类加载器加载。
3)AppClassLoader
:应用类加载器,它主要负责加载应用程序中的类,即 classpath 中指定的类文件。
package jdk.internal.loader;
// ...
// ...
/**
* Creates and provides access to the built-in platform and application class
* loaders. It also creates the class loader that is used to locate resources
* in modules defined to the boot class loader.
*/
public class ClassLoaders {
private ClassLoaders() { }
private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess();
// the built-in class loaders
private static final BootClassLoader BOOT_LOADER;
private static final PlatformClassLoader PLATFORM_LOADER;
private static final AppClassLoader APP_LOADER;
// ...
// ...
}
三个加载器之间的关系:AppClassLoader
的父加载器是 PlatformClassLoader
,而 PlatformClassLoader
的父加载器是 BootClassLoader
。下面是笔者写的一个单元测试,在 Java 21 中可以验证通过:
class ClassLoaderTest {
@Test
void should_validate_parent_class_loader() {
ClassLoader classLoader = this.getClass().getClassLoader();
Assertions.assertEquals("app", classLoader.getName());
ClassLoader parentClassLoader = classLoader.getParent();
Assertions.assertEquals("platform", parentClassLoader.getName());
ClassLoader grandpaClassLoader = parentClassLoader.getParent();
Assertions.assertNull(grandpaClassLoader);
}
}
回到 ServiceLoader
类加载器的问题上,为何要限定为线程上下文加载器?这是因为某些 SPI 接口是由 Java 核心类库提供的。前面提到,核心类库基本都由 BootClassLoader
或 PlatformClassLoader
进行加载,但 SPI 接口的实现类一般是第三方提供的。根据 JVM 的类加载机制, BootClassLoader
或 PlatformClassLoader
是无法加载第三方类库的,为了打破这个限制,便使用了 AppClassLoader
。当 SPI 接口是核心类库时,AppClassLoader
会委派给父加载器去加载,而 SPI 接口实现类如果是第三方提供的,则由 AppClassLoader
自己进行加载。
下面是笔者写的一个代码示例:
class SpiClassLoaderTest {
@Test
void should_validate_spi_class_loader() {
Class<java.sql.Driver> javaSqlDriverClass = java.sql.Driver.class;
ClassLoader driverClassLoader = javaSqlDriverClass.getClassLoader();
System.out.println("java.sql.Driver class loader is: " + driverClassLoader.getClass().getCanonicalName());
ServiceLoader<java.sql.Driver> driverServiceLoader = ServiceLoader.load(javaSqlDriverClass);
Assertions.assertTrue(driverServiceLoader.stream().findAny().isPresent());
driverServiceLoader.stream().forEach(s -> {
java.sql.Driver driver = s.get();
printDriverInfo(driver);
ClassLoader classLoader = driver.getClass().getClassLoader();
System.out.println("driver class loader is: " + classLoader.getClass().getCanonicalName());
});
}
private static void printDriverInfo(Driver driver) {
System.out.println("driver: " + driver.getClass());
System.out.println("driver.minorVersion: " + driver.getMinorVersion() +
", majorVersion: " + driver.getMajorVersion());
}
}
为了能够演示,需要一个 JDBC 的驱动,为此笔者在 build.gradle
中增加了 MySQL JDBC 的依赖:
dependencies {
// ...
// ...
implementation ('mysql:mysql-connector-java:8.0.33')
}
输出结果为:
java.sql.Driver class loader is: jdk.internal.loader.ClassLoaders.PlatformClassLoader
driver: class com.mysql.cj.jdbc.Driver
driver.minorVersion: 0, majorVersion: 8
driver class loader is: jdk.internal.loader.ClassLoaders.AppClassLoader
从输出的日志可以看到,SPI 接口 java.sql.Driver
是由平台类加载器进行加载的,而 MySQL Driver 则是由应用类加载器加载。既然提到了 JDBC,笔者便来梳理一下 JDBC 的演进思路。这部分内容主要来自刘欣老师的《码农翻身 (豆瓣) 》,最近他又出了《码农翻身 2》,有兴趣的小伙伴可以去瞅瞅。
JDBC 的演化
既然 mysql-connector-java 驱动实现了 JDBC 规范,并且遵循 SPI 约定,那么我们就来看看相关配置:
对应 Driver
类的代码如下,它继承自 NonRegisteringDriver
:
package com.mysql.cj.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!");
}
}
}
package com.mysql.cj.jdbc;
import com.mysql.cj.Constants;
//...
//...
public class NonRegisteringDriver implements Driver {
public NonRegisteringDriver() throws SQLException {
}
// ...
// ...
public boolean acceptsURL(String url) throws SQLException {
try {
return ConnectionUrl.acceptsUrl(url);
} catch (CJException var3) {
throw SQLExceptionsMapping.translateException(var3);
}
}
public Connection connect(String url, Properties info) throws SQLException {
try {
try {
if (!ConnectionUrl.acceptsUrl(url)) {
return null;
} else {
ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
switch (conStr.getType()) {
case SINGLE_CONNECTION:
return ConnectionImpl.getInstance(conStr.getMainHost());
case FAILOVER_CONNECTION:
case FAILOVER_DNS_SRV_CONNECTION:
return FailoverConnectionProxy.createProxyInstance(conStr);
case LOADBALANCE_CONNECTION:
case LOADBALANCE_DNS_SRV_CONNECTION:
return LoadBalancedConnectionProxy.createProxyInstance(conStr);
case REPLICATION_CONNECTION:
case REPLICATION_DNS_SRV_CONNECTION:
return ReplicationConnectionProxy.createProxyInstance(conStr);
default:
return null;
}
}
} catch (UnsupportedConnectionStringException var5) {
return null;
} catch (CJException var6) {
throw (UnableToConnectException)ExceptionFactory.createException(UnableToConnectException.class, Messages.getString("NonRegisteringDriver.17", new Object[]{var6.toString()}), var6);
}
} catch (CJException var7) {
throw SQLExceptionsMapping.translateException(var7);
}
}
// ...
// ...
}
可以看到,前面提到 SPI 的三点约定,mysql-connector-java 中都是有的,只不过 MySQL Driver 通过 DriverManager.registerDriver(new Driver());
自己主动注册驱动类,至于为什么,下文将会聊到。
如果让我们来设计 JDBC,我们会怎么做?估计很难,但基本的东西应该会有,比如:
- Connection:连接,对接数据库的通信协议;
- PreparedStatement:预处理语句,向数据库发送需要执行的 SQL 语句;
- ResultSet:结果集,接收数据库执行 SQL 后返回的数据;
- ……
// 伪代码
public class JDBCExample1 {
public static void main(String[] args) throws Exception {
Properties properties = new Properties();
properties.put("host", "127.0.0.1");
properties.put("port", "3306");
properties.put("database", "test_db");
properties.put("username", "root");
properties.put("password", "123123");
Connection connection = new MysqlConnection(properties);
String query = "SELECT * FROM employees WHERE department = ?";
PreparedStatement preparedStatement = connection.prepareStatement(query);
preparedStatement.setString(1, "Engineering");
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
String id = rs.getString("id");
// ...
}
rs.close();
preparedStatement.close();
connection.close();
}
}
上面的伪代码看上去还不错,第三方提供 jar 包,比如 mysql-connector-java-1.0.jar
,然后就可以跑起来了。但仔细一想就发现扩展性不足,换一个 Oracle 数据库,上面硬编码的 MysqlConnection
就得改,即使不换数据库,也不能保证第三方不会在 mysql-connector-java-2.0.jar
新版本中把 MysqlConnection
改名,比如改成 MysqlConnectionImpl
。
为此,我们需要继续抽象,把数据库细节屏蔽掉,于是便有了 Driver
,一个简单的方式是使用简单工厂,比如下面的代码:
// 伪代码
public class Driver {
public static Connection getConnection(String dbType, Properties properties) {
if("mysql".equals(dbType)) {
return new MysqlConnection(properties);
}
if("oracle".equals(dbType)) {
return new OracleConnection(properties);
}
// ...
// ...
}
}
跟之前一版相比,已有进步,但仍然有硬编码,即 MysqlConnection
,OracleConnection
等。 如果要加新的数据库,还得改 Driver
类,怎么办?我们可以将数据库类型与对应连接的实现类外挂到配置文件中,比如 connection-type.properties
:
mysql=com.mysql.jdbc.MysqlConnection
oracle=com.oracle.jdbc.OracleConnection
db2=com.ibm.jdbc.DB2Connection
#...
#...
接着,我们调整 Driver
类,让其从配置文件中加载:
// 伪代码
public class Driver {
public static Connection getConnection(String dbType, Properties properties) {
Class<?> clazz = getConnectionClass(dbType);
try {
Constructor<?> constructor = clazz.getConstructor(Properties.class);
return (Connection)constructor.newInstance(properties);
} catch (Exception ex) {
//...
}
}
private static Class<?> getConnectionClass(String dbType) {
// get Class from `connection-type.properties`
//...
}
}
到这里,前面提到的问题都解决了,但每个使用者都得提供一个叫 connection-type.properties
的配置文件,如果写错了呢?还是不够自动和灵活。能否将创建过程扔给各个厂商,从而隐藏 Connection
实例的创建过程?答案是肯定的。为此,我们可以将 Driver
从一个实现类,变成一个接口,然后让各个厂商去实现它。于是便有了本小节开头的代码和截图,即 mysql-connector-java 的实现方式。
让各个厂商自己实现 Driver
,由每个 Driver
自己创建自己的 Connection(这种方式又叫工厂方法),即:
mysql Driver --> new MysqlConnection()
oracle Driver --> new OracleConnection()
- ……
// 伪代码
public class XxxDriver implements java.sql.Driver {
// ...
@Override
public Connection connect(String url, Properties info) throws SQLException {
// ...
}
// ...
}
那我们该如何使用 Driver
?一种方式就是使用反射,比如:
// 伪代码
public class JDBCExample2 {
public Connection createConnection(String url, Properties properties) {
Class<?> clazz = Class.forName("com.mysql.jdbc.Driver");
Driver driver = (Driver)clazz.newInstance();
return driver.connect(url, properties);
}
}
但使用反射,还是不够友好,如果有多个不同的 Driver
呢?更好的方式,则是提供一个全局 Driver
调度器,让各个厂商自己注册 Driver
,以便使用者可以统一管理。这个调度器就是 DriverManager
:
public class DriverManager {
// List of registered JDBC drivers
private static final CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
// ...
public static void registerDriver(java.sql.Driver driver) throws SQLException {
registerDriver(driver, null);
}
public static void registerDriver(java.sql.Driver driver, DriverAction da) throws SQLException {
/* Register the driver if it has not already been added to our list */
if (driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
// ...
public static Connection getConnection(String url, java.util.Properties info) throws SQLException {
return (getConnection(url, info, Reflection.getCallerClass()));
}
private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
// ...
for (DriverInfo aDriver : registeredDrivers) {
// ...
try {
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
return con;
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
// ...
}
//...
}
}
于是,各个厂商实现的 Driver
就像下面这样了:
// 伪代码
public class XxxDriver implements java.sql.Driver {
static {
DriverManager.registerDriver(new XxxDriver());
}
// ...
public boolean acceptsURL(String url) throws SQLException {
return url.startWith("jdbc:Xxxdb");
}
@Override
public Connection connect(String url, Properties info) throws SQLException {
if(acceptsURL(url)) {
return new XxxConnection(info);
}
return null;
}
// ...
}
如此一来,使用起来就更方便了:
// 伪代码
public class JDBCExample3 {
public Connection createConnection(String url, Properties properties) {
Class<?> clazz = Class.forName("com.mysql.jdbc.Driver");
return DriverManager.getConnection(url, properties);
}
}
现在我们知道,初始化 Driver
已经不需要使用 Class.forName()
,SPI 对底层进行了封装,调用 ServiceLoader.load()
方法之后,DriverManager
就已经注册了对应的 Driver
:
// 伪代码
public class JDBCExample4 {
public Connection createConnection(String url, Properties properties) {
ServiceLoader<java.sql.Driver> driverServiceLoader = ServiceLoader.load(java.sql.Driver.class);
return DriverManager.getConnection(url, properties);
}
}
当然,内部实现的细节肯定比笔者这里的分析要复杂的多,但弄懂了基本原理,需要深入分析的时候也就不会觉得手足无措了。