0%

从JDBC Driver看java SPI

最近为项目整合动态多数据源,忽然想到一个问题:每种数据库需要特定的JDBC,在使用的时候只需要引入对应依赖包,那么java包是怎么引用的呢?这里至少需要解决这样的问题:

  • 必须是由java定义统一接口,各个数据库厂商或者开源社区针对数据库提供实现,不然适配这么多数据库是完全不可能也不合理的,毫无扩展性可言
  • 只能在运行时加载JDBC驱动实现类,所以需要提供一种方式,使得类加载器能够找到各个驱动的实现类完成加载

带着问题浏览了java.sql下的关键类,了解到这种机制称为SPI,即Service Provider Interface。这种模式适合为框架提供扩展点,不难联想到,spring通过spring.factories加载类也是SPI模式的应用

java.sql.DriverManager加载驱动类流程

DriverManager类为我们提供了一个使用SPI的很好的样例,所以还是从这里入手,将java SPI作为黑盒,先看下基于SPI完成jdbc驱动类加载和使用的整体流程

java.sql下关于驱动加载的类是java.sql.DriverManager和java.sql.Driver,其中,Driver类是为不同驱动提供的统一接口:The interface that every driver class must implement,而驱动类的加载是在DriverManager类中进行的

通过类的注释可以了解到,指定驱动类有方式:

  1. 通过JVM参数:the DriverManager class will attempt to load the driver classes referenced in the “jdbc.drivers” system property
  2. 通过JAVA标准的SPI机制:The DriverManager methods getConnection and getDrivers have been enhanced to support the Java Standard Edition Service Provider mechanism. JDBC 4.0 Drivers must include the file META-INF/services/java.sql.Driver

加载驱动类的关键代码在loadInitialDrivers函数中:

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
private static void loadInitialDrivers() {
String drivers;
try {
// 读取JVM参数指定的驱动类类名
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() {

// 通过SPI机制加载jar包中指定的驱动类
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类加载器问题思考与简单模拟热部署中了解过,mysql等jdbc Driver类中,通过static代码块的方式,在Driver类加载时完成初始化,关键的代码为:

1
2
3
4
5
6
7
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}

这里进行了Driver类的实例化,最终调用回到了DriverManager类的registerDriver方法,这里关键的代码是记录了Driver实现类的信息

1
2
3
4
5
/* Register the driver if it has not already been added to our list */
if(driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
}

而在对数据库进行操作时,可以看到,getConnection()方法中有遍历选取Driver实现类的步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 遍历所有已注册的Driver实现类,选择与当前连接的数据库匹配的执行操作
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}

} else {
println(" skipping: " + aDriver.getClass().getName());
}

}

所以,整个的流程是:DriverManager类通过SPI机制加载所有的Driver实现类 –> Driver实现类通过类加载机制,在加载时将自身信息注册到DriverManager –> 连接数据库时,DriverManager遍历所有注册的Driver实现类,选取匹配的实现类执行

java SPI

下面继续来看下java 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
29
30
// 接口MyInterface com.connorma.spi_learn.MyInterface
public interface MyInterface {

void func();
}

// 实现类A com.connorma.spi_learn.MyInterfaceA
public class MyInterfaceA implements MyInterface {

static {
System.out.println(MyInterfaceA.class.getName());
}

@Override
public void func() {
System.out.println("A");
}
}
// 实现类B com.connorma.spi_learn.MyInterfaceB
public class MyInterfaceB implements MyInterface {

static {
System.out.println(MyInterfaceB.class.getName());
}

@Override
public void func() {
System.out.println("B");
}
}

然后在resources目录下新建目录META-INF/services,添加文件“com.connorma.spi_learn.MyInterface”,即接口类的全限定名,文件内容写入:

1
2
com.connorma.spi_learn.MyInterfaceA
com.connorma.spi_learn.MyInterfaceB

即将两个实现类的全限定名按行写入

这样打包为jar之后,可以看到jar包内包含了META-INF/services/com.connorma.spi_learn.MyInterface文件

在代码中可以使用ServiceLoader类加载:

1
2
3
4
5
6
7
ServiceLoader<MyInterface> serviceLoader = ServiceLoader.load(MyInterface.class);

Iterator<MyInterface> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
MyInterface itf = iterator.next();
itf.func();
}

执行以上代码则输出:

1
2
3
4
com.connorma.spi_learn.MyInterfaceA
A
com.connorma.spi_learn.MyInterfaceB
B

这只是一个简单的使用演示,真正使用的使用方式,那么需要像DriverManager一样,处理实现类的选择和使用等问题

ServiceLoader的加载方式

从上面演示代码的输出可以看出,ServiceLoader类在调用load函数时并不会完成实现类的加载,而是在以懒加载的形式,在通过迭代器获取类实例时才加载的——实际上看代码可以发现,ServiceLoader中定义了内部类LazyIterator,是在迭代时才去读取META-INF/services下的文件、加载类的

