跳到主要内容
  1. 所有文章/
  2. Java基础知识笔记汇总/

Java的SPI机制理解

·📄 1593 字·🍵 4 分钟

SPI介绍 #

SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”。

理解:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。

很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。

SPI解决了什么问题 #

在面向对象的设计原则中,一般推荐模块之间基于接口编程,通常情况下调用方模块是不会感知到被调用方模块的内部具体实现。一旦代码里面涉及具体实现类,就违反了开闭原则。如果需要替换一种实现,就需要修改代码(比如换一种日志框架的实现,难道需要修改代码实现类吗?)。

为了实现在模块装配的时候不用在程序里面动态指明,这就需要一种服务发现机制。Java SPI 就是提供了这样一个机制:为某个接口寻找服务实现的机制。这有点类似 IoC 的思想,将装配的控制权移交到了程序之外。

和API的不同理解 #

image_DXe152bRKo.png

举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。

通过SLF4J 理解SPI #

SLF4J (Simple Logging Facade for Java)是 Java 的一个日志门面(接口),其具体实现有几种,比如:Logback、Log4j、Log4j2 等等,而且还可以切换,在切换日志具体实现的时候我们是不需要更改项目代码的,只需要在 Maven 依赖里面修改一些 pom 依赖就好了。

image-20220723213306039-165858318917813.png

这就是依赖 SPI 机制实现的,那我们接下来就实现一个简易版本的日志框架。

完整代码参考:

sky-max-hub/spi-test (github.com)

具体模块的划分 #

image_fAohpFCDd4.png

spi-consumer-slf4j #

定义接口和获取实现类。

public interface Logger {
    void info(String msg);
    void debug(String msg);
}
public class LoggerService {
    private static final LoggerService SERVICE = new LoggerService();

    private final Logger logger;

    private final List<Logger> loggerList;

    private LoggerService() {
        ServiceLoader<Logger> loader = ServiceLoader.load(Logger.class);
        List<Logger> list = new ArrayList<>();
        for (Logger log : loader) {
            list.add(log);
        }
        // LoggerList 是所有 ServiceProvider
        loggerList = list;
        if (!list.isEmpty()) {
            // Logger 只取一个
            logger = list.get(0);
        } else {
            logger = null;
        }
    }

    public static LoggerService getService() {
        return SERVICE;
    }

    public void info(String msg) {
        if (logger == null) {
            System.out.println("info 中没有发现 Logger 服务提供者");
        } else {
            logger.info(msg);
        }
    }

    public void debug(String msg) {
        if (loggerList.isEmpty()) {
            System.out.println("debug 中没有发现 Logger 服务提供者");
        }
        loggerList.forEach(log -> log.debug(msg));
    }
}

spi-provider-log4j #

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

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

接下来同样将 service-provider 项目打包成 jar 包,这个 jar 包就是服务提供方的实现。通常我们导入 maven 的 pom 依赖就有点类似这种,只不过我们现在没有将这个 jar 包发布到 maven 公共仓库中,所以在需要使用的地方只能手动的添加到项目中。

public class Log4j implements Logger {
    @Override
    public void info(String s) {
        System.out.println("Log4j info 打印日志:" + s);
    }

    @Override
    public void debug(String s) {
        System.out.println("Log4j debug 打印日志:" + s);
    }
}

在META-INF.services文件夹中指定org.zrw.spi.Logger文件。

org.zrw.spi.service.Log4j

spi-provider-logback #

和spi-provider-log4j同理类似。

spi-runner #

运行测试:

public class Main {
    public static void main(String[] args) {
        LoggerService loggerService = LoggerService.getService();
        loggerService.info("你好");
        loggerService.debug("测试Java SPI 机制");
    }
}

切换为:

 <dependency>
    <groupId>org.zrw</groupId>
    <artifactId>spi-provider-log4j</artifactId>
    <version>1.0-SNAPSHOT</version>
 </dependency>

打印:

Log4j info 打印日志:你好 Log4j debug 打印日志:测试Java SPI 机制

切换为:

<dependency>
    <groupId>org.zrw</groupId>
    <artifactId>spi-provider-logback</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

打印:

Logback info 打印日志:你好 Logback debug 打印日志:测试Java SPI 机制

SPI缺点 #

通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如:

  • 需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
  • 当多个 ServiceLoader 同时 load 时,会有并发问题。