02.5 - OSGI 与 SPI 模块化
定位: 从层级委派到网状依赖——模块化类加载的演进与 SPI 桥接机制 面试高频度: ⭐⭐⭐ 考查方式: SPI 原理全链路、OSGI 类加载机制、JDBC 驱动加载过程
一、为什么需要模块化类加载?
双亲委派模型解决了一个核心问题——安全隔离和唯一性。但它有一个根本局限:类加载器层级是一个树状结构,而依赖关系是一个网状结构。
双亲委派的树状结构: 实际依赖是网状:
Bootstrap ┌────────────────┐
↓ │ Spring WebMVC │
Extension └───────┬────────┘
↓ ╱ ╲
Application ┌──────┘ └──────┐
↓ ▼ ▼
Custom CL ┌──────────┐ ┌─────────────┐
│ Spring │ │ Jackson │
└──────────┘ └─────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Spring Core │ │ Jackson Core│
└──────────────┘ └──────────────┘当一个库依赖两个不同的模块,而这两个模块又依赖同一个模块的不同版本时——树状层级无法表达这种关系。这就是 OSGI 和 JPMS 试图解决的问题。
而 SPI 解决的是另一个问题:接口定义方(核心库)和实现方(应用提供)的分离——双亲委派的单向委派无法支持"核心库调用应用实现"。
二、原理拆解
2.1 OSGI:从层级到网状
OSGI(Open Service Gateway Initiative)将每个模块(Bundle)视为一个独立的 ClassLoader 网域。
传统双亲委派: OSGI 网状依赖:
┌───────────────┐ ┌──────────────────┐
│ ClassLoader │ │ Bundle A │
│ │ │ │ Import: B, C │
│ ▼ │ │ Export: A.api │
│ parent │ └────┬──────┬──────┘
└───────────────┘ │ │
│ │
┌─────┘ └─────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ Bundle B │ │ Bundle C │
│ Import: C │ │ Export: C │
│ Export: B │ └──────────┘
└──────────┘Bundle 的核心概念:
| 概念 | 说明 | 类比 |
|---|---|---|
| Bundle | OSGI 中的模块单位 = jar + META-INF/MANIFEST.MF 中的 OSGI 元数据 | 一个独立的组件 |
| Import-Package | 声明本 Bundle 需要依赖哪些包 | 告诉 OSGI 框架:"我需要这个" |
| Export-Package | 声明本 Bundle 对外提供哪些包 | 告诉 OSGI 框架:"我提供这个" |
| Bundle ClassLoader | 每个 Bundle 有自己的 ClassLoader | 加载本 Bundle 的类 + Import 的包 |
OSGI 类加载策略(与双亲委派完全不同):
BundleA 需要加载 com.example.service.UserService
│
▼
① 检查自己是否 Export 了该包 → 是则直接在自己内部查找
↓ 否
② 在 Import-Package 列表中查找该包
│
├─ 找到 → 委派给 export 该包的 Bundle 的 ClassLoader
│
└─ 未找到 → 走 Bundle 的父委派链 (通常是 AppClassLoader)OSGI 类加载的本质是声明依赖 → 按声明路由,而不是双亲委派的"层级递推"。
OSGI 为什么可以热插拔?
因为依赖关系是通过 Import-Package / Export-Package 声明的,OSGI 框架维护了一个服务注册表(Service Registry):
① 安装新 Bundle
↓
② OSGI 框架解析 Bundle 的 Import/Export 声明
↓
③ 更新服务注册表 (新的 Export 可用, 依赖图重新计算)
↓
④ 依赖 Bundle 自动获取新的服务引用
↓
⑤ 旧的 Bundle 可以卸载 → 注册表再次更新代价: 类加载复杂度剧增。ClassNotFoundException 和 NoClassDefFoundError 变得非常普遍——因为一个类可能因为 Import-Package 没写对、Export 版本不匹配、或者 Bundle 启动顺序不当而无法加载。调试 OSGI 类加载问题是出了名的痛苦。
2.2 SPI 机制详解
SPI(Service Provider Interface)是 Java 内置的服务发现机制。它的核心思想:接口在核心库中定义,实现在应用代码中,由 ServiceLoader 发现并加载。
三要素:
┌─ SPI 接口 ─────────────────────────────────────┐
│ java.sql.Driver (定义在 rt.jar) │
│ │
│ 由 Bootstrap ClassLoader 加载 │
└────────────────┬────────────────────────────────┘
│
▼
┌─ META-INF/services/ 约定 ──────────────────────┐
│ META-INF/services/java.sql.Driver │
│ ───────────────────────────── │
│ com.mysql.cj.jdbc.Driver ← 实现类的全限定名 │
│ org.postgresql.Driver │
│ │
│ 文件在 mysql-connector-java.jar 中 │
└────────────────┬────────────────────────────────┘
│
▼
┌─ ServiceLoader ─────────────────────────────────┐
│ ServiceLoader.load(Driver.class) │
│ ↓ │
│ 读取 META-INF/services/java.sql.Driver │
│ ↓ │
│ 用 TCCL 加载实现类 │
│ ↓ │
│ 返回 LazyIterator(按需加载) │
└─────────────────────────────────────────────────┘JDBC 驱动加载的全链路追踪:
// ① 用户代码调用
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/db");
// ② DriverManager 是 Bootstrap ClassLoader 加载的 (rt.jar)
// 它需要找到可用的 Driver 实现
// ③ DriverManager 的静态初始化块中:
static {
loadInitialDrivers();
}
// ④ loadInitialDrivers() 中使用 ServiceLoader
private static void loadInitialDrivers() {
// ServiceLoader.load(Driver.class) 的内部:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// ServiceLoader 会:
// a. 获取 TCCL: Thread.currentThread().getContextClassLoader()
// (TCCL = AppClassLoader, 因未显式设置过)
// b. 用 TCCL 获取资源: tccl.getResources("META-INF/services/java.sql.Driver")
// c. 读取实现类名: "com.mysql.cj.jdbc.Driver"
// d. 用 TCCL 加载: tccl.loadClass("com.mysql.cj.jdbc.Driver")
// e. 触发 Driver 的静态初始化块
}
// ⑤ MySQL Driver 的静态块中:
static {
java.sql.DriverManager.registerDriver(new Driver());
// MySQL 驱动把自己注册到 DriverManager 的驱动列表中
}
// ⑥ DriverManager.getConnection() 遍历已注册的驱动列表
// 找到匹配的驱动返回连接┌─ 完整链路图 ───────────────────────────────────────────────────────────┐
│ │
│ 应用层 (AppClassLoader) │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ DriverManager.getConnection(url) │ │
│ │ ↑ 调用 Bootstrap 加载的 DriverManager │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Bootstrap ClassLoader 领域 │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ DriverManager.loadInitialDrivers() │ │
│ │ ↓ │ │
│ │ ServiceLoader.load(Driver.class) │ │
│ │ ↓ │ │
│ │ tccl = Thread.currentThread().getContextClassLoader() │ │
│ │ ↓ (tccl = AppClassLoader) │ │
│ │ tccl.getResources("META-INF/services/java.sql.Driver") │ │
│ │ ↓ (mysql-connector-java.jar 中的文件) │ │
│ │ "com.mysql.cj.jdbc.Driver" │ │
│ │ ↓ │ │
│ │ tccl.loadClass("com.mysql.cj.jdbc.Driver") ← 关键: TCCL 桥接 │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ AppClassLoader 领域 │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ 加载 com.mysql.cj.jdbc.Driver │ │
│ │ → 触发静态块 → DriverManager.registerDriver(new Driver()) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ DriverManager.getConnection() 遍历已注册的驱动列表 → 返回 Connection │
└──────────────────────────────────────────────────────────────────────────┘2.3 Java 9 模块化系统 (JPMS)
JPMS(Java Platform Module System)在 JDK 9 引入,通过 module-info.java 声明模块依赖:
// module-info.java
module com.example.myapp {
requires java.sql; // 依赖 java.sql 模块
requires transitive spring.core; // 传递依赖
exports com.example.myapp.api; // 对外暴露的 API
provides java.sql.Driver with com.example.MyDriver; // SPI 提供
uses com.example.spi.Plugin; // SPI 消费
}JPMS 与 ClassLoader 的关系:
JDK 8 及之前: JDK 9+:
┌─ 类加载器控制一切 ─┐ ┌─ 模块系统优先 ──────┐
│ │ │ │
│ ClassLoader │ │ Module Layer │
│ 决定类的来源 │ │ layer1 │
│ │ │ module A, B │
│ 双亲委派 │ │ ↓ │
│ 确保核心不替代 │ │ ClassLoader │
│ │ │ 查找模型受 module │
└────────────────────┘ │ 约束 │
└─────────────────────┘关键变化:
- 模块路径 (module path) vs 类路径 (classpath): 模块路径上的 jar 必须包含
module-info.java,JVM 会执行严格的模块边界检查。类路径仍然存在,但被视为"未命名模块" - 封装性: 模块可以隐藏内部包——即使反射也无权访问(不再是
private就能完全隐藏的旧模式) - 类加载器不再决定一切: 模块系统在 ClassLoader 之上增加了模块层。ClassLoader 的查找规则受模块边界的约束
三、图解全景
传统层级委派 vs OSGI 网状依赖 vs JPMS 模块图
┌─ 传统双亲委派 ───────────────────────┐
│ │
│ Bootstrap → Ext → App → Custom │
│ │
│ 单向树状结构 │
│ 无横向依赖 │
│ 依赖信息不透明 │
└──────────────────────────────────────┘
┌─ OSGI 网状依赖 ───────────────────────┐
│ │
│ ┌─────┐ Import ┌─────┐ │
│ │ A │ ←───────── │ B │ │
│ └──┬──┘ └──┬──┘ │
│ │ Export │ Export │
│ ▼ ▼ │
│ ┌─────┐ ┌─────┐ │
│ │ C │ │ D │ │
│ └─────┘ └─────┘ │
│ │
│ 显式声明 Import/Export │
│ 热插拔: 运行时替换 Bundle │
│ 每个 Bundle 独立 ClassLoader │
└──────────────────────────────────────┘
┌─ JPMS ───────────────────────────────┐
│ │
│ module com.app { │
│ requires java.sql; │
│ requires spring.core; │
│ exports com.app.api; │
│ } │
│ │
│ 强封装: 未 Export 的包不可访问 │
│ 模块路径 vs 类路径 │
│ 模块层 > ClassLoader 层 │
└──────────────────────────────────────┘四、实战验证
4.1 ServiceLoader 的简单实现
// SPIDemo.java —— 演示 ServiceLoader 原理
// 第一步: 定义 SPI 接口
// File: com/example/spi/LoggerService.java
package com.example.spi;
public interface LoggerService {
void log(String message);
}
// 第二步: 提供实现
// File: com/example/spi/impl/ConsoleLogger.java
package com.example.spi.impl;
import com.example.spi.LoggerService;
public class ConsoleLogger implements LoggerService {
@Override
public void log(String message) {
System.out.println("[Console] " + message);
}
}
// 第三步: 在 META-INF/services 中注册
// File: META-INF/services/com.example.spi.LoggerService
// 内容: com.example.spi.impl.ConsoleLogger
// 第四步: 使用 ServiceLoader
// File: SPIMain.java
import com.example.spi.LoggerService;
import java.util.ServiceLoader;
public class SPIMain {
public static void main(String[] args) {
// ServiceLoader 会读取 META-INF/services/ 下的文件
ServiceLoader<LoggerService> loader = ServiceLoader.load(LoggerService.class);
System.out.println("TCCL used by ServiceLoader: " +
Thread.currentThread().getContextClassLoader());
for (LoggerService logger : loader) {
logger.log("Hello from SPI!");
}
}
}# 编译和运行
javac -d out src/com/example/spi/*.java src/com/example/spi/impl/*.java
# 创建 META-INF/services 目录
mkdir -p out/META-INF/services
echo "com.example.spi.impl.ConsoleLogger" > out/META-INF/services/com.example.spi.LoggerService
cd out
java SPIMain
# 输出: [Console] Hello from SPI!4.2 验证 TCCL 的核心作用
// TCCLChainDemo.java —— 展示 TCCL 的继承链
public class TCCLChainDemo {
public static void main(String[] args) {
// 主线程的 TCCL
Thread mainThread = Thread.currentThread();
System.out.println("Main TCCL: " + mainThread.getContextClassLoader());
// 子线程默认继承父线程的 TCCL
Thread child = new Thread(() -> {
System.out.println("Child TCCL (inherited): " +
Thread.currentThread().getContextClassLoader());
});
child.start();
// 修改 TCCL
Thread worker = new Thread(() -> {
System.out.println("Worker TCCL (custom): " +
Thread.currentThread().getContextClassLoader());
});
// 设置自定义 TCCL —— 模拟 SPI 框架设置 TCCL
worker.setContextClassLoader(new java.net.URLClassLoader(new java.net.URL[0]));
worker.start();
}
}预期输出(ClassLoader 对象的具体名称会有不同):
Main TCCL: sun.misc.Launcher$AppClassLoader@xxxx
Child TCCL (inherited): sun.misc.Launcher$AppClassLoader@xxxx
Worker TCCL (custom): java.net.URLClassLoader@xxxx五、面试视角
| 追问 | 答案要点 |
|---|---|
| SPI 的原理是什么?三要素分别是什么? | 三要素:SPI 接口(核心库定义)、META-INF/services/ 文件(声明实现类)、ServiceLoader(加载入口)。核心库通过 TCCL 加载应用提供的实现类 |
| JDBC 驱动加载的完整链路是怎样的? | DriverManager(Bootstrap 加载)→ ServiceLoader.load(Driver.class) → 读取 META-INF/services/java.sql.Driver → TCCL 加载实现类 → 触发静态块 → registerDriver() |
| 线程上下文类加载器(TCCL)在 SPI 中起了什么作用? | 它是 Bootstrap 加载的核心库到 AppClassLoader 加载的应用类之间的"反向通道"。核心库代码通过 Thread.currentThread().getContextClassLoader() 获取应用类加载器,加载 SPI 实现 |
| OSGI 的类加载机制和双亲委派有什么不同? | OSGI 不是树状委派,而是声明式的网状路由。每个 Bundle 独立声明 Import-Package(依赖什么)和 Export-Package(对外提供什么)。类加载请求根据 Import 声明路由到对应的 Bundle ClassLoader |
| OSGI 为什么能实现热插拔? | 依赖关系是通过元数据声明的(不是硬编码的引用)。OSGI 框架维护服务注册表,安装/卸载 Bundle 时更新注册表,依赖方通过注册表自动发现新服务或感知服务下线 |
| OSGI 的代价是什么? | 类加载复杂度激增。ClassNotFoundException 和 NoClassDefFoundError 常见,调试困难。Bundle 依赖的版本匹配问题、启动顺序问题都是常见痛点 |
| Java 9 模块化对类加载有什么影响? | 引入 module-info.java 和模块路径。ClassLoader 不再是唯一决定类来源的因素——模块系统在类加载器之上增加了模块边界约束。未 export 的包即使通过反射也无法访问 |
| JPMS 的 module-info.java 中的 provides 和 uses 是做什么的? | provides = 声明本模块是 SPI 实现方(提供什么实现)。uses = 声明本模块需要消费什么 SPI 服务。JPMS 内置了 ServiceLoader 的模块化版本支持 |
| 双亲委派、OSGI、JPMS 三者如何对比? | 双亲委派:树状委派,适合安全隔离但不能表达网状依赖。OSGI:声明式路由,灵活但复杂。JPMS:在 JDK 层推广模块化,增加了模块层约束但底层仍基于 ClassLoader |
📚 相关链接
- **类加载生命周期** — 类加载的基础流程
- **双亲委派模型与打破** — 双亲委派为何需要被打破
- **自定义类加载器实战** — OSGI Bundle 本身就是自定义 ClassLoader 的复杂实现
- ← 返回 **类加载索引**