LazyIterator类的hasNext()方法最终调用了内部的hasNextService()方法,返回下一个实现类的类名,关键代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);

while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();

而LazyIterator类的next()方法最终调用了内部的nextService()方法,在其中进行了类的加载和实例化,关键代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
String cn = nextName;
nextName = null;
Class<?> c = null;
// 加载实现类
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
// 实现类实例化
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}

java SPI的问题

ServiceLoader类的实现简单清晰,但是真打算使用的时候,细细一想,感觉就是注释里说的:A simple service-provider loading facility.

ServiceLoader的实现并没有提供太多的自定义选项,功能上支持的很简单。

ServiceLoader只能一次性读取所有META-INF/services下的文件,之后在迭代中依次加载类和实例化。这样,无法有效的指定加载某个或某种实现类,也存在类加载、实例化的资源消耗。设想一种场景:在使用jdbc驱动类中,虽然依赖同时引入了mysql、pg的驱动,但是希望在运行中可以指定加载某种数据库的驱动类;想实现这种方式,最容易想到的是在指定驱动类类名时可以通过键值对进行,比如:

1
2
mysql=com.mysql.cj.jdbc.Driver
postgresql=org.postgresql.Driver

这样在运行时就可以根据配置参数决定要加载的实现类

但是,ServiceLoader类并没有开放这类自定义扩展的能力

这个例子并不太准确,毕竟mysql/pg的驱动类并不是可以相互替换的实现,只是用于说明问题

dubbo的SPI

之前了解了java SPI后也并没有继续去看其他框架的扩展点设计,直到最近看到了dubbo的SPI实现,很好的解决了上文提到的java SPI的问题,提供了以下特性(from dubbo文档):

  • 按需加载。Dubbo 的扩展能力不会一次性实例化所有实现,而是用哪个扩展类则实例化哪个扩展类,减少资源浪费。
  • 增加扩展类的 IOC 能力。Dubbo 的扩展能力并不仅仅只是发现扩展服务实现类,而是在此基础上更进一步,如果该扩展类的属性依赖其他对象,则 Dubbo 会自动的完成该依赖对象的注入功能。
  • 增加扩展类的 AOP 能力。Dubbo 扩展能力会自动的发现扩展类的包装类,完成包装类的构造,增强扩展类的功能。
  • 具备动态选择扩展实现的能力。Dubbo 扩展会基于参数,在运行时动态选择对应的扩展类,提高了 Dubbo 的扩展能力。
  • 可以对扩展实现进行排序。能够基于用户需求,指定扩展实现的执行顺序。
  • 提供扩展点的 Adaptive 能力。该能力可以使的一些扩展类在 consumer 端生效,一些扩展类在 provider 端生效。

在此学习了解下,这里使用的是dubbo 3.1的源代码

dubbo SPI的整体处理流程肯定还是相同的:首先查找、加载扩展类,然后完成类的实例化和后置处理(为扩展类进行依赖注入等)

dubbo SPI的使用

首先从用户视角看下如何使用dubbo SPI声明扩展类,相关的注解有:

  • @Adaptive,自适应加载扩展类,一般在扩展点接口的函数上使用,需要函数包含一个org.apache.dubbo.common.URL类型的参数,将在运行时根据URL中的参数值确定加载的实现类
  • @Activate,在扩展类上使用,可通过group、value配置扩展类的加载规则,可通过order指定扩展类的加载次序

dubbo SPI扩展类示例

下面以一个示例说明dubbo SPI的使用方式。假设存在一个接口定义了发起网络请求的方法,扩展类分别实现了使用http、https协议的请求方式

为了方便演示定义一个接口用于扩展

1
2
3
4
5
6
// 接口  com.connorma.dubbo_spi_learn.RequestInterface
@SPI("http")
public interface RequestInterface {

void call(URL url);
}

其中,@SPI注解用于标记一个扩展点接口,http表示默认使用的扩展类的key

然后添加两个实现类:

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
// 实现类1  http=com.connorma.dubbo_spi_learn.HttpRequestCaller
public class HttpRequestCaller implements RequestInterface {

static {
System.out.println(HttpRequestCaller.class.getName());
}

@Override
public void call(URL url) {
System.out.println("do call with http, url: " + url.toFullString());
}
}

// 实现类2 https=com.connorma.dubbo_spi_learn.HttpsRequestCaller
public class HttpsRequestCaller implements RequestInterface {

static {
System.out.println(HttpsRequestCaller.class.getName());
}

@Override
public void call(URL url) {
System.out.println("do call with https, url: " + url.toFullString());
}
}

并添加文件META-INF/dubbo/com.connorma.dubbo_spi_learn.RequestInterface,内容为:

1
2
http=com.connorma.dubbo_spi_learn.HttpRequestCaller
https=com.connorma.dubbo_spi_learn.HttpsRequestCaller

文件内容是按行的键值对,为每个扩展类指定一个key;之前@SPI注解指定的默认扩展类是key为http的com.connorma.dubbo_spi_learn.HttpRequestCaller

