前言
ServiceLoader
是Java
提供的一套**SPI(Service Provider Interface,常译:服务发现)**框架,用于实现服务提供方与服务使用方解耦- 在这篇文章里,我将带你理解
ServiceLoader
的原理与设计思想,希望能帮上忙。请点赞,你的点赞和关注真的对我非常重要!
目录
- SPI 简介
- 定义
- 一个服务的注册与发现机制*
- 作用
通过解耦服务提供者与服务使用者,帮助实现模块化、组件化
- 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
抽象出一个服务接口,数据库驱动实现类统一实现这个接口:
1 | java复制代码public interface Driver { |
步骤2:实现服务接口
服务提供者(数据库厂商)提供一个或多个实现这个服务的类(驱动实现类),具体如下:
- mysql:
com.mysql.cj.jdbc.Driver.java
1 | scala复制代码public class Driver extends NonRegisteringDriver implements java.sql.Driver { |
- oracle:
oracle.jdbc.driver.OracleDriver.java
1 | java复制代码public class OracleDriver implements Driver { |
步骤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 | csharp复制代码// DriverManager.java |
可以看到,DriverManager
的静态代码块调用loadInitialDrivers ()
,方法内部通过ServiceLoader
提供的迭代器Iterator<Driver>
遍历了所有驱动实现类,但是为什么在迭代里没有任何操作呢?
1 | scss复制代码while(driversIterator.hasNext()) { |
在下一节,我们深入ServiceLoader
的源码来解答这个问题。
- ServiceLoader 源码解析
# 提示
ServiceLoader
中有一些源码使用了安全检测,如AccessController.doPrivileged()
,在以下代码摘要中省略
- 工厂方法
ServiceLoader
提供了三个静态泛型工厂方法,内部最终将调用ServiceLoader.load(Class,ClassLoader)
,具体如下:
1 | java复制代码// 1. |
可以看到,三个方法仅在传入的ClassLoader
参数有区别,若还不了解ClassLoader
,请务必阅读[《Java | 带你理解 ClassLoader 的原理与设计思想》](Editting…)
- 构造方法
1 | ini复制代码private final Class<S> service; |
可以看到,ServiceLoader
的构造器中创建了LazyIterator
迭代器的实例,这是一个“懒加载”的迭代器。那么这个迭代器在哪里使用的呢?继续往下看~
- 外部迭代器
1 | typescript复制代码private LazyIterator lookupIterator; |
可以看到,ServiceLoader
里有一个泛型方法Iterator<S> iterator()
,它包装了providers
集合迭代器和lookupIterator
两个迭代器,迭代过程中优先从providers
获取元素。
为什么要优先从providers
集合中取元素呢?阅读源码发现,LazyIterator#next()
会将每轮迭代中取到的元素put
到providers
集合中,providers
其实是LazyIterator
的内存缓存。
- 内部迭代器
# 提示
以下代码摘要中省略了源码中的
try-catch
1 | ini复制代码// ServiceLoader.java |
- 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 | java复制代码// 服务接口 |
ServiceLoader
没有提供服务的注销机制
服务实现类实例被创建后,它的垃圾回收的行为与Java
中的其他对象一样,只有这个对象没有到GC Root
的强引用,才能作为垃圾回收。
- 问题回归
现在我们回到阅读DriverManager
源码提出的疑问:
1 | scss复制代码while(driversIterator.hasNext()) { |
为什么next()
操作既不取得服务实现类对象,后续也没有任何处理呢?我们再回去看下LazyIterator#next()
的源码:
1 | ini复制代码// ServiceLoader.java |
- 使用类加载器loader加载:
Class<?> c = Class.forName(cn, false, loader);
这里传参使用false
,类加载器将执行加载 -> 链接,不会执行初始化
- 使用类加载器loader加载:
- 根据 Class 实例化服务实现类
由于创建类实例前一定会保证类加载完成,因此这里类加载器隐式执行了初始化,这就包括了类的静态代码块执行
- 根据 Class 实例化服务实现类
回过头看com.mysql.cj.jdbc.Driver
和oracle.jdbc.driver.OracleDriver
源码,我们都发现了类似的静态代码块:
1 | php复制代码static { |
可以看到,它们都调用了DriverManager#registerDriver
注册了一个服务实现类实例,保存在CopyOnWriteArrayList
中,后续获取数据库连接时是从这个列表中获取数据库驱动。现在,你理解了吗?
- 总结
ServiceLoader
基于 SPI 思想,可以实现服务提供方与服务使用方解耦,是模块化、组件化的一种实现方式
ServiceLoader
是一个相对简易的框架,往往只在Java
源码中使用,为了满足复杂业务的需要,一般会使用提供SPI
功能的第三方框架,例如后台的Dubbo
、客户端的ARouter
与WMRouter
等
在后面的文章中,我将与你探讨ARouter
与WMRouter
的源码实现,欢迎关注彭旭锐的博客。
参考资料
WMRouter Github
—— meituanARouter Github
—— alibabaServiceLoader
—— Android Developers- 《美团猫眼电影android模块化实战–可能是最详细的模块化实战》 —— 陈文超happylion 著
- 《WMRouter:美团外卖Android开源路由框架》 —— 子健 渊博 云驰 (美团技术团队)著
- 《Android组件化方案及组件消息总线modular-event实战》 —— 海亮 (美团技术团队)著
推荐阅读
- Android | 带你理解 NativeAllocationRegistry 的原理与设计思想
- Android | 一文带你全面了解 AspectJ 框架
- Android | 使用 AspectJ 限制按钮快速点击
- Android | 这是一份详细的 EventBus 使用教程
- 开发者 | 浅析App社交分享的5种形式
- 开发者 | 那些令人“奔溃”的 UI 验收
- 计算机组成原理 | Unicode 和 UTF-8是什么关系?
- 计算机组成原理 | 为什么浮点数运算不精确?(阿里笔试)
感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的Github!
本文转载自: 掘金