Java 带你理解 ServiceLoader 的原理与

前言

  • ServiceLoaderJava提供的一套**SPI(Service Provider Interface,常译:服务发现)**框架,用于实现服务提供方与服务使用方解耦
  • 在这篇文章里,我将带你理解ServiceLoader的原理与设计思想,希望能帮上忙。请点赞,你的点赞和关注真的对我非常重要!

目录


  1. SPI 简介

  • 定义
  • 一个服务的注册与发现机制*
  • 作用
    通过解耦服务提供者与服务使用者,帮助实现模块化、组件化


  1. ServiceLoader 使用步骤

我们直接使用JDBC的例子,帮助各位建立起对ServiceLoader 的基本了解,具体如下:

我们都知道JDBC编程有五大基本步骤:

    1. 执行数据库驱动类加载(非必须):Class.forName("com.mysql.jdbc.driver")
    1. 连接数据库:DriverManager.getConnection(url, user, password)
    1. 创建SQL语句:Connection#.creatstatement();
    1. 执行SQL语句并处理结果集:Statement#executeQuery()
    1. 释放资源:ResultSet#close()Statement#close()Connection#close()

操作数据库需要使用厂商提供的数据库驱动程序,直接使用厂商的驱动耦合太强了,更推荐的方法是使用DriveManager管理类:

步骤1:定义服务接口

JDBC抽象出一个服务接口,数据库驱动实现类统一实现这个接口:

1
2
3
4
5
6
7
java复制代码public interface Driver {
// 创建数据库连接
Connection connect(String url, java.util.Properties info)
throws SQLException;

// 省略其他方法...
}

步骤2:实现服务接口

服务提供者(数据库厂商)提供一个或多个实现这个服务的类(驱动实现类),具体如下:

  • mysqlcom.mysql.cj.jdbc.Driver.java
1
2
3
4
5
6
7
8
9
10
11
scala复制代码public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
// 注册驱动
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
// 省略...
}
  • oracleoracle.jdbc.driver.OracleDriver.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class OracleDriver implements Driver {
private static OracleDriver defaultDriver = null;
static {
try {
if (defaultDriver == null) {
//1. 单例
defaultDriver = new OracleDriver();
// 注册驱动
DriverManager.registerDriver(defaultDriver);
}
} catch (RuntimeException localRuntimeException) {
;
} catch (SQLException localSQLException) {
;
}
}

// 省略...
}

步骤3:注册实现类到配置文件

java的同级目录中新建目录resources/META-INF/services,新建一个配置文件java.sql.Driver(文件名为服务接口的全限定名),文件中每一行是实现类的全限定名,例如:

1
复制代码com.mysql.cj.jdbc.Driver

我们可以解压mysql-connector-java-8.0.19.jar包,找到对应的META-INF文件夹。

步骤4:加载服务

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
csharp复制代码// DriverManager.java
static {
loadInitialDrivers();
}

private static void loadInitialDrivers() {
// 省略次要代码...
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 使用ServiceLoader遍历实现类
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// 获得迭代器
Iterator<Driver> driversIterator = loadedDrivers.iterator();
// 迭代
try{
while(driversIterator.hasNext()) {
driversIterator.next();
// 疑问:为什么没有任何处理?
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
// 省略次要代码...
}

可以看到,DriverManager的静态代码块调用loadInitialDrivers (),方法内部通过ServiceLoader提供的迭代器Iterator<Driver> 遍历了所有驱动实现类,但是为什么在迭代里没有任何操作呢?

1
2
3
4
scss复制代码while(driversIterator.hasNext()) {
driversIterator.next();
// 疑问:为什么没有任何处理?
}

在下一节,我们深入ServiceLoader的源码来解答这个问题。


  1. ServiceLoader 源码解析

# 提示

ServiceLoader中有一些源码使用了安全检测,如AccessController.doPrivileged(),在以下代码摘要中省略

  • 工厂方法
    ServiceLoader提供了三个静态泛型工厂方法,内部最终将调用ServiceLoader.load(Class,ClassLoader),具体如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码// 1.
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
// 使用双亲委派模型中最顶层的ClassLoader
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}

// 2.
public static <S> ServiceLoader<S> load(Class<S> service) {
// 使用线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

// 3.
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
return new ServiceLoader<>(service, loader);
}

可以看到,三个方法仅在传入的ClassLoader参数有区别,若还不了解ClassLoader,请务必阅读[《Java | 带你理解 ClassLoader 的原理与设计思想》](Editting…)

  • 构造方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码private final Class<S> service;
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
reload();
}

