抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

什么是Java SPI

参照百度百科的SPI的解释:百度百科

定义

SPI(Service Provider Interface):是一个内置的java标准,允许不同的开发去实现某个特定的服务,服务提供一个标准的接口及用于提供某种特定的服务的接口

性质

SPI是Java 1.5新添加的一个内置标准,允许不同的开发者去实现某个特定的服务。一个Service就是一个接口或抽象类,而Service Provider是这个Service的一个特定实现类 。

Java SPI的应用

一个简单的场景,通过定一个FactoryBean的标准接口,用于生成不同的bean的逻辑。

第一步:定义一个标准的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.yidan.spi;

/**
* @author wuxuan.chai
* @date 2021/2/2 6:19 下午
*/
public interface FactoryBeanSpi {
/**
* 服务实现类单利
* @param tClass 实现类
* @param <T> 类型枚举
* @return 服务实现类单利
*/
<T> T getInstance(Class<T> tClass) throws IllegalAccessException, InstantiationException;

/**
* 服务
* @return 服务
*/
String getService();
}

第二步:写一个实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.yidan.spi.services;

import com.yidan.spi.FactoryBeanSpi;

/**
* @author wuxuan.chai
* @date 2021/2/2 6:24 下午
*/
public class MySpiFactoryBean implements FactoryBeanSpi {
@Override
public <T> T getInstance(Class<T> tClass) throws IllegalAccessException, InstantiationException {
System.out.println("MySpiFactoryBean在构建:" + tClass.getName());
return tClass.newInstance();
}

@Override
public String getService() {
return "MySpiFactoryBean";
}
}

第三步:创建SPI的配置文件

在resources目录下创建META-INF/services文件目录,然后根据第一步的接口类全限定名称,创建一个文件,如:com.yidan.spi.FactoryBeanSpi,内容为:

1
com.yidan.spi.services.MySpiFactoryBean

这个文件中指定的是我们所描述的实现类的类全限定名,可以有多个实现

第四步:使用SPI去加载我们的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.yidan.spi.services;

import com.yidan.spi.FactoryBeanSpi;

import java.util.Optional;
import java.util.ServiceLoader;
import java.util.stream.StreamSupport;

/**
* @author wuxuan.chai
* @date 2021/2/2 6:27 下午
*/
public class MyBeanManager {
public static FactoryBeanSpi fetchFactoryBean() {
final ServiceLoader<FactoryBeanSpi> mySpiFactoryBeans = ServiceLoader.load(FactoryBeanSpi.class);
final Optional<FactoryBeanSpi> factoryBeanOptional = StreamSupport.stream(mySpiFactoryBeans.spliterator(), false).findFirst();
final FactoryBeanSpi factoryBean = factoryBeanOptional.orElseThrow(() -> new RuntimeException("SPI 加载失败,未找到:" + FactoryBeanSpi.class.getName() + "的实现"));
return factoryBean;
}

public static void main(String[] args) throws InstantiationException, IllegalAccessException {
final FactoryBeanSpi mySpiFactoryBean = fetchFactoryBean();
mySpiFactoryBean.getInstance(MyBean.class);
final String service = mySpiFactoryBean.getService();
System.out.println(service);
}
}

在这里面定义了一个MyBean,通过刚才实现的MySpiFactoryBean去构建这个MyBean实例,然后去做个简单的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.yidan.spi.services;

/**
* @author wuxuan.chai
* @date 2021/2/2 6:27 下午
*/
public class MyBean {

public MyBean(){
System.out.println("MyBean构造方法");
}
}

在MyBeanManager中,fetchFactoryBean的方法中通过调用ServiceLoader.load(FactoryBeanSpi.class);找到所有的在com.yidan.spi.FactoryBeanSpi文件中注册的实现类的对应实例,然后加载到JVM中,然后就能够去使用我们的实现了。

执行结果:

1
2
3
4
MySpiFactoryBean在构建:com.yidan.spi.services.MyBean
MyBean构造方法
MySpiFactoryBean

通过这个简单的例子,可以简单阐述了SPI机制的原理以及使用

除此之外,SPI的加载机制还可以支持自定义类加载器,调用如:ServiceLoader.load(Class class,ClassLoader classLoader);这么久意味着,这个SPI是可以动态加载的,如一些应用的插件开发,在设计的时候可以使用SPI机制,设计好插件的接口标准,通过SPI动态加载插件,实现不重启应用并且功能拓展。

Java SPI的用途

目前,在我们的日常开发中,接触到的比较多的SPI加载机制应该就是java的java.sql.Driver,本次主要探索下java的驱动标准接口下,各个数据库厂商的驱动实现,以及DriverManager的SPI加载。

在创建数据库连接做查询的时候,我们一般步骤为,加载驱动->创建连接->创建预执行查询->执行查询->释放连接资源。在JDBC 4.0之前的代码一般都是这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//加载驱动
Class.forName("com.mysql.jdbc.Driver");
//创建连接
final Connection connection = DriverManager.getConnection("数据库jdbcURL", "数据库username", "数据库password");
//预执行
final PreparedStatement preparedStatement = connection.prepareStatement("select 1");
//执行
final ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()){
//TODO 获取结果
}

//释放资源
preparedStatement.close();
connection.close();

在这里面我们需要Class.forName将driverClass加载到jvm中,让DriverManager通过发现Driver的实现类,反射获取driver的实例,通过调用driver.acceptUrl,去根据jdbcurl去获取驱动类型匹配driver。

而在JDBC4.0之后不需要这么做了,因为DriverManager默认初始化的时候通过SPI加载了所有的驱动实现类,所以不需要再继续做Class.forName了。下面摘了java.sql.DriverManager的部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class DriverManager {
/**
* 通过检查系统属性JDBC来加载初始的JDBC驱动程序,然后使用{@code ServiceLoader}(SPI)机制
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

//在加载初始化的驱动中,首先根据环境变量jdbc.drivers去加载驱动,再根据SPI获取驱动实现类,SPI的驱动实现类直接通过AccessController.doPrivileged直接加载到了JVM中,环境变量加载的通过Class.forName加载JVM中,所以我们在调用Driver.getConnection不需要在之前调用Class.forName了
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()

AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});

println("DriverManager.initialize: jdbc.drivers = " + drivers);

if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}


}

除了java.sql.Driver使用了SPI机制外,Spring中也使用了SPI,后续感兴趣去学习学习,加深对Spring的理解。

评论