您当前的位置:首页 > 电脑百科 > 程序开发 > 架构

解析SPI机制:实现灵活插件式架构

时间:2023-08-27 15:31:16  来源:稀土掘金  作者:半亩方塘立身

什么是SPI机制

SPI(Service Provider Interface)JAVA中一种服务提供者接口的设计模式,它提供了一种机制,允许组件在不同的实现之间进行插拔,从而实现松耦合的架构。SPI通常用于实现插件化、可扩展的应用程序,使开发人员能够轻松地添加、替换或定制系统中的功能模块。

当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的工具类是:java.util.ServiceLoader

SPI示例

一个常见的 SPI 示例是 Java 的日志框架 SLF4J(Simple Logging Facade for Java)。SLF4J 允许应用程序在运行时选择不同的日志实现,而无需修改代码。以下是一个简化的示例:

  1. 定义日志接口: 首先,定义一个日志接口,例如 Logger,其中包含了常见的日志方法,如 info(), debug(), error() 等。

 
java
复制代码
public interface Logger { void info(String message); void debug(String message); void error(String message); // ...其他日志方法 }
  1. 编写日志实现类: 然后,为不同的日志实现编写实现类,这些实现类分别对接各种日志框架,比如 Log4j、Logback、JDK Logging 等。

 
java
复制代码
// Log4jLogger.java public class Log4jLogger implements Logger { private org.Apache.log4j.Logger logger; public Log4jLogger(Class<?> clazz) { logger = org.apache.log4j.Logger.getLogger(clazz); } // 实现 Logger 接口的方法,使用 Log4j 进行日志记录 // ... }
  1. SPI 配置文件:META-INF/services 目录下创建一个文件,以接口全限定名为文件名,写入实现类的全限定名。对于 SLF4J,该文件名为 org.slf4j.Logger

 
shell
复制代码
# 文件:META-INF/services/org.slf4j.Logger com.example.logging.Log4jLogger
  1. 使用日志接口: 在应用程序中,您只需要使用 SLF4J 提供的接口进行日志记录,而不需要关心具体的日志实现。

 
java
复制代码
import org.slf4j.LoggerFactory; public class MAIn { private static final Logger logger = LoggerFactory.getLogger(Main.class); public static void main(String[] args) { logger.info("This is an info message."); logger.debug("This is a debug message."); logger.error("This is an error message."); } }

通过这种方式,我们可以轻松地更改底层的日志实现,而不需要修改应用程序的代码。这就是 SPI 的一个实际应用示例,它展示了如何通过接口、实现类、SPI 配置文件以及运行时加载机制,实现插拔式的日志框架。

SPI原理

SPI(Service Provider Interface)的原理涉及 Java 的类加载机制、反射以及配置文件加载。以下是SPI的工作原理:

  1. 接口定义: 首先,您需要定义一个接口,该接口描述了一组服务或功能的方法。这个接口将作为服务提供者和服务使用者之间的约定。

  2. 服务提供者接口: 在SPI中,您通常会定义一个专门的接口,用于服务提供者注册和实例化。这个接口可能包括方法用于获取特定的服务实例。

  3. 服务提供者实现: 不同的模块、库或插件可以提供针对接口的不同实现。每个实现都需要提供一个特定的类,实现服务提供者接口,并在实现类中提供相关的功能代码。

  4. 服务配置文件: SPI的核心是一个配置文件,通常命名为 META-INF/services/<接口全限定名>。在这个文件中,您列出了实现您接口的类的名称。

    • 对于每个接口,都可以在 META-INF/services 目录下创建一个以接口全限定名为文件名的文件。
    • 在这个文件中,每一行包含一个实现类的全限定名。这些实现类是用于提供特定服务的。
  5. 加载机制: 当需要使用某个服务时,您可以通过 Java 的类加载机制以及反射来加载并实例化相应的实现类。具体过程如下:

    • 通过 SPI 配置文件,找到接口对应的实现类的全限定名列表。
    • 使用 ClassLoader 加载这些实现类的类对象。
    • 使用反射实例化这些类,得到服务提供者的实例。
  6. 服务使用者: 服务使用者是通过服务提供者接口来获取服务实例的组件。它可以从已注册的服务提供者中选择一个合适的实现,然后使用该实现的功能。

在运行时,SPI机制允许系统自动加载并实例化服务提供者的实现类,从而实现插拔式的架构。这样,您可以在不修改核心代码的情况下,通过添加新的实现类来扩展系统功能,实现更好的可扩展性和灵活性。

下面我们看下JDK中ServiceLoader<S>方法的具体实现:

首先,ServiceLoader实现了Iterable接口,所以它有迭代器的属性,这里主要都是实现了迭代器的hasNextnext方法。这里主要都是调用的lookupIterator的相应hasNextnext方法,lookupIterator是懒加载迭代器。

其次LazyIterator中的hasNext方法,静态变量PREFIX就是”META-INF/services/”目录,这也就是为什么需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件。

最后,通过反射方法Class.forName()加载类对象,并用newInstance方法将类实例化,并把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型)然后返回实例对象。

所以我们可以看到ServiceLoader不是实例化以后,就去读取配置文件中的具体实现,并进行实例化。而是等到使用迭代器去遍历的时候,才会加载对应的配置文件去解析,调用hasNext方法的时候会去加载配置文件进行解析,调用next方法的时候进行实例化并缓存。

所有的配置文件只会加载一次,服务提供者也只会被实例化一次,重新加载配置文件可使用reload方法

SPI机制的缺陷

通过上面的解析,可以发现,我们使用SPI机制的缺陷:

  • 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
  • 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。
  • 多个并发多线程使用 ServiceLoader 类的实例是不安全的。

Dubbo SPI机制

Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。 Dubbo 中,SPI 主要有两种用法,一种是加载固定的扩展类,另一种是加载自适应扩展类。这两种方式会在下面详细的介绍。 需要特别注意的是: 在 Dubbo 中,基于 SPI 扩展加载的类是单例的。

Dubbo SPI 优势

  1. 按需加载,Dubbo SPI配置文件采用KV格式存储,key 被称为扩展名,当我们在为一个接口查找具体实现类时,可以指定扩展名来选择相应的扩展实现,只实例化这一个扩展实现即可,无须实例化 SPI 配置文件中的其他扩展实现类,避免资源浪费,此外通过 KV 格式的 SPI 配置文件,当我们使用的一个扩展实现类所在的 jar 包没有引入到项目中时,Dubbo SPI 在抛出异常的时候,会携带该扩展名信息,而不是简单地提示扩展实现类无法加载。这些更加准确的异常信息降低了排查问题的难度,提高了排查问题的效率。;
  2. 增加扩展类的 IOC 能力,Dubbo 的扩展能力并不仅仅只是发现扩展服务实现类,而是在此基础上更进一步,如果该扩展类的属性依赖其他对象,则 Dubbo 会自动的完成该依赖对象的注入功能;
  3. 增加扩展类的 AOP 能力,Dubbo 扩展能力会自动的发现扩展类的包装类,完成包装类的构造,增强扩展类的功能;

Dubbo SPI原理

Dubbo 扩展加载流程

Dubbo 加载扩展的整个流程如下:

主要步骤为 4 个:

  • 读取并解析配置文件
  • 缓存所有扩展实现
  • 基于用户执行的扩展名,实例化对应的扩展实现
  • 进行扩展实例属性的 IOC 注入以及实例化扩展的包装类,实现 AOP 特性

如何使用 Dubbo 扩展能力进行扩展

下面以扩展协议为例进行说明如何利用 Dubbo 提供的扩展能力扩展 Triple 协议。

(1) 在协议的实现 jar 包内放置文本文件:META-INF/dubbo/org.apache.dubbo.remoting.api.WireProtocol


 
properties
复制代码
tri=org.apache.dubbo.rpc.protocol.tri.TripleHttp2Protocol

