Java9新特性系列(module&spi)

上两篇已经深入分析了Java9新特性系列(深入理解模块化),以及Java9新特性系列(module&maven&starter),有读者又提到了与模块化相关的spi,本篇将进行分析。

SPI是什么?

提到SPI呢,就不得不提一下API:

API:Application Programming Interface,即应用程序编程接口,在程序外部进行调用

SPI:Service Provider Interface,服务提供商接口

SPI核心思想是什么?

我们知道,一个系统中会有很多模块,比如数据库模块、日志模块、调度模块、各种业务模块等等,每一类的模块都有很多种实现,
数据库可以用mysql、oracle等,日志可以用log4j、logback等,那么对于不同的场景有不同的选型,如何能做到可插拔呢,那就是SPI了。

可插拔原则可以理解为系统与插件的关系,系统提供了一些接口,第三方插件进行实现。

面向接口编程,不绑定实现,为了在模块装配的时候不在代码中动态去指定具体的实现,就需要去发现具体的实现,即服务发现,其实就类似于回调,只不过回调的时候需要找到具体的实现,spi就帮我们做了去寻找实现的工作。

这一思想在模块化设计中尤为重要。

SPI实现方式?

SPI具体的实现方式分两种:

  • 应用系统自身提供默认实现
  • 第三方提供实现

SPI有什么例子呢?

下面我们就以jdk中的jdbc模块儿进行分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;

int getMajorVersion();

int getMinorVersion();

boolean jdbcCompliant();

public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}

我们可以看到,java.sql.Driver接口定义了每个实现必须实现的一些方法,接下来我们就看具体的实现:

  • mysql

META-INF/services/java.sql.Driver

1
2
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

在这个文件中指定了具体的实现

  • oracle

META-INF/services/java.sql.Driver

1
oracle.jdbc.OracleDriver

在JDBC4以前,我们还需要使用比如Class.forName(“com.mysql.jdbc.Driver”)的方式来装载驱动。

如上图所示:
JDBC也基于spi的机制来发现驱动提供商了,可以通过META-INF/services/java.sql.Driver文件里指定实现类的方式来暴露驱动提供者。

其中,META-INF/services/是固定的,java.sql.Driver为接口对应的package,文件中为具体的实现类。

SPI如何被框架发现呢?

框架可以使用java提供的java.util.ServiceLoader类得到SPI的实现。我们来看下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
private static void loadInitialDrivers() {
...
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 获取具体的实现类
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});

...

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);
}
}
}

使用ServiceLoader<XXX> loadedDrivers = ServiceLoader.load(XXX.class);,实际上就是到具体的路径下读取文件内容。

SPI应用

用过阿里Dubbo的开发者都知道,Dubbo对JDK中SPI进行了扩展和改进,这个在以后dubbo相关的文章中再进行介绍~

好了,本期就讲到这里,下期我们讲讲Java9中到另一个新工具JShell,敬请期待~