Java 服务提供发现机制

  |  

摘要: 本文通过一个组装计算机的例子看一下 Java 服务提供和发现机制,以及服务加载器的用法。

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


服务提供发现机制 API

Java 给出了一种服务提供发现机制,即 SPI。SPI 全称是 ServiceProvider Interface,是 Java 提供的一套用来被第三方实现或者扩展的接口。

SPI 的工作机制如下图:

在 SPI 机制下,我们定义了一个接口,但没有给出具体实现,这些实现可以交由第三方来提供。我们的应用程序只依赖于该接口,在运行时,根据某种机制找到一个第三方提供的实现类来完成整个应用。不同的第三方可以提供不同的实现,这就扩展了程序的功能,这有点类似于依赖注入容器的思想。

在 SPI 机制中,有三个参与角色:

  • 服务:即对外开放的接口或者基类,通常接口居多。
  • 服务提供者:第三方提供的接口实现类,或者子类。实现类必须有一个无参的构造方法
  • 服务加载器:发现并加载在运行时环境中部署的服务提供者。

服务加载器可以直接用 java.util.ServiceLoader调用该类下的静态方法 load,可以得到 ServiceLoader 类的实例

1
public static <S> ServiceLoader<S> load(Class<S> service)

它会使用当前线程的上下文类加载器为给定的服务类型创建新的服务加载器。

有两种方式得到服务提供者:

(1) 调用 ServiceLoader 对象的 iterator 方法,通过返回的迭代器来迭代处理可用的服务提供者。迭代器会延迟加载并实例化服务实现类的对象。因为服务加载器找到实现类之后,会实例化出实现类的对象,创建方式是反射而不是 new,所以要求服务实现类必须要有一个无参的构造方法。 ServiceLoader 类实现了 Iterable 接口,因此你也可以使用 for each 循环来遍历所有可用的服务提供者。

(2) 使用流来查找可用的服务提供者,ServiceLoader 类的 stream 方法返回一个包含 ServiceLoader.Provider 对象的流。

1
public Stream<ServiceLoader.Provider<S>> stream()

Provider 是 ServiceLoaderr 类中定义的一个静态接口,该接口只有两个方法:

1
2
3
4
5
// 返回服务提供者的类型。
Class<? extends S> type()

// 返回服务实现类的实例。
S get()

例子: 找到并加载 CPU 和显卡接口的实现类

Java接口作为模块间通信的协议 中,我们实现了模拟计算机组装的程序,具体就是要在主板上插入 CPU 和 GPU。其中 CPU/GPU 是接口,各个第三方都可以实现自己的 CPU/GPU,并在主类方法中进行实例化,然后插入主板对象。

为了处理更换具体 CPU/GPU 的需求,在 依赖注入容器 中,我们用依赖注入容器来处理实例化具体的 CPU/GPU 的问题。

这里我们看一下利用 SPI 机制找到并加载 CPU/GPU 接口的实现类具体需要怎么做,可以与依赖注入容器的做法进行对比。本例的根目录为 service。

(1) 定义服务

服务就是对外开放的标准接口。就是在 service 目录下的下面三个文件:

1
2
3
CPU.java
GraphicsCard.java
Mainboard.java

编译:

1
javac -d . *.java

(2) 服务实现类

服务实现类一般是由第三方提供,所以都位于第三方定义的包中。

在 service 目录下,新建 spi 文件夹,将 IntelCPU.java 和 NVIDIACard.java 复制到 spi 文件夹中,修改这两个类的包名,并分别导入 CPU 和 GraphicsCard 接口。

1
2
3
4
5
6
7
8
9
package computer.spi;

import computer.CPU;

public class IntelCPU implements CPU {
public void calculate() {
System.out.println("Intel CPU calculate.");
}
}
1
2
3
4
5
6
7
8
9
package computer.spi;

import computer.GraphicsCard;

public class NVIDIACard implements GraphicsCard {
public void display() {
System.out.println("Display something.");
}
}

之后在 spi 文件夹下编译这两个文件:

1
2
export CLASSPATH=.;..
javac -d . *.java

(3) 让 ServiceLoader 找到这服务实现类

要想让 ServiceLoader 能够找到这两个实现类,我们需要把实现类的完整限定名添加到 META-INF/services 目录下的以接口的完整限定名命名的文件中

新建文件夹 META-INF/services,并在该目录下建立两个文件,文件名为 CPU 和 GraphicsCard 接口的完整限定名,如下:

文件 computer.CPU,内容:

1
computer.spi.IntelCPU

文件 computer.GraphicsCard,内容:

1
computer.spi.NVIDIACard

(4) 将服务实现类打包为 JAR

第三方给出的服务实现一般是以 JAR 包提供的,因此需要在 spi 目录下将服务实现类与 META-INF 目录一起打包为 JAR 文件。

1
jar cvf myspi.jar computer META-INF

查看 myspi.tar 内部结构:

1
jar -tvf myspi.jar

结果如下:

1
2
3
4
5
6
7
8
9
META-INF/
META-INF/MANIFEST.MF
computer/
computer/spi/
computer/spi/IntelCPU.class
computer/spi/NVIDIACard.class
META-INF/services/
META-INF/services/computer.GraphicsCard
META-INF/services/computer.CPU

(5) 用了 ServiceLoader 加载服务实现类

在 service 目录下新建 Computer.java,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package computer;

import java.util.ServiceLoader;
import java.util.Optional;

public class Computer {
/**
* 使用迭代器得到 CPU 接口的实现类
*/
private static CPU getCPU() {
ServiceLoader<CPU> cpuLoader = ServiceLoader.load(CPU.class);
for(CPU cpu: cpuLoader) {
return cpu;
}
return null;
}

/**
* 使用流得到 GraphicsCard 接口的实现类
*/
private static GraphicsCard getGraphicsCard() {
ServiceLoader<GraphicsCard> gcLoader = ServiceLoader.load(GraphicsCard.class);
Optional<GraphicsCard> optGC = gcLoader.stream()
.findFirst()
.map(ServiceLoader.Provider::get);
return optGC.orElse(null);
}

public static void main(String[] args) {
Mainboard mb = new Mainboard();
mb.setCpu(getCPU());
mb.setGraphicsCard(getGraphicsCard());
mb.run();
}
}

在创建ServiceLoader类实例的时候,并没有开始加载服务实现类。在迭代的时候,或者流的终端操作触发时,才会去扫描JAR包中的META-INF/services目录下的文件,根据文件名和文件内容进行解析,若解析成功,则通过反射API调用服务实现类的无参构造方法创建实现类的对象。

在不同的JAR包中可能会有相同服务接口的不同实现类,如果需要使用特定的服务实现类,则可以通过实现类的Class对象来进行判断。

(6) 编译和运行

编译:

1
javac -d . Computer.java

添加依赖的 JAR 包:

1
export CLASSPATH=.:./spi/myspi.jar

运行:

1
java computer.Computer

结果如下:

1
2
3
Starting computer...
Intel CPU calculate.
Display something.

Share