Java SPI 完整加载流程详解-JAR 包到类实例化
Java SPI 完整加载流程详解
- 前言
- 一、项目结构示例
- 假设我们有这样的项目结构:
- 代码示例
- 1. 服务接口 (service-api.jar)
- 2. 服务提供者1 (provider1.jar)
- 3. 服务提供者2 (provider2.jar)
- 4. 应用程序 (app.jar)
- 二、完整的加载流程(源码级别)
- 第1步:调用 ServiceLoader.load()
- 第2步:ServiceLoader 构造方法
- 第3步:开始遍历(触发延迟加载)
- 第4步:LazyIterator.hasNext() - 查找配置文件
- 第5步:ClassLoader.getResources() - 扫描 JAR 包
- ClassLoader.getResources() 的工作原理:
- AppClassLoader 如何查找资源:
- 第6步:parse() - 读取配置文件内容
- URL.openStream() 如何从 JAR 包读取文件:
- 第7步:parseLine() - 解析每一行
- 第8步:LazyIterator.next() - 加载并实例化类
- 三、完整的调用链可视化
- 四、关键技术点详解
- 1. ClassLoader.getResources() 如何扫描 JAR 包
- 2. URL.openStream() 如何读取 JAR 包内的文件
- 3. Class.forName() 如何从 JAR 包加载类
- 五、实际执行流程示例
- 启动命令
- 执行步骤
- 六、总结:SPI 如何加载 JAR 包中的类
- 核心机制
- 关键点
前言
如果可以理解SPI,无论是学习Java SPI还是Spring SPI或者xxx SPI都有很大的帮助,最近想深究了一下,看了看源码的部分,觉得挺有帮助.
项目就整了一个很简单了,能跑就行,代码什么的不用纠结,用AI随便生成一个就能跑.
本篇文章不太适合纯新手阅读,最好有读源码的经验,不然可能会比较懵.
一、项目结构示例
假设我们有这样的项目结构:
my-app/
├── app.jar (应用程序)
│ └── com/example/App.class
│
├── service-api.jar (服务接口)
│ └── com/example/spi/MyService.class
│
├── provider1.jar (服务提供者1)
│ ├── META-INF/services/
│ │ └── com.example.spi.MyService
│ │ 内容: com.example.spi.impl.Provider1
│ └── com/example/spi/impl/Provider1.class
│
└── provider2.jar (服务提供者2)├── META-INF/services/│ └── com.example.spi.MyService│ 内容: com.example.spi.impl.Provider2└── com/example/spi/impl/Provider2.class
代码示例
1. 服务接口 (service-api.jar)
package com.example.spi;public interface MyService {void execute();
}
2. 服务提供者1 (provider1.jar)
package com.example.spi.impl;public class Provider1 implements MyService {public Provider1() { } // 必须有无参构造器@Overridepublic void execute() {System.out.println("Provider1 执行");}
}
配置文件 provider1.jar/META-INF/services/com.example.spi.MyService
:
com.example.spi.impl.Provider1
3. 服务提供者2 (provider2.jar)
package com.example.spi.impl;public class Provider2 implements MyService {public Provider2() { }@Overridepublic void execute() {System.out.println("Provider2 执行");}
}
配置文件 provider2.jar/META-INF/services/com.example.spi.MyService
:
com.example.spi.impl.Provider2
4. 应用程序 (app.jar)
package com.example;import com.example.spi.MyService;
import java.util.ServiceLoader;public class App {public static void main(String[] args) {// 使用 SPI 加载所有服务提供者ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);for (MyService service : loader) {service.execute();}}
}
二、完整的加载流程(源码级别)
第1步:调用 ServiceLoader.load()
// 应用程序代码
ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
内部执行:
// ServiceLoader.java:211-214
public static <S> ServiceLoader<S> load(Class<S> service) {// 获取线程上下文类加载器(通常是 AppClassLoader)ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}// ServiceLoader.java:203-207
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {return new ServiceLoader<>(service, loader);
}
第2步:ServiceLoader 构造方法
// ServiceLoader.java:122-180
private ServiceLoader(Class<S> svc, ClassLoader cl) {service = Objects.requireNonNull(svc, "Service interface cannot be null");// service = MyService.classloader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;// loader = AppClassLoader (负责加载 classpath 上的所有 JAR 包)acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;reload(); // 初始化延迟加载迭代器
}public void reload() {providers.clear();lookupIterator = new LazyIterator(service, loader);// 创建延迟迭代器,但此时还没有查找配置文件
}
此时的状态:
service
=MyService.class
loader
=AppClassLoader
providers
= 空的 LinkedHashMaplookupIterator
= 新创建的 LazyIterator
第3步:开始遍历(触发延迟加载)
// 应用程序代码
for (MyService service : loader) {service.execute();
}
这会调用:
// ServiceLoader.java:177-200 (iterator方法)
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 lookupIterator.hasNext(); // ← 触发延迟加载}public S next() {if (knownProviders.hasNext())return knownProviders.next().getValue();return lookupIterator.next(); // ← 触发延迟加载}};
}
第4步:LazyIterator.hasNext() - 查找配置文件
这是关键! 这里是从 JAR 包中查找配置文件的地方:
// ServiceLoader.java:271-294 (LazyIterator.hasNextService)
private boolean hasNextService() {if (nextName != null) {return true;}// ========== 关键点1: 查找配置文件 ==========if (configs == null) {try {// 构造配置文件的完整路径String fullName = PREFIX + service.getName();// fullName = "META-INF/services/com.example.spi.MyService"if (loader == null)configs = ClassLoader.getSystemResources(fullName);else// ★★★ 关键调用:从所有 JAR 包中查找配置文件 ★★★configs = loader.getResources(fullName);// 返回 Enumeration<URL>,包含所有匹配的配置文件 URL} catch (IOException x) {fail(service, "Error locating configuration files", x);}}// ========== 关键点2: 解析配置文件 ==========while ((pending == null) || !pending.hasNext()) {if (!configs.hasMoreElements()) {return false; // 没有更多配置文件了}// 获取下一个配置文件的 URLURL configUrl = configs.nextElement();// configUrl 示例:// jar:file:/path/to/provider1.jar!/META-INF/services/com.example.spi.MyService// jar:file:/path/to/provider2.jar!/META-INF/services/com.example.spi.MyService// ★★★ 解析配置文件,读取类名 ★★★pending = parse(service, configUrl);// pending 是包含类全限定名的迭代器}nextName = pending.next();// nextName = "com.example.spi.impl.Provider1" (第一次)// nextName = "com.example.spi.impl.Provider2" (第二次)return true;
}
第5步:ClassLoader.getResources() - 扫描 JAR 包
这是你问题的核心答案!
// 调用链
configs = loader.getResources("META-INF/services/com.example.spi.MyService");
ClassLoader.getResources() 的工作原理:
// ClassLoader.java:1017-1028
public Enumeration<URL> getResources(String name) throws IOException {@SuppressWarnings("unchecked")Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];// 1. 先查找父类加载器的资源if (parent != null) {tmp[0] = parent.getResources(name);} else {tmp[0] = getBootstrapResources(name);}// 2. 再查找当前类加载器的资源tmp[1] = findResources(name);// 3. 合并所有结果return new CompoundEnumeration<>(tmp);
}
AppClassLoader 如何查找资源:
AppClassLoader 继承自 URLClassLoader,它的 findResources()
方法会:
- 遍历 classpath 上的所有路径(包括所有 JAR 包)
- 检查每个 JAR 包中是否存在指定路径的文件
- 返回所有匹配文件的 URL
伪代码说明:
// URLClassLoader 的内部逻辑(简化版)
protected Enumeration<URL> findResources(String name) {List<URL> results = new ArrayList<>();// classpath 包含所有的 JAR 包路径for (URL jarUrl : classpath) {// 打开 JAR 文件JarFile jarFile = new JarFile(jarUrl);// 查找指定的条目JarEntry entry = jarFile.getEntry(name);// name = "META-INF/services/com.example.spi.MyService"if (entry != null) {// 构造完整的 URLURL resourceUrl = new URL("jar:" + jarUrl + "!/" + name);// 示例: jar:file:/path/to/provider1.jar!/META-INF/services/com.example.spi.MyServiceresults.add(resourceUrl);}}return Collections.enumeration(results);
}
实际返回的 Enumeration:
URL 1: jar:file:/path/to/provider1.jar!/META-INF/services/com.example.spi.MyService
URL 2: jar:file:/path/to/provider2.jar!/META-INF/services/com.example.spi.MyService
第6步:parse() - 读取配置文件内容
// ServiceLoader.java:231-253
private Iterator<String> parse(Class<?> service, URL u) {InputStream in = null;BufferedReader r = null;ArrayList<String> names = new ArrayList<>();try {// ★★★ 关键:打开 URL 流,读取配置文件内容 ★★★in = u.openStream();// 这会从 JAR 包中提取文件内容r = new BufferedReader(new InputStreamReader(in, "utf-8"));int lc = 1;// 逐行解析while ((lc = parseLine(service, u, r, lc, names)) >= 0);// names 现在包含: ["com.example.spi.impl.Provider1"]} catch (IOException x) {fail(service, "Error reading configuration file", x);} finally {// 关闭流if (r != null) r.close();if (in != null) in.close();}return names.iterator();
}
URL.openStream() 如何从 JAR 包读取文件:
// 当 URL 是 jar:file:/path/to/provider1.jar!/META-INF/services/com.example.spi.MyService 时
InputStream in = url.openStream();// 内部流程:
// 1. 解析 JAR URL
// 2. 打开 JAR 文件(ZIP 格式)
// 3. 定位到 "META-INF/services/com.example.spi.MyService" 条目
// 4. 解压该条目
// 5. 返回输入流
第7步:parseLine() - 解析每一行
// ServiceLoader.java:202-229
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,List<String> names) {String ln = r.readLine();if (ln == null) {return -1; // 文件结束}// 处理注释int ci = ln.indexOf('#');if (ci >= 0) ln = ln.substring(0, ci);// 去除空白ln = ln.trim();// ln = "com.example.spi.impl.Provider1"int n = ln.length();if (n != 0) {// 验证格式(不能有空格或制表符)if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))fail(service, u, lc, "Illegal configuration-file syntax");// 验证类名格式int cp = ln.codePointAt(0);if (!Character.isJavaIdentifierStart(cp))fail(service, u, lc, "Illegal provider-class name: " + ln);for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {cp = ln.codePointAt(i);if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))fail(service, u, lc, "Illegal provider-class name: " + ln);}// 添加到列表(避免重复)if (!providers.containsKey(ln) && !names.contains(ln))names.add(ln);}return lc + 1;
}
第8步:LazyIterator.next() - 加载并实例化类
// ServiceLoader.java:296-321 (LazyIterator.nextService)
private S nextService() {if (!hasNextService())throw new NoSuchElementException();String cn = nextName;// cn = "com.example.spi.impl.Provider1"nextName = null;Class<?> c = null;try {// ★★★ 关键点3: 使用 ClassLoader 加载类 ★★★c = Class.forName(cn, false, loader);// 参数说明:// - cn: 类的全限定名// - false: 不初始化类(不执行静态代码块)// - loader: AppClassLoader// Class.forName 会:// 1. 在 classpath 的所有 JAR 包中查找 com/example/spi/impl/Provider1.class// 2. 从 provider1.jar 中读取字节码// 3. 加载到 JVM} catch (ClassNotFoundException x) {fail(service, "Provider " + cn + " not found");}// ========== 关键点4: 类型检查 ==========if (!service.isAssignableFrom(c)) {fail(service, "Provider " + cn + " not a subtype");// 确保 Provider1 implements MyService}try {// ★★★ 关键点5: 反射实例化 ★★★S p = service.cast(c.newInstance());// 等价于: MyService p = (MyService) new Provider1();// ========== 关键点6: 缓存实例 ==========providers.put(cn, p);// providers = {"com.example.spi.impl.Provider1" -> Provider1实例}return p;} catch (Throwable x) {fail(service, "Provider " + cn + " could not be instantiated", x);}throw new Error(); // 不会执行到这里
}
三、完整的调用链可视化
应用程序└─> ServiceLoader.load(MyService.class)└─> new ServiceLoader(MyService.class, AppClassLoader)└─> reload()└─> new LazyIterator(MyService.class, AppClassLoader)└─> for (MyService s : loader) ← 触发延迟加载└─> iterator().hasNext()└─> lookupIterator.hasNext()└─> hasNextService()│├─> loader.getResources("META-INF/services/com.example.spi.MyService")│ ││ ├─> URLClassLoader.findResources()│ │ ││ │ ├─> 扫描 provider1.jar│ │ │ └─> 找到: jar:file:/.../provider1.jar!/META-INF/services/...│ │ ││ │ └─> 扫描 provider2.jar│ │ └─> 找到: jar:file:/.../provider2.jar!/META-INF/services/...│ ││ └─> 返回 Enumeration<URL> (2个URL)│├─> parse(configUrl1)│ └─> url.openStream() ← 从 JAR 包读取文件│ └─> BufferedReader.readLine()│ └─> "com.example.spi.impl.Provider1"│└─> pending.next()└─> nextName = "com.example.spi.impl.Provider1"└─> iterator().next()└─> lookupIterator.next()└─> nextService()│├─> Class.forName("com.example.spi.impl.Provider1", false, AppClassLoader)│ └─> 从 provider1.jar 加载 Provider1.class│├─> service.isAssignableFrom(Provider1.class) ← 类型检查│├─> Provider1.class.newInstance() ← 反射实例化│ └─> new Provider1()│└─> providers.put("com.example.spi.impl.Provider1", instance) ← 缓存└─> 返回 instance
四、关键技术点详解
1. ClassLoader.getResources() 如何扫描 JAR 包
// AppClassLoader 的 classpath 包含所有 JAR 包:
// classpath = [
// file:/path/to/app.jar,
// file:/path/to/service-api.jar,
// file:/path/to/provider1.jar,
// file:/path/to/provider2.jar
// ]configs = loader.getResources("META-INF/services/com.example.spi.MyService");// 内部流程:
// 1. 遍历 classpath
// 2. 对于每个 JAR 文件:
// - 打开 JAR (ZIP 格式)
// - 查找条目 "META-INF/services/com.example.spi.MyService"
// - 如果找到,添加 URL: jar:file:/path/to/xxx.jar!/META-INF/services/...
// 3. 返回所有找到的 URL
2. URL.openStream() 如何读取 JAR 包内的文件
URL url = new URL("jar:file:/path/to/provider1.jar!/META-INF/services/com.example.spi.MyService");
InputStream in = url.openStream();// 内部流程:
// 1. 识别 URL 协议为 "jar"
// 2. 解析出 JAR 文件路径: /path/to/provider1.jar
// 3. 解析出条目路径: META-INF/services/com.example.spi.MyService
// 4. 使用 JarFile 打开 JAR 文件
// 5. 获取指定条目的输入流
// 6. 返回可以读取文件内容的 InputStream
3. Class.forName() 如何从 JAR 包加载类
Class<?> c = Class.forName("com.example.spi.impl.Provider1", false, loader);// 内部流程:
// 1. ClassLoader 查找类文件:
// - 将类名转换为路径: com/example/spi/impl/Provider1.class
// - 在 classpath 的所有 JAR 包中查找该路径
// - 找到: provider1.jar!/com/example/spi/impl/Provider1.class
//
// 2. 读取字节码:
// - 打开 JAR 文件
// - 提取 Provider1.class 的字节码
//
// 3. 加载到 JVM:
// - defineClass(字节码) → 创建 Class 对象
// - 链接(验证、准备、解析)
// - 返回 Class<?> 对象
五、实际执行流程示例
启动命令
java -cp app.jar:service-api.jar:provider1.jar:provider2.jar com.example.App
执行步骤
-
JVM 启动,创建 AppClassLoader
- classpath = [app.jar, service-api.jar, provider1.jar, provider2.jar]
-
加载并执行 App.main()
ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
- 创建 ServiceLoader 实例
- service = MyService.class
- loader = AppClassLoader
-
开始遍历
for (MyService s : loader) {
-
第一次迭代
-
hasNext()
→hasNextService()
-
getResources("META-INF/services/com.example.spi.MyService")
- 返回 2 个 URL:
jar:file:/.../provider1.jar!/META-INF/services/com.example.spi.MyService
jar:file:/.../provider2.jar!/META-INF/services/com.example.spi.MyService
- 返回 2 个 URL:
-
打开第一个 URL,读取内容:
com.example.spi.impl.Provider1
-
Class.forName("com.example.spi.impl.Provider1")
- 从 provider1.jar 加载 Provider1.class
-
new Provider1()
→ 返回实例 -
输出: “Provider1 执行”
-
-
第二次迭代
- 读取第二个配置文件
- 内容:
com.example.spi.impl.Provider2
Class.forName("com.example.spi.impl.Provider2")
- 从 provider2.jar 加载 Provider2.class
new Provider2()
→ 返回实例- 输出: “Provider2 执行”
-
结束
- 没有更多配置文件
hasNext()
返回 false
六、总结:SPI 如何加载 JAR 包中的类
核心机制
-
配置发现:
ClassLoader.getResources()
- 扫描 classpath 上所有 JAR 包
- 查找
META-INF/services/服务接口全限定名
文件 - 返回所有匹配文件的 URL 列表
-
配置解析:
URL.openStream()
+BufferedReader
- 打开 JAR 包内的配置文件
- 逐行读取实现类的全限定名
- 验证类名格式
-
类加载:
Class.forName(类名, false, ClassLoader)
- 使用 ClassLoader 在 JAR 包中查找 .class 文件
- 读取字节码并加载到 JVM
- 进行类型检查
-
实例化:
Class.newInstance()
- 反射调用无参构造器
- 创建服务实例
-
缓存:
providers.put(类名, 实例)
- 避免重复加载
- 保持实例化顺序
关键点
- JAR 包就是 ZIP 文件,可以像访问文件系统一样访问其中的内容
- ClassLoader 知道 classpath 上的所有 JAR 包
- getResources() 会遍历所有 JAR 包查找指定路径的文件
- URL 协议
jar:file:...!/...
可以直接读取 JAR 包内的文件 - Class.forName() 会在所有 JAR 包中查找并加载类
现在你应该完全理解 SPI 如何从 JAR 包中加载配置和类了!