当前位置: 首页 > news >正文

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 = 空的 LinkedHashMap
  • lookupIterator = 新创建的 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() 方法会:

  1. 遍历 classpath 上的所有路径(包括所有 JAR 包)
  2. 检查每个 JAR 包中是否存在指定路径的文件
  3. 返回所有匹配文件的 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

执行步骤

  1. JVM 启动,创建 AppClassLoader

    • classpath = [app.jar, service-api.jar, provider1.jar, provider2.jar]
  2. 加载并执行 App.main()

    ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
    
    • 创建 ServiceLoader 实例
    • service = MyService.class
    • loader = AppClassLoader
  3. 开始遍历

    for (MyService s : loader) {
    
  4. 第一次迭代

    • 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
    • 打开第一个 URL,读取内容: com.example.spi.impl.Provider1

    • Class.forName("com.example.spi.impl.Provider1")

      • 从 provider1.jar 加载 Provider1.class
    • new Provider1() → 返回实例

    • 输出: “Provider1 执行”

  5. 第二次迭代

    • 读取第二个配置文件
    • 内容: com.example.spi.impl.Provider2
    • Class.forName("com.example.spi.impl.Provider2")
      • 从 provider2.jar 加载 Provider2.class
    • new Provider2() → 返回实例
    • 输出: “Provider2 执行”
  6. 结束

    • 没有更多配置文件
    • hasNext() 返回 false

六、总结:SPI 如何加载 JAR 包中的类

核心机制

  1. 配置发现: ClassLoader.getResources()

    • 扫描 classpath 上所有 JAR 包
    • 查找 META-INF/services/服务接口全限定名 文件
    • 返回所有匹配文件的 URL 列表
  2. 配置解析: URL.openStream() + BufferedReader

    • 打开 JAR 包内的配置文件
    • 逐行读取实现类的全限定名
    • 验证类名格式
  3. 类加载: Class.forName(类名, false, ClassLoader)

    • 使用 ClassLoader 在 JAR 包中查找 .class 文件
    • 读取字节码并加载到 JVM
    • 进行类型检查
  4. 实例化: Class.newInstance()

    • 反射调用无参构造器
    • 创建服务实例
  5. 缓存: providers.put(类名, 实例)

    • 避免重复加载
    • 保持实例化顺序

关键点

  • JAR 包就是 ZIP 文件,可以像访问文件系统一样访问其中的内容
  • ClassLoader 知道 classpath 上的所有 JAR 包
  • getResources() 会遍历所有 JAR 包查找指定路径的文件
  • URL 协议 jar:file:...!/... 可以直接读取 JAR 包内的文件
  • Class.forName() 会在所有 JAR 包中查找并加载类

现在你应该完全理解 SPI 如何从 JAR 包中加载配置和类了!

http://www.dtcms.com/a/491622.html

相关文章:

  • MySQL Workbench:MySQL官方管理开发工具
  • 七宝网站建设行业seo网站优化方案
  • Unity 光照贴图异常修复笔记
  • 算法训练之BFS解决最短路径问题
  • h5手机端网站开发西安软件开发公司
  • DataFrame对象的iterrows()方法
  • 【Java零基础·第8章】面向对象(四):继承、接口与多态深度解析
  • 网站规划建设与管理维护大作业中国传统文化网页设计
  • 空气能空调如何做网站做酒店网站多少钱
  • 小道消息:某国产数据库迁移中途失败
  • AI+量化 的数据类型有哪些
  • 外贸网站如何seo推广常用网站如何在桌面做快捷方式
  • 遇到的问题:缺少ClickTo Run Service
  • [创业之路-699]:企业与高校:模式错配的警示与适配路径的探索
  • 电脑做系统都是英文选哪个网站怎么做局域网网站
  • 源丰建设有限公司网站如何做推广最有效果
  • 合规守护经营,道本科技智慧合同管理系统助力小微企业迈入发展快车道[赞啊][赞啊][赞啊]
  • 站点推广是什么意思wordpress双语插件
  • LLMs-from-scratch :embeddings 与 linear-layers 的对比
  • 量化交易的思维导图
  • 商城网站建设框架网站有哪些
  • 漏洞扫描POC和web漏洞扫描工具
  • go资深之路笔记(八) 基准测试
  • 第1讲:Go调度器GMP模型深度解析
  • C++ 关键字 static 面试高频问题汇总
  • 网站建设jnlongji百度技术培训中心
  • m版网站开发怎样创建网页
  • 基于自适应差分进化算法的MATLAB实现
  • 男人女人做那事网站如何创建一个互联网平台
  • RocketMQ 与 Kafka 架构与实现详解对比