从 Apollo 客户端源码学 SPI

起因

Apollo 客户端的使用,是需要指定 AppId 的。

Apollo 原生提供了 Java 的客户端,指定 AppId 的方式可以通过以下四种:

  • System Property:-Dapp.id=YOUR-APP-ID
  • System Environment:APP_ID=YOUR-APP-ID
  • Spring Boot 的 application.properties:app.id=YOUR-APP-ID
  • classpath:/META-INF/app.properties:app.id=YOUR-APP-ID

按理来说是够用了,但是对我来说还差那么一点:

不想用 app.id 这个 key 怎么办?

当然这个点也不是我拍脑袋想出来,而是实际工作中,因为我们的服务都已经有一个服务标识,举个栗子就是:-Dapp.name=chestnut

运维同学也不想在 System Property 加辣么多重复的参数。

所以能不能让 Apollo 的 AppId 从我们的 -Dapp.name 这里取呢?

答案是肯定的,可以。

方案

首先找到这个 app.id 是哪定义的。

通过翻 Apollo Java 客户端的代码就可以轻松找到,在 com.ctrip.framework.foundation.internals.provider.DefaultApplicationProvider 里:(省略了部分无关代码)

1
2
3
4
5
6
7
8
9
10
private void initAppId() {
// 1. Get app.id from System Property
appId = System.getProperty("app.id");

//2. Try to get app id from OS environment variable
appId = System.getenv("APP_ID");

// 3. Try to get app id from app.properties.
appId = appProperties.getProperty("app.id");
}

看到这肯定心想:糟了,竟然是写死的,只能 fork 出来改源码了。

不过先别急,要是仅仅是改源码的话,也就不用水这篇文章了。

我们先看看 Apollo 是怎么调到 initAppId 的。顺着 initAppId 向上翻,不难看出调用链是:

1
2
3
4
DefaultProviderManager constructor
-> DefaultApplicationProvider.initialize()
-> DefaultApplicationProvider.initialize(InputStream in)
-> DefaultApplicationProvider.initAppId()

那么是不是我们参考 DefaultProviderManager 重写个 ProviderManager,将 ApplicationProvider 来个“狸猫换太子”就 ok 了?

看到这是不是感觉,我们的解决方案往往就是这么朴实无华。

但当我感到枯燥的时候,再往上翻 DefaultProviderManager 构造器的调用时,一句“卧槽”涌上心头,IDEA 竟提示我

No usages found in All Places

但可以看到它的接口 ProviderManager 是声明在了 Foundation 里,再往里边看,就可以看到 ServiceBootstrap 里的 ServiceLoader 了。

看到这就该看看本文的主角 SPI 了。

SPI

SPI 全称是 Service Provider Interfaces,是 Java 提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI 的作用就是为这些被扩展的 API 寻找服务实现。

到底是个啥玩意?说人话就是用来被第三方实现的 API。

如果大家仔细观察的话,SPI 用到了非常多的地方,比如 Dubbo、JDBC Driver、日志处理框架等等。

但大多数开发人员可能对这个 JDK 中内置的玩意并不熟悉,我们先看一个用 JDK 的 SPI 的简单实现。

demo

首先定义一个接口:

1
2
3
public interface HelloService {
void hello();
}

它有两个实现:

1
2
3
4
5
6
public class HelloServiceImplA implements HelloService {
@Override
public void hello() {
System.out.println("Hello! I am ImplA");
}
}
1
2
3
4
5
6
public class HelloServiceImplB implements HelloService {
@Override
public void hello() {
System.out.println("Hello! I am ImplB");
}
}

接着,我们需要在 META-INF/services 下新建文件,文件名为接口全类名,文件内容即接口实现类全类名(多个实现类换行表示)

最后,就需要借助上边提到的 ServiceLoader 加载实现类并调用

1
2
3
4
5
6
7
8
9
10
public class HelloServiceTest {
public static void main(String[] args) {
ServiceLoader<HelloService> serviceLoader = ServiceLoader.load(HelloService.class);
Iterator<HelloService> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
HelloService helloService = iterator.next();
helloService.hello();
}
}
}

这样一个简单的 SPI 的 demo 就完成了。

可以看到其中最为核心的就是通过 ServiceLoader 这个类来加载具体的实现类的。

SPI 原理

通过上面简单的 demo,可以看到最关键的实现就是 ServiceLoader 这个类,可以看下这个类的源码,如下:

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
public final class ServiceLoader<S> implements Iterable<S> {

//扫描目录前缀
private static final String PREFIX = "META-INF/services/";

// 被加载的类或接口
private final Class<S> service;

// 用于定位、加载和实例化实现方实现的类的类加载器
private final ClassLoader loader;

// 上下文对象
private final AccessControlContext acc;

// 按照实例化的顺序缓存已经实例化的类
private LinkedHashMap<String, S> providers = new LinkedHashMap<>();

// 懒查找迭代器
private java.util.ServiceLoader.LazyIterator lookupIterator;

// 私有内部类,提供对所有的 service 的类的加载与实例化
private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
String nextName = null;

//...
private boolean hasNextService() {
if (configs == null) {
try {
// 获取目录下所有的类
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
//...
}
//...
}
}

private S nextService() {
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 使用反射加载类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
}
try {
// 实例化
S p = service.cast(c.newInstance());
// 放进缓存
providers.put(cn, p);
return p;
} catch (Throwable x) {
//..
}
//..
}
}
}

可以看出 SPI 的底层实现主要是使用了反射机制,通过全类名实例化接口实现,从而发起调用。

这里的代码只贴出了部分关键的实现,大家有兴趣的可以自己去研究,下面贴出比较直观的 SPI 加载的主要流程供参考:

Apollo 与 SPI

Apollo 作为一个通用的分布式配置中心,在 Java 客户端的代码中也不少使用了 SPI。

上边提到的 ServiceBootstrap,就是封装了SPI 通用的操作方法。

所以要实现文章开头的功能(不用 app.id 这个 key 怎么办?)就很容易了,这里就不贴代码了。

总结

SPI 机制为很多框架扩展提供了可能,不需要改动源码就可以实现扩展、解耦,实现扩展对原来的代码几乎没有侵入性,只需要添加配置就可以实现扩展,符合开闭原则。

对于 SPI 的使用,本文结合定制 Apollo 的一个功能,简单介绍了 SPI 的使用与原理,就当抛砖引玉吧~


从 Apollo 客户端源码学 SPI
https://www.haoyizebo.com/posts/dc154e79/
作者
一博
发布于
2021年3月2日
许可协议