Java SPI 机制
SPI的英文全称为Service Provider Interface, 简单翻译为服务提供者接口, 是jdk提供给“服务提供商”或者“插件开发者”使用的接口.
Java是一门面向对象的语言, 在模块之间一般会采用面向接口的编程的方式, 而在时机编程过程中,api的实现是封装在jar里面的, 当我们想要换一种实现方式的时候, 还需要生成新的jar替换以前实现的类. 使用spi机制就可以直接使用一个新的jar替换旧的实现jar即可.
i.e. 日志模块, xml解析模块, jdbc驱动模块, 这些都在装载模块的时候不具体指明实现类,在使用时给予服务返现的机制动态加载, .
SPI 规范
定义服务的通用接口, 不同的实现厂商/开发者基于通用接口及通具体的实现:
- 服务提供者在jar包的META-INF/services/目录下, 新建一个文件为SPI接口“全限定类名”, 文件内容为通用接口的具体实现类的“全限定类名”
- 将SPI所在jar放在主程序的classpath中
- 服务调用方使用 java.util.ServiceLoader 动态加载具体的实现类到JVM中
SPI经典案例
数据驱动
jdk提供了java.sql.Driver的通用接口, 不同的数据库厂商会提供对应的数据库驱动, 如:mysql, oracle, h2 驱动等, 以h2 为例, 实现类为org.h2.Driver, 在对应的jar下可以看到META-INFO/services目录下名为java.sql.Driver的文件, 其内容为org.h2.Dr
在我们使用JDBC获取连接的时候, 通常调用DriverManager.getConnection()来获取连接对像, 在DriveManager被加载的时候会调用ServiceLoader动态获取classpath下注册的驱动实现
public class DriverManager {
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
...
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
...
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
...
}
});
}
思考
当项目中引入多个jdbc驱动的时候(不同的环境使用不同的数据库), 她会具体的使用哪个驱动呢
答案: 在getDriver的时候会根据传入的url 做matche返回对应的驱动, 所以即使在项目导入了多个数据库驱动, 在调用getDriver/getConnection的时候也会根据url 返回正确的数据库Driver
public class DriverManager {
@CallerSensitive
public static Driver getDriver(String url)
throws SQLException {
// Walk through the loaded registeredDrivers attempting to locate someone
// who understands the given URL.
for (DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerClass)) {
...
if(aDriver.driver.acceptsURL(url)) {// 在这个地方会去匹配对应的url来返回对应Driver
// Success!
println("getDriver returning " + aDriver.driver.getClass().getName());
return (aDriver.driver);
...
}
}
}
Sl4j
slf4j是典型的日志框架的通用接口, 用的较多的实现有log4j和logback, 当要用logback替换掉log4j的时候只需要引入对应的jar即可, 当然这个需要你代码中使用的都是接口编程. 具体的就不再详细展开了.
SPI 实例
number-operate-api
定义通用接口
public interface NumberOperate {
int operate(int a,int b);
}
numberAdd
依赖number-operate-api, 并做加法实现, 在resources/META-INF/services下创建名称为com.larzhu.numberopearteapi.NumberOperate 的文件, 文件内容为实现类的全限定类名:com.larzhu.numberadd.NumberSubtraction
public class NumberAddService implements NumberOperate {
@Override
public int operate(int a, int b) {
return a+b;
}
}
numberSubtraction
依赖number-operate-api, 并做减法实现, 在resources/META-INF/services下创建名称为com.larzhu.numberopearteapi.NumberOperate 的文件, 文件内容为实现类的全限定类名:com.larzhu.numberadd.NumberSubtraction
public class NumberSubtraction implements NumberOperate {
@Override
public int operate(int a, int b) {
return a-b;
}
}
Number-operate-Consumer
具体的调用者, 依赖于通用接口及对应的实现jar, 利用serviceLoader加载spi对应的实现类,
- 当引入numberAdd.jar的时候计算结果为“—15”,
- 当引入numberSubtraction.jar的时候结果为“—5”,
- 当同时引入两个jar, 会输出“—5—15”
public class Test {
public static void main(String[] args) {
ServiceLoader<NumberOperate> numberOperates = ServiceLoader.load(NumberOperate.class);
for (NumberOperate op: numberOperates) {
int operate = op.operate(10, 5);
System.out.printf("---"+operate);
}
}
}
总结
SPI机制是在java jdk提供的一种将接口与实现分开的一种重要方式, 只提供接口, 具体的实现由子项目/厂商提供,
- 提供通用接口
- 实现jar在resource/META-INF/services目录下创建以接口全限定类名为名称的文件 , 内容为实现类的全限定类名.
- 服务调用时 用java.util.ServiceLoader.load(Interface.class) 来加载具体的实现
- 当有多个实现jar被导入时,会将多个实现同时加载, 可以学习jdbc, DriverManger的方式通过url或其他的参数来区分加载具体的某个实现类
Spring, Dubbo都有自己的spi机制, Spring的spi是在META-INF/spring.factories 文件中定义对应的接口与实现, springboot在EnableAutoConfiguration注解的时候import了AutoConfigurationImportSelector ,AutoConfigurationImportSelector里面的 selectImports 会被掉用, 而这个方法中就会利用SpringFacotriesLoader加载META-INF/spring.factories下的所有spi类.