简介

SPI 是 Java 提供的一种插件化机制,核心思想是将服务接口和服务实现分离,使得服务调用者和服务提供者解耦。服务提供者只需要按照约定的接口进行实现,并在配置文件中声明,服务调用者就可以在运行时通过 SPI 机制发现并使用这些实现。

Java SPI 的工作流程

  1. 定义接口:定义一个服务接口。
  2. 实现接口:编写接口的具体实现类。
  3. 配置文件:在 META-INF/services/ 目录下创建配置文件,并列出实现类。
  4. 加载服务:使用 ServiceLoader.load() 方法加载接口的所有实现类。ServiceLoader 会扫描 META-INF/services/ 目录下的配置文件,并通过反射实例化实现类。
  5. 服务调用: 遍历 ServiceLoader 加载的实现类,调用其方法。

Java SPI 的优点

  1. 解耦:接口与实现分离,便于扩展和维护。
  2. 动态加载:通过配置文件动态加载实现类,无需修改代码。
  3. 标准化:Java 标准库提供了 SPI 机制,使用简单。

Java SPI 的缺点

  1. 配置文件依赖:必须严格按照规范配置 META-INF/services/ 文件。
  2. 一次性加载ServiceLoader 会加载所有实现类,无法按需加载。
  3. 缺乏依赖管理:无法处理实现类之间的依赖关系。
  4. 多个并发多线程使用 ServiceLoader 类的实例是不安全的。

Java SPI 的应用场景

  1. 数据库驱动加载:JDBC 使用 SPI 动态加载数据库驱动。
  2. 日志框架适配:SLF4J 使用 SPI 加载具体的日志实现(如 Logback、Log4j)。
  3. 插件化开发:在需要动态扩展功能的场景中,SPI 是一种常用的解决方案。

Java SPI 示例代码

  1. 服务接口(Service Interface)
    定义一个接口,表示某种服务或功能。例如:

    1
    2
    3
    public interface MessageService {
    String getMessage();
    }
  2. 服务实现(Service Implementation)
    提供接口的具体实现类。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class EmailService implements MessageService {
    @Override
    public String getMessage() {
    return "Email Message";
    }
    }

    public class SmsService implements MessageService {
    @Override
    public String getMessage() {
    return "SMS Message";
    }
    }
  3. 配置文件(Service Configuration File)
    META-INF/services/ 目录下创建一个以服务接口全限定名命名的文件(如 com.example.MessageService),并在文件中列出实现类的全限定名。例如:

    1
    2
    com.example.EmailService
    com.example.SmsService
  4. 服务加载器(ServiceLoader)
    使用 java.util.ServiceLoader 动态加载服务实现类。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import java.util.ServiceLoader;

    public class SPIMain {
    public static void main(String[] args) {
    ServiceLoader<MessageService> services = ServiceLoader.load(MessageService.class);
    for (MessageService service : services) {
    System.out.println(service.getMessage());
    }
    }
    }

Java SPI 的工作原理

  1. 首先,ServiceLoader实现了Iterable接口,所以它有迭代器的属性,这里主要都是实现了迭代器的hasNext和next方法。这里主要都是调用的lookupIterator的相应hasNext和next方法,lookupIterator是懒加载迭代器。

  2. 其次,LazyIterator中的hasNext方法,静态变量PREFIX就是”META-INF/services/”目录,这也就是为什么需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件。

  3. 最后,通过反射方法Class.forName()加载类对象,并用newInstance方法将类实例化,并把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型) 然后返回实例对象

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// ServiceLoader实现了Iterable接口,可以遍历所有的服务实现者
public final class ServiceLoader<S> implements Iterable<S>
{
// 查找配置文件的目录
private static final String PREFIX = "META-INF/services/";
// 表示要被加载的服务的类或接口
private final Class<S> service;
// 这个ClassLoader用来定位,加载,实例化服务提供者
private final ClassLoader loader;
// 访问控制上下文
private final AccessControlContext acc;
// 缓存已经被实例化的服务提供者,按照实例化的顺序存储
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 迭代器
private LazyIterator lookupIterator;
}
// 服务提供者查找的迭代器
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
// hasNext方法
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
// next方法
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
};
}
// 服务提供者查找的迭代器
private class LazyIterator implements Iterator<S> {
// 服务提供者接口
Class<S> service;
// 类加载器
ClassLoader loader;
// 保存实现类的url
Enumeration<URL> configs = null;
// 保存实现类的全名
Iterator<String> pending = null;
// 迭代器中下一个实现类的全名
String nextName = null;

public boolean hasNext() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}

public S next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service, "Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service, "Provider " + cn + " could not be instantiated: " + x, x);
}
throw new Error(); // This cannot happen
}
}

