持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情
SPI 机制
SPI 全称为 Service Provider Interface , 是 JDK 内置的一种服务提供发现机制.
很多框架都使用到该机制来进行服务的扩展发现, 简单来说, 它就是一种动态替换发现的方式。举个例子来说, 有个接口, 想运行时动态的给它添加实现, 你只需要添加一个实现, 而后, 把新加的实现, 描述给 JDK 知道就行啦(通过改一个文本文件即可).
Java SPI 实际上是基于接口的编程+策略模式+配置文件组合实现的动态加载机制.
系统设计的各个抽象, 往往有很多不同的实现方案, 在面向的对象的设计里, 一般推荐模块之间基于接口编程, 模块之间不对实现类进行硬编码. 一旦代码里涉及具体的实现类, 就违反了可拔插的原则, 如果需要替换一种实现, 就需要修改代码. 为了实现在模块装配的时候能不在程序里动态指明, 这就需要一种服务发现机制.
Java SPI 就是提供这样的一个机制:为某个接口寻找服务实现的机制. 有点类似 IOC 的思想, 就是将装配的控制权移到程序之外, 在模块化设计中这个机制尤其重要. 所以 SPI 的核心思想就是解耦.
使用场景
SPI 适用于:调用者根据实际使用需要, 启用, 扩展, 或者替换框架的实现策略.
比较常见的例子 :
- 数据库驱动加载接口实现类的加载. JDBC 加载不同类型数据库的驱动.
- 日志门面接口实现类加载. SLF4J加载不同提供商的日志实现类
- Spring 框架. Spring 中大量使用了 SPI 机制, 比如:自动类型转换 Type Conversion SPI (Converter SPI、Formatter SPI) 等.
- Dubbo 框架爱. Dubbo 中也大量使用 SPI 的方式实现框架的扩展, 不过它对 Java 提供的原生 SPI 做了封装, 允许用户扩展实现 Filter 接口.
如何使用 SPI 机制
使用流程
实现的流程如下 :
1.应用程序调用 ServiceLoader.load() 方法
ServiceLoader.load() 方法内先创建一个新的 ServiceLoader 对象, 并实例化该类中的成员变量, 包括:
- loader ( ClassLoader, 类加载器)
- acc ( AccessControlContext, 访问控制器 )
- providers ( LinkedHashMap, 用于缓存加载成功的类 )
- lookupIterator ( 实现迭代器功能 )
2.应用程序通过迭代器接口获取对象实例
ServiceLoader 先判断成员变量 providers 对象中 ( LinkedHashMap ) 是否有缓存实例对象, 如果有缓存, 直接返回.
如果没有缓存, 执行类的装载, 实现如下 :
2.1读取META-INF/services/下的配置文件, 获得所有能被实例化的类的名称
注意 ServiceLoader 可以跨越 jar 包获取 META-INF 下的配置文件.
2.2 通过反射方法 Class.forName() 加载类对象, 并进行对象的实例化.
2.3 把实例化后的类缓存到 providers 对象中, ( LinkedHashMap ), 然后返回实例对象.
一个 demo
要使用 Java SPI, 需要遵循如下约定:
-
当服务提供者提供了接口的一种具体实现后, 在 jar 包的
META-INF/services目录下创建一个以接口全类名为名的文件, 文件内容为实现类的全名, 实现类可以有多个. -
接口实现类所在的 jar 包放在主程序的 classpath 中.
-
主程序通过 java.util.ServiceLoder 动态装载实现模块, 它通过扫描
META-INF/services目录下的配置文件找到实现类的全限定名, 把该类加载到 JVM. -
SPI 的实现类必须携带一个不带参数的构造方法.
最终的目录路径如下 :
1.实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class User {
private String name;
private int age;
}
2.接口
public interface UserParse {
List<User> parse(String filePath);
}
- 实现
实现1:
public class UserParse4JSON implements UserParse{
@Override
public List<User> parse(String filePath) {
List<User> users = new ArrayList<>();
users.add(new User("json1",1));
users.add(new User("json2",2));
users.add(new User("json3",3));
return users;
}
}
实现2:
public class UserParse4TXT implements UserParse{
@Override
public List<User> parse(String filePath) {
List<User> users = new ArrayList<>();
users.add(new User("txt1",1));
users.add(new User("txt2",2));
users.add(new User("txt3",3));
return users;
}
}
4.解析管理器
import java.util.ServiceLoader;
/**
* 解析管理器
*/
public class UserParseManager {
private static HashMap<String,UserParse> map = new HashMap<>();
static {
ServiceLoader<UserParse> serviceLoader = ServiceLoader.load(UserParse.class);
for (UserParse userParse : serviceLoader){
map.put(keyConvert(userParse),userParse);
}
}
public static UserParse getUserParse(String type){
Objects.requireNonNull(type, "type is not null");
type = UserParseManager.typeConvert(type);
UserParse userParse = map.get(type);
Objects.requireNonNull(userParse, "userParse is not null");
return userParse;
}
private static String typeConvert(String type){
return type.toUpperCase();
}
private static String keyConvert(UserParse userParse){
String key = userParse.getClass().getSimpleName();
return key.substring(10,key.length());
}
}
5.解析工具类
public class UserParseUtil {
public static List<User> parse(String filePath,String fileType){
Objects.requireNonNull(filePath, "filePath is not null");
Objects.requireNonNull(fileType, "fileType is not null");
//获取解析器
UserParse userParse = UserParseManager.getUserParse(fileType);
//解析
return userParse.parse(filePath);
}
}
6.配置
编写MEAT-INF.services文件
it.com.spi.impl.UserParse4TXT
it.com.spi.impl.UserParse4JSON
7.进行测试
public class Test {
public static void main(String[] args){
String filePath = "xxx";
String type = "json";
//解析工具类
List<User> users = UserParseUtil.parse(filePath, type);
users.forEach(System.out::println);
}
}