本文承接上文《JDBC获取连接中的java知识(1):类的加载》,通过学习本文可以习得以下内容
- SPI模式是什么,怎么用;
- ServiceLoader源码分析
- 一种对插件和中台友好的架构设计;
提出问题
上次讲到了JDBC中的DriverManager的类初始化时会loadInitialDrivers,参考其源码可以发现loaderDrivers的核心流程是ServiceLoader相关的操作,创建serviceLoader并不断调用interator.next。执行之后registeredDrivers就有了可以用来提供服务的driver实例。但这中间是如何实现并产出driver实例的?
// 1
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
// 2
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
SPI的含义
接口角度
SPI全称是service provider interface和API(application programming interface)的语意非常接近,两者都是对类、接口和方法的描述。
但不同的是,如果说API是提供信息给调用方使用,API告诉调用者一个类、接口和方法如何使用来达到调用者的目的;SPI则是提出要求让服务提供方实现,SPI告诉实现者要满足什么要求才能提供服务。
API和SPI可能是不同的class/interface,也可以有双重身份。比如本文涉及的JDBC的Connection具有双重身份:因为jdbc直接把connection提供给用户使用,所以connection是jdbc提给应用方的api;同时connection要由驱动实现方实现,对db的driver它就是一个SPI了。Driver其实更像一个单纯的SPI,因为jdbc主要通过DriverManager而非Driver本身提供服务给应用方。
模型角度
SPI同时是一种编程架构和类动态加载的实现机制,这也就是下文要讲的内容。
SPI中的设计思想
在上一节classloading中讲到loading中的二进制字节流可以来自于任意地方,那么是不是可以在使用时按照一种规范统一加载、初始化、实例化一些自己想要的类呢? 具体到一个interface可以有多个接口实现方,接口实现方是不是按照约定的规范实现自己的implementation之后就能自动提供服务呢呢?
顺着这个思路就捋顺了SPI的设计。
在SPI模型中这个供大家来实现的interface称为ServiceProviderInterface也就是SPI,各个实现这个接口的具体类称为ServiceProvider,负责统一动态加载providers然后进行实例化的类就是ServiceLoader,约定的规范就是在META-INF/services路径下的配置文件。
JDBC中SPI的核心组成
通过上图SPI模型的核心组成已经很清晰了:
- ServiceProviderInterface:这个类完全由应用方定义。在JDBC中,Driver.class是Java系统希望所有DBMS实现的标准服务接口,所以这里java.sql.Driver就是ServiceProviderInterface;
- ServiceProvider:这个就是完整实现(1)的具体类,比如mysql的connector-J中实现Driver的com.mysql.cj.jdbc.Driver;
- ServiceLoader:java.util.ServiceLoader实现了发现providers,并进行加载实例化的全过程;ServiceLoader是整个SPI模型的核心,来自于java核心库,下一节将展开分析它的实现;
- META-INF/services/SPI-name下的配置文件:这个配置文件是ServiceLoader进行扫描的目标,并按照其中信息进行类的加载。比如在mysql-connector中的文件如下图所示,以SPI命名并以ServiceProvider的fullName为内容;
ServiceLoader
下面重点分析一下ServiceLoader类的设计。
serviceLoader fields
// 约定好的配置路径
private static final String PREFIX = "META-INF/services/";
// load的对象
// The class or interface representing the service being loaded
private final Class<S> service;
// 需要用到的类加载器
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// 资源权限控制
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// 用于存储加载后的serviceProvider实例,注意不是static的;
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 核心设计,懒加载的实现类
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
构造器需要的信息:为谁代言,怎么加载
load服务具体实现时,首先要明确实现的interface是哪个,并且用哪个ClassLoader加载; 于是构造器的两个入参就是Class<S>和Classloader。
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
- 当传入的Classloader是null时,ServiceLoader会直接去读SystemClassloader,一般情况下scl就是上一节中讲到的Laucher中的appClassloader;
- 另一个值得注意的是AccessControlContext acc这个字段来帮助判断获得资源的权限验证;
- 最后一个关键点就是reload方法,reload方法首先清空了之前实例并初始化了用于加载的关键的lazyIterator
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
ServiceLoader的构造器是私有的,开放了三个静态方法来获得SL实例。
JDBC中只传入了class作为入参,可以发现Serviceloader直接使用当前threadContext中的classloader,这是一种非常简便的方法来确认classloader。如果用户没有设置过threadContextClassloader,那么在上一节中讲过,launcher函数会把thread中的cl设置为appClassloader。
// jdbc中加载方法
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// ServiceLoader中该方法定义
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
iterator的组成
这里先看一下JDBC是如何加载所有drivers的,JDBC获得了iterator之后不断遍历,然后就结束了。 这里可能会有个疑问,JDBC没有使用任何遍历后的结果,也没有特殊的操作,为什么这个遍历之后就可以有可以用的数据库驱动能直接获得connection了呢。
这个问题的答案有两部分:1)iterator在遍历过程中做了什么;2)JDBC的Driver是如何设计的;
// JDBC对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;
// driversIterator.next的源码实现
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
serviceloader返回的iterator其实内部分为两部分:一个是之前讲过的实例化缓存好的providers的iterator命名为knownIterator,一个上文初始化的LazyIterator命名为lookupIterator;在hasNext和next调用时,总是优先返回已知的provider,如果为null或者false再去调用lookupIterator。
而在这个serviceloader第一次使用时providers总是空的,因为ServiceLoader整体的加载策略是一个lazy的策略:实例化serviceloader并不会触发eager的类的加载与实例化,而是在用时一个个加载,这也是为什么执行lookup的类称为lazy的缘故。
第一次providers都是空的,那么jdbc调用的next实际都是在执行lookupIterator的next,也就是LazyIterator的next。而类的加载和实例化的核心就在Lazy这个类里。
LazyLookup by iterator
常常觉得这个类有点歧义,它实际上是在用遍历的方法懒惰的寻找serviceProvider,可能叫做LazyLookup更合适。 LazyInterator的核心实现是hasNextService和nextService,也就是判断是否还有provider,有则加载实例化返回。
hasNextService
hasNextService的核心就是使用classloader,在所负责的类的路径中,按约定好的路径META-INF/services/SPI-name来寻找provider,如果能找到就在每次调用时解析出一个provider的名字,存在nextName里。
注意这里的实现是lazy的,并不会把所有的configs的内容全部解析完,而是调用一次解析一次。
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// --- 约定路径
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// --- classloader 委派模型来查找资源
// --- 查找结果存在configs里,只搜索一次,搜索是一次性的
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// ---解析文件中serviceProvider名字的过程,由于一个文件中可能有多个provider,所以pending实际也是一个iterator;
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
nextService
在hasNextService解析名字之后,nextService直接根据name进行类的加载、校验和实例化,并把实例化的结果放到providers里。
Class<?> c = null;
try {
// 加载类,但并不进行实例化,false字段关闭initialization
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
// 校验
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
// 实例化并缓存
S p = service.cast(c.newInstance());
providers.put(cn, p);
自动注册的实例
讲到这里serviceLoader如何工作的核心机制就讲完了,但是刚刚有个尾巴,就是jdbc代码不断调用next并没有对返回的实例做了什么特殊处理?当时说答案有两部分:1)iterator做了什么,根据刚刚的讲解,当jdbc代码中不断调用next时serviceLoader把所有支持的drvier都加载并实例化了;2)但是显然JDBC并没有使用返回的实例,这是因为JDBC使用的是另一种机制,也就是实例的自动注册。
JDBC借用SPI模式下的两个特点(1)发现所有providers(2)初始化类。当类初始化时,Driver中的static代码就可以将Driver实例自动注册到manager中去提供服务了。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
// 类初始化的自动注册过程
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
整个JDBC获得连接中隐藏的设计可以总结为下图。这套设计及其中使用的服务发现、iterator实现懒加载、实例自动注册、classLoader进行资源扫描等思路方法都非常值得学习。
插件式管理,中台的应用
这一部分应用场景实在是抛砖引玉。
插件式的产品
上图提供了一种插件式的管理方案,providers就是实现了产品基本接口的个性化插件,可以通过serviceLoader自动加载并实例化,providers可以自动注册到产品的插件管理中心。
业务中台
这种架构模式在业务开发中常常用于中台。举个例子来说,当一个业务的主要业务是A,但是业务方BCD的逻辑实现都可能都略有不同。这个时候中台开发者开发者只需要实现核心逻辑,把个性化的业务逻辑定义成一个ServiceProvider,业务方ABCD自己实现provider让中台项目能通过classLoader(可能是自定义的)加载这些类就可以了。