JavaSPI机制
一.SPI的介绍
-
SPI即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,即为专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
-
是 Java 提供的,旨在由第三方实现或扩展的 API,它是一种用于动态加载服务的机制。Java 中 SPI 机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦。
-
SPI将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,修改或者替换服务实现并不需要修改调用方。同时,SPI机制也解决了Java类加载体系中双亲委派模型带来的限制---->破坏了双亲委派模型
-
很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口
双亲委派模型参考文章:双亲委派模型
Java SPI的四个要素:
- SPI接口:为服务提供者实现类约定的接口或者抽象类
- SPI实现类: 实际提供服务的实现类
- SPI配置: Java SPI 机制约定的配置文件,提供查找服务实现类的逻辑。配置文件必须置于
META-INF/services
目录中,并且,文件名应与服务提供者接口的完全限定名保持一致。文件中的每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称。 - ServiceLoader: Java SPI 的核心类,用于加载 SPI 实现类。
ServiceLoader
中有各种实用方法来获取特定实现、迭代它们或重新加载服务。
全限定名称,也叫完全限定名,指的是一个类、接口或成员在Java程序中唯一的、完整的路径名。
它由两部分组成:
包名:类所在的包,反映了类的命名空间和目录结构。
类名:类本身的名称。
格式为:
包名.类名
eg:
src/
└── com/
└── company/
└── model/
├── User.java
└── Product.java
User
类:
包名:
com.company.model
简单类名:
User
全限定名称:
com.company.model.User
Java SPI的运行流程
二.API和SPI的区别
-
当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API。这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。
-
当接口存在于调用方这边时,这就是 SPI 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。
作用:
提供了一种组件发现和注册的方式,可以用于实现各种插件,或者灵活替换框架所使用的组件
优点:
基于面向接口编程,实现模块之间的解耦
设计思想:
面向接口+配置文件+反射技术
三.ServiceLoader
ServiceLoader是一种加载服务实现的工具
使用Java的SPI机制是需要依赖ServiceLoader来实现的。它是 JDK 提供的一个工具类,位于package.java.util 包下
下面是核心load方法:
public static <S> ServiceLoader<S> load(Class<S> service) {//c1是线程上下文类加载器(ThreadContextClassLoader)//这是每个线程持有的类加载器//JDK允许应用程序或者容器(如Web应用服务器)设置这个类加载器//以便核心类库能够通过他来加载应用程序类//线程上下文类加载器就是为了做类加载双亲委派模型的逆序而创建的。ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader) {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();
}public void reload() {providers.clear();lookupIterator = new LazyIterator(service, loader);
}
使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了,双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情。 《深入理解Java虚拟机(第三版)》
简易版ServiceLoader:
package edu.jiangxuan.up.service;import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;public class MyServiceLoader<S> {// 对应的接口 Class 模板private final Class<S> service;// 对应实现类的 可以有多个,用 List 进行封装private final List<S> providers = new ArrayList<>();// 类加载器private final ClassLoader classLoader;// 暴露给外部使用的方法,通过调用这个方法可以开始加载自己定制的实现流程。public static <S> MyServiceLoader<S> load(Class<S> service) {return new MyServiceLoader<>(service);}// 构造方法私有化private MyServiceLoader(Class<S> service) {this.service = service;this.classLoader = Thread.currentThread().getContextClassLoader();doLoad();}// 关键方法,加载具体实现类的逻辑private void doLoad() {try {// 读取所有 jar 包里面 META-INF/services 包下面的文件,这个文件名就是接口名,然后文件里面的内容就是具体的实现类的路径加全类名Enumeration<URL> urls = classLoader.getResources("META-INF/services/" + service.getName());// 挨个遍历取到的文件while (urls.hasMoreElements()) {// 取出当前的文件URL url = urls.nextElement();System.out.println("File = " + url.getPath());// 建立链接URLConnection urlConnection = url.openConnection();urlConnection.setUseCaches(false);// 获取文件输入流InputStream inputStream = urlConnection.getInputStream();// 从文件输入流获取缓存BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));// 从文件内容里面得到实现类的全类名String className = bufferedReader.readLine();while (className != null) {// 通过反射拿到实现类的实例Class<?> clazz = Class.forName(className, false, classLoader);// 如果声明的接口跟这个具体的实现类是属于同一类型,(可以理解为Java的一种多态,接口跟实现类、父类和子类等等这种关系。)则构造实例if (service.isAssignableFrom(clazz)) {Constructor<? extends S> constructor = (Constructor<? extends S>) clazz.getConstructor();S instance = constructor.newInstance();// 把当前构造的实例对象添加到 Provider的列表里面providers.add(instance);}// 继续读取下一行的实现类,可以有多个实现类,只需要换行就可以了。className = bufferedReader.readLine();}}} catch (Exception e) {System.out.println("读取文件异常。。。");}}// 返回spi接口对应的具体实现类列表public List<S> getProviders() {return providers;}
}
这里先大概解释一下:Java 中的 SPI 机制就是在每次类加载的时候会先去找到 class 相对目录下的 META-INF
文件夹下的 services 文件夹下的文件,将这个文件夹下面的所有文件先加载到内存中,然后根据这些文件的文件名和里面的文件内容找到相应接口的具体实现类,找到实现类后就可以通过反射去生成对应的对象,保存在一个 list 列表里面,所以可以通过迭代或者遍历的方式拿到对应的实例对象,生成不同的实现。
所以会提出一些规范要求:文件名一定要是接口的全类名,然后里面的内容一定要是实现类的全类名,实现类可以有多个,直接换行就好了,多个实现类的时候,会一个一个的迭代加载。
eg:
主要流程:
- 通过 URL 工具类从 jar 包的
/META-INF/services
目录下面找到对应的文件, - 读取这个文件的名称找到对应的 spi 接口,
- 通过
InputStream
流将文件里面的具体实现类的全类名读取出来, - 根据获取到的全类名,先判断跟 spi 接口是否为同一类型,如果是的,那么就通过反射的机制构造对应的实例对象,
- 将构造出来的实例对象添加到
Providers
的列表中
大致了解Java SPI工作原理:
通过ClassLoader加载SPI配置文件,解析SPI服务,然后通过反射,实例化SPI服务
四.Java SPI的应用
简单找了一个实例:
/*** @author jimoer**/
public interface SpiInterfaceService {/*** 打印参数* @param parameter 参数*/void printParameter(String parameter);
}
package com.jimoer.spi.service.one;
import com.jimoer.spi.app.SpiInterfaceService;/*** @author jimoer**/
public class SpiOneService implements SpiInterfaceService {/*** 打印参数** @param parameter 参数*/@Overridepublic void printParameter(String parameter) {System.out.println("我是SpiOneService:"+parameter);}
}
package com.jimoer.spi.service.two;
import com.jimoer.spi.app.SpiInterfaceService;
/*** @author jimoer**/
public class SpiTwoService implements SpiInterfaceService {/*** 打印参数** @param parameter 参数*/@Overridepublic void printParameter(String parameter) {System.out.println("我是SpiTwoService:"+parameter);}
}
/*** @author jimoer**/
public class SpiService {public static void main(String[] args) {ServiceLoader<SpiInterfaceService> spiInterfaceServices = ServiceLoader.load(SpiInterfaceService.class);Iterator<SpiInterfaceService> iterator = spiInterfaceServices.iterator();while (iterator.hasNext()){SpiInterfaceService sip = iterator.next();sip.printParameter("参数");}}
}
<dependencies><dependency><groupId>com.jimoer.spi</groupId><artifactId>spi-service-one</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>com.jimoer.spi</groupId><artifactId>spi-service-two</artifactId><version>1.0-SNAPSHOT</version></dependency>
</dependencies>
输出:
我是SpiTwoService:参数
我是SpiOneService:参数
又找了一个实例-----JDBC
五.面试题总结
什么是SPI?
- SPI即是Server Provider Interface,即服务提供者的接口,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者的一种接口。
- SPI将具体的服务接口和服务实现分离开来,将服务调用方和服务实现者解耦。修改或者替换服务实现并不用修改调用方
- 很多框架都使用了 Java 的 SPI机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。
服务接口
java.sql.Driver
(JDBC定义的接口)服务调用方
Java应用程序(使用JDBC API的代码)
// 应用程序代码,服务调用方 Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");
服务实现方
数据库厂商提供的JDBC驱动实现(如MySQL驱动)
- 实现类:
com.mysql.cj.jdbc.Driver
- 在JAR包中创建文件:
META-INF/services/java.sql.Driver
- 文件内容:
com.mysql.cj.jdbc.Driver
工作流程
- 应用程序(服务调用方)使用
DriverManager.getConnection()
方法DriverManager
通过SPI机制(ServiceLoader)扫描所有JAR包中的META-INF/services/java.sql.Driver
文件- 找到MySQL驱动的实现类
com.mysql.cj.jdbc.Driver
- 动态加载并实例化该实现类
- 使用该实现类建立与MySQL数据库的连接
SPI和API有什么区别?
- 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API,这种接口和实现都是放在实现方的。
- 当接口存在于调用方这边时,就是 SPI由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。
SPI的优缺点?
通过 SPI机制能够大大地提高接口设计的灵活性,但是 SPI机制也存在一些缺点,比如:
- 需要遍历加载所有的实现类不能做到按需加载,这样效率还是相对较低的。
- 当多个 ServiceLoader同时load 时,会有并发问题。
该文本为学习整理的笔记,参考文章:
JavaSPI机制详解
Java中的SPI