打破双亲委派的实际案例
双亲委派模型保证了 Java 核心类的安全性,但在实际应用中,很多框架都需要打破这一模型来实现特定功能。本文通过真实案例分析打破双亲委派的原因和方式。
案例1:JDBC 的 SPI 机制
问题背景
Connection conn = DriverManager.getConnection(url, user, password);
|
解决方案:线程上下文类加载器
private static void loadInitialDrivers() { 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 应用隔离
需求
- 不同 Web 应用使用不同版本的同一库(如 Spring 3 vs Spring 4)
- 应用热部署,不重启 Tomcat
- 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 { Class<?> clazz = findLoadedClass(name); if (clazz == null) { clazz = findClass(name); } 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 { Class<?> clazz = findLocalClass(name); if (clazz != null) return clazz; clazz = findImportedClass(name); if (clazz != null) return clazz; 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 { if (name.startsWith("org.springframework.boot.loader.")) { return findClass(name); } try { return super.loadClass(name, resolve); } catch (ClassNotFoundException ex) { 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) { 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 类加载的默认规则,但在框架开发中,根据具体需求打破这一规则是常见且必要的做法。