Java SPI 完整加载流程详解-JAR 包到类实例化
Java SPI 完整加载流程详解
- 前言
-
- 一、项目结构示例
- 二、完整的加载流程(源码级别)
- 三、完整的调用链可视化
- 四、关键技术点详解
- 五、实际执行流程示例
- 六、总结: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() { } // 必须有无参构造器
@Override
public 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() { }
@Override
public 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.class
loader = (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.classloader=AppClassLoaderproviders= 空的 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; // 没有更多配置文件了
}
// 获取下一个配置文件的 URL
URL 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) {
// 构造完整的 URL
URL resourceUrl = new URL("jar:" + jarUrl + "!/" + name);
// 示例: jar:file:/path/to/provider1.jar!/META-INF/services/com.example.spi.MyService
results.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.MyServicejar: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 包中加载配置和类了!
更多推荐
所有评论(0)