SPI 的合作作用:解耦。
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及Dubbo 的扩展实现等。
定义接口:首先,在API中定义一个接口或者抽象类作为服务接口,所有服务提供者都需要按照这个接口来实现其功能。
创建配置文件:在jar包的META-INF/services目录下,创建一个以服务接口全限定名命名的文本文件,该文件中每一行写入一个实现了服务接口的具体类的全限定名。
服务发现与加载:当应用程序需要使用某项服务时,通过ServiceLoader.load(ServiceInterface.class)方法动态加载服务接口的所有实现类,并将其实例化。这样,系统就可以在运行时根据不同的配置加载不同的服务实现。
SPI常用于框架扩展、插件机制以及模块间解耦等场景,使得系统的可扩展性和灵活性大大增强。例如,数据库驱动的加载就是Java SPI的一个典型应用案例。
对于 Java 原生 SPI,只需要满足下面几个条件:
定义服务的通用接口,针对通用的服务接口,提供具体的实现类
在 src/main/resources/META-INF/services 或者 jar包的 META-INF/services/ 目录中,新建一个文件,文件名为 接口的全名。文件内容为该接口的具体实现类的全名
将 spi 所在 jar 放在主程序的 classpath 中
服务调用方用java.util.ServiceLoader,用服务接口为参数,去动态加载具体的实现类到JVM中,然后就可以正常使用服务了
SPI其实是一种思想:约定大于配置
简单来说就是:jdk提供一个公共API,具体的实现由具体的应用程序自己完成,并约定一套规则来让java的类加载机制发现实现类所在位置
通过这个约定,就不需要把服务放在代码中了,通过模块被装配的时候就可以发现服务类了
很多开源第三方jar包都有基于SPI的实现,在jar包META-INF/services中都有相关配置文件。
如下几个常见的场景:
看看 Dubbo 的扩展实现,就知道 SPI 机制用的多么广泛:
调用ServiceLoader.load(),创建一个ServiceLoader实例对象
创建LazyIterator实例对象lookupIterator
通过lookupIterator.hasNextService()方法读取固定目录META-INF/services/下面service全限定名文件,放在Enumeration对象configs中
解析configs得到迭代器对象Iterator pending
通过lookupIterator.nextService()方法初始化读取到的实现类,通过Class.forName()初始化
从上面的步骤可以总结以下两点
实现类工程必须创建定目录META-INF/services/,并创建service全限定名文件,文件内容是实现类全限定名
实现类必须有一个无参构造函数
优点
通过 SPI 机制能够大大地提高接口设计的灵活性,可以另起jar实现,不必写在一个模块中
缺点
是Java平台中的一种标准服务,它提供了一组API,允许Java应用程序查找和访问命名以及目录服务。这个接口的设计目的是统一不同命名服务和目录服务的访问方式,使得开发人员无需关注具体的底层实现细节,就能在分布式环境中查找、绑定和管理资源。
通过JNDI,开发者可以:
JNDI常用于以下场景:
依赖引用
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>21.9.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
解释图1:
解释图2:
除此之外,在很多开源框架里面都借鉴了Java SPI的思想,提供了自己的SPI框架,比如Dubbo定义了ExtensionLoader,实现功能的扩展。Spring提供了SpringFactoriesLoader,实现外部功能的集成。
当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。通过这个约定,就不需要把服务放在代码中了,通过模块被装配的时候就可以发现服务类了。
有了SPI机制之后,Class.forName(“com.mysql.jdbc.Driver”);这条语句就不需要了,
java.util.ServiceLoader会负责到jar包的META-INF/services/java.sql.Driver中获取具体驱动实现类
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class JdbcDriverManagerDemo {
static final String MYSQL_JDBC_DRIVER = "com.mysql.cj.jdbc.Driver";
private static final String MYSQL_URL = "jdbc:mysql://localhost:3306/lelele?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC";
private static final String USER = "root";
private static final String PASSWORD = "admin123";
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// 有了SPI机制之后,这条语句就不要了,
// java.util.ServiceLoader会负责到jar包的META-INF/services/java.sql.Driver中获取具体驱动实现类
// Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(MYSQL_URL, USER, PASSWORD);
ps = conn.prepareStatement("select * from xin_stu_t_bak");
rs = ps.executeQuery();
while (rs.next()) {
System.out.println(rs.getInt("id"));
}
// 处理查询结果
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
System.out.println("ID: " + id + ", Name: " + name);
}
} catch (SQLException se) {
// 处理SQL错误
se.printStackTrace();
} catch (Exception e) {
// 处理其他异常
e.printStackTrace();
} finally {
// 关闭资源,确保在任何情况下都能正确关闭
try {
if (rs != null)
rs.close();
if (ps != null)
ps.close();
if (conn != null)
conn.close();
} catch (SQLException se2) {
se2.printStackTrace();
}
}
System.out.println("end!");
}
}
源码分析参照 本文章1.5节 具体的SPI 源码分析(SPI的核心就是ServiceLoader.load()方法)
下面是获取连接对象代码调试中的一些断点截图
? - 如果不做任何的设置,Java应用的线程的上下文类加载器默认就是AppClassLoader。在核心类库使用SPI接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到SPI实现的类。线程上下文类加载器在很多SPI的实现中都会用到。
? - 通常我们可以通过Thread.currentThread().getClassLoader()和Thread.currentThread().getContextClassLoader()获取线程上下文类加载器。
Spring Factories自动装配借用了SPI机制,区别如图:
Spring Boot的自动装配机制确实与SPI(Service Provider Interface)有关联,但并不是直接使用Java标准SPI来实现自动装配。Spring Boot借鉴了SPI的思想,在其内部设计了一套更加灵活和强大的自动配置体系。
在Spring Boot中,META-INF/spring.factories文件的使用方式类似于SPI的服务提供者配置文件(META-INF/services/下资源文件),它允许jar包声明自己提供的自动配置类。这些自动配置类通过条件注解(如@ConditionalOnClass、@ConditionalOnBean等)来检查应用环境并决定是否生效,从而实现了根据项目依赖和运行时环境进行自动装配的功能。
因此,虽然Spring Boot没有完全采用Java SPI的标准流程,但其自动装配过程中对第三方库和服务的发现和加载机制受到了SPI思想的启发,并在此基础上进行了扩展和创新。
另:
META-IF/spring.factories是在Maven引入的Jar包中,每一个Jar都有自己META-IF/spring.factories,所以SpringBoot是去每一个Jar包里面寻找META-IF/spring.factories,而不是我的项目中存在META-IF/spring.factories(当然也可以存在,但是我项目的META-IF/spring.factories肯定没有类似以下这些东西)
@SpringBootApplication注解作用图
好处:简化了配置
Springboot的SPI机制是怎么实现的?
程序启动,注册配置类处理器
Spring刷新上下文,执行配置类处理器
扫描spring.factories将得到的BeanDefinition注册到容器
spring实例化/初始化这些BeanDefinition
Spring Boot 的自动装配(Auto-configuration)原理(上面也是)
启动类与@SpringBootApplication注解:
@EnableAutoConfiguration:
spring.factories中的自动配置类:
条件化装配:
覆盖默认配置:
自定义自动配置:
另:META-INF下另一个文件MANIFEST.MF的作用
MANIFEST.MF
是Java应用程序和JAR文件中用于存储元数据(metadata)的一个重要文件。它位于JAR文件的META-INF目录下,包含了关于该JAR包及其组件的重要信息。MANIFEST.MF
是Java应用打包和部署过程中不可或缺的一部分,为Java虚拟机(JVM)提供了有关如何加载和执行JAR内容的关键指导信息。在JAR文件中,MANIFEST.MF
文件常见的用途包括:
Main-Class声明:对于可执行的JAR文件(也称为Runnable JAR或Self-executable JAR),需要在MANIFEST.MF
文件中指定一个主类(Main-Class)。例如:
Main-Class: com.example.Main
这样,用户可以直接通过命令行 java -jar myapp.jar
来运行这个JAR程序。
Class-Path声明:用于定义当前JAR文件依赖的其他外部JAR文件的路径。例如:
Class-Path: lib/library.jar lib/anotherlibrary.jar
这样,当运行此JAR时,Java会自动加载指定路径下的库。
Sealed指示:可以用来表示JAR是否被密封(sealed),即所有包内的类都必须来自同一个代码源,以增强安全性。
签名信息:如果JAR文件被数字签名,那么相关的签名证书、签名者信息等也会记录在MANIFEST.MF
中。
服务提供者信息:在实现Java SPI(Service Provider Interface)时,MANIFEST.MF
也可以包含ServiceProvider-Impl
条目,列出实现了某个接口的服务提供者的全限定类名。
OSGi Bundle信息:在OSGi框架中,每个Bundle都有自己的MANIFEST.MF
文件,其中包含了Bundle的标识符、版本、导入导出的包、激活器等重要信息。