杂谈 Java SPI

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

虽然是一个老概念 , 但是都怪自己的轻微强迫症 , 有个点没搞清楚 , 就非要把这个概念翻出来看看, 这一篇是对 Java SPI 概念的一个完善 , 为后续的 Dubbo 等框架的分析做准备

Java SPI 是 JDK提供的SPI(Service Provider Interface)机制 , SPI 机制的核心在于 ServiceLoader , 用于加载接口对应的实现类

用一句话来解释 , 就是以 Java 的方式查找接口对应的实现类 , 实现解耦 (区别于 Spring 里面的Bean工具 , SPI 的方式更加底层)

二 . 知识点

SPI 机制中有四个组成部分 :

  • Service Providers : 服务器供应商
    • Installing Service Providers : 安装服务供应商
    • Loading Service providers : 装载服务供应商
  • Service Loader : 服务承载程式

Service Provider : 服务提供者
Service Provider 是 SPI 的特定实现。服务提供者包含一个或多个实现或扩展服务类型的具体类 , 服务提供者是通过我们放在资源目录 META-INF/services 中的提供者配置文件来配置和标识的

ServiceLoader
ServiceLoader 是 SPI 的核心类 , 它的作用是发现和惰性加载实现 , 它使用上下文类路径来定位提供程序实现并将它们放在内部缓存中。

常用的 SPI 类

  • CurrencyNameProvider : 为currency类提供本地化的货币符号
  • LocaleNameProvider : 为Locale类提供本地化名称
  • TimeZoneNameProvider : 为TimeZone类提供本地化的时区名称
  • DateFormatProvider : 提供指定区域的日期和时间格式
  • NumberFormatProvider : 为NumberFormat类提供货币值、整数和百分比值.
  • Driver : 从4.0版本开始,JDBC API支持SPI模式
  • PersistenceProvider : 提供JPA API的实现。
  • JsonProvider : 提供JSON处理对象
  • JsonbProvider : 提供JSON绑定对象
  • Extension : 为CDI容器提供扩展
  • ConfigSourceProvider : 提供用于检索配置属性的源

三 . SPI 的使用

SPI 的使用中我分成了三个包 :

1
2
3
4
5
6
7
8
9
10
java复制代码// provider-api : API 描述包 , 包含 Provider 接口 
|- ExchangeRateProvider : Provider 接口
|- QuoteManager : 一个需要我们通过 SPI 构建的业务接口
|- ProviderManager : Provider 管理器 , 加载 Provider 实现类

//provider-impl : 接口实现类 , 也是我们最终需要 Loader 出的类
|- YahooFinanceExchangeRateProvider
|- YahooQuoteManagerImpl

//server-application : 业务包 , 业务处理 , 获得 API 类

3.1 provider-api 包

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
java复制代码// Step 1 : Provider 接口 , 我们用它返回一个通用的业务类
public interface ExchangeRateProvider {
QuoteManager create();
}

// Step 2 : 这个是我们的业务类
public interface QuoteManager {
List<String> getQuotes(String baseCurrency, LocalDate date);
}

// Step 3 : Provider 管理工具 , 加载出 Provider
public class ProviderManager {

private Logger logger = LoggerFactory.getLogger(this.getClass());

public Iterator<ExchangeRateProvider> providers(boolean refresh) {
logger.info("------> [Step 1 : 进入 Provider 处理流程] <-------");
ServiceLoader<ExchangeRateProvider> loader = ServiceLoader.load(ExchangeRateProvider.class);

if (refresh) {
loader.reload();
}
Iterator<ExchangeRateProvider> provider = loader.iterator();


return provider;

}


}

3.2 provider-impl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码// 实现类
public class YahooFinanceExchangeRateProvider implements ExchangeRateProvider {

private Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
public QuoteManager create() {
logger.info("------> this is create <-------");
return new YahooQuoteManagerImpl();
}
}

public class YahooQuoteManagerImpl implements QuoteManager {

@Override
public List<String> getQuotes(String baseCurrency, LocalDate date) {
return new ArrayList<>();
}
}

3.3 Server applicaiton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class StartService implements ApplicationRunner {

private Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
public void run(ApplicationArguments args) throws Exception {
logger.info("------> [App 中获取] <-------");

ProviderManager providerManager = new ProviderManager();

Iterator<ExchangeRateProvider> providers = providerManager.providers(true);


while (providers.hasNext()) {
logger.info("------> [providers 获取完成 :{}] <-------", providers.next().create());
}

}
}

PS : 这里有个很重要的东西 , 你需要 META-INF/services 中添加对应的文件

  • 文件名 : Provider 接口
  • 文件内容 : 涉及到的实现类

image.png

这个文件放在 impl 和 app 中都行 , 实际上引包了就会扫描

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<dependency>
<groupId>com.gang.study</groupId>
<artifactId>provider-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>com.gang.study</groupId>
<artifactId>provider-impl</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

