Java | 带你理解 ServiceLoader 的原理与设计
前言
-
ServiceLoader
是Java
提供的一套SPI(Service Provider Interface,常译:服务发现)框架,用于实现服务提供方与服务使用方解耦 - 在这篇文章里,我将带你理解
ServiceLoader
的原理与设计思想,希望能帮上忙
目录

1. SPI 简介
- 定义
一个服务的注册与发现机制 - 作用
通过解耦服务提供者与服务使用者,帮助实现模块化、组件化
2. ServiceLoader 使用步骤
我们直接使用JDBC
的例子,帮助各位建立起对ServiceLoader
的基本了解,具体如下:
我们都知道JDBC
编程有五大基本步骤:
- 执行数据库驱动类加载(非必须):
Class.forName("com.mysql.jdbc.driver")
- 执行数据库驱动类加载(非必须):
- 连接数据库:
DriverManager.getConnection(url, user, password)
- 连接数据库:
- 创建SQL语句:
Connection#.creatstatement();
- 创建SQL语句:
- 执行SQL语句并处理结果集:
Statement#executeQuery()
- 执行SQL语句并处理结果集:
- 释放资源:
ResultSet#close()
、Statement#close()
与Connection#close()
- 释放资源:
操作数据库需要使用厂商提供的数据库驱动程序,直接使用厂商的驱动耦合太强了,更推荐的方法是使用DriveManager
管理类:
步骤1:定义服务接口
JDBC
抽象出一个服务接口,数据库驱动实现类统一实现这个接口:
public interface Driver {
// 创建数据库连接
Connection connect(String url, java.util.Properties info)
throws SQLException;
// 省略其他方法...
}
步骤2:实现服务接口
服务提供者(数据库厂商)提供一个或多个实现这个服务的类(驱动实现类),具体如下:
-
mysql:
com.mysql.cj.jdbc.Driver.java
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!");
}
}
// 省略...
}
-
oracle:
oracle.jdbc.driver.OracleDriver.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
(文件名为服务接口的全限定名),文件中每一行是实现类的全限定名,例如:
com.mysql.cj.jdbc.Driver
我们可以解压mysql-connector-java-8.0.19.jar
包,找到对应的META-INF
文件夹。
步骤4:加载服务
// 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>
遍历了所有驱动实现类,但是为什么在迭代里没有任何操作呢?
while(driversIterator.hasNext()) {
driversIterator.next();
// 疑问:为什么没有任何处理?
}
在下一节,我们深入ServiceLoader
的源码来解答这个问题。
3. ServiceLoader 源码解析
# 提示 #
ServiceLoader
中有一些源码使用了安全检测,如AccessController.doPrivileged()
,在以下代码摘要中省略
-
工厂方法
ServiceLoader
提供了三个静态泛型工厂方法,内部最终将调用ServiceLoader.load(Class,ClassLoader)
,具体如下:
// 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...)
- 构造方法
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
迭代器的实例,这是一个“懒加载”的迭代器。那么这个迭代器在哪里使用的呢?继续往下看~
- 外部迭代器
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()
会将每轮迭代中取到的元素put
到providers
集合中,providers
其实是LazyIterator
的内存缓存。
- 内部迭代器
# 提示 #
以下代码摘要中省略了源码中的
try-catch
// 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();
}
4. 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
实现类:
// 服务接口
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
的强引用,才能作为垃圾回收。
5. 问题回归
现在我们回到阅读DriverManager
源码提出的疑问:
while(driversIterator.hasNext()) {
driversIterator.next();
// 疑问:为什么没有任何处理?
}
为什么next()
操作既不取得服务实现类对象,后续也没有任何处理呢?我们再回去看下LazyIterator#next()
的源码:
// 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;
}
- 使用类加载器loader加载:
Class<?> c = Class.forName(cn, false, loader);
这里传参使用false
,类加载器将执行加载 -> 链接,不会执行初始化
- 使用类加载器loader加载:
- 根据 Class 实例化服务实现类
由于创建类实例前一定会保证类加载完成,因此这里类加载器隐式执行了初始化,这就包括了类的静态代码块执行
- 根据 Class 实例化服务实现类
回过头看com.mysql.cj.jdbc.Driver
和oracle.jdbc.driver.OracleDriver
源码,我们都发现了类似的静态代码块:
static {
try {
// 注册驱动
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
可以看到,它们都调用了DriverManager#registerDriver
注册了一个服务实现类实例,保存在CopyOnWriteArrayList
中,后续获取数据库连接时是从这个列表中获取数据库驱动。现在,你理解了吗?
6. 总结
-
ServiceLoader
基于 SPI 思想,可以实现服务提供方与服务使用方解耦,是模块化、组件化的一种实现方式
-
-
ServiceLoader
是一个相对简易的框架,往往只在Java
源码中使用,为了满足复杂业务的需要,一般会使用提供SPI
功能的第三方框架,例如后台的Dubbo
、客户端的ARouter
与WMRouter
等
-
在后面的文章中,我将与你探讨ARouter
与WMRouter
的源码实现,欢迎关注彭旭锐的博客。
参考资料
-
WMRouter Github
—— meituan -
ARouter Github
—— alibaba -
ServiceLoader
—— Android Developers - 《美团猫眼电影android模块化实战--可能是最详细的模块化实战》 —— 陈文超happylion 著
- 《WMRouter:美团外卖Android开源路由框架》 —— 子健 渊博 云驰 (美团技术团队)著
- 《Android组件化方案及组件消息总线modular-event实战》 —— 海亮 (美团技术团队)著
推荐阅读
- Java | 详解 Unicode 字符集
- Java | 这几道阿里Java浮点数笔试题,让我陷入了沉思
- Android | 一文带你全面了解 AspectJ 框架
- Android | 使用 AspectJ 限制按钮快速点击
- Android | 这是一份详细的 EventBus 使用教程
- 开发者 | 浅析App社交分享的5种形式
- 开发者 | 几个提高远程办公效率的小建议
- 开发者 | 那些令人“奔溃”的 UI 验收
感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的简书!
