Skip to content

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 的核心概念

概念说明类比
BundleOSGI 中的模块单位 = 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 驱动加载的全链路追踪

java
// ① 用户代码调用
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 声明模块依赖:

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   │
└────────────────────┘                   │  约束               │
                                         └─────────────────────┘

关键变化

  1. 模块路径 (module path) vs 类路径 (classpath): 模块路径上的 jar 必须包含 module-info.java,JVM 会执行严格的模块边界检查。类路径仍然存在,但被视为"未命名模块"
  2. 封装性: 模块可以隐藏内部包——即使反射也无权访问(不再是 private 就能完全隐藏的旧模式)
  3. 类加载器不再决定一切: 模块系统在 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 的简单实现

java
// 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!");
        }
    }
}
bash
# 编译和运行
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 的核心作用

java
// 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 的复杂实现
  • ← 返回 **类加载索引**

Knowledge4J — Java 知识库