一、SPI的机制定义
SPI,全称Service Provider Interface, 是Java中提供的一种服务发现机制,它允许应用程序动态地加载和使用第三方提供的服务实现,而无需在代码中显示引用这些实现类。通过将服务接口和其实现分离,从而具备更加好的可扩展性和可维护性。
Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制,将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。允许服务提供者通过实现调用者特定的接口来扩展系统,并且调用者能够动态地发现和加载这些服务提供者的实现类。
二、SPI的实现原理
2.1 Java SPI 实际上是"基于接口的编程+策略模式+配置文件"组合实现的动态加载机制。
2.2 实现SPI的步骤如下:
-
在服务提供者的JAR文件内,创建一个名为
META-INF/services
的目录。 -
在该目录下创建一个文件,文件名为服务接口的全限定名。
-
在该文件中列出服务接口的所有实现类。
2.3 ServiceLoader动态加载接口实现类的原理:类加载ClassLoader+迭代器模式
1. 初始化ServiceLoader类,传入自己想要加载的接口类
ServiceLoader<MyDriver> loader = ServiceLoader.load(MyDriver.class);
2. 内部还是用的当前线程上下文的ClassLoader
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
3. 创建懒加载迭代器LazyIterator
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();
}
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
4. 通过遍历迭代器,寻找服务的实现类
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
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);
}
throw new Error(); // This cannot happen
}
private static final String PREFIX = "META-INF/services/";
5. 发现服务提供者的目录为fullName:"META-INF/services/"+服务类名
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
三、SPI在框架中的使用场景
调用者根据实际使用需要,动态启用或扩展的实现策略
-
日志门面接口实现类加载SLF4J加载不同提供商的日志实现类
-
数据库驱动加载接口实现类的加载JDBC加载不同类型数据库的驱动
-
SpringBoot的SPI机制:我们可以在 spring.factories 中加上我们自定义的自动配置类,事件监听器或初始化器等;
四、实际使用案例:
-
比如我现在有一个获取业务方数据的功能,获取数据的方式和想要的数据信息由我自己决定,但是想要的业务方数据很多,而且数据提供方不确定,也不知道有多少。此时可以采用JAVA中的SPI机制思想,由自己决定获取数据的接口,未来服务方只需要提供这个接口的实现类即可。
调用方接口制定:
public interface ThirdPartyQueryService {
/**
* 获取第三方服务信息
*
* @param projectId 项目id
* @param infoId 信息id
* @param userId 用户id
* @return 信息
*/
ThirdPatyData queryThirdPartyData(Integer projectId, Long infoId, Long userId);
}