Java中SPI机制介绍

SPI机制简介

SPI的全名为Service Provider Interface。大多数开发人员可能不熟悉,因为这个是针对厂商或者插件的。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java spi机制的思想。我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块、jdbc模块的方案等。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

总结的来说,SPI是一种旨在由插件/模块实现或扩展的API。
让我们以一个例子来理解。

示例

假设我们正在设计应用程序框架F。我们希望F是可扩展的。我们将创建对外界可见的API接口/类,通常称为服务来描述F的作用。一个特定的应用程序A将调用该API来实现一个目标。F的相同接口/类(SPI)的集合通过插件和模块被视为扩展点。这些插件/模块通常称为服务提供商。他们提供具体的实现或扩展框架的这些接口/类来提供功能。

创建四个工程

  • FrameworkF: 框架工程,内含main入口,对服务进行调用,依赖于下面三个工程
  • ApiX: 服务接口工程,定义服务的接口
  • PluginY: 服务提供商Y,实现了ApiX中的服务
  • PluginZ:服务提供商Z,实现了ApiX中的服务

在FrameworkF的main函数中有如下调用

1
2
3
4
5
6
7
8
public static void main(String[] args) {
ServiceLoader<MyService> loads = ServiceLoader.load(MyService.class);
for (MyService myService : loads) {
int res = myService.add(1, 2);
System.out.println("res: "+res);
}
System.out.println("end");
}

在ApiX的定义服务

1
2
3
4
5
package com.example.apix;

public interface MyService{
public int add(int a,int b);
}

在插件Y中,实现接口。
并定义了SPI接口文件,就是PluginY\src\main\resources\META-INF\services\com.example.apix.MyService

同理插件Z中,也一样的实现了。

在执行在FrameworkF的main函数时,可能会有如下输出:

1
2
3
4
5
call MyServicePluginYImpl add -- pluginY
res: 3
call MyServicePluginZImpl add -- pluginZ
res: 3
end

main中找到了pluginY和pluginX的实现类,并进行了调用。

具体的代码放在了github上,见这里

SPI具体约定

当服务的提供者(如上面的pluginY,pluginZ),提供了服务接口的一种实现之后(如MyServicePluginYImpl,MyServicePluginZImpl),在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件(如com.example.apix.MyService)。该文件里就是实现该服务接口的具体实现类(如com.example.pluginy.MyServicePluginYImpl)。而当外部程序装配这个模块的时候(FrameworkF 加上PluginY的依赖),就能通过该jar包(PluginY和PluginZ的jar)META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。
基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。
jdk提供服务实现查找的一个工具类:java.util.ServiceLoader

应用场景

jdbc

在jdbc的jar包中有文件META-INF/services/java.sql.Driver有文件内容com.mysql.jdbc.Driver指定实现类的方式来暴露驱动提供者。

tomcat

在web容器启动时为提供给第三方组件机会做一些初始化的工作,例如注册servlet或者filtes等,servlet规范中通过ServletContainerInitializer实现此功能。就必须在对应的jar包的META-INF/services 目录创建一个名为javax.servlet.ServletContainerInitializer的文件,文件内容指定具体的ServletContainerInitializer实现类,那么,当web容器启动时就会运行这个初始化器做一些组件内的初始化工作。

common-logging

有些文章是说common-logging中有,但我没有找到。

参考