深入剖析Spring boot自动装配原理
ServiceLoader是jdk6里面引进的一个特性。它用来实现SPI(Service Provider Interface),一种服务发现机制,很多框架用它来做来做服务的扩展发现。
系统里抽象的各个模块一般会有很多种不同的实现,如JDBC、日志等。通常模块之间我们均是基于接口进行编程,而不是对实现类进行硬编码。这时候就需要一种动态替换发现的机制,即在运行时动态地给接口添加实现,而不需要在程序中指明。
引用自JDK文档对于java.util.ServiceLoader的描述:
服务是一个熟知的接口和类(通常为抽象类)集合。服务提供者是服务的特定实现。提供者中的类通常实现接口,并子类化在服务本身中定义的子类。服务提供者可以以扩展的形式安装在 Java 平台的实现中,也就是将 jar 文件放入任意常用的扩展目录中。也可通过将提供者加入应用程序类路径,或者通过其他某些特定于平台的方式使其可用。
为了加载,服务由单个类型表示,也就是单个接口或抽象类。一个给定服务的提供者包含一个或多个具体类,这些类扩展了此服务类型,具有特定于提供者的数据和代码。提供者类通常不是整个提供者本身而是一个代理,它包含足够的信息来决定提供者是否能满足特定请求,还包含可以根据需要创建实际提供者的代码。提供者类的详细信息高度特定于服务;任何单个类或接口都不能统一它们,因此这里没有定义任何这种类型。此设施唯一强制要求的是,提供者类必须具有不带参数的构造方法,以便它们可以在加载中被实例化。
当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/ 目录里同时创建一个以服务接口命名的该服务接口具体实现类文件。当外部程序装配该模块时,通过该jar包META-INF/services/里的配置文件找到具体的实现类名,从而完成模块的注入,而不需要在代码里定制。
在JDBC中使用了ServiceLoader对不同数据库驱动进行加载。
DriverManager通过ServiceLoader加载数据库驱动,JDBC原理可移步【JDBC原理】。
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator driversIterator = loadedDrivers.iterator();
ServiceLoader 使用小栗子
首先定义一个SPIService接口
package com.study;
public interface SPIService {
void excute();
}
再定义两个实现类
package com.study.impl;
import com.study.SPIService;
public class SPIServiceImpl1 implements SPIService {
@Override
public void excute() {
System.out.println("SPIServiceImpl1");
}
}
package com.study.impl;
import com.study.SPIService;
public class SPIServiceImpl2 implements SPIService {
@Override
public void excute() {
System.out.println("SPIServiceImpl2");
}
}
在ClassPath路径下配置添加文件,META-INF/services/com.study.SPIService,文件名为接口的全限定类名。在配置文件中加入两个实现类的全限定类名。
com.study.impl.SPIServiceImpl1
com.study.impl.SPIServiceImpl2
写一个测试类SPITest.java
public class SPITest {
public static void main(String[] args) {
ServiceLoader<SPIService> loaders = ServiceLoader.load(SPIService.class);
Iterator<SPIService> it = loaders.iterator();
while (it.hasNext()) {
SPIService spiSer= it.next();
spiSer.excute();
}
}
}
运行结果:
SPIServiceImpl1
SPIServiceImpl2
ServiceLoader的load方法将在META-INF/services/com.study.SPIService中配置的子类都进行了加载。
ServiceLoader的核心源码分析
public final class ServiceLoader<S> implements Iterable<S>{
// 需要加载的资源的配置文件路径
private static final String PREFIX = "META-INF/services/";
// 加载的服务类或接口
private Class<S> service;
// 类加载时用到的类加载器
private ClassLoader loader;
// 基于实例的已加载的顺序缓存类,其中Key为实现类的全限定类名
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// "懒查找"迭代器,ServiceLoader的核心
private LazyIterator lookupIterator;
public void reload() {
// 清空缓存
providers.clear();
// 构造LazyIterator实例
lookupIterator = new LazyIterator(service, loader);
}
// 私有构造方法
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = svc;
loader = cl;
reload();
}
}
ServiceLoader只有一个私有的构造函数,也就是它不能通过构造函数实例化,但是要实例化ServiceLoader必须依赖于它的静态方法调用私有构造去完成实例化操作。
来看ServiceLoader的提供的静态方法,这几个方法都可以用于构造ServiceLoader的实例。
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前线程的线程上下文类加载器实例,确保通过此classLoader也能加载到项目中的资源文件
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
load(Class<S> service, ClassLoader loader)是典型的静态工厂方法,直接调用ServiceLoader的私有构造器进行实例化,除了需要指定加载类的目标类型,还需要传入类加载器的实例。load(Class<S>service)实际上也是委托到load(Class<S> service, ClassLoader loader),不过它使用的类加载器指定为线程上下文类加载器,一般情况下线程上下文类加载器获取到的就是应用类加载器(系统类加载器)。loadInstalled(Class<S> service)方法又看出了"双亲委派模型"的影子,它指定类加载器为最顶层的启动类加载器,最后也是委托到load(Class<S> service, ClassLoader loader)。
这里重点关注为什么类加载器使用线程上下文类加载器?以JDBC加载MySQL驱动举例。
Java类加载的过程通常是遵循双亲委派模型的。但是对于SPI接口实现类的加载就需要破坏双亲委派模型。
首先java.sql.DriverManager是由启动类加载器加载的,创建真正的Dirver对象时需要使用到mysql提供的实现:com.mysql.jdbc.Dirver,即要初始化该类。但是启动类加载器加载DirverManager的时候,使用到了启动类加载器无法加载的类,这时候就需要由系统类加载器来加载。com.mysql.jdbc.Dirver通常放在类路径下的(其实不一定)。到这里和线程上下文类加载器没由任何关系。在DriverManager中使用系统类加载的时候,可以直接使用静态方法ClassLoader.getSystemClassLoader(),但是这种情况的前提是com.mysql.jdbc.Dirver类在类路径下。如果不在类路径下,而且在系统环境中有其他的类加载器,在通过其他类加载器可能出现无法正确加载扩展点的情况。比如某个类的字节码是在数据库中存储,这时我们需要自定义一个类加载器去加载它,这个类加载器会告诉DriverManager去我们指定放的地方取。因此Thread.currentThread().setContextClassLoader(自定义加载器/默认是系统类加载器); 这个就是线程上下文类加载器起到的介质作用。线程上下文中默认放的是系统类加载器。
ServiceLoader实现了Iterable接口。
public Iterator<S> iterator() {
// Iterator的匿名实现
return new Iterator<S>() {
// 基于实例的已加载的顺序缓存类Map的Entry迭代器实例
Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
// 先从缓存中判断是否有下一个实例,否则通过懒加载迭代器LazyIterator去判断是否存在下一个实例
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
// 如果缓存中判断是否有下一个实例,如果有则从缓存中的值直接返回,否则通过懒加载迭代器LazyIterator获取下一个实例
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
// 不支持移除
public void remove() {
throw new UnsupportedOperationException();
}
};
}
LazyIterator本身也是一个Iterator接口的实现,它是ServiceLoader的一个私有内部类。
private class LazyIterator implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
// 加载的资源的URL集合
Enumeration<URL> configs = null;
// 所有需要加载的实现类的全限定类名的集合
Iterator<String> pending = null;
// 下一个需要加载的实现类的全限定类名
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
public boolean hasNext() {
// 如果下一个需要加载的实现类的全限定类名不为null,则说明资源中存在内容
if (nextName != null) {
return true;
}
// 如果加载的资源的URL集合为null则尝试进行加载
if (configs == null) {
try {
// 资源的名称,META-INF/services/ + '需要加载的类的全限定类名'
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 从ClassPath加载资源
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 从资源中解析出需要加载的所有实现类的全限定类名
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
// 获取下一个需要加载的实现类的全限定类名
nextName = pending.next();
return true;
}
public S next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
String cn = nextName;
nextName = null;
try {
// 反射构造Class<S>实例,同时进行初始化,并且强制转化为对应的类型的实例
S p = service.cast(Class.forName(cn, true, loader).newInstance());
// 添加进缓存,Key为实现类的全限定类名,Value为实现类的实例
providers.put(cn, p);
return p;
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
} catch (Throwable x) {
fail(service, "Provider " + cn + " could not be instantiated: " + x, x);
}
throw new Error(); // This cannot happen
}
public void remove() {
throw new UnsupportedOperationException();
}
}
ServiceLoader总结
JDK提供了一种帮第三方实现者加载服务的便捷方式,如JDBC、日志等,第三方实现者需要遵循约定把具体实现的类名放在/META-INF里。当JDK启动时会去扫描所有jar包里符合约定的类名,再调用forName进行加载,如果JDK的ClassLoader无法加载,就使用当前执行线程的线程上下文类加载器。
但是在通过SPI查找服务的具体实现时,会遍历所有的实现进行实例化,并在循环中找到需要的具体实现。
作者:java知路
欢迎关注微信公众号 :java知路