Java-SPI机制
形而上 Lv4

随着应用程序越来越复杂,对于我们开发人员来说,如何实现高效的组件化和模块化已经成为了一个重要的问题。而 Java SPI(Service Provider Interface)机制,作为一种基于接口的服务发现机制,可以帮助我们更好地解决这个问题。这样会程序具有高度的灵活性、解耦、可扩展性!

SPI概念与原理

概念

Java SPI(Service Provider Interface)是Java官方提供的一种服务发现机制,它允许在运行时动态地加载实现特定接口的类,而不需要在代码中显式地指定该类,从而实现解耦和灵活性。

具体机制如下图:
spi机制

实现原理

Java SPI 的实现原理基于 Java 类加载机制和反射机制。

当使用 ServiceLoader.load(Class service) 方法加载服务时,会检查 META-INF/services 目录下是否存在以接口全限定名命名的文件。如果存在,则读取文件内容,获取实现该接口的类的全限定名,并通过 Class.forName() 方法加载对应的类。

在加载类之后,ServiceLoader 会通过反射机制创建对应类的实例,并将其缓存起来。

这里涉及到一个懒加载迭代器的思想:

当我们调用 ServiceLoader.load(Class service) 方法时,并不会立即将所有实现了该接口的类都加载进来,而是返回一个懒加载迭代器。

只有在使用迭代器遍历时,才会按需加载对应的类并创建其实例。

这种懒加载思想有以下两个好处:

  • 节省内存
    如果一次性将所有实现类全部加载进来,可能会导致内存占用过大,影响程序的性能。

  • 增强灵活性
    由于 ServiceLoader 是动态加载的,因此可以在程序运行时添加或删除实现类,而无需修改代码或重新编译。

总的来说,Java SPI 的实现原理比较简单,利用了 Java 类加载和反射机制,提供了一种轻量级的插件化机制,可以很方便地扩展功能。

优缺点

  1. 优点
  • 松耦合性:SPI具有很好的松耦合性,应用程序可以在运行时动态加载实现类,而无需在编译时将实现类硬编码到代码中。
  • 扩展性:通过SPI,应用程序可以为同一个接口定义多个实现类。这使得应用程序更容易扩展和适应变化。
  • 易于使用:使用SPI,应用程序只需要定义接口并指定实现类的类名,即可轻松地使用新的服务提供者。
  1. 缺点
  • 配置较麻烦:SPI需要在META-INF/services目录下创建配置文件,并将实现类的类名写入其中。这使得配置相对较为繁琐。
  • 安全性不足:SPI提供者必须将其实现类名称写入到配置文件中,因此如果未正确配置,则可能存在安全风险。
  • 性能损失:每次查找服务提供者都需要重新读取配置文件,这可能会增加启动时间和内存开销。

应用场景

Java SPI机制是一种服务提供者发现的机制,适用于需要在多个实现中选择一个进行使用的场景。

常见的应用场景包括:
spi应用场景

我们上面对Java SPI的缺点说了一下,我们来说一下:
Spring的SPI机制相对于Java原生的SPI机制进行了改造和扩展,主要体现在以下几个方面:

  • 支持多个实现类:Spring的SPI机制允许为同一个接口定义多个实现类,而Java原生的SPI机制只支持单个实现类。这使得在应用程序中使用Spring的SPI机制更加灵活和可扩展。

  • 支持自动装配:Spring的SPI机制支持自动装配,可以通过将实现类标记为Spring组件(例如@Component),从而实现自动装配和依赖注入。这在一定程度上简化了应用程序中服务提供者的配置和管理。

  • 支持动态替换:Spring的SPI机制支持动态替换服务提供者,可以通过修改配置文件或者其他方式来切换服务提供者。而Java原生的SPI机制只能在启动时加载一次服务提供者,并且无法在运行时动态替换。

  • 提供了更多扩展点:Spring的SPI机制提供了很多扩展点,例如BeanPostProcessor、BeanFactoryPostProcessor等,可以在服务提供者初始化和创建过程中进行自定义操作。

其他框架也是对Java SPI进行改造和扩展增强,从而更好的提供服务!

使用步骤

  1. 定义接口:首先需要定义一个接口,所有实现该接口的类都将被注册为服务提供者。

  2. 创建实现类:创建一个或多个实现接口的类,这些类将作为服务提供者。

  3. 配置文件:在 META-INF/services 目录下创建一个以接口全限定名命名的文件,文件内容为实现该接口的类的全限定名,每个类名占一行。

  4. 加载使用服务:使用 java.util.ServiceLoader 类的静态方法 load(Class service) 加载服务,默认情况下会加载 classpath 中所有符合条件的提供者。调用 ServiceLoader 实例的 iterator() 方法获取迭代器,遍历迭代器即可获取所有实现了该接口的类的实例。

使用 Java SPI 时,需要注意以下几点:

  • 接口必须是公共的,且只能包含抽象方法。

  • 实现类必须有一个无参构造函数。

  • 配置文件中指定的类必须是实现了相应接口的非抽象类。

  • 配置文件必须放在 META-INF/services 目录下。

  • 配置文件的文件名必须为接口的全限定名。

实践案例

组织模块

  1. 接口层
    3
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //MLogger.java
    package h.xd.spi;

    public interface MLogger {

    void info(String msg);

    void debug(String msg);

    }
  2. spi实现层
    4
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //MLogback.java
    package h.xd.spi.impl;


    import h.xd.spi.MLogger;

    public class MLogback implements MLogger {
    @Override
    public void info(String msg) {
    System.err.println("MLogback info " + msg);
    }

    @Override
    public void debug(String msg) {
    System.err.println("MLogback debug " + msg);
    }
    }

    1
    2
    //h.xd.spi.MLogger
    h.xd.spi.impl.MLogback
  3. 客户端层
    5
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
36
37
38
39
40
41
42
43
44
45
// SpiLogger.java
package com.h.xd.spi;

import h.xd.spi.MLogger;

import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;

public class SpiLogger implements MLogger {

private final MLogger logger;

public SpiLogger() {
ServiceLoader<MLogger> load = ServiceLoader.load(MLogger.class);
List<MLogger> mLoggerList = new ArrayList<>();
for(MLogger mLogger: load){
mLoggerList.add(mLogger);
}
if(!mLoggerList.isEmpty()){
this.logger = mLoggerList.get(0);
}else{
this.logger = null;
}
}

@Override
public void info(String msg) {
if(logger != null){
logger.info(msg);
}else {
System.out.println("info无SPI");
}
}

@Override
public void debug(String msg) {
if(logger != null){
logger.debug(msg);
}else {
System.out.println("info无SPI");
}
}
}

1
2
3
4
5
6
7
8
9
//Main.java
package com.h.xd;

public class Main {
public static void main(String[] args) {
SpiLogger spiLogger = new SpiLogger();
spiLogger.debug("hello");
}
}
  • 本文标题:Java-SPI机制
  • 本文作者:形而上
  • 创建时间:2022-02-03 17:59:00
  • 本文链接:https://deepter.gitee.io/2022_02_03_java_spi/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!