请点赞关注,你的支持对我意义重大。
🔥 Hi,我是小彭。本文已收录到 GitHub · AndroidFamily 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] 带你建立核心竞争力。
前言
大家好,我是小彭。
过去两年,我们在掘金平台上发布过一些文章,小彭也受到了大家的意见和鼓励。最近,小彭会陆续搬运到公众号上。
记录:2022 年 9 月 6 日修订:优化文章结构
学习路线图:
1.1 什么是服务发现
服务发现(Service Provider Interface,SPI)是一个服务的注册与发现机制,通过解耦服务提供者与服务使用者,实现了服务创建 & 服务使用的关注点分离。 服务提供模式可以为我们带来以下好处:
- 1、在外部注入或配置依赖项,因此我们可以重用这些组件。当我们需要修改依赖项的实现时,不需要大量修改很多处代码,只需要修改一小部分代码;
- 2、可以注入依赖项的模拟实现,让代码测试更加容易。
服务发现示意图
1.2 服务发现和依赖注入的区别
服务发现和依赖注入都是控制反转 Ioc 的实现形式之一。 IoC 可以认为是一种设计模式,但是由于理论成熟的时间相对较晚,所以没有包含在《设计模式 · GoF》之中,即: 当依赖方需要使用依赖项时,不再直接构造对象,而是由外部 IoC 容器来创建并提供依赖。
- 1、服务提供模式: 从外部服务容器抓取依赖对象,调用方可以 “主动” 控制请求依赖对象的时机;
- 2、依赖注入: 并以参数的形式注入依赖对象,调用方 “被动” 接收外部注入的依赖对象。
在分析 ServiceLoader 的使用原理之前,我们先来介绍下 ServiceLoader 的使用步骤。
我们直接以 JDBC 作为例子,其中「2、连接数据库」内部就是用了 ServiceLoader。为什么连接数据库需要使用 SPI 设计思想呢?因为操作数据库需要使用厂商提供的数据库驱动程序,如果直接使用厂商的驱动耦合太强了,而使用 SPI 设计就能够实现服务提供者与服务使用者解耦。
以下为使用步骤,具体分为 5 个步骤:
- 1、(非必须)执行数据库驱动类加载:
1 | java复制代码Class.forName("com.mysql.jdbc.driver") |
- 2、连接数据库:
1 | java复制代码DriverManager.getConnection(url, user, password) |
- 3、创建SQL语句:
1 | java复制代码Connection#.creatstatement(); |
- 4、执行SQL语句并处理结果集:
1 | java复制代码Statement#executeQuery() |
- 5、释放资源:
1 | java复制代码ResultSet#close() |
下面,我们一步步手写 JDBC 中关于 ServiceLoader 的相关源码:
步骤 1:定义服务接口
定义一个驱动接口,这个接口将由数据库驱动实现类实现。在服务发现框架中,这个接口就是服务接口。
1 | java复制代码public interface Driver { |
步骤 2:实现服务接口
数据库厂商提供一个或多个实现 Driver 接口的驱动实现类,以 mysql 和 oracle 为例:
- mysql:
com.mysql.cj.jdbc.Driver.java
1 | java复制代码// 已简化 |
- oracle:
oracle.jdbc.driver.OracleDriver.java
1 | java复制代码// 已简化 |
步骤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:(使用方)加载服务
DriverManaer.java
1 | java复制代码// 已简化 |
可以看到,DriverManager 被类加载时(static{})会调用 loadInitialDrivers()
。这个方法内部通过 ServiceLoader 提供的迭代器 Iterator 遍历了所有驱动实现类。那么,ServiceLoader 是如何实例化 Driver 接口的实现类的呢?下一节,我们会深入 ServiceLoader 的源码来解答这个问题。
3.1 ServiceLoader 入口方法
ServiceLoader 提供了三个静态泛型工厂方法,内部最终将调用 ServiceLoader.load(Class, ClassLoader)
,其中第一个参数就是服务接口的 Class 对象。
ServiceLoader.java
1 | java复制代码// 方法 1: |
可以看到,三个方法仅在传入的类加载器不同,最终只是返回了一个面向服务接口 S 的 ServiceLoader 对象。我们先看一下构造器里做了什么工作。
3.2 ServiceLoader 构造方法
ServiceLoader.java
1 | java复制代码// 已简化 |
可以看到,ServiceLoader 的构造器中主要就是实例化了一个 LazyIterator
迭代器的实例,这是一个「懒加载」的迭代器。这个迭代器里做了什么呢?我们继续往下看
3.3 LazyIterator 迭代器
ServiceLoader.java
1 | java复制代码// -> 3、实例化 LazyIterator |
以上代码已经非常简化了,LazyIterator 的要点如下:
- hasNext() 判断逻辑:
- 3.1.1 拼接配置文件路径:「META-INF/services/服务接口的全限定名」;
- 3.1.2 解析配置文件资源的迭代器;
- 3.1.3 找到下一个实现类的全限定名。
- next() 逻辑:
- 3.2.1 使用类加载器 loader 加载(不执行初始化);
- 3.2.2 使用反射创建服务类实例;
- 3.2.3 服务实现类缓存到 providers。
小结一下: LazyInterator 会解析「META-INF/services/服务接口的全限定名」配置,遍历每个服务实现类全限定类名,执行类加载(未初始化),最后将服务实现类缓存到 providers。
那么,这个迭代器在哪里使用的呢?继续往下看~
3.4 包装迭代器
其实 ServiceLoader 本身就是实现 Iterable 接口的:
ServiceLoader.java
1 | java复制代码public final class ServiceLoader<S> implements Iterable<S> |
让我们来看看 ServiceLoader 中的 Iterable#iterator()
是如何实现的:
1 | java复制代码private LazyIterator lookupIterator; |
可以看到,ServiceLoader 里有一个泛型方法 Iterator iterator(),它包装了 providers 集合迭代器和 lookupIterator 两个迭代器。对于已经 “发现” 的服务实现类会被缓存到 providers 集合中,包装类的作用就是优先读取缓存而已。
理解 ServiceLoader 源码之后,我们总结要点如下:
4.1 约束
1、服务实现类必须实现服务接口 S( if (!service.isAssignableFrom(c))
);
2、服务实现类需包含无参的构造器,LazyInterator 是反射创建实现类市里的( S p = service.cast(c.newInstance())
);
3、配置文件需要使用 UTF-8 编码( new BufferedReader(new InputStreamReader(in, "utf-8"))
)。
4.2 懒加载
ServiceLoader 使用「懒加载」的方式创建服务实现类实例,只有在迭代器推进的时候才会创建实例( nextService()
)。
4.3 内存缓存
ServiceLoader 使用 LinkedHashMap 缓存创建的服务实现类实例。
提示: LinkedHashMap 在迭代时会按照 Map#put 执行顺序遍历。
4.4 没有服务注销机制
服务实现类实例被创建后,它的垃圾回收的行为与 Java 中的其他对象一样,只有这个对象没有到 GC Root 的强引用,才能作为垃圾回收。而 ServiceLoader 内部只有一个方法来完全清除 provices 内存缓存。
1 | csharp复制代码public void reload() { |
4.5 没有服务筛选机制
当存在多个提供者时,ServiceLoader 没有提供筛选机制,使用方只能在遍历整个迭代器中的所有实现,从发现的实现类中决策出一个最佳实现。举个例子,我们可以使用字符集的表示符号来获得一个对应的 Charset 对象:Charset.forName(String)
,这个方法里面就只会选择匹配的 Charaset 对象。
CharsetProvider.java
1 | java复制代码服务接口 |
Charset.java
1 | java复制代码public static Charset forName(String charsetName) { |
- 服务发现 SPI 是控制反转 IoC 的实现方式之一,而 ServiceLoader 是 JDK 中实现的 SPI 框架。ServiceLoader 本身就是一个 Iterable 接口,迭代时会从
META-INF/services
配置中解析接口实现类的全限定类名,使用反射创建服务实现类对象; - ServiceLoader 是 JDK 自带的服务发现框架,原理也相对简单,比如 Charset、AnnocationProcessor 等功能都是基于 ServiceLoader 实现的。另一方面,ServiceLoader 是一个相对简易的框架,为了满足复杂业务的需要,一般会使用其他第三方框架,例如后台的 Dubbo、客户端的 ARouter 与 WMRouter等。
我是小彭,带你构建 Android 知识体系。技术和职场问题,请关注公众号 [彭旭锐] 私信我提问。
本文转载自: 掘金