Java 双亲委派机制解析和破坏双亲委派的方式
最近在准备面试,正把平时积累的笔记、项目中遇到的问题与解决方案、对核心原理的理解,以及高频业务场景的应对策略系统梳理一遍,既能加深记忆,也能让知识体系更扎实,供大家参考,欢迎讨论。
在 Java 中,每个类都有一个对应的 类加载器(ClassLoader),负责将类的字节码加载到 JVM 中。Java 系统中,类加载器通常采用 双亲委派(Parent Delegation)机制,以保证 Java 核心类的安全和加载稳定性
,但在插件化、多版本依赖或动态扩展场景下存在限制。开发者可以结合 自定义类加载器 或 SPI 机制 灵活处理类加载问题。
一、类加载器分类
-
启动类加载器(BootstrapClassLoader)
由 C++ 实现,加载JAVA_HOME/lib
目录或通过-Xbootclasspath
指定路径中的核心类库(如rt.jar
)。 -
扩展类加载器(ExtClassLoader)
加载JAVA_HOME/lib/ext
或通过java.ext.dirs
指定路径下的类库。 -
应用程序类加载器(AppClassLoader)
加载用户 classpath 下的类库。 -
自定义类加载器
继承java.lang.ClassLoader
实现,用于动态加载特定类或插件。用于扩展功能。
二、双亲委派机制概述
双亲委派机制核心思想:类加载请求先委派给父类加载器,父类加载器无法加载时,子类加载器才尝试加载。先父后子,父做得到,子不重复。
流程如下:
- 类加载器收到请求,先判断类是否已加载。
- 未加载时,向父类加载器委派
loadClass()
。 - 父类加载器无法加载,子类加载器再尝试加载。
- 父加载器为
null
时,由启动类加载器处理。
优点:
- 核心类优先加载,保证安全性。
- 避免类重复加载,防止类型冲突。
- 提升程序稳定性,核心 API 不易被篡改。
三、破坏双亲委派的方式
虽然双亲委派机制可靠,但在某些场景下会被破坏:
-
自定义类加载器
继承ClassLoader
并重写loadClass()
,改变加载顺序。 -
Tomcat 类加载器
优先加载自己目录下的 class 文件,实现 Web 应用隔离。 -
Java SPI(Service Provider Interface)
SPI 通过 AppClassLoader 加载服务实现类,用于插件机制,绕过父加载器。
Java SPI 深入理解
Java SPI(Service Provider Interface)是一种服务发现机制
,用于在运行时动态加载接口实现类
。通过 SPI,Java 框架或程序可以在不修改代码的情况下,加载第三方实现类,实现插件化或扩展功能。
我们以 MySQL JDBC 驱动 为例,结合源码分析 SPI 的实现。
一、MySQL JDBC SPI 文件
MySQL JDBC 驱动在 JAR 包中有一个文件:
META-INF/services/java.sql.Driver
内容如下:
com.mysql.cj.jdbc.Driver
- 文件名为接口全限定名
java.sql.Driver
- 文件内容为实现类全限定名
com.mysql.cj.jdbc.Driver
- 放在 JAR 包
META-INF/services/
下,这是 SPI 的约定
💡 注意:SPI 文件本身只是静态配置,只有被 ServiceLoader
加载时才生效。
二、Driver 接口源码分析
JDBC 定义了标准接口:
MySQL 提供了实现类 com.mysql.cj.jdbc.Driver
,源码关键部分:
- 静态块
static {}
会在类加载时执行,把 Driver 注册到DriverManager
。 - 核心逻辑:SPI 机制结合
ServiceLoader
或DriverManager
自动加载,实现动态扩展。
三、SPI 加载机制源码分析
DriverManager
内部通过 SPI 加载驱动:
JDBC 驱动自动加载的核心逻辑——JDBC 通过 “ 先读系统配置、再用 SPI 机制
” 的两步走策略,实现初始驱动的加载,无需开发者手动调用Class.forName(“驱动类名”)。
ServiceLoader.load()
核心源码逻辑:
- 为指定的服务接口或抽象类创建一个 ServiceLoader 实例,用于动态加载实现类。
- Thread.currentThread().getContextClassLoader();这是为了让服务加载与当前线程相关的类加载器一致
-
查找配置文件方法:
META-INF/services/java.sql.Driver
方法ServiceLoader.iterator()
简洁要点
返回值:一个迭代器Iterator<S>
,用于遍历服务实现。
延迟加载:- 已缓存的实现(
providers
)优先返回。 - 未缓存的实现通过
lookupIterator
才查找META-INF/services/...
、加载类并实例化。
- 已缓存的实现(
-
实例化对象并注册:Class.forName(cn, false, loader) 会在运行时根据配置动态加载指定的类(全限定名如 com.mysql.cj.jdbc.Driver),无需在编译时写死类名,比较灵活。
* SPI 加载实现类:直接通过 AppClassLoader 或线程上下文类加载器加载第三方实现类,不走父加载器优先顺序。允许动态扩展,加载用户提供的驱动或插件,绕过父加载器限制。以 MySQL JDBC 为例,SPI 机制特点:
-
接口与实现解耦
应用只依赖java.sql.Driver
,不依赖具体 MySQL 驱动。 -
动态加载实现类
通过ServiceLoader
或DriverManager
自动发现 SPI 文件并实例化。 -
破坏双亲委派顺序
直接由 AppClassLoader 加载用户驱动,实现插件化和动态扩展。 -
易于扩展
添加新的数据库驱动,只需将 JAR 放入 classpath,不修改应用代码。
额外参考:日志框架logback的spi实现