开始前,会这些就够了
- 能读懂 Java 接口、实现类、公开无参构造器和 Iterator。
- 了解
ClassLoader用于查找类与资源;不需要 Maven、Spring 或模块系统。 - 使用完整 JDK 17 或更新版本,允许在临时目录写入本程序的服务描述文件。
- 需要补基础时:先分清类加载与初始化
遇到不熟悉的词,先看这 2 个解释
- 服务提供者
- 能实现某个服务接口的具体实现。
ServiceLoader按服务描述信息找到并在需要时创建它。 - 提供者缓存
- 同一个
ServiceLoader记住已经得到的提供者;本例 iterator 再次遍历会返回已缓存实例。
启动日志显示 ServiceLoader.load 已经返回,程序于是宣布插件就绪。直到第一个请求要使用插件时,构造器才抛出错误。问题不一定是运行中配置变了,也可能是“创建查找器”和“真正创建提供者”本来就不在同一步。
这个项目把接口、成功实现、失败实现和观察代码都放在一个 Java 文件里。运行时自动建立 META-INF/services 文件,不需要你手工复制资源;最后按计数核对对象何时出现、何时复用和何时关闭。
把例子带到自己的电脑上
下载完整练习包
需要 JDK 17 或更新版本,使用 Java 标准库;制作时实测 Windows / JDK 17.0.20.1 / 21.0.12。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- ServiceLoaderLifecycleDemo.java:完整可运行示例
- README.md:运行说明与独立练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和产物验证记录
- SHA256SUMS.txt:内容摘要
在这个文件夹打开 PowerShell,逐行运行
java -version
javac -version
javac -encoding UTF-8 --release 17 ServiceLoaderLifecycleDemo.java
java -cp . ServiceLoaderLifecycleDemo解压到独立练习文件夹,在含 ServiceLoaderLifecycleDemo.java 的目录打开终端运行。单线程类路径ServiceLoader,程序自动生成/删除两个服务描述文件;缓存身份、reload、旧iterator失效、显式close和构造失败cause;JDK17/21各12项检查。未测试模块路径、并发使用、外部JAR或类卸载。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
接口、实现和描述文件分别负责什么
Greeting 是调用方需要的接口,Good 是公开且具备公开无参构造器的实现类。服务描述文件的文件名是接口的二进制名字,文件内容是实现类的二进制名字。因为示例使用嵌套类,名字里会出现美元符号;代码用 getName 自动取得,避免手写漏掉这一点。
程序在自己的临时目录建立 META-INF/services,并创建 URLClassLoader 读取那里资源。实现类仍由父加载器提供;这个额外加载器只补充本地服务元数据,所以没有编译下载来的插件,也没有把两份同名接口分别加载。
服务发现的这层约定和反射路由扫描不同。你不必在入口处直接 new Good,也不用扫描所有注解方法,但描述文件、实现类型和构造条件仍然需要正确。当前实验采用类路径形式,不展开模块路径的 uses/provides 规则。
load 返回的是查找器,第一次 next 才创建本例对象
Good 的构造器每次运行都会增加 created。调用 ServiceLoader.load 后计数为零,拿到 iterator 后仍为零;第一次调用 next,计数才变成一,并得到 hello-1。这些数字来自实际输出,而不是从方法名推测。
这说明仅仅保存一个 ServiceLoader 引用,不能证明服务提供者已经能正常构造。如果应用要求启动阶段就确认配置可用,就需要明确触发所要求范围内的查找与实例化,并决定发现错误时如何报告。
这里使用 iterator 路径,不使用 stream().map(Provider::get)。两者都有延迟行为,但 API 对缓存和实例获取的描述不同,不能把当前实例身份断言未经检查就挪到另一种调用方式。
同一个查找器的下一轮迭代会复用已创建实例
第一次 next 后,再从同一个 loader 获取新的 iterator 并调用 next,拿到的仍是刚才那个对象,created 也仍为一。程序用引用相同做检查,不是仅比较 hello 字符串恰好相等。
缓存属于这个 ServiceLoader 的行为范围。它并不证明整个 JVM 对这个服务永远只有一个实例,也没有给实现类添加线程安全保证。若多个使用者共享同一可变 provider,仍需要说明状态与并发访问约定。
本例只在主线程顺序执行,以免把缓存观察和并发竞争混在一起。ServiceLoader 实例本身不保证可被多个线程安全地同时使用,不能因为它内部有缓存就自动当成全局并发注册表。
reload 清缓存,却不撤销你已经拿到的引用
调用 reload 后 created 仍为一,说明它没有立刻再构造一遍。接下来使用新 iterator,才得到第二个 Good,计数变成二。之前的 iterator 已经失效,继续 hasNext 在本例抛出 ConcurrentModificationException。
更容易遗漏的是旧实例:主程序仍握着第一个 Good,它依然可以返回 hello-1。Good 还实现了 AutoCloseable,但 reload 并没有调用它的 close;closed 计数仍为零。程序最后由拥有这些实例的代码显式关闭两者。
因此重新发现提供者、停止使用旧实例、关闭旧实例拥有的资源以及卸载类,并不是一个按钮的四种说法。当前例子只清除一个查找器的缓存,不替换 class 字节,也不声称已经实现插件热升级。
提供者构造失败,可能在迭代时才变成可见错误
BrokenService 使用另一份独立描述文件,指向构造器必定抛 IllegalStateException 的 Broken。创建其 ServiceLoader 时 attempts 仍为零;调用 next 才实际尝试构造,并抛出 ServiceConfigurationError。
实验同时核对异常原因仍是那一个 IllegalStateException,避免日志只留下笼统的“插件加载失败”。ServiceConfigurationError 继承 Error,普通 catch(Exception) 不会接住它;示例只在预期的实验位置捕获这个具体类型。
描述文件损坏、实现类不可见和构造器失败并不是同一种配置问题,也不保证都在同一行触发。生产诊断应保留服务接口、提供者和 cause,再根据启动契约决定终止或降级,不能在无穷循环里吞掉错误继续试。
保存单文件,实跑并核对每项观察
把代码保存为 ServiceLoaderLifecycleDemo.java,在这个文件所在目录执行下方命令。需要完整 JDK 17 或更新版本;只有 java 而没有 javac 的环境还不能完成这里的编译步骤。无需 Maven、外部依赖或 IDE 插件。
2026 年 10 月 17 日已使用 Microsoft OpenJDK 17.0.20.1 和 21.0.12,各自在新建临时目录中以 --release 17 编译并执行。两版输出相同,每次完成 12 项检查。下方保留实际输出,失败时不会打印 PASS。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
/** Single source: the program creates and removes its own META-INF/services files. */
public class ServiceLoaderLifecycleDemo {
private static int checks;
public interface Greeting { String hello(); }
public interface BrokenService { }
public static final class Good implements Greeting, AutoCloseable {
static int created;
static int closed;
private final int id;
private boolean isClosed;
public Good() { id = ++created; }
public String hello() {
if (isClosed) throw new IllegalStateException("closed");
return "hello-" + id;
}
public void close() { if (!isClosed) { isClosed = true; closed++; } }
}
public static final class Broken implements BrokenService {
static int attempts;
public Broken() { attempts++; throw new IllegalStateException("provider constructor failed"); }
}
private static void descriptor(Path root, Class<?> service, Class<?> provider) throws IOException {
Path directory = root.resolve("META-INF").resolve("services");
Files.createDirectories(directory);
Files.writeString(directory.resolve(service.getName()), provider.getName() + "\n", StandardCharsets.UTF_8);
}
private static void check(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
checks++;
}
private static void removeOwnedDirectory(Path directory) throws IOException {
try (var paths = Files.walk(directory)) {
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) Files.delete(path);
}
}
public static void main(String[] args) throws Exception {
Path directory = Files.createTempDirectory("qinghuan-services-");
Good first = null, second = null;
try {
descriptor(directory, Greeting.class, Good.class);
descriptor(directory, BrokenService.class, Broken.class);
// Provider classes stay in the parent loader; this loader supplies only local metadata.
try (URLClassLoader resources = new URLClassLoader(new URL[]{directory.toUri().toURL()},
ServiceLoaderLifecycleDemo.class.getClassLoader())) {
ServiceLoader<Greeting> loader = ServiceLoader.load(Greeting.class, resources);
check(Good.created == 0, "load must not construct the provider yet");
var oldIterator = loader.iterator();
check(Good.created == 0, "creating an iterator does not construct provider");
first = (Good) oldIterator.next();
check(Good.created == 1 && first.hello().equals("hello-1"), "first demand creates provider");
check(loader.iterator().next() == first && Good.created == 1, "second iteration reuses cached instance");
System.out.println("after_load=0; after_first_next=1; second_iterator_same_instance=true");
loader.reload();
check(Good.created == 1, "reload clears cache without immediate construction");
boolean stale = false;
try { oldIterator.hasNext(); }
catch (ConcurrentModificationException expected) { stale = true; }
check(stale, "discard iterators made before reload");
second = (Good) loader.iterator().next();
check(second != first && Good.created == 2, "new iteration constructs new instance");
check(first.hello().equals("hello-1") && Good.closed == 0, "reload did not close or revoke old instance");
System.out.println("after_reload_before_next=1; after_new_next=2; old_iterator=ConcurrentModificationException");
System.out.println("old_instance_still_usable=true; automatically_closed=0");
first.close(); second.close();
check(Good.closed == 2, "caller explicitly closes owned providers");
ServiceLoader<BrokenService> broken = ServiceLoader.load(BrokenService.class, resources);
check(Broken.attempts == 0, "broken provider not constructed by load");
boolean causePreserved = false;
try { broken.iterator().next(); }
catch (ServiceConfigurationError expected) {
causePreserved = expected.getCause() instanceof IllegalStateException
&& expected.getCause().getMessage().equals("provider constructor failed");
}
check(causePreserved && Broken.attempts == 1, "iteration exposes provider construction failure");
System.out.println("broken: load_returned=true; next=ServiceConfigurationError; cause=IllegalStateException");
}
} finally {
if (first != null) first.close();
if (second != null) second.close();
removeOwnedDirectory(directory);
}
check(!Files.exists(directory), "temporary descriptors removed");
System.out.println("PASS checks=" + checks + "; providers_closed=2; temporary_files_cleaned=true");
}
}
怎样运行:在源码所在目录,先单独执行 javac -encoding UTF-8 --release 17 ServiceLoaderLifecycleDemo.java;编译成功后,再执行 java -cp . ServiceLoaderLifecycleDemo。两条命令分别回车。2026-10-17 已在 Windows、JDK 17.0.20.1 / 21.0.12 编译运行此份源码并通过检查。单线程类路径ServiceLoader,程序自动生成/删除两个服务描述文件;缓存身份、reload、旧iterator失效、显式close和构造失败cause;JDK17/21各12项检查。未测试模块路径、并发使用、外部JAR或类卸载。
运行后对照结果
after_load=0; after_first_next=1; second_iterator_same_instance=true
after_reload_before_next=1; after_new_next=2; old_iterator=ConcurrentModificationException
old_instance_still_usable=true; automatically_closed=0
broken: load_returned=true; next=ServiceConfigurationError; cause=IllegalStateException
PASS checks=12; providers_closed=2; temporary_files_cleaned=true让元数据、查找器和服务实例各有结束步骤
程序在 finally 中关闭已创建的 Good,URLClassLoader 使用 try-with-resources,最后删除自己生成的临时资源目录。没有外部 JAR,没有网络路径,也没有留给读者补齐的服务文件。
这次只检查服务发现与实例生命周期。Good 的 close 用一个计数表示释放动作,并没有真实文件句柄或连接池,因此只能证明 reload 没有替你调用关闭方法,不能拿它当作任何插件资源都已正确回收的性能或泄漏测试。
读完后应能列出这条顺序:描述文件可找到、提供者可创建、实例被调用、实例停止使用、资源被关闭。每一步都有自己可观察的结果,前一步成功不能自动替下一步签字。
记住这三点
ServiceLoader的发现与实例化会延迟发生,load 返回不等于提供者构造成功。- 同一 loader 的 iterator 会复用已缓存实例;reload 后要丢弃旧 iterator。
- reload 不关闭或撤销调用方已持有的实例,资源所有者必须明确。
别把例子用过头
- 只在 load 外层捕获异常,却没有观察实际迭代和构造。
- 把 reload 当成关闭全部旧插件、替换类定义或保证类卸载。
- 用字符串相等代替实例身份比较。
- 假定
catch(Exception)会捕获ServiceConfigurationError,或把共享 loader 当成并发安全。
自己试一小步
若两个提供者都能处理同一种任务,你如何做出可验证的选择,而不是碰巧用第一个?
- 在单文件副本中增加第二个合法提供者,让接口暴露你设计的能力或优先级信息。
- 描述文件仍由程序生成;明确选择规则、冲突时的处理和不适配时的结果。
- 分别测试一名适配者、多名适配者和没有适配者,记录实际构造了哪些实例。
- 在选择、使用与 reload 之后明确关闭哪些已取得实例,提交计数和资源所有权说明。
做到这些,就算完成
- 选择规则不依赖文件或类路径碰巧的先后顺序。
- 优先级相同或能力冲突有明确处理,没有静默覆盖。
- 记录实际创建和关闭的实例数量,不把发现元数据等同于已调用服务。
- 练习只使用自己定义的实现与临时描述文件,并保留异常原因。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Java 17:ServiceLoader 的部署、缓存与 reload (新标签页)↗
核对类路径描述文件、延迟实例化、iterator缓存、reload和并发限制。
- Java 17:ServiceConfigurationError (新标签页)↗
确认该类型继承 Error,并用于服务发现、加载或实例化失败。
- Java 17:URLClassLoader 的目录资源与关闭 (新标签页)↗
核对目录URL、父加载器查找和资源关闭职责。