Java SPI
[TOC]
1. Java SPI
SPI (Service Provider Interface)
是 JDK
内置的一种服务提供发现机制。 SPI 主要是用来扩展程序的,所以对开发者来说用到的不是很多。主要在一些开源框架、软件厂商为了扩展功能时使用。比如我们的系统中有一个 日志模块 , 关于日志现在有很多可以选择的 sl4j,log4j 等等,面向对象编程为了满足可插拔的业务很少会显式指定写死使用哪个日志框架,否则的话如果我们要换日志框架的话需要修改代码。这个时候我们就需要一种可插拔的服务发现机制,而 SPI 就是干这件事的,它为指定的接口寻找实现。
2. 数据库驱动的应用SPI案例
拿数据库驱动举个例子,一般是 Mysql 使用 SPI 机制定义接口,然后不同的厂商根据自己的需要去实现
DriverManager
是JDBC
里管理和注册不同数据库驱动的工具类,针对一个数据库可能有不同的数据库驱动的实现。
比如Mysql
驱动在最初的时候我们通过Class.forName("com.mysql.jdbc.Driver")
这个来加载指定的驱动,然后获取连接。
在JDBC4.0
之后不需要Class.forName
来加载,直接获取连接即可。这里就是使用了SPI
机制,在Java
中定义了java.sql.Driver
,并没有具体实现,具体的实现都是由不同的厂商来提供的。在mysql-connector-java-5.1.45.jar
这个包中,src/META-INF/services
目录下会有一个名字为java.sql.Driver
的文件里面定义着指定的驱动实现。
3. Demo
当服务的提供者提供了服务接口的实现之后,在 src/META-INF/service/
目录里同时创建一个以服务接口命名的文件,这个文件就是实现该服务接口的实现类。而当外部的程序装配这个模块的时候就能通过 src/META-INF/services/
里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。 基于这个过程就能很好的找到服务接口的实现类而不需要在代码中指定服务接口的实现。JDK提供了服务实现查找的工具类 java.util.ServiceLoader
,看下一个流程:
第一步定义一个服务的接口 IDemoAService 以及 DemoAService 和 DemoBService 2个实现类
/**
* IDemoService
*/
public interface IDemoService {
/**
* spi测试
*/
void test();
}
@Slf4j
public class DemoBService implements IDemoService {
@Override
public void test() {
log.info("demo2");
}
}
@Slf4j
public class DemoAService implements IDemoService {
@Override
public void test() {
log.info("DemoA");
}
}
第二步在 resource
目录下新建 META-INF/services
目录,并且建文件命名为(不需要后缀): cn.xisole.sky.spi.IDemoService
文件内容如下
cn.xisole.sky.spi.DemoAService
cn.xisole.sky.spi.DemoBService
第三步在客户端测试代码如下:可以看到通过JDK内置SPI 可以将配置的两个实现类都找到了 此处可以根据自己的需求来进行配置,比如我只想要DemoAService 那就可以只配 DemoAService到文件里 对于程序是无入侵的
/**
* SPIClient
*/
public class SPIClient {
public static void main(String[] args) {
ServiceLoader<IDemoService> demoServices = ServiceLoader.load(IDemoService.class);
Iterator<IDemoService> iterator = demoServices.iterator();
while (iterator.hasNext()){
IDemoService id = iterator.next();
id.test();
}
}
}
输出日志:
20:18:10.691 [main] INFO cn.xisole.sky.spi.DemoAService - DemoA
20:18:10.694 [main] INFO cn.xisole.sky.spi.DemoBService - demo2
4. Java SPI 的原理
从客户端测试的时候可以看到使用了 ServiceLoader 的类加载器去加载的 IDemoService(会破坏双亲委派模型),先了解一下它的源码如下,从下面的源码里,PREFIX
属性可以看到之前为什么要在 "META-INF/services/"
下创建文件。在 ServiceLoader 被实例化的时候,会从当前的线程里拿到当前类的加载器,加载器是个 AppClassLoader。它和 Class.forName(xxx) 这种方式加载还是有区别的,Class.forName(XXX) 的加载器是 BootStrapClassLoader
Class.forName()调用时候,Class类本身是由启动类加载器(BootStrapClassLoader)加载的,SPI也是由启动类加载器(BootStrapClassLoader)加载的,但是SPI具体的实现由各个厂商提供,BootStrapClassLoader 不可能加载到(如果对这句话不太明白的话建议刷一下另一篇文章哈 Java中的类加载器
), 所以 Class.forName()去 加载SPI实现类的时候,需要指定一个类加载器,那么这个类加载器就是调用者的类加载器 也就是刚刚说的 AppClassLoader ,这样加载就不是父加载器加载的了,这种现象就叫破坏了 双亲委派
,其实就是不是让父类加载器去加载了而是使用了自己指定的加载器加载。然后得到迭代器遍历的时候从配置文件中进行对接口实现的懒加载。
public final class ServiceLoader<S>
implements Iterable<S>
{
// 前缀,加载这个路径下的文件,呼应前面为啥要在这个路径下创建文件了
private static final String PREFIX = "META-INF/services/";
// 表示正在加载的服务的类或接口
private final Class<S> service;
// 用于定位,加载和实例化提供程序的类加载器
private final ClassLoader loader;
// ServiceLoader 被创建的时候 定义的一个访问上下文
private final AccessControlContext acc;
// 缓存服务的实现
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 当前的lazy-lookup迭代器
private LazyIterator lookupIterator;
/**
* 构造器,当被实例化的时候,从线程中获得 ClassLoader
*/
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();
}
// 方法若干···
}
5. 总结
SPI机制在实际开发中使用得场景也有很多。特别是统一标准的不同厂商实现,当开源框架或者厂商定义标准之后,具体厂商或者框架开发者实现,之后提供给开发者使用。优缺点如下:
- 优点:
使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。 - 缺点:
虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。其实这种场景下我们其实想要的是一个 Map 的形式而不是Collection形式的,Map 可能更能符合我们的需求。