面试官:Java 的 SPI 都不了解?这很难让你经过啊!
导言
今天和咱们共享一下一个在 Java
开发中非常重要的概念—— SPI(Service Provider Interface)
。SPI
直译叫做服务供给者接口,是一种用于动态加载服务的机制。它不仅能够协助咱们构建愈加灵敏和可扩展的运用程序,还能让咱们的代码愈加简练和易于保护。期望经过本文,咱们能够对 SPI
有一个全面而深化的了解,并能学会在实践项目中去运用它。
Java SPI
机制概述
界说与开展
SPI
是一种服务发现机制,它答应咱们的运用程序在运行时动态地发现和加载服务供给者。简略来说,SPI
便是经过一种标准化的办法来进行功用扩展,而无需修正中心代码。这种机制使得运用程序能够愈加灵敏地习惯不同的需求和环境。
SPI
是 Java
的一个内置标准,Java
中 JDBC
便是运用 SPI
机制来加载不同的数据库驱动,如 MySQL
、PostgreSQL
等。跟着 Java
渠道的开展,SPI
机制也逐步被广泛运用于 Java
生态中的各种其他场景,如日志结构、音讯行列等。作为 Java
的标准扩展机制,SPI
极大地简化了插件化开发,使得运用更易于扩展。
SPI
机制的组成要素
SPI
机制首要由以下几个要害组件构成(以 JDBC
中 MySQL
驱动程序为例):
- 服务接口:界说服务的标准接口,一切服务供给者有必要完成此接口。
java.sql.Driver
- 服务供给者:完成了服务接口的详细完成类。
com.mysql.cj.jdbc.Driver
- 装备文件:坐落
META-INF/services
目录下的文件,文件名是服务接口的全约束名,文件内容是服务供给者完成类的全约束名列表。
- 服务加载器:
ServiceLoader
类,担任读取装备文件并加载服务供给者。java.util.ServiceLoader
总结:经过上述这几个要害要素,咱们不难看出,其实 SPI
机制的中心思维便是:解耦合。它拟定了一套接口标准和一套服务发现机制,将服务的详细完成转移到运用之外,经过标准化装备的办法动态进行服务的加载,进步的运用的灵敏性和扩展性。
Java SPI
的作业原理及源码剖析
作业原理
Java SPI
机制经过 ServiceLoader
类来完成服务的动态加载。ServiceLoader
会查找 META-INF/services
目录下的装备文件,然后依据装备文件中的信息加载相应的服务供给者。
源码剖析
接下来,咱们经过阅览源码的办法,来看一下 ServiceLoader
的作业流程,搞清楚 ServiceLoader
怎么解析并加载服务的,咱们就把握了 SPI
的作业原理了。
先来看一下 ServiceLoader
类的成员变量:
public final class ServiceLoader<S> implements Iterable<S> {
// 装备文件目录
private static final String PREFIX = "META-INF/services/";
// 需要被 SPI 加载的服务
private final Class<S> service;
// 用于加载和实例化 SPI 服务的类加载器
private final ClassLoader loader;
// 创立 ServiceLoader 时的拜访操控上下文
private final AccessControlContext acc;
// 按实例化次序缓存 SPI 服务供给者
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 当时的懒查找迭代器
private LazyIterator lookupIterator;
}
下面,咱们以 JDBC
加载数据库驱动程序时的代码片段为例,看一下 SPI
是怎么运用的:
public class DriverManager {
static {
// 经过查看体系特点 jdbc.properties 加载初始 JDBC 驱动程序,然后运用 ServiceLoader 机制
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
}
private static void loadInitialDrivers() {
// 加载 java.sql.Driver 类型的服务,回来 ServiceLoader 实例
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// ServiceLoader 完成了 Iterable 接口偏重写了 iterator 办法,调用 iterator 办法回来一个迭代器
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
// 迭代器的遍历操作,获取一切可用的服务供给者实例
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
}
在这段代码中:
DriverManager
在静态代码块中调用了loadInitialDrivers
办法。ServiceLoader.load(Driver.class)
:创立了一个ServiceLoader
实例,该实例担任查找并加载完成了Driver
接口的一切服务供给者。loadedDrivers.iterator()
:获取一个迭代器,用于遍历一切已加载的Driver
实例。
能够看到,当运用 SPI
机制动态加载服务时,首要是经过 ServiceLoader.load
办法来完成的,这个办法会创立一个 ServiceLoader
实例。然后调用 iterator
办法,经过回来的迭代器获取一切可用的服务供给者实例。当调用 iterator
办法时,在办法内部 ServiceLoader
会先判别缓存 providers
中是否有数据:假如有,则直接回来缓存 providers
的迭代器;假如没有,则回来懒查找迭代器的迭代器。接下来,咱们来看下这部分的源码:
// service是需要被加载的 SPI 接口类型
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 获取当时线程上下文的类加载器,用于加载 SPI 服务,然后调用重载结构办法。
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
// 创立 ServiceLoader 实例
return new ServiceLoader<>(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 办法,从头加载 SPI 服务
reload();
}
public void reload() {
// 清空缓存中一切已实例化的 SPI 服务
providers.clear();
// 创立懒查找迭代器,用于推迟加载服务供给者。
lookupIterator = new LazyIterator(service, loader);
}
// Iterable 接口完成,回来一个匿名内部类迭代器
public Iterator<S> iterator() {
return new Iterator<S>() {
// 已缓存的 SPI 服务供给者的迭代器
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
// 优先判别缓存中是否存在,有则回来
if (knownProviders.hasNext())
return true;
// 没有,则回来懒查找迭代器的迭代器
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
从上述源码中能够看出,假如缓存中没有的话,那么会履行懒查找迭代器 lookupIterator
的办法,下面咱们看下 LazyIterator
类中的中心办法:hasNextService
与 hasNextService
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// 拼接 META-INF/services/ + SPI 接口的全约束名
String fullName = PREFIX + service.getName();
// 经过类加载器,加载 fullName 途径的资源文件,也便是 SPI 的装备文件
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;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 依据从装备文件中解析到的 `SPI` 完成类的全约束名,经过反射获取其 Class 目标
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
// 类型查验,校验下供给的 SPI 完成是否为 SPI 服务接口类型
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
// 创立 SPI 服务目标
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
}
好了,到这儿的话 Java SPI
的中心代码咱们根本现已剖析完了,经过上述对 ServiceLoader
的源码剖析,信任咱们对 Java SPI
机制的作业原理现已有了深化的了解,正所谓“实践出真知”,咱们能够去自界说 SPI
手动实践一下啦~
API
与 SPI
的差异
API (Application Programming Interface)
API
是运用程序编程接口,界说了一组规矩和协议,用于不同软件组件之间的交互。API
一般由一组函数、办法、类、变量等组成,为开发者供给了拜访特定功用或数据的办法。API
的规划意图是为了封装复杂性,供给一个明晰、共同的接口,使得开发者能够更便利地运用底层功用。经过 API
,开发者能够运用预界说的功用而无需了解其内部完成细节。
SPI (Service Provider Interface)
SPI
是一种服务供给者接口,它界说了一种服务的标准接口,答应不同的服务供给者完成这个接口。SPI
的首要意图是为了完成服务的动态发现和加载,然后进步体系的灵敏性和可扩展性。与 API
不同,SPI
着重的是服务供给者的发现和加载,而不是直接供给功用。
综上所述,API
与 SPI
的本质差异在于:
API
由服务供给方供给接口标准,界说了怎么运用其功用,并向外部露出这些接口。SPI
由服务调用方供给接口标准,界说了一个标准接口,然后由不同的服务供给者完成这个接口,然后完成服务的动态发现和加载。
差异比照
为了便利了解,请看下图:
SPI
机制的优劣势
优势
- 解耦服务接口与完成:将服务接口和完成别离,使得服务接口无需重视服务完成类的详细完成,完成了服务接口与服务完成的解耦。
- 便于扩展和保护:比如在新增服务供给者时,只需增加新的完成类和装备文件,无需修正现有代码。
缺乏
- 强依靠类加载器:
SPI
强依靠于类加载器,它的完成类有必要放置在运用的类途径下才干被动态的发现和加载,这约束了服务发现的灵敏性。 - 不能按需加载:
SPI
会对类途径下的完成进行悉数加载,在很多服务供给者的状况下,加载进程或许会有功用开支。
Spring
结构中的 SPI
Spring
结构并没有直接运用 Java
的 SPI
机制,而是采用了相似 SPI
的机制完成了自己的扩展点机制。以 Spring Boot
的主动安装为例:Spring Boot
的主动安装机制经过扫描 spring.factories
文件中的装备,加载相应的主动装备类,而这种约好装备的办法便是经过 SPI
机制完成的。
按需加载
Spring Boot
的主动装备机制可经过条件注解(如 @ConditionalOnClass
、@ConditionalOnMissingBean
等)来决议是否加载某个装备类。这种办法使得 Spring Boot
能够依据当时环境和依靠状况,按需加载装备类,避免了 Java SPI
中悉数加载形成的不必要的功用开支。
关于 Spring Boot
主动安装的原理,请看我的这篇文章 SpringBoot 主动安装原理
还有哪些 SPI
运用事例?
- JDBC:
JDBC
运用SPI
机制来加载不同的数据库驱动。例如,MySQL
和PostgreSQL
都有各自的JDBC
驱动完成,但它们都完成了java.sql.Driver
接口。经过SPI
机制,JDBC
能够动态加载所需的数据库驱动,而无需硬编码。 - Dubbo:
Dubbo
是一个高功用的Java RPC
结构,它运用SPI
机制来扩展其功用。Dubbo
经过META-INF/dubbo/
目录下的装备文件来加载各种扩展点,如协议、过滤器、注册中心等。这使得Dubbo
具有高度的可扩展性和灵敏性。 - SLF4J:它运用
SPI
机制来发现和加载详细的日志完成。用户能够依据需要挑选或替换日志完成,而无需修正运用程序代码。
结语
到这儿,关于 Java
的 SPI
机制就介绍完了,感谢咱们的阅览!假如你有任何疑问或主张,欢迎在谈论区留言沟通。
更多精彩内容,请微信查找并重视【Java驿站】大众号。