打破双亲委派的实际案例

打破双亲委派的实际案例

双亲委派模型保证了 Java 核心类的安全性,但在实际应用中,很多框架都需要打破这一模型来实现特定功能。本文通过真实案例分析打破双亲委派的原因和方式。

案例1:JDBC 的 SPI 机制

问题背景

// DriverManager 在 rt.jar 中,由 Bootstrap ClassLoader 加载
Connection conn = DriverManager.getConnection(url, user, password);

// 但 MySQL 驱动在 classpath 中,由 App ClassLoader 加载
// Bootstrap ClassLoader 无法加载 App ClassLoader 的类!

解决方案:线程上下文类加载器

// DriverManager.getConnection() 内部
private static void loadInitialDrivers() {
// 获取线程上下文类加载器(默认是 App ClassLoader)
ClassLoader cl = Thread.currentThread().getContextClassLoader();

// 使用上下文类加载器加载驱动
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class, cl);
for (Driver driver : loadedDrivers) {
drivers.add(driver);
}
}

打破方式

Bootstrap ClassLoader 加载 DriverManager
|
v
DriverManager 调用 Thread.currentThread().getContextClassLoader()
|
v
App ClassLoader 加载 mysql-connector-java.jar 中的 Driver

本质:父加载器请求子加载器完成类加载,逆向委派。

案例2:Tomcat 的 Web 应用隔离

需求

  1. 不同 Web 应用使用不同版本的同一库(如 Spring 3 vs Spring 4)
  2. 应用热部署,不重启 Tomcat
  3. JSP 修改后立即生效

Tomcat 类加载器结构

Bootstrap ClassLoader

Extension ClassLoader

System ClassLoader

Common ClassLoader (CATALINA_BASE/lib)
├── Catalina ClassLoader (CATALINA_HOME/lib)
└── Shared ClassLoader
├── WebApp ClassLoader 1 (webapps/app1/WEB-INF/lib)
│ └── 先自己加载,找不到再委派
├── WebApp ClassLoader 2 (webapps/app2/WEB-INF/lib)
└── ...

WebAppClassLoader 的实现

public class WebAppClassLoader extends URLClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 1. 先自己加载(破坏双亲委派)
Class<?> clazz = findLoadedClass(name);
if (clazz == null) {
clazz = findClass(name); // 先在WEB-INF/lib和classes中找
}

// 2. 找不到再系统加载(委托给父加载器)
if (clazz == null) {
clazz = super.loadClass(name);
}

return clazz;
}
}

加载顺序

1. JVM Bootstrap 类(java.lang.*)
2. Web 应用 /WEB-INF/classes
3. Web 应用 /WEB-INF/lib/*.jar
4. System ClassLoader
5. Common ClassLoader

案例3:OSGi 模块化

需求

每个 Bundle(模块)有自己的类加载器,实现:

  • 版本隔离:不同 Bundle 使用不同版本的库
  • 动态加载/卸载 Bundle

OSGi 类加载器

Bundle A (ClassLoader A)
├── 导入: org.slf4j (版本1.7)
└── 导出: com.example.service

Bundle B (ClassLoader B)
├── 导入: org.slf4j (版本1.7) -> 共享
├── 导入: com.example.service -> 从A加载
└── 导入: org.apache.commons -> 从C加载

Bundle C (ClassLoader C)
└── 导出: org.apache.commons

实现方式

public class BundleClassLoader extends ClassLoader {
private Bundle bundle;

@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {

// 1. 检查是否在当前 Bundle 中
Class<?> clazz = findLocalClass(name);
if (clazz != null) return clazz;

// 2. 检查 Import-Package,委托给其他 Bundle
clazz = findImportedClass(name);
if (clazz != null) return clazz;

// 3. 委托给父加载器
return super.loadClass(name, resolve);
}
}

案例4:Spring Boot 的 LaunchedURLClassLoader

需求

Spring Boot 将依赖打包为可执行 fat-jar,需要特殊方式加载嵌套的 jar。

结构

myapp.jar
├── META-INF/
│ └── MANIFEST.MF
├── BOOT-INF/
│ ├── classes/ (应用类)
│ └── lib/
│ ├── spring-core.jar
│ ├── spring-boot.jar
│ └── ...
└── org/springframework/boot/loader/
└── LaunchedURLClassLoader.class

LaunchedURLClassLoader

public class LaunchedURLClassLoader extends URLClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {

// 1. 处理 org.springframework.boot.loader 包(自身)
if (name.startsWith("org.springframework.boot.loader.")) {
return findClass(name);
}

// 2. 其他类先委派(保持双亲委派)
try {
return super.loadClass(name, resolve);
} catch (ClassNotFoundException ex) {
// 3. 找不到再从 BOOT-INF/classes 和 BOOT-INF/lib 加载
return findClass(name);
}
}
}

特点

  • 大部分类保持双亲委派
  • 只有特定包和找不到的类才自己加载
  • 支持 jar:file:/path/myapp.jar!/BOOT-INF/lib/spring.jar!/ 这样的 URL

案例5:Java Agent 与字节码增强

需求

在运行时修改类的字节码(如链路追踪、监控)。

Instrumentation 加载

public class MyAgent {
public static void premain(String args, Instrumentation inst) {
// Agent 由 -javaagent 指定,通常由 App ClassLoader 加载
// 但需要修改 Bootstrap 加载的类(如 java.net.URL)

inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
// 修改字节码
return modifyByteCode(classfileBuffer);
}
});
}
}

打破方式

Instrumentation 接口允许重新转换由 Bootstrap ClassLoader 加载的类
Agent 的 ClassTransformer 可以修改核心类

打破双亲委派的三种方式

方式 案例 实现
重写 loadClass() Tomcat、OSGi 先自己加载,找不到再委派
线程上下文类加载器 JDBC、JNDI Thread.setContextClassLoader()
Instrumentation Java Agent JVM 接口修改已加载类

总结

场景 打破原因 实现方式
JDBC 父加载器无法加载子类路径的驱动 线程上下文类加载器
Tomcat Web 应用隔离和热部署 WebAppClassLoader 先自己加载
OSGi 模块版本隔离 每个 Bundle 独立类加载器
Spring Boot 加载嵌套 jar LaunchedURLClassLoader
Java Agent 修改核心类字节码 Instrumentation API

双亲委派是 Java 类加载的默认规则,但在框架开发中,根据具体需求打破这一规则是常见且必要的做法。


   转载规则


《打破双亲委派的实际案例》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录