Dubbo实现的SPI机制

Dubbo 对 Java 原生的 SPI 机制进行了增强和扩展,提供了更强大、更灵活的服务发现和加载功能。下面将详细介绍如何使用 Dubbo 实现的 SPI 机制,包括定义接口、实现接口、配置 SPI、加载和使用扩展等步骤。

添加依赖

首先,需要在项目中添加 Dubbo 的依赖。如果使用 Maven 项目,可以在 pom.xml 中添加以下依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>3.2.3</version>
</dependency>

定义服务接口

使用 @SPI 注解标记服务接口,该注解是 Dubbo SPI 的核心注解,用于标识这是一个可扩展的接口。示例如下:

1
2
3
4
5
6
7
import org.apache.dubbo.common.extension.SPI;

// 使用 @SPI 注解标记接口
@SPI
public interface HelloService {
void sayHello();
}

实现服务接口

提供多个实现类来实现上述定义的服务接口。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 实现类 1
public class EnglishHelloService implements HelloService {
@Override
public void sayHello() {
System.out.println("Hello!");
}
}

// 实现类 2
public class ChineseHelloService implements HelloService {
@Override
public void sayHello() {
System.out.println("你好!");
}
}

配置 SPI

META-INF/dubbo 目录下创建一个以服务接口全限定名命名的文件,文件内容为键值对形式,键为扩展点名称,值为实现类的全限定名。例如,对于 HelloService 接口,文件路径为 META-INF/dubbo/com.example.HelloService,文件内容如下:

1
2
english = com.example.EnglishHelloService
chinese = com.example.ChineseHelloService

加载和使用扩展

使用 ExtensionLoader 类来加载和使用扩展。ExtensionLoader 是 Dubbo SPI 机制的核心类,负责从配置文件中加载扩展实现。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.apache.dubbo.common.extension.ExtensionLoader;

public class Main {
public static void main(String[] args) {
// 获取 ExtensionLoader 实例
ExtensionLoader<HelloService> extensionLoader = ExtensionLoader.getExtensionLoader(HelloService.class);

// 根据扩展点名称获取具体的实现
HelloService englishService = extensionLoader.getExtension("english");
englishService.sayHello();

HelloService chineseService = extensionLoader.getExtension("chinese");
chineseService.sayHello();
}
}

代码解释

  • @SPI 注解:用于标记服务接口,表示这是一个可扩展的接口。
  • META-INF/dubbo 目录:Dubbo 约定在该目录下查找 SPI 配置文件。
  • 配置文件格式:使用键值对形式,键为扩展点名称,值为实现类的全限定名。
  • ExtensionLoader:负责加载和管理扩展实现。通过 getExtensionLoader 方法获取 ExtensionLoader 实例,再通过 getExtension 方法根据扩展点名称获取具体的实现。

其他特性

  • 自适应扩展:Dubbo 支持自适应扩展,通过 @Adaptive 注解可以在运行时根据参数动态选择扩展实现。
  • 自动包装:Dubbo 支持自动包装扩展实现,通过实现 Wrapper 类可以对扩展实现进行包装和增强。

与Spring集成

创建一个 Spring 配置类,用于将 Dubbo SPI 扩展点注入到 Spring 容器中。

1
2
3
4
5
6
7
8
9
10
11
12
import org.apache.dubbo.common.extension.ExtensionLoader;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DubboSPIConfig {
@Bean
public MyService myService() {
ExtensionLoader<MyService> extensionLoader = ExtensionLoader.getExtensionLoader(MyService.class);
return extensionLoader.getExtension("myImpl");
}
}

SPI方式比较

对比维度 Java 原生 SPI Dubbo 的 SPI
配置文件位置 META - INF/services 目录下 默认 META - INF/dubbo 目录,也支持 META - INF/dubbo/internalMETA - INF/services 目录
配置文件内容格式 实现类全限定名,每行一个 键值对形式,键为扩展点名称,值为实现类全限定名
加载类 java.util.ServiceLoader org.apache.dubbo.common.extension.ExtensionLoader
加载方式 一次性加载并实例化配置文件中所有实现类 按需加载,可根据扩展点名称获取指定实现类
高级特性 无自动激活、自适应扩展、扩展点包装等高级特性 支持自动激活(@Activate 注解)、自适应扩展(@Adaptive 注解)、扩展点包装
性能和灵活性 一次性加载可能造成资源浪费,缺乏灵活扩展定制能力 按需加载,性能较好,借助各种注解和机制可灵活定制,适合复杂分布式系统
获取实现类方式 通过迭代器遍历获取所有实现类实例 通过扩展点名称获取指定实现类实例