引言
今天和大家分享一下一个在 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驿站】公众号。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |