SPI 在 Java 中的实现与应用

本文涉及的产品
RDS MySQL DuckDB 分析主实例,基础系列 4核8GB
RDS Agent(兼容OpenClaw),2核4GB
RDS DuckDB + QuickBI 企业套餐,8核32GB + QuickBI 专业版
简介: sharding-jdbc 是一款用于分库分表的中间件,在数据库分布式场景中,为了保证数据库主键的唯一性,会采取相应的主键生成策略,而主键生成策略有很多种实现。sharding-jdbc 在主键生成策略使用了 SPI 进行装配。

1 SPI 的概念
API
API 在我们日常开发工作中是比较直观可以看到的,比如在 Spring 项目中,我们通常习惯在写 service 层代码前,添加一个接口层,对于 service 的调用一般也都是基于接口操作,通过依赖注入,可以使用接口实现类的实例。

简单形容就是这样的:

图 1:API
如上图所示,服务调用方无需关心接口的定义与实现,只进行调用即可,接口、实现类都是由服务提供方提供。服务提供方提供的接口与其实现方法就可称为 API,API 中所定义的接口无论是在概念上还是具体实现,都更接近服务提供方(实现方),通常接口与实现类在同一包中;

SPI
如果我们将接口的定义放在调用方,服务的调用方定义一个接口规范,可以由不同的服务提供者实现。并且,调用方能够通过某种机制来发现服务提供方,通过调用接口使用服务提供方提供的功能,这就是 SPI 的思想。

SPI 的全称是 Service Provider Interface,字面意思就是服务提供者的接口,是由服务提供者定义的接口。

图 2:SPI
服务提供方按接口规范实现服务,服务调用方通过某种机制为这个接口寻找到这个服务, SPI 的特点很明显:接口的定义(调用方提供)与具体实现是隔离的(服务提供方提供),使用接口的实现类需要依赖某种服务发现机制。

通过对比,我们可以看出接口在 API 与 SPI 中的含义还是有很大的不同,总的来说,API 中的接口是更像是服务提供者给调用者的一个功能列表,而 SPI 中更多强调的是,服务调用者对服务实现的一种约束。

2 为什么要使用 SPI
面向接口编程:面向对象的设计与编程中,我们经常强调 “依赖抽象而不是具体”,这样做就是为了实现高内聚、低耦合,提供代码灵活性和可维护性等等。
提供标准标准但没有具体实现的业务场景:SPI 机制的使用场景就是没有统一实现标准的业务场景。一般就是,服务调用方有定义好的标准接口,但是没有统一的实现,需要服务提供方提供其具体实现。
解耦:SPI 机制优势就是低耦合。将接口的定义以及具体实现分离,可以实现运行时根据业务实际场景启用或者替换具体实现类。
3 Java 中如何使用 SPI

接口定义、服务实现这些我们都轻车熟路,调用方直接依赖接口不依赖具体实现,这是依赖倒置原则,我们在 Spring 项目中使用 API 时,会使用 Spring 的依赖注入(DI)来实现 “服务发现”,同样地,SPI 的重点也是如何让调用方发现接口的具体实现,也就是上文提到的某种服务发现机制。

SPI 的服务发现机制是由 ServiceLoader 提供,ServiceLoader 是 Java 在 JDK 6 中引进的新特性,它主要是用来发现并加载一系列的 service provider。当服务的提供者,提供了服务接口的一种实现之后,只需要在 jar 包的 META-INF/services/ 目录里同时创建一个以服务接口命名的文件,该文件的内容就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该 jar 包 META-INF/services/ 里的配置文件找到具体的实现类名,并加载实现类,完成依赖的注入,这就是 Java SPI 的服务发现机制。

下面就结合一个示例来具体讲讲。若有这样一个需求,需要使用一个接口来完成内容查找服务,接口的具体实现交给其他服务提供方,实现可能是基于文件系统的查找,也可能是基于数据库的查找。

(1)定义接口

作为服务调用方,需要先定义一套接口规范,用来规范之后的服务提供方按规范来实现接口。这样,不管是谁提供的实现方法,调用方都可以按相同的方式来调用接口。

创建一个项目 search-standard,提供一个查找服务标准接口,先定义调用方的内容查找方法:

package com.gwz.spi.learn;

import java.util.List;
// 查找服务接口
public interface Search {
// 按关键字查询内容方法
String searchDoc(String keyword);
}
这个接口就是给服务提供方来实现的,将它打包发布 mvn clean install,确保 maven 仓库中有该 jar 包,之后提供者在项目中就可以引入这个 jar 包了。

(2)服务实现

制定并发布完标准接口后,我们假设第一个服务提供方提供了一种文件查找的实现。新建项目 search-file,并引入刚才发布的标准接口 jar 包:


com.gwz.search
search-standard
1.0-SNAPSHOT

实现定义好的接口:

package com.gwz.file.search;

import com.gwz.spi.learn.Search;

public class FileSearch implements Search {

@Override
public String searchDoc(String keyword) {
    return "文件查找:" + keyword;
}

}
并在项目的 resources 的目录下,创建 META-INF/services 目录,然后以前面定义的接口名 com.gwz.spi.learn.Search 创建文件,并在文件中写入实现类的全限定名。

一个服务方的简单实现就完成了,用 maven 打成 jar 包,发布到 maven 之后就可以提供给调用方使用了。

接着,按上述实现方式,再创建一个项目 search-database 使用数据库的实现接口:

package com.gwz.database.search;

import com.gwz.spi.learn.Search;

public class DatabaseSearch implements Search {
@Override
public String searchDoc(String keyword) {
return "数据库查找:" + keyword;
}
}
同样,打包发布后就可以提供给调用方使用了。

(3)服务发现

接下来关键的一步就是服务发现,服务发现需要依赖 ServiceLoader 的使用。创建一个新项目 search-sever,引入上面打好的两个提供方的 jar 包。



com.gwz.search
search-file
1.0-SNAPSHOT


com.gwz.search
search-database
1.0-SNAPSHOT


虽然每个服务提供者对于接口都有不同的实现,但是作为调用者来说,它并不需要关心具体的实现类,我们要做的是通过接口来调用服务提供者实现的方法。

下面,就是关键的服务发现环节,使用 ServiceLoader 来加载具体的实现类,调用方只需调用对应接口方法即可。

package com.gwz.search.impl;

import com.gwz.spi.learn.Search;
import java.util.ServiceLoader;

public class SearchDoc {

public static void main(String[] args) {
    new SearchDoc().searchDocByKeyWord("hello world");
}

public void searchDocByKeyWord(String keyWord) {

    ServiceLoader<Search> searchServiceLoader = ServiceLoader.load(Search.class);

    for (Search search : searchServiceLoader){
        String doc = search.searchDoc(keyWord);
        System.out.println(doc);
    }
}

}
测试结果:

可以看到,通过定义的 Search 发现了两个实现类。整段代码中没有出现过具体的服务实现类,操作都是通过接口调用。

4 Java SPI 原理
了解了 SPI 的工作流程,我们应该有以下疑问:

为什么要在服务提供方的 META-INF/services/ 目录里同时创建一个以服务接口命名的文件?放在其他目录里面不行吗,文件名我随意命名不可以吗?
为什么文件的内容需要是实现该服务接口的具体实现类?
ServiceLoader 是如何发现接口的服务类的?
接下来我们看一下 ServiceLoader 的源码就可以解答了。

上述例子中,通过 ServiceLoader.load (Search.class) 来加载 Search 接口的实现类,我们知道 Java 加载类都离不开类加载器,查看 ServiceLoader.load () 方法的源码就会发现,SPI 加载类使用的是线程上下文加载器,可通过 java.lang.Thread#setContextClassLoader 方法进行设置,若未设置则会从父线程中继承,在应用程序全局都未设置的情况下,默认是应用程序类加载器,线程上下文加载器加载所需的 SPI 代码,实际上是父类加载器请求子类加载器来完成加载类的动作,打破了双亲委派模型的层次结构。

load 方法实际上构造了一个 ServiceLoader 实例对象,该对象保持了一个加载 SPI 类代码的线程上下文加载器的引用 loader、一个所需要加载实现类的接口类型的引用 service、一个已经成功服务提供者(接口的具体实现类)的缓存 providers。

上述例子中我们使用了 foreach 遍历调用接口方法,本质上是通过调用迭代器 Iterable 的 next () 方法来获取的具体实现类,因为 ServiceLoader 实现了 Iterable 这一接口,而整个服务发现的核心,就在它的 iterator () 方法中。

这里面有两个关键的东西,一是 providers,在迭代器中会先从服务类缓存中查找服务类,若查不到就用 lookupIterator 查找。接着往下看 LazyIterator 的 hasNext () 和 next ()源码实现。

acc 是一个安全管理器,debug 看值是 null,所以看 hasNextService () 和 nextService () 方法就可以了。在 hasNextService () 方法中,会通过 PREFIX + service.getName () 来拼接一个资源路径 URL,拼接这个 URL 的目的就是为了能供通过该 URL 获取文件中的内容,看到这里应该就明白了为什么实现 SPI 服务时,需要创建名为 META-INF/services/ 的文件夹,以及以接口名命名的文件,这是由 PREFIX 与 service.getName () 决定的,那么下图中的实现类名称是如何来的呢?

既然我们这里可以通过 PREFIX + service.getName () 来拼接一个资源路径 URL,那是不是可以通过该 URL 来获取所指向的文件资源的内容?答案是肯定的,正如源码中 parse () 方法的实现一样, 通过 Java 提供的 InputStream 读取 URL 指向的文件内容。

在读取文件内容时 ServiceLoader 主要做了几件事:

获取文本内容(实现类全路径名);
校验内容合法性(是否符合 Java 类命名规范);
若 providers 缓存中不存在该实现类(未加载),则保存该实现类全路径名以供下面流程进行加载该类;

接下来,在 nextService () 方法中,则会通过上述解析到的实现类全路径名加载这个实现类,然后实例化对象,最终放入缓存中去。

在迭代器的迭代过程中,会基于 Java 反射机制去完成所有实现类的实例化,这样就可以调用接口方法来使用 SPI 服务提供方实现的具体功能。

综上,Java SPI 的实现是依靠 ServiceLoader,ServiceLoader 通过使用线程上下文类加载器来加载 SPI 接口实现类,实现类的全路径名需配置在 META-INF/services/ 目录下,以接口名命名的文件内容中,ServiceLoader 会读取文件中的全路径名,通过反射机制实例化接口实现类。

5 应用
(1)日志框架 slf4j

SPI 的实际应用,最常见的应该是日志框架 slf4j,它就是个日志门面,并不提供具体的实现,需要绑定其他具体实现。例如可使用 log4j2 作为具体的绑定器,只需要在 pom 中引入 slf4j-log4j12,就可以使用具体功能。


org.slf4j
slf4j-api
2.0.3


org.slf4j
slf4j-log4j12
2.0.3

引入项目后,点开它的 jar 包看一下具体结构:

jar 包的 META-INF.services 里面,通过 SPI 注入了 Reload4jServiceProvider 这个实现类,它实现了 SLF4JServiceProvider 这一接口,在它的初始化方法 initialize () 中,会完成初始化等工作,后续可以继续获取到 LoggerFactory 和 Logger 等具体日志对象。

(2)DriverManager

DriverManager 是 JDBC 里管理数据库驱动的的工具类。一个数据库可能会存在不同实现的数据库驱动。我们在使用特定的驱动实现时,通过一个简单的配置就而不用修改代码就可以达到效果。 例如,在运用 Class.forName ("com.mysql.jdbc.Driver") 加载 mysql 驱动后,会执行其中的静态代码把 driver 注册到 DriverManager 中。

查看 JDBC 源码可知,驱动实现接口 java.sql.Driver,然后通过 registerDriver 把当前 driver 加载到 DriverManager 中。查看 DriverManager 的源码,可以看到其内部的静态代码块 loadInitialDrivers 方法中使用了 ServiceLoader:

查看 mysql-connector-java 的 jar 包在 META-INF/services 接口路径文件中的内容,可以看到 com.mysql.jdbc.Driver。

(3)sharding-jdbc

sharding-jdbc 是一款用于分库分表的中间件,在数据库分布式场景中,为了保证数据库主键的唯一性,会采取相应的主键生成策略,而主键生成策略有很多种实现。sharding-jdbc 在主键生成策略使用了 SPI 进行装配。

源码中的 ShardingRule.class 主要封装分库分表的策略规则,包括主键生成,核心在于底层调用的 register 方法,其中也是使用了 ServiceLoader:

这里就是应用的 SPI 机制,再看下 Jar 包的 META-INF/services/ 目录:

有两个实现,对应了 sharding-jdbc 的提供的两种生成策略分别是雪花算法和 UUID。

6 总结

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
相关文章
|
7月前
|
人工智能 算法 Java
Java与AI驱动区块链:构建智能合约与去中心化AI应用
区块链技术和人工智能的融合正在开创去中心化智能应用的新纪元。本文深入探讨如何使用Java构建AI驱动的区块链应用,涵盖智能合约开发、去中心化AI模型训练与推理、数据隐私保护以及通证经济激励等核心主题。我们将完整展示从区块链基础集成、智能合约编写、AI模型上链到去中心化应用(DApp)开发的全流程,为构建下一代可信、透明的智能去中心化系统提供完整技术方案。
455 3
|
9月前
|
存储 数据采集 搜索推荐
Java 大视界 -- Java 大数据在智慧文旅旅游景区游客情感分析与服务改进中的应用实践(226)
本篇文章探讨了 Java 大数据在智慧文旅景区中的创新应用,重点分析了如何通过数据采集、情感分析与可视化等技术,挖掘游客情感需求,进而优化景区服务。文章结合实际案例,展示了 Java 在数据处理与智能推荐等方面的强大能力,为文旅行业的智慧化升级提供了可行路径。
Java 大视界 -- Java 大数据在智慧文旅旅游景区游客情感分析与服务改进中的应用实践(226)
|
9月前
|
机器学习/深度学习 数据采集 数据可视化
Java 大视界 -- 基于 Java 的大数据可视化在城市空气质量监测与污染溯源中的应用(216)
本文探讨Java大数据可视化在城市空气质量监测与污染溯源中的创新应用,结合多源数据采集、实时分析与GIS技术,助力环保决策,提升城市空气质量管理水平。
Java 大视界 -- 基于 Java 的大数据可视化在城市空气质量监测与污染溯源中的应用(216)
|
9月前
|
存储 监控 数据可视化
Java 大视界 -- 基于 Java 的大数据可视化在企业生产运营监控与决策支持中的应用(228)
本文探讨了基于 Java 的大数据可视化技术在企业生产运营监控与决策支持中的关键应用。面对数据爆炸、信息孤岛和实时性不足等挑战,Java 通过高效数据采集、清洗与可视化引擎,助力企业构建实时监控与智能决策系统,显著提升运营效率与竞争力。
|
9月前
|
Java 大数据 数据处理
Java 大视界 -- 基于 Java 的大数据实时数据处理在工业互联网设备协同制造中的应用与挑战(222)
本文探讨了基于 Java 的大数据实时数据处理在工业互联网设备协同制造中的应用与挑战。文章分析了传统制造模式的局限性,介绍了工业互联网带来的机遇,并结合实际案例展示了 Java 在多源数据采集、实时处理及设备协同优化中的关键技术应用。同时,也深入讨论了数据安全、技术架构等挑战及应对策略。
|
9月前
|
数据采集 搜索推荐 Java
Java 大视界 -- Java 大数据在智能教育虚拟学习环境构建与用户体验优化中的应用(221)
本文探讨 Java 大数据在智能教育虚拟学习环境中的应用,涵盖多源数据采集、个性化推荐、实时互动优化等核心技术,结合实际案例分析其在提升学习体验与教学质量中的成效,并展望未来发展方向与技术挑战。
|
7月前
|
消息中间件 缓存 Java
Spring框架优化:提高Java应用的性能与适应性
以上方法均旨在综合考虑Java Spring 应该程序设计原则, 数据库交互, 编码实践和系统架构布局等多角度因素, 旨在达到高效稳定运转目标同时也易于未来扩展.
527 8
|
8月前
|
人工智能 Java API
Java与大模型集成实战:构建智能Java应用的新范式
随着大型语言模型(LLM)的API化,将其强大的自然语言处理能力集成到现有Java应用中已成为提升应用智能水平的关键路径。本文旨在为Java开发者提供一份实用的集成指南。我们将深入探讨如何使用Spring Boot 3框架,通过HTTP客户端与OpenAI GPT(或兼容API)进行高效、安全的交互。内容涵盖项目依赖配置、异步非阻塞的API调用、请求与响应的结构化处理、异常管理以及一些面向生产环境的最佳实践,并附带完整的代码示例,助您快速将AI能力融入Java生态。
1274 12
|
8月前
|
安全 Java API
Java SE 与 Java EE 区别解析及应用场景对比
在Java编程世界中,Java SE(Java Standard Edition)和Java EE(Java Enterprise Edition)是两个重要的平台版本,它们各自有着独特的定位和应用场景。理解它们之间的差异,对于开发者选择合适的技术栈进行项目开发至关重要。
1302 1
|
9月前
|
设计模式 XML 安全
Java枚举(Enum)与设计模式应用
Java枚举不仅是类型安全的常量,还具备面向对象能力,可添加属性与方法,实现接口。通过枚举能优雅实现单例、策略、状态等设计模式,具备线程安全、序列化安全等特性,是编写高效、安全代码的利器。