执行代码,加载扩展类和调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
ExtensionLoader<RequestInterface> loader = ExtensionLoader.getExtensionLoader(RequestInterface.class);

Map<String, String> params1 = new HashMap<>();
params1.put("type", "http");
URL url1 = new URL("test-call", "localhost", 8080, params1);

Map<String, String> params2 = new HashMap<>();
params2.put("type", "https");
URL url2 = new URL("test-call", "localhost", 8080, params2);

RequestInterface defReqCaller = loader.getDefaultExtension();
defReqCaller.call(url1);
defReqCaller.call(url2);

输出为:

1
2
3
4
com.connorma.dubbo_spi_learn.HttpRequestCaller
com.connorma.dubbo_spi_learn.HttpsRequestCaller
do call with http, url: test-call://localhost:8080?type=http
do call with http, url: test-call://localhost:8080?type=http2

这里演示的代码额外的引入了URL参数,实际在这个例子里并没有作用,可以结合使用@Adaptive的示例来看

使用@Adaptive根据URL参数动态选用扩展类

在以上的代码中提供了两个扩展类,key分别为http和https,并在接口定义时指定了默认使用key为http的扩展类

接下来将演示通过@Adaptive注解,根据URL携带的参数,动态选取扩展类

只需要为接口的方法上添加@Adaptive注解:

1
2
3
4
5
6
7
// 接口  com.connorma.dubbo_spi_learn.RequestInterface
@SPI("http")
public interface RequestInterface {

@Adaptive(value = "type")
void call(URL url);
}

加载扩展类时使用getAdaptiveExtension()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ExtensionLoader<RequestInterface> loader = ExtensionLoader.getExtensionLoader(RequestInterface.class);

Map<String, String> params1 = new HashMap<>();
params1.put("type", "http");
URL url1 = new URL("test-call", "localhost", 8080, params1);

Map<String, String> params2 = new HashMap<>();
params2.put("type", "https");
URL url2 = new URL("test-call", "localhost", 8080, params2);

RequestInterface defReqCaller = loader.getDefaultExtension();
defReqCaller.call(url1);
defReqCaller.call(url2);

RequestInterface adaptiveReqCaller = loader.getAdaptiveExtension();
adaptiveReqCaller.call(url1);
adaptiveReqCaller.call(url2);

输出为:

1
2
3
4
5
6
7
com.connorma.dubbo_spi_learn.HttpRequestCaller
com.connorma.dubbo_spi_learn.HttpsRequestCaller
do call with http, url: test-call://localhost:8080?type=http
do call with http, url: test-call://localhost:8080?type=https
call with adaptive extension:
do call with http, url: test-call://localhost:8080?type=http
do call with https, url: test-call://localhost:8080?type=https

可以看到,当指定使用adaptive extension时,@Adaptive生效,根据URL参数type的值,分别调用了key为http和https的两个扩展类

使用@Activate动态选用扩展类

了解了@Adaptive的功能之后,@Activate注解的功能也就了然了:在根据URL参数选用扩展类之上,增加了优先级更高的使用group参数选取,并且多个扩展类之间可以指定次序

对应@Activate注解的字段,ExtensionLoader类的getActivateExtension函数允许指定group和key获取扩展类示例

dubbo SPI加载扩展类流程

dubbo扩展类的加载和实例化等操作在ExtensionLoader中进行,其中还涉及到几个类:

  • LoadingStrategy,加载策略接口,可指定扩展类声明文件的路径、包含/排除的包名等;LoadingStrategy本身就由java SPI加载实现类来使用——所以说,只要我们自定义一个LoadingStrategy,就可以在其中自定义扩展类声明文件路径等信息

扩展类的查找由getExtensionClasses()进而调用loadExtensionClasses(),最终进入了loadDirectory() –> loadDirectoryInternal(),不过最终的查找、解析扩展类声明文件、加载类的方式与java SPI并无大的差异,只是在最终缓存类对象时,根据@Adaptive和@Activate注解使用的不同情况,分类进行了缓存

扩展类的实例化和后置处理

ExtensionLoader中关键的方法:

  • 使用getExtension()方法,默认的加载方式
  • 使用getAdaptiveExtension()方法,处理@Adaptive注解
  • 使用getActivateExtension()方法,处理@Activate注解

扩展类的实例化和后置处理中,以createAdaptiveExtension()方法内部为例,关键代码为:

1
2
3
4
5
6
7
8
9
10
// 实例化
T instance = (T) getAdaptiveExtensionClass().newInstance();
// 类比spring Ioc处理过程中的BeanPostProcessor,调用ExtensionPostProcessor进行初始化前的处理
instance = postProcessBeforeInitialization(instance, null);
// 对扩展类的setter依赖注入
injectExtension(instance);
// 类比spring Ioc处理过程中的BeanPostProcessor,调用ExtensionPostProcessor进行初始化完成后的处理
instance = postProcessAfterInitialization(instance, null);
// init操作,处理对象的生命周期
initExtension(instance);