(2) 实现类内容


 
java
复制代码
@Activate public class TripleHttp2Protocol extends Http2WireProtocol { // ... }

说明下:Http2WireProtocol 实现了 WireProtocol 接口

(3) Dubbo 配置模块中,扩展点均有对应配置属性或标签,通过配置指定使用哪个扩展实现。比如:


 
xml
复制代码
<dubbo:protocol name="tri" />

从上面的扩展步骤可以看出,用户基本在黑盒下就完成了扩展。

Dubbo 扩展的应用

Dubbo 的扩展能力非常灵活,在自身功能的实现上无处不在。

Dubbo 扩展能力使得 Dubbo 项目很方便的切分成一个一个的子模块,实现热插拔特性。用户完全可以基于自身需求,替换 Dubbo 原生实现,来满足自身业务需求。

Dubbo SPI 源码分析

上面看了 Dubbo SPI 通过 ExtensionLoader加载扩展。 ExtensionLoadergetExtensionLoader 方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoadergetExtension 方法获取拓展类对象。这其中,getExtensionLoader 方法用于从缓存中获取与拓展类对应的 ExtensionLoader,若缓存未命中,则创建一个新的实例。该方法的逻辑比较简单,本章就不进行分析了。下面我们从 ExtensionLoadergetExtension 方法作为入口,对拓展类对象的获取过程进行详细的分析。

carbon (5).png

上面代码的逻辑比较简单,首先检查缓存,缓存未命中则创建拓展对象。下面我们来看一下创建拓展对象的过程是怎样的。

createExtension 方法的逻辑稍复杂一下,包含了如下的步骤:

  1. 通过 getExtensionClasses 获取所有的拓展类
  2. 通过反射创建拓展对象
  3. 向拓展对象中注入依赖
  4. 将拓展对象包裹在相应的 WrApper 对象中

以上步骤中,第一个步骤是加载拓展类的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。在接下来的章节中,将会重点分析 getExtensionClasses 方法的逻辑,以及简单介绍 Dubbo IOC 的具体实现。

我们在通过名称获取拓展类之前,首先需要根据配置文件解析出拓展项名称到拓展类的映射关系表(Map<名称, 拓展类>),之后再根据拓展项名称从映射关系表中取出相应的拓展类即可。相关过程的代码分析如下:

这里也是先检查缓存,若缓存未命中,则通过 synchronized 加锁。加锁后再次检查缓存,并判空。此时如果 classes 仍为 null,则通过 loadExtensionClasses 加载拓展类。下面分析 loadExtensionClasses 方法的逻辑。

loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。SPI 注解解析过程比较简单,无需多说。下面我们来看一下 loadDirectory 做了哪些事情。

loadDirectory 方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源。我们继续跟下去,看一下 loadResource 方法的实现。

loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法用于主要用于操作缓存,该方法的逻辑如下:

如上,loadClass 方法操作了不同的缓存,比如 cachedAdaptiveClass、cachedWrapperClasses 和 cachedNames 等等。除此之外,该方法没有其他什么逻辑了。


作者:半亩方塘立身
链接:https://juejin.cn/post/7271597656118624275
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


Tags:插件式架构   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
解析SPI机制:实现灵活插件式架构
什么是SPI机制SPI(Service Provider Interface)是Java中一种服务提供者接口的设计模式,它提供了一种机制,允许组件在不同的实现之间进行插拔,从而实现松耦合的架构。SPI通常用于...【详细内容】
2023-08-27  Search: 插件式架构  点击:(252)  评论:(0)  加入收藏
▌简易百科推荐
对于微服务架构监控应该遵守的原则
随着软件交付方式的变革,微服务架构的兴起使得软件开发变得更加快速和灵活。在这种情况下,监控系统成为了微服务控制系统的核心组成部分。随着软件的复杂性不断增加,了解系统的...【详细内容】
2024-04-03  步步运维步步坑    Tags:架构   点击:(5)  评论:(0)  加入收藏
大模型应用的 10 种架构模式
作者 | 曹洪伟在塑造新领域的过程中,我们往往依赖于一些经过实践验证的策略、方法和模式。这种观念对于软件工程领域的专业人士来说,已经司空见惯,设计模式已成为程序员们的重...【详细内容】
2024-03-27    InfoQ  Tags:架构模式   点击:(13)  评论:(0)  加入收藏
哈啰云原生架构落地实践
一、弹性伸缩技术实践1.全网容器化后一线研发的使用问题全网容器化后一线研发会面临一系列使用问题,包括时机、容量、效率和成本问题,弹性伸缩是云原生容器化后的必然技术选择...【详细内容】
2024-03-27  哈啰技术  微信公众号  Tags:架构   点击:(10)  评论:(0)  加入收藏
DDD 与 CQRS 才是黄金组合
在日常工作中,你是否也遇到过下面几种情况: 使用一个已有接口进行业务开发,上线后出现严重的性能问题,被老板当众质疑:“你为什么不使用缓存接口,这个接口全部走数据库,这怎么能扛...【详细内容】
2024-03-27  dbaplus社群    Tags:DDD   点击:(11)  评论:(0)  加入收藏
高并发架构设计(三大利器:缓存、限流和降级)
软件系统有三个追求:高性能、高并发、高可用,俗称三高。本篇讨论高并发,从高并发是什么到高并发应对的策略、缓存、限流、降级等。引言1.高并发背景互联网行业迅速发展,用户量剧...【详细内容】
2024-03-13    阿里云开发者  Tags:高并发   点击:(6)  评论:(0)  加入收藏
如何判断架构设计的优劣?
架构设计的基本准则是非常重要的,它们指导着我们如何构建可靠、可维护、可测试的系统。下面是这些准则的转换表达方式:简单即美(KISS):KISS原则的核心思想是保持简单。在设计系统...【详细内容】
2024-02-20  二进制跳动  微信公众号  Tags:架构设计   点击:(36)  评论:(0)  加入收藏
详解基于SpringBoot的WebSocket应用开发
在现代Web应用中,实时交互和数据推送的需求日益增长。WebSocket协议作为一种全双工通信协议,允许服务端与客户端之间建立持久性的连接,实现实时、双向的数据传输,极大地提升了用...【详细内容】
2024-01-30  ijunfu  今日头条  Tags:SpringBoot   点击:(10)  评论:(0)  加入收藏
PHP+Go 开发仿简书,实战高并发高可用微服务架构
来百度APP畅享高清图片//下栽のke:chaoxingit.com/2105/PHP和Go语言结合,可以开发出高效且稳定的仿简书应用。在实现高并发和高可用微服务架构时,我们可以采用一些关键技术。首...【详细内容】
2024-01-14  547蓝色星球    Tags:架构   点击:(115)  评论:(0)  加入收藏
GraalVM与Spring Boot 3.0:加速应用性能的完美融合
在2023年,SpringBoot3.0的发布标志着Spring框架对GraalVM的全面支持,这一支持是对Spring技术栈的重要补充。GraalVM是一个高性能的多语言虚拟机,它提供了Ahead-of-Time(AOT)编...【详细内容】
2024-01-11    王建立  Tags:Spring Boot   点击:(124)  评论:(0)  加入收藏
Spring Boot虚拟线程的性能还不如Webflux?
早上看到一篇关于Spring Boot虚拟线程和Webflux性能对比的文章,觉得还不错。内容较长,抓重点给大家介绍一下这篇文章的核心内容,方便大家快速阅读。测试场景作者采用了一个尽可...【详细内容】
2024-01-10  互联网架构小马哥    Tags:Spring Boot   点击:(115)  评论:(0)  加入收藏
相关文章
    无相关信息
站内最新
站内热门
站内头条