public void reload() {
// 清空 providers
providers.clear();
// 实例化 LazyIterator
lookupIterator = new LazyIterator(service, loader);
}

可以看到,ServiceLoader的构造器中创建了LazyIterator迭代器的实例,这是一个“懒加载”的迭代器。那么这个迭代器在哪里使用的呢?继续往下看~

  • 外部迭代器
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
typescript复制代码private LazyIterator lookupIterator;

// 返回一个新的迭代器,包装了providers和lookupIterator
public Iterator<S> iterator() {
return new Iterator<S>() {

Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();

public boolean hasNext() {
// 优先从knownProviders取
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}

public S next() {
// 优先从knownProviders取
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}

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

};
}

可以看到,ServiceLoader里有一个泛型方法Iterator<S> iterator(),它包装了providers集合迭代器和lookupIterator两个迭代器,迭代过程中优先从providers获取元素。

为什么要优先从providers集合中取元素呢?阅读源码发现,LazyIterator#next()会将每轮迭代中取到的元素putproviders集合中,providers其实是LazyIterator的内存缓存。

  • 内部迭代器
# 提示

以下代码摘要中省略了源码中的try-catch

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
ini复制代码// ServiceLoader.java

private static final String PREFIX = "META-INF/services/";

private class LazyIteratorimplements Iterator<S> {

Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;

private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}

private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
// configs 未初始化才执行
// 配置文件:META-INF/services/服务接口的全限定名
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
}

while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 分析点1:解析配置文件资源
pending = parse(service, configs.nextElement());
}
// nextName:下一个实现类的全限定名
nextName = pending.next();
return true;
}

private S nextService() {
if (!hasNextService()) throw new NoSuchElementException();
String cn = nextName;
nextName = null;
// 1. 使用类加载器loader加载
Class<?> c = Class.forName(cn, false, loader);
if (!service.isAssignableFrom(c)) {
ClassCastException cce = new ClassCastException(service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
fail(service, "Provider " + cn + " not a subtype", cce);
}
// 2. 根据Class实例化服务实现类
S p = service.cast(c.newInstance());
// 3. 服务实现类缓存到 providers
providers.put(cn, p);
return p;
}

public boolean hasNext() {
return hasNextService();
}

public S next() {
return nextService();
}

public void remove() {
throw new UnsupportedOperationException();
}
}
// 分析点1:解析配置文件资源,实现类的全限定名列表迭代器
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
// 使用 UTF-8 编码输入配置文件资源
InputStream in = u.openStream();
BufferedReader r = new BufferedReader(new InputStreamReader(in, "utf-8"));
ArrayList<String> names = new ArrayList<>();
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
return names.iterator();
}
  1. ServiceLoader 要点

理解ServiceLoader源码之后,我们总结要点如下:

  • 约束
