SPI的全称是(Service Provider Interface)是服务提供接口的意思。如果我们不写框架性代码或者开发插件的话,对于SPI机制可能不会那么熟悉,但如果我们阅读诸如Dubbo
、JDBC
数据库驱动包、Spring
以及最近比较流行的Spring Boot
相关starter
组件源码的话,就会发现SPI机制及其思想在这些框架中有大量的应用。
我们知道系统中抽象的各个模块,往往会有很多不同的实现方案,例如我们常见的日志模块方案
、xml解析模块
、JDBC驱动模块
等。在面向对象的设计思想中,我们一般推荐模块之间的对接基于面向接口的编程方式,而不是直接面向实现类硬编码。因为,一旦代码里涉及具体的实现类的耦合,就违反了可插拔、闭开等原则,如果需要替换一种实现方式,就需要修改代码。如果我们希望实现在模块装配的时候能够不在程序硬编码指定,那就需要一种服务发现的机制(PS:不要和现在微服务的服务发现机制搞混淆了)。
JAVA中的SPI技术就是提供了这样一个为某个接口寻找服务实现类的机制,这一点也类似于Spring框架中的IOC思想
,就是将程序加载装配的控制权移到程序之外,这个机制在组件模块化
设计中非常重要!那么在JAVA中SPI机制具体是如何约定的呢?
在JAVA SPI机制中约定,当服务的提供者(例如某个新的日志组件),提供了服务接口的某种实现之后,在jar包的META-INF/services/目录中同时创建一个以该服务接口命名的文件,文件中填写了实现该服务接口具体实现类的全限定类名。这样,在当外部程序装配这个模块的时候,就可以通过该jar包中META-INF/services/目录里的配置文件找到具体的实现类路径,从而可以通过反射机制装载实例化,从而完成模块的注入。例如,我们Dubbo框架时,除了引入核心依赖jar外,还会有很多扩展组件如dubbo-monitor
,如果我们需要引入此组件只需要简单引入
就可以,而不需要做额外的集成
,主要原因就是因为该组件时以SPI机制进行的集成
。
综上所述,SPI机制实际上就是“基于接口的编程+策略模式+配置文件”组合实现的一种动态加载机制,在JDK中提供了工具类:“java.util.ServiceLoader”来实现服务查找。
JDK SPI机制实现示例
JDK中自带对SPI机制的支持,主要是涉及java.util.ServiceLoader
类的使用,接下来,我们通过一个简单的代码示例来理解下JAVA中SPI机制的实现方式吧!我们先通过一张图来看看使用JAVA SPI机制需要遵循什么规范吧:
└── src/main/java
├── cn
│ └── wudimanong
│ └── spi
│ ├── SPIService.java
│ ├── impl
│ │ ├── SpiImpl1.java
│ │ └── SpiImpl2.java
│ └── SPIMain.java
└── META-INF
└── services
└── com.wudimanong.spi.SPIService
1、我们需要在项目中创建目录META-INF\services
2、定义一个接口服务提供,如SpiService
public interface SpiService {
void execute();
}
3、分别定义两个服务接口实现类,如:SpiImpl1,SpiImpl2
public class SpiImpl1 implements SPIService {
@Override
public void execute() {
System.out.println("SpiImpl1 Hello.");
}
}
// ------------------------------------------
public class SpiImpl2 implements SPIService {
@Override
public void execute() {
System.out.println("SpiImpl2 Hello.");
}
}
4、我们在ClassPath
路径下添加一个配置文件,文件的名称是接口的全限定类名,内容则是实现类的全限定类名,如果是多个实现类则用换行符分割,文件路径如下
文件内容如下:
cn.wudimanong.spi.impl.SpiImpl1
cn.wudimanong.spi.impl.SpiImpl2
这样,我们基本上就遵循JAVA SPI的机制定义了组件基本结构,最后我们通过编写测试类,看看如果使用SPI机制,客户端代码应该如何写:
public class SPIMain {
public static void main(String[] args) {
ServiceLoader<SPIService> loaders =
ServiceLoader.load(SPIService.class);
Iterator<SPIService> spiServiceIterator = loaders.iterator();
System.out.println("classPath:" + System.getProperty("java.class.path"));
while (spiServiceIterator.hasNext()) {
SPIService spiService = spiServiceIterator.next();
System.out.println(spiService.execute());
}
}
}
可以看到在引入方加载组件模块时是通过ServiceLoader
这个类来操作的,如果我们打开该类的源码,就可以看到,其主要实现逻辑其实就是在META-INF/services/目录中查找实现类,并进行相关实例化操作。关于该类的部分关键源代码如下:
public final class ServiceLoader<S> implements Iterable<S>{
//配置文件的路径
private static final String PREFIX = "META-INF/services/";
//加载的服务类或接口
private final Class<S> service;
//已加载的服务类集合
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
//类加载器
private final ClassLoader loader;
//内部类,真正加载服务类
private LazyIterator lookupIterator;
public void reload() {
//先清空
providers.clear();
//实例化内部类
lookupIterator = new LazyIterator(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
//要加载的接口
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//类加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
//访问控制器
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
//reload方法如上
reload();
}
}
而后面查找类和创建实现类的过程就都是在LazyIterator
类完成的了,由于篇幅的关系,感兴趣的朋友可以点开源码自行阅读一番!
JDBC数据库驱动包中SPI机制分析
通过上面的描述,相信大家对Java SPI机制的实现应该是有了一个基本的认识,接下来我们以JDBC数据库驱动设计来看下Java SPI机制的真实应用场景
。我们知道通常各大数据库厂商(如Mysql、Oracle)都会根据一个统一的规范,如:java.sql.Driver
去开发各自的驱动实现逻辑。
而我们在使用的jdbc的时候客户端却是不需要改变代码的,直接引入不同的SPI接口服务即可。例如以Mysql的JDBC驱动jar来说:
这样在引入mysql驱动包后jdbc连接代码java.sql.DriverManager
,就会使用SPI机制来加载具体的jdbc实现,关键源码如下:
public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
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;
}
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) {
}
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);
}
}
}
}
可以看到粉红色代码部分,JDBC驱动管理代码就是通过原生的Java SPI机制来实现加载具体的Mysql JDBC驱动的。
Dubbo框架中SPI机制分析
需要说明的是虽然Java 提供了对SPI机制的默认实现支持,但是并不表示所有的框架都会默认使用这种Java自带的逻辑,SPI机制更多的是一种实现思想,而具体的实现逻辑,则是可以自己定义的。例如我们说Dubbo框架中大量使用了SPI技术,但是Dubbo并没有使用JDK原生的ServiceLoader,而是自己实现了ExtensionLoader
来加载扩展点,所以我们看Dubbo框架源码的时候,千万不要被配置目录是/META-INF/dubbo/internal,而不是META-INF/services/所迷惑了。
相应地如果其他框架中也使用了自定义的SPI机制实现,也不要疑惑,它们也只是重新定义了类似于ServiceLoader类的加载逻辑而已,其背后的设计思想和原理则都是一样的!例如,以Dubbo中卸载协议的代码举例:
private void destroyProtocols() {
ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
for (String protocolName : loader.getLoadedExtensions()) {
try {
Protocol protocol = loader.getLoadedExtension(protocolName);
if (protocol != null) {
protocol.destroy();
}
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
而ExtensionLoader中扫描的配置路径如下:
public class ExtensionLoader<T> {
private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class);
private static final String SERVICES_DIRECTORY = "META-INF/services/";
private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";
private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";
private static final Pattern NAME_SEPARATOR = Pattern.compile("\\s*[,]+\\s*");
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();
}
以上通过对Java自带SPI机制的示例以及对Dubbo和JDBC驱动框架中对SPI机制应用的分析,相信大家应该是有了一个总体的原理性的认识了。如果需要更加深入的了解一些细节的实现逻辑就需要大家好好去看看ServiceLoader的源码了,如果其他框架单独实现了SPI机制,其相应的实现加载工具类也需要具体看看它们的源码是怎么实现的了!