前言
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!
本文转载自: 掘金