+ 服务实现类必须实现服务接口`S`,参见源码:`if (!service.isAssignableFrom(c))`
+ 服务实现类需包含无参的构造器,`ServiceLoader`将通过该无参的构造器来创建服务实现者的实例,参见源码:`S p = service.cast(c.newInstance());`
+ 配置文件需要使用`UTF-8`编码,参见源码:`new BufferedReader(new InputStreamReader(in, "utf-8"));`
  • 懒加载
    ServiceLoader使用“懒加载”的方式创建服务实现类实例,只有在迭代器推进的时候才会创建实例,参见源码:nextService()
  • 内存缓存
    ServiceLoader使用LinkedHashMap缓存创建的服务实现类实例,LinkedHashMap在二次迭代时会按照Map#put执行顺序遍历
  • 服务实现的选择
    当存在多个提供者时,服务消费者模块不一定要全部使用,而是需要根据某些特性筛选一种最佳实现。ServiceLoader的机制只能在遍历整个迭代器的过程中,从发现的实现类中决策出一个最佳实现,例如使用Charset.forName(String)获得Charset实现类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码// 服务接口
public abstract class CharsetProvider {
public abstract Charset charsetForName(String charsetName);
// 省略其他方法...
}

// Charset.java
public static Charset forName(String charsetName) {
// 以下只摘要与ServiceLoader有关的逻辑
ServiceLoader<CharsetProvider> sl = ServiceLoader.load(CharsetProvider.class, cl);
Iterator<CharsetProvider> i = sl.iterator();
for (Iterator<CharsetProvider> i = providers(); i.hasNext();) {
CharsetProvider cp = i.next();
// 满足匹配条件,return
Charset cs = cp.charsetForName(charsetName);
if (cs != null)
return cs;
}
}
  • ServiceLoader没有提供服务的注销机制
    服务实现类实例被创建后,它的垃圾回收的行为与Java中的其他对象一样,只有这个对象没有到GC Root的强引用,才能作为垃圾回收。

  1. 问题回归

现在我们回到阅读DriverManager源码提出的疑问:

1
2
3
4
scss复制代码while(driversIterator.hasNext()) {
driversIterator.next();
// 疑问:为什么没有任何处理?
}

为什么next()操作既不取得服务实现类对象,后续也没有任何处理呢?我们再回去看下LazyIterator#next()的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码// ServiceLoader.java

// next() 直接调用 nextService()
private S nextService() {
if (!hasNextService()) throw new NoSuchElementException();
String cn = nextName;
nextName = null;
// 1. 使用类加载器loader加载
Class<?> c = Class.forName(cn, false, loader);
if (!service.isAssignableFrom(c)) {
ClassCastException cce = new ClassCastException(service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
fail(service, "Provider " + cn + " not a subtype", cce);
}
// 2. 根据Class实例化服务实现类
S p = service.cast(c.newInstance());
// 3. 服务实现类缓存到 providers
providers.put(cn, p);
return p;
}
    1. 使用类加载器loader加载:Class<?> c = Class.forName(cn, false, loader);
      这里传参使用false,类加载器将执行加载 -> 链接,不会执行初始化
    1. 根据 Class 实例化服务实现类
      由于创建类实例前一定会保证类加载完成,因此这里类加载器隐式执行了初始化,这就包括了类的静态代码块执行

回过头看com.mysql.cj.jdbc.Driveroracle.jdbc.driver.OracleDriver源码,我们都发现了类似的静态代码块:

1
2
3
4
5
6
7
8
php复制代码static {
try {
// 注册驱动
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}

可以看到,它们都调用了DriverManager#registerDriver注册了一个服务实现类实例,保存在CopyOnWriteArrayList中,后续获取数据库连接时是从这个列表中获取数据库驱动。现在,你理解了吗?


  1. 总结

    1. ServiceLoader基于 SPI 思想,可以实现服务提供方与服务使用方解耦,是模块化组件化的一种实现方式
    1. ServiceLoader是一个相对简易的框架,往往只在Java源码中使用,为了满足复杂业务的需要,一般会使用提供SPI功能的第三方框架,例如后台的Dubbo、客户端的ARouterWMRouter

在后面的文章中,我将与你探讨ARouterWMRouter的源码实现,欢迎关注彭旭锐的博客。


参考资料


推荐阅读

感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的Github!

本文转载自: 掘金

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

0%