ServiceLoader / SPI 服务发现机制源码
概述
ServiceLoader 是 JDK 的服务提供者加载机制(SPI):服务接口定义能力,实现类以配置文件(META-INF/services/接口全限定名)或模块描述(provides ... with ...)的形式注册,运行时由 ServiceLoader 统一发现并实例化。它解耦了"接口定义方"与"实现提供方",是 DriverManager、Charset、FileSystemProvider 等大量 JDK 内建扩展点的共同基石。
JDK 9 模块化之后 ServiceLoader 支持两条查找路径:classpath(META-INF/services 文件)与模块路径(ModuleLayer 中的 provides 声明),两者组合构成完整发现链。本文基于 OpenJDK 21 源码拆解这条链路。
核心源码解析
① ServiceLoader.load(Class<S> service) 的创建
java
public final class ServiceLoader<S> implements Iterable<S> {
private final Class<S> service; // 服务接口类型
private final ClassLoader loader; // 加载实现类的类加载器
private final AccessControlContext acc; // 安全上下文
private final ModuleLayer layer; // 模块查找的层(JDK 9+)
private LinkedHashMap<String, S> providers; // 已实例化缓存
private LazyClassPathLookupIterator lookupIterator1; // classpath 查找
private ModuleServicesLookupIterator lookupIterator2; // 模块路径查找
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader(); // ① TCCL
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
return new ServiceLoader<>(Reflection.getCallerClass(), service, loader);
}
}load(service)默认用线程上下文类加载器(TCCL)——这是 SPI 惯例:框架代码(如 JDBC)由系统类加载器加载,却要加载应用 classpath 中的实现,必须经由 TCCL 桥接。layer字段取当前模块层(ModuleLayer.boot()通常),仅当服务接口来自该层时才启用模块路径查找。- 构造只是装配字段,不扫描任何内容;真正的查找全部延迟到迭代开始。
② ServiceLoader.iterator() 的懒加载迭代
java
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String, S>> knownProviders = providers.entrySet().iterator(); // ① 先迭代已加载
public boolean hasNext() {
if (knownProviders.hasNext()) return true;
return lookupIterator1.hasNext() || lookupIterator2.hasNext(); // ② 再懒扫描
}
...
};
}- 迭代顺序:已实例化的缓存优先,然后 classpath 查找器(
lookupIterator1),最后模块路径查找器(lookupIterator2)。 LazyClassPathLookupIterator.hasNext()首次调用才扫描配置资源;Class.forName(providerName, false, loader)只加载类不初始化,实例化推迟到next()。- 整个迭代是懒加载的:不迭代就不会读取任何
META-INF/services文件,实现类也只有在next()时才会被真正实例化。
③ META-INF/services/ 文件的解析
java
// LazyClassPathLookupIterator.nextProviderClass 链路
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
InputStream in = u.openStream();
BufferedReader r = new BufferedReader(new InputStreamReader(in, "utf-8")); // ① UTF-8 读取
List<String> names = new ArrayList<>();
int lc = 1;
String line;
while ((line = r.readLine()) != null) {
parseLine(service, u, lc, names, line); // ② 逐行解析
lc++;
}
...
}
private int parseLine(Class<?> service, URL u, int lc, List<String> names, String line) {
int ci = line.indexOf('#'); // ③ 注释
if (ci >= 0) line = line.substring(0, ci);
line = line.trim(); // ④ 去首尾空白
if (line.length() == 0) return 0;
if ((line.indexOf(' ') >= 0) || (line.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax"); // ⑤ 行内不能有空白
int cp = line.codePointAt(0);
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name");
...
names.add(line); // ⑥ 收集类名
return 1;
}- 配置文件按 UTF-8 逐行读取,
#后是注释,每行一个实现类的全限定名。 - 语法校验严格:行内出现空格/制表符直接报错,类名必须以 Java 标识符起始字符开头——保证配置内容可被
Class.forName安全加载。 load()加载配置文件的 URL:Enumeration<URL>遍历所有资源(多个 jar 可各有一份),类名去重后逐个解析。
④ ServiceLoader.stream() 的 Provider 懒实例化
java
public Stream<Provider<S>> stream() {
int size = ...;
Stream<Provider<S>> stream = StreamSupport.stream(...);
...
return stream;
}
public static interface Provider<S> extends Supplier<S> {
Class<? extends S> type(); // ① 实现类类型(不实例化即可获取)
S get(); // ② 实例化并缓存
}
// 实现:LazyProvider / ProviderImpl
public S get() {
if (!isInitialized) {
ctor = ...; // 缓存构造器
isInitialized = true;
}
try {
return ctor.newInstance(); // ③ 反射创建实例
} catch (Exception e) { ... }
}- 与
iterator()返回裸实例不同,stream()返回Provider对象:先拿到type()(类元数据)做筛选,需要时才调用get()实例化。 ProviderImpl会缓存Constructor并标记初始化状态,避免重复反射查找;实例化走Constructor.newInstance。stream()的流是惰性求值的:filter/map等中间操作不会触发服务扫描,只有终端操作才驱动迭代。
⑤ ModuleServicesLookupIterator 的模块路径扫描
java
// ModuleServicesLookupIterator
private boolean hasNextService() {
if (next == null) {
if (!hasNext) {
Set<String> providerNames = new HashSet<>();
for (Module module : layer.modules()) { // ① 遍历层中所有模块
String pn = service.getPackageName();
if (module.getDescriptor().exports().stream()
.anyMatch(e -> e.source().equals(pn))) { // ② 模块必须导出该包
for (Provides provides : module.getDescriptor().provides()) {
if (provides.service().equals(service.getName())) {
for (String impl : provides.providers()) {
providerNames.add(impl); // ③ 收集实现类名
}
}
}
}
}
...
}
}
return next != null;
}- 模块路径查找直接读
ModuleDescriptor.provides():模块声明provides com.example.Service with com.example.Impl,运行时不读任何配置文件。 - 前置条件:服务接口所在包必须被模块导出(
exports),未导出则跳过——模块封装性约束。 next()用Class.forName(module, impl)在模块上下文中加载实现类,再反射实例化;类加载失败抛ServiceConfigurationError。
⑥ reload() 方法的缓存清除
java
public void reload() {
providers.clear(); // ① 清空已实例化缓存
lookupIterator1 = new LazyClassPathLookupIterator(service, loader, acc); // ② 重建
lookupIterator2 = new ModuleServicesLookupIterator(service, layer, acc); // ③ 重建
}ServiceLoader实例本身可复用:reload()丢弃全部已实例化的Provider缓存并重建两个查找器,实现"同一服务重新发现"的语义。- 典型用途:实现类列表变化(如热部署新增 provider)后重新扫描;代价是重新读取配置文件与
Class.forName。 - 注意
reload()后旧迭代器失效,必须通过新iterator()重新迭代。
⑦ ServiceLoader 的线程安全
java
// 字段声明
private LinkedHashMap<String, S> providers = new LinkedHashMap<>(); // 非线程安全providers是普通LinkedHashMap,非并发容器;iterator()内部读缓存、next()写缓存,多线程并发迭代/实例化会互相干扰。- JDK 文档明确:多个线程共享同一个
ServiceLoader实例时需外部同步(如synchronized)。 - 实践中更常见的做法是"每个线程/每次使用各
load一次",因为load本身极廉价(只装配字段),天然规避共享问题。
⑧ ServiceLoader.load(service, ClassLoader) vs loadInstalled(service)
java
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getPlatformClassLoader(); // JDK 9+:平台类加载器
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}loadInstalled固定用平台类加载器(ClassLoader.getPlatformClassLoader())加载——只能发现 JDK 自身注册的服务实现,应用与扩展 classpath 的实现不可见。- 语义定位:
loadInstalled找"JDK 自带实现",load(service, cl)找"指定加载器可见的实现",load(service)找"当前线程上下文可见的实现"。三者按需选用。 - 命名来源是历史遗留:JDK 8 时代它叫 Extension Class Loader(扩展目录),JDK 9 模块化后扩展机制移除,改为平台类加载器承载同样的"JDK 自带"语义。