Java SPI

179 阅读2分钟

基本概念

在面向对象的设计里面,模块之间推荐基于接口编程,而不是对实现类进行硬编码,这样做也是为了模块设计的可拔插原则。为了在模块装配的时候不在程序里指明是哪个实现,就需要一种服务发现的机制,JDK 的 SPI 就是为某个接口寻找服务实现。

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,可以很容易的通过 SPI 机制为程序提供拓展功能。

API & SPI

APIApplication Programming Interface)在大多数情况下,都是 实现方 制定接口并完成对接口的实现,调用方 仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。

SPIService Provider Interface)是 调用方 来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。  从使用人员上来说,SPI 被框架扩展人员使用。

原理

JDK 提供了服务实现查找的工具类:java.util.ServiceLoader。约定在 classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后文件里面记录的是此 jar 包提供的具体实现类的全限定名

这样引用了某个 jar 包的时候就可以去找这个 jar 包的 META-INF/services/ 目录,再根据接口名找到文件,然后读取文件里面的内容去进行实现类的加载与实例化

示例

SPI 最经典的应用之一就是 java.sql.Driver 接口。

image.png

文件内容则是该服务接口具体的实现类。

image.png

Demo

首先定义一个服务接口

public interface HelloService {
    String sayHello();
}

然后定义2个实现类

public class AHelloService implements HelloService{
    @Override
    public String sayHello() {
        return "Hello A";
    }
}

public class BHelloService implements HelloService{
    @Override
    public String sayHello() {
        return "Hello B";
    }
}

然后在项目的 resources 目录下定义配置文件

image.png

最后编写 main 方法用于测试

public static void main(String[] args) {
    ServiceLoader<HelloService> serviceLoader = ServiceLoader.load(HelloService.class);
    Iterator<HelloService> iterator = serviceLoader.iterator();
    while (iterator.hasNext()) {
        HelloService service = iterator.next();
        System.out.println(service.sayHello());
    }
}

image.png

缺陷

Java SPI 无法按需加载实现类。

Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且将实现类全部实例化,假设一个实现类初始化过程比较消耗资源且耗时,但是代码里面又用不上它,这就产生了资源的浪费。