PS : 内容已提交到 Git , 欢迎 Star !!!! 👉 Case/java/spi

四 . SPI 源码深入

如果就这么结束当然不符合我一贯的做法 , 源码还是要看一下的, 别说 , 还真有一些启发

4.1 运行的走向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码// Step 1 : 发起 Providers 加载操作
Iterator<ExchangeRateProvider> providers = providerManager.providers(true);

// Step 2 : ServiceLoader 执行加载
ServiceLoader<ExchangeRateProvider> loader = ServiceLoader.load(ExchangeRateProvider.class);

// Step 3 : ServiceLoader 构造器
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}

// Step 4 : reload 加载
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}

4.2 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码// 默认从  META-INF/services/ 路径下加载
private static final String PREFIX = "META-INF/services/";

// 表示正在加载的服务的类或接口
private final Class<S> service;

// 类加载器用于定位、加载和实例化提供程序
private final ClassLoader loader;

// 创建ServiceLoader时获取的访问控制上下文
private final AccessControlContext acc;

// 缓存的提供商,按实例化顺序
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// 惰性查找迭代器 , 最终获取会通过这个定制的迭代器
private LazyIterator lookupIterator;

4.3 resource 的加载

这里的核心是做了一个定制的实现类 LazyIterator

前面看了 ServiceLoader 的构建 , 这里来看一下 resource 的加载

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
java复制代码// 这里不需要深入太多 , 主要是资源加载处理 Service 类

C- ServiceLoader
public Iterator<S> iterator() {

// 核心一 : 对迭代器做了简单的实现 , 用来调用定制的迭代器 LazyIterator
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();

public boolean hasNext() {
if (knownProviders.hasNext()){
return true;
}
// --> 最终调用 hasNextService()
return lookupIterator.hasNext();
}

public S next() {
if (knownProviders.hasNext()){
return knownProviders.next().getValue();
}
return lookupIterator.next();
}

public void remove() {
throw new UnsupportedOperationException();
}

};
}

// 判断是否存在下一个
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// META-INF/services/java.util.spi.ResourceBundleControlProvider
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 通过 classLoader 加载 resource
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());
}
// 获得实现类的名称 com.gang.spi.demo.service.YahooFinanceExchangeRateProvider
nextName = pending.next();
return true;
}

// 读取 Resource 中资源
private Iterator<String> parse(Class<?> service, URL u)throws ServiceConfigurationError{
InputStream in = null;
BufferedReader r = null;
// 加载 SPI impl l
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
// Stream 逐行加载
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
// .... 省略 close
}
return names.iterator();
}

4.4 load 处理加载 Class

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
java复制代码// 迭代器迭代
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}

// 实例化 ProviderImpl
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 通过 cn 获取对象的 class 类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
// new ServiceConfigurationError(service.getName() + ": " + msg)
fail(service,"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,"Provider " + cn + " not a subtype");
}
try {
// 实例化 Service : com.gang.spi.api.service.ExchangeRateProvider
S p = service.cast(c.newInstance());
// cn : com.gang.spi.demo.service.YahooFinanceExchangeRateProvider
// p : com.gang.spi.demo.service.YahooFinanceExchangeRateProvider
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,"Provider " + cn + " could not be instantiated",x);
}
throw new Error(); // This cannot happen
}

PS: 核心就2个 ,一个是hasNext 中加载 resource , 再在 nextService 中实例化对应的server

这样的好处是 , 只有在正在使用的时候 , 才会真的去实例化这个对象 !!!

五 . 定制与比较

这里主要对比 Spring SPI 的加载方式 , 详见这一篇 盘点 SpringBoot : Factories 处理流程

文件的配置方面 : Spring 中使用的是 SpringFactoriesLoader , 其实与 Java 的方式是很像的 , 但是 Spring 的模式下 , 允许一个 factories 文件装载更多的类 , 使用更加简单 .


资源的加载方法 : 2 者都是通过 classLoader 加载 Resource , 并没有太大本质的区别


而资源的实例化方面 : Spring 通过一个 instantiateFactory 方法触发 ,但是同样的 , 也是 class 反射的原理


高并发情况 : 在调用 hasNext 的时候 , 加载 resource , 在迭代时才实例化 看起来好像没有什么问题 , 但是其 classLoader , provider 都是放在 ServiceLoader 对象属性中 , 多线程情况下会存在冲突

而 Spring 的模式 , 在 Server 启动时 , 就加载对象 Factories , 相对安全很多.


总得来说 , Spring Factories 的 Java SPI 的逻辑思路是一致的 , Java SPI 通过 LazyIterator 加载的方式比较骚气 ,但是相对而言获取起来就会很复杂 .

所以 , 完全可以使用 Spring Factories 来完成你自己想要的业务.

总结

定制 Iterator 完成业务是一个不错的思路

参考与感谢

www.baeldung.com/java-spi

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%