你真的理解Java SPI吗?从源码到实战的深度思考 [特殊字符]

你真的理解Java SPI吗?从源码到实战的深度思考 [特殊字符]

目录

前言:重新认识SPI

这篇文章《Java SPI机制初探》来自得物技术团队,系统介绍了Java SPI的概念、原理以及在JDBC、Spring、Dubbo等框架中的应用。文章从SPI的基础概念出发,深入分析了ServiceLoader的源码实现,并结合实际场景讲解了SPI的优缺点和解决方案。

说实话,SPI这个名词一直出现在我耳边,但从未真正了解过。这次正好借着这篇文章来学习一下,看看和自己印象中的是否一致。看完之后,发现SPI其实没有我想象中那么复杂,但背后的设计思想确实值得深入思考。

核心思考一:SPI的本质是什么?

API vs SPI:控制权的反转

文章开篇就对比了API和SPI的区别,这个对比让我对SPI有了更清晰的认识:

  • API:接口实现方同时负责接口定义和接口实现,接口控制权在服务提供方
  • SPI:服务调用方负责接口定义,不同的接口实现方根据接口定义可以有不同的实现,接口控制权在服务调用方

这个区别很关键。我自己的理解是:在API模式下,服务提供者需要提供接口定义和对应的实现,但是不论怎样以后的拓展都需要服务提供者进行开发并打包。虽然调用方没有必要进行更改,但是对于服务提供方来说,增加了额外的负担。

如果这时保持接口定义不变,而是给出足够的空间来让下游调用方挑选不同的接口实现,也就是说让下游调用方来挑选上游的服务提供者,这样的话应该就是SPI。

这个理解对吗?我觉得从本质上看,SPI的核心就是控制权的反转

在API模式下,服务提供方定义接口并提供实现,调用方被动接受;而在SPI模式下,调用方定义接口,由不同的服务提供方来提供实现,调用方可以动态选择使用哪个实现。

这种设计在框架开发中特别有意义。比如日志框架SLF4J定义了统一的接口,具体的实现(Log4j、Logback、Logback Classic等)由不同的厂商提供,用户可以根据需要选择和切换实现,而不需要修改代码。这就是SPI的价值所在。

Java中的SPI实践:ServiceLoader

在Java中,SPI是通过ServiceLoader来实现的。ServiceLoader是JDK中实现SPI的核心,它本质上就是读取约定目录(/META-INF/services/)下对应接口全限定命名的文件,然后通过反射全量加载文件中定义的所有接口实现类,从而将接口与实现进行解耦。

这个设计很巧妙。通过约定优于配置的方式,在指定的目录下按照约定的格式定义实现类,框架就能自动发现和加载这些实现类。这样既保持了灵活性,又避免了复杂的配置。

在Java开发中,这种思想其实很常见。比如Spring的自动装配、Servlet的ServletContainerInitializer、JDBC的Driver加载,本质上都是SPI机制的不同实现。

核心思考二:ServiceLoader的优与劣

ServiceLoader的工作原理

文章对ServiceLoader的源码做了详细分析,我重点看了几个核心点:

  1. 约定目录/META-INF/services/接口全限定名,文件内容是实现类的全限定名
  2. 懒加载:只有调用iterator()方法或遍历的时候才会去加载对应的实现类
  3. 缓存机制:已经加载的实现类会缓存在LinkedHashMap中,避免重复加载
  4. 顺序迭代:按照配置文件中的顺序依次加载实现类

懒加载这个设计挺有意思的。它不会一次性加载所有实现类,而是在需要的时候才加载,这样可以避免不必要的性能开销。不过懒加载也有个问题:如果某个实现类加载失败,后续的合法类也可能无法读取。因为ServiceLoader使用的是顺序迭代器,一旦中间某个类出问题,整个迭代过程就会中断。

ServiceLoader的优势

从文章的总结来看,ServiceLoader主要有这几个优势:

  1. 解耦:接口与实现分离,无需在代码中硬编码实现类。这一点在框架开发中特别重要,可以让框架保持足够的灵活性,同时也不增加用户的负担。
  2. 扩展性:新增实现只需添加配置,无需修改已有代码。这个优势在插件化架构中体现得淋漓尽致,比如Eclipse的插件系统、IntelliJ IDEA的插件机制,本质上都是类似的思路。
  3. 动态加载:能够实现插件化架构,通过动态加载具体的服务实现增减模块,而无需重新发布整个系统。

ServiceLoader的局限性

但是,ServiceLoader也有几个明显的局限性:

  1. 线程不安全:文章提到"ServiceLoader非线程安全,不能保证单例"。这个确实是个问题,在多线程环境下使用需要格外小心。
  2. 性能开销:每次迭代都重新加载文件(可以通过缓存解决),并且会全量配置文件中指定的所有实现类。即使某些实现类不会被用到,也会被加载进来,造成资源浪费。
  3. 无健壮性:配置错误(如类未找到)会抛出异常而非优雅降级。这个在文章中也提到了,顺序迭代器的问题。
  4. 缺少命名机制:Java SPI配置文件中只是简单列出了所有扩展实现,没有给他们命名,无法在程序中准确引用。如果只需要某个特定的实现,却要把所有实现都加载一遍,确实有点浪费。

这些局限性在实际项目中可能会带来一些问题,特别是在对性能和资源消耗比较敏感的场景下。

核心思考三:Dubbo如何优化SPI?

Dubbo的改进:按需加载

文章提到,Dubbo的ExtensionLoader在JDK SPI的基础上做了优化,主要是为了解决资源浪费的问题。Dubbo通过key-value方式给每个实现类命名,可以按需加载。

这个改进很实用。比如配置文件可以这样写:

demoSpiImpl = com.xxx.xxx.DemoSpiImpl anotherImpl = com.xxx.xxx.AnotherSpiImpl 

需要哪个实现,就通过名称来获取,而不是一次性加载所有实现。这样就避免了不必要的资源浪费。

ExtensionLoader<DemoSpi> extensionLoader =ExtensionLoader.getExtensionLoader(DemoSpi.class);DemoSpi demoSpi = extensionLoader.getExtension("demoSpiImpl");

对比JDK的ServiceLoader,Dubbo的ExtensionLoader支持按名称获取实现,确实更加灵活和高效。

IOC和AOP支持

文章还提到,Dubbo的ExtensionLoader支持依赖注入(IOC)和包装类(AOP)。在createExtension方法中,Dubbo会:

  1. 创建扩展实例
  2. 向实例中注入依赖
  3. 将扩展对象包裹在相应的Wrapper对象中
  4. 初始化扩展对象

这个设计让我想到Spring的Bean生命周期管理,Dubbo的ExtensionLoader也有类似的机制。特别是Wrapper的支持,可以实现AOP特性,比如为Protocol接口实现添加监听、日志等包装逻辑。

多个加载目录

Dubbo支持从多个目录加载扩展配置:

  • META-INF/services/
  • META-INF/dubbo/
  • META-INF/dubbo/internal/

这种设计考虑到了不同场景的需求。services/目录是为了兼容JDK SPI,dubbo/是Dubbo自己的扩展,dubbo/internal/是Dubbo内部使用的扩展。这种分级管理,既保持了兼容性,又提供了足够的扩展空间。

核心思考四:实战中的坑与最佳实践

加载顺序问题

文章提到了一个很实际的问题:多个实现类加载顺序问题如何解决?

ServiceLoader加载服务提供者实现的顺序由classpath中jar包的顺序决定,如果你的逻辑依赖于获取到的第一个实现,在不同环境下可能会出现加载顺序不一致导致的异常问题。

这个坑确实容易踩。比如在开发环境和测试环境,classpath中jar包的加载顺序可能不一样,导致应用行为不一致。文章给出的建议是:“避免依赖加载顺序是最佳实践”。

我觉得这个建议很中肯。如果确实需要控制顺序,可以考虑:

  1. 使用注解:比如Spring的@Order@Priority,或者自定义排序注解
  2. 配置显式声明:在配置文件中定义顺序,或者在代码中手动排序
  3. 使用有序集合:比如LinkedHashMap来维护加载顺序

不过,最稳妥的方式还是避免依赖加载顺序。如果业务逻辑对顺序有要求,那应该通过显式的方式来控制,而不是依赖SPI机制的默认行为。

类重复加载问题

文章还提到了另一个问题:类重复加载问题如何解决?

如果classpath中存在多个jar包都申明了同一个接口的相同实现类,或者同一个jar包被不同类加载器加载多次,会导致同一个实现类被多次加载和实例化,可能导致单例失效或资源冲突。

这个问题确实没有万能的解法。文章提到可以通过identity方法判断,也可以通过模块化框架,但是都比较繁琐。

我觉得,最好的办法就是避免类重复,从根本上避免这个问题。具体来说:

  1. 依赖管理:在Maven或Gradle中明确依赖,避免引入重复的jar包
  2. 类加载器管理:合理设计类加载器层级,避免同一类被多个类加载器加载
  3. SPI配置管理:规范SPI配置文件的放置位置,避免冲突

在实际项目中,可以通过mvn dependency:tree之类的工具检查依赖冲突,及时发现和解决问题。

term查询是什么?

在批注中提到了一个疑问:“term查询的话用keyword更好,什么是term查询?”

这个问题其实和前面的字段类型选择有关。在Elasticsearch中,term查询是精确匹配查询,不会对查询词进行分词处理。比如查询status="ACTIVE",term查询会精确匹配"ACTIVE"这个完整的值。

为什么term查询用keyword更好?因为keyword类型不进行分词,存储的就是原始值,所以term查询可以直接匹配。而text类型会进行分词,原始的"ACTIVE"可能被分词成"act""ive"之类的token,term查询就匹配不上了。

这个知识点在之前那篇ES的文章中其实也提到过,只是当时没太在意。现在结合SPI的学习,发现技术之间的关联性还是挺强的。很多设计模式和思想,在不同的领域都有体现。

总结与后续计划

看完这篇文章,我对SPI有了更深入的理解。以前只知道SPI是用来动态加载实现的,但没有深入思考背后的设计思想。这次学习,让我对以下几个方面有了更清晰的认识:

  1. 控制权的反转:SPI的本质是将接口定义的控制权从服务提供方转移到服务调用方,这样调用方可以根据需要选择不同的实现,而服务提供方只需要按照接口定义提供实现即可。
  2. 约定优于配置:ServiceLoader通过约定目录和文件格式来实现自动发现和加载,避免了复杂的配置。这种思想在很多框架中都有体现,值得借鉴。
  3. 权衡与取舍:ServiceLoader虽然实现了灵活性和扩展性,但也有性能、线程安全、健壮性等问题。在实际应用中,需要根据场景权衡选择。Dubbo的ExtensionLoader就是在JDK SPI的基础上做了优化,解决了资源浪费等问题。
  4. 最佳实践:避免依赖加载顺序、避免类重复加载、使用注解或配置控制顺序,这些都是实战中需要注意的点。

SPI机制虽然看起来简单,但背后体现的设计思想很有价值。掌握了这个机制,以后再看框架的源码,比如Spring Boot的自动装配、Dubbo的扩展机制,会更容易理解。

参考资料

Read more

【c++篇】:深入剖析vector--模拟实现属于自己的c++动态数组

【c++篇】:深入剖析vector--模拟实现属于自己的c++动态数组

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨ ✨ 个人主页:余辉zmh–ZEEKLOG博客 ✨文章所属专栏:c++篇–ZEEKLOG博客 文章目录 * 前言 * 一.`vector`类的默认成员函数 * 整体框架 * 构造函数 * 析构函数 * 拷贝构造函数 * 赋值运算符重载函数 * 测试 * 二.`vector`类的访问和迭代器相关函数 * 访问函数 * 迭代器函数 * 测试 * 三.`vector`类的容量相关函数 * 容量大小函数 * 扩容函数 * 测试 * 四.`vector`类的修改相关函数 * 插入函数 * 删除函数 * 测试 * 五.迭代器失效问题 * 迭代器失效的原因 * 避免迭代器失效的方法 * 示例代码分析 * 总结 * 六.完整代码文件 * `vector.h`文件

By Ne0inhk
Java项目:Java脚手架项目的意义和环境搭建(一)

Java项目:Java脚手架项目的意义和环境搭建(一)

文章目录 * 前言 * 1、目的和意义 * 2、前置知识 * 一、框架 * 二、建立Git仓库并建立分支 * 三、环境的搭建 * 1. 创建后端工程 * 2. 中间件的部署 * 2.1 初始化数据库 * 2.2 配置 nacos 的配置 * 2.3 配置 redis 的配置 * 2.4 docker-compose的容器化编排 * 2.5 环境搭建 * 结果 * END 鸡汤: ● 世界或许嘈杂,但你的节奏独一无二。慢慢来,你奔赴的远方,正因你的坚持而闪闪发光。 ● 累了就深呼吸,对自己说:“我已经做得很好了。” 你值得被温柔以待,包括被你自己。 前言 1、

By Ne0inhk
Java外功基础(1)——Spring Web MVC

Java外功基础(1)——Spring Web MVC

1.前置知识 1.1 Tomcat 定义:Tomcat是一个开源的轻量级Web(Http)服务器和Servlet容器。它实现了Java Servlet等Java EE规范的核心功能,常用于部署和运行Java Web应用程序 。换言之,Tomcat就是一个严格遵循Servlet规范开发出来的、可以独立安装和运行的Java Web服务器/Servlet容器核心功能:Servlet容器:支持Servlet的执行,处理HTTP请求和响应Web服务器:提供静态资源(如HTML)的访问能力,支持基本的HTTP服务安装与版本对应: tomcat官网:Apache Tomcat®目录结构:bin:存放可执行文件,如startup.batconf:存放配置文件lib:存放Tomcat运行所需的jar文件logs:存储日志文件temp:存放临时文件,如上传的文件或缓存数据webapps:默认web应用部署目录work:服务器的工作目录,存放运行时生成的临时文件(编译文件) 1.2 Servlet 1.2.1 定义

By Ne0inhk
Java 大视界 -- Java 大数据机器学习模型在自然语言处理中的少样本学习与迁移学习融合

Java 大视界 -- Java 大数据机器学习模型在自然语言处理中的少样本学习与迁移学习融合

Java 大视界 -- Java 大数据机器学习模型在自然语言处理中的少样本学习与迁移学习融合 * 引言:从虚拟偶像情感计算到语言智能的 “显微镜” 革命 * 正文:从理论架构到工业落地的全链条创新 * 一、NLP 领域的 “数据贫困” 困境与破局逻辑 * 1.1 少样本场景的核心挑战 * 1.2 Java 大数据的 “三维穿透” 技术架构 * 二、工业级融合模型的技术实现与代码解析 * 2.1 预训练模型迁移优化(BERT 医疗领域深度微调) * 2.2 原型网络(Prototypical Network)少样本分类 * 三、实战案例:从医疗语义分析到跨境电商智能客服 * 3.1 医疗场景:罕见病实体识别的 “样本逆袭” * 3.2 跨境电商:阿拉伯语商品类目分类的

By Ne0inhk