Java 中的 SPI 机制

110 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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. 实现

实现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);
    }
}