Java中的SPI和API你懂了吗?

160 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

什么是SPI

1、 SPI 的英文全拼是 Service Provider Interface ,意思就是:“服务提供者的接口”,即专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。

2、谈到SPI我们经常会和APi作为对比。下图是我画的一个表格作为对比。

APISPI
范围开发人员使用框架使用
实现方式在实现方实现接口,调用放无法实现和选择哪种接口在调用方实现接口,调用方选择具体哪种接口实现
例子日常使用Log日志,数据库驱动

3、画个个图对比一下

1665838737963.png

当实现方提供了接口和实现,就是一般我们定义一个接口然后定义接口实现,我们通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。

当接口存在于调用方这边时,定义接口但不去实现而是由具体的调用放实现接口 ,这就是SPI,由接口调用方确定接口规则,调用方来对接口进行具体的实现。

举例说明

下面来仿照 SLF4J 的日志门面来实现一个SPI模式。

对于日志我们修改使用哪种日志的时候不需要我们改代码,只需要我们修改pom文件的依赖即可,这就是SPI的体现。

1、文件目录结构

1665842743893.png

1、接口方

  • LoggerInterface类
public interface LoggerInterface {
    void info(String msg);
    void debug(String msg);
}
​
  • LoggerService类
public class LoggerService {
​
    private static final LoggerService SERVICE = new LoggerService();
    
    /**
     * 具体的一个服务实现类
     */
    private final LoggerInterface loggerInterface;
​
    private LoggerService(){
        ServiceLoader<LoggerInterface> load = ServiceLoader.load(LoggerInterface.class);
​
        List<LoggerInterface> loggers = new ArrayList<>();
        for (LoggerInterface logger : load) {
            loggers.add((logger));
        }
        /**
         * 所有的Logger服务的实现类
         */
​
        if (loggers.size()>0){
            loggerInterface = loggers.get(0);
        }else {
            loggerInterface = null;
        }
    }
    public static LoggerService getService() {
        return SERVICE;
    }
​
    public void info(String msg) {
        if (loggerInterface == null) {
            System.out.println("info 中没有发现 Logger 服务提供者");
        } else {
            loggerInterface.info(msg);
        }
    }
​
    public void debug(String msg) {
        if (loggerInterface==null) {
            System.out.println("debug 中没有发现 Logger 服务提供者");
        }else {
            loggerInterface.debug(msg);
        }
    }
}
  • MainTest测试方法
public class MainTest {
    public static void main(String[] args) {
        LoggerService service = LoggerService.getService();
        service.info("hello spi info");
        service.debug("hello spi debug");
    }
}
  • 运行MainTest方法

此时我们只是空有接口,并没有为 LoggerInterface接口提供任何的实现,所以输出结果中没有按照预期打印相应的结果。

1665842406465.png

使用maven或者idea把这个模块打成一个jar包给调用发引用。

下面展示打成jar的关键步骤。

1665842780496.png

1665842816918.png

2、调用方

public class LogBackImpl implements LoggerInterface {
​
​
    public void info(String s) {
        System.out.println("Logback info 打印日志:" + s);
    }
​
    public void debug(String s) {
        System.out.println("Logback debug 打印日志:" + s);
    }
}

resouce目录下新建 META-INF/services 文件夹,然后新建文件 com.spi.LoggerInterface (SPI 的全类名),文件里面的内容是:spi.caller.LogBackImpl (LogBackImpl 的全类名,即 SPI 的实现类的包名 + 类名)。

这是 JDK SPI 机制 ServiceLoader 约定好的标准。

Java 中的 SPI 机制就是在每次类加载的时候会先去找到 class 相对目录下的 META-INF 文件夹下的 services 文件夹下的文件,将这个文件夹下面的所有文件先加载到内存中,然后根据这些文件的文件名和里面的文件内容找到相应接口的具体实现类,找到实现类后就可以通过反射去生成对应的对象,保存在一个 list 列表里面,所以可以通过迭代或者遍历的方式拿到对应的实例对象,生成不同的实现。

所以会提出一些规范要求:文件名一定要是接口的全类名,然后里面的内容一定要是实现类的全类名,实现类可以有多个,直接换行就好了,多个实现类的时候,会一个一个的迭代加载。

创建好文件之后再次运行TestSpi(方法内容和MainTest方法内容一样)的方法。 1665842343407.png

总结

通过使用 SPI 机制,可以看出(LoggerService)和 服务提供者两者之间的耦合度非常低,如果说我们想要换一种实现,那么其实只需要修改 spi-caller 项目中针对 LoggerInterface 接口的实现,只需要换一个 jar 包即可。这就是SLF4J 原理。