作者介绍: 大家好,我是 CodeStats。一个在底层技术上"考古"了四年的硬核爱好者,也是 WWAIC(全周项目AI编程)范式的提出者和实践者。我曾手写过一个完整的 Java Web 框架(从 IoC 容器到嵌入式 Tomcat,代码全开源),也喜欢用通俗的语言拆解 CPU、JVM、操作系统的运行本质。我的技术信条:所有高深的技术,最后都能用大白话讲清楚。如果讲不清楚,说明还没真正理解。
本文能获得什么
-
掌握自定义加载器: 搞懂不指定父加载器时默认是谁,以及如何手动指定爸爸
-
实战顺序推演: 在自定义加载器存在时,类到底是从 classpath 先找,还是从自定义路径先找
-
理解破坏(重点): 什么是线程上下文类加载器(Thread Context ClassLoader)
-
拿下 MySQL 经典面试题: JDBC 驱动是如何利用 SPI 破坏双亲委派的,以及为什么现在可以不写
Class.forName
目录
-
提问七:Java 自定义类加载器默认父加载器和如何指定父类加载器
-
提问八:如果自定义类加载器默认父类是 App 类加载器,加载类会优先从应用类路径加载还是自定义类加载器加载
-
提问九:MySQL 是如何打破双亲委派机制的,为什么需要
Class.forName加载驱动
七、Java 自定义类加载器默认父加载器和如何指定父类加载器
7.1 不指定时,默认爸爸是谁?
直接看 java.lang.ClassLoader 的无参构造器源码:
java
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
getSystemClassLoader() 返回的是 AppClassLoader。
所以结论非常明确:如果你自定义类加载器时,构造方法里没有传入 parent,那么它的默认父加载器就是 AppClassLoader(系统类加载器)。
java
// 默认情况:爸爸是 AppClassLoader
public class MyClassLoader extends ClassLoader {
public MyClassLoader() {
super(); // 隐式调用无参构造,parent = AppClassLoader
}
}
7.2 如何手动指定爸爸?
通过 ClassLoader 的带参构造器传入 parent 对象:
java
public class MyClassLoader extends ClassLoader {
// 方式一:构造时传入指定的父加载器
public MyClassLoader(ClassLoader parent) {
super(parent);
}
// 方式二:如果继承 URLClassLoader,可以同时指定 URL 和父加载器
public MyClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
}
手动指定的三种常见玩法:
java
// 1. 指定爸爸为 ExtClassLoader
ClassLoader extParent = ClassLoader.getSystemClassLoader().getParent();
MyClassLoader loader1 = new MyClassLoader(extParent);
// 2. 指定爸爸为 Bootstrap(传入 null)
MyClassLoader loader2 = new MyClassLoader(null);
// 3. 指定爸爸为另一个自定义加载器(构建加载器链)
MyClassLoader loader3 = new MyClassLoader(loader1);
八、如果自定义类加载器默认父类是 App 类加载器,加载类会优先从应用类路径加载还是自定义类加载器加载
8.1 答案:绝对优先从应用类路径(AppClassLoader)加载
这是一个"送分题",但很多人因为没看源码而答错。
因为双亲委派的铁律是 "先向上,再向下" 。既然你的爸爸是 AppClassLoader,那么请求路径如下:
text
自定义类加载器收到加载请求
↓(立刻向上甩锅)
AppClassLoader(爸爸)收到请求,继续向上甩锅给 Ext
↓
ExtClassLoader(爷爷)收到请求,继续向上甩锅给 Bootstrap
↓
BootstrapClassLoader(祖宗)尝试加载 —— 找不到
↓
ExtClassLoader 尝试加载 —— 找不到
↓
AppClassLoader 尝试加载 —— 从 classpath 找
├─ 找到了 → 直接返回(自定义加载器根本没机会)
└─ 找不到 → 抛异常
↓(异常捕获后)
自定义加载器 finally 调用自己的 findClass() 尝试加载
8.2 如何破坏顺序,让自定义加载器"插队"?
如果你非要自定义加载器优先(比如实现应用隔离,不想让 classpath 里的同名类污染自己),那就必须重写 loadClass 方法,强行打破双亲委派:
java
public class HotSwapClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 1. 核心类库(java. 开头的)还是交给爸爸,避免破坏 JVM 安全
if (name.startsWith("java.")) {
return super.loadClass(name);
}
// 2. 先查缓存
Class<?> c = findLoadedClass(name);
if (c != null) return c;
try {
// 3. 重点:自己先找(插队加载)
c = findClass(name);
if (c != null) return c;
} catch (ClassNotFoundException e) {
// 自己找不到,再丢给父加载器去兜底
}
return super.loadClass(name);
}
}
九、MySQL 是如何打破双亲委派机制的,为什么需要 Class.forName 加载驱动
这是 Java 进阶路上最经典的"双亲委派破坏"案例,面试必问。我们用"讲故事"的方式彻底讲透。
9.1 矛盾爆发点:爸爸想用儿子的东西,但双亲委派不准
在 JDBC 规范中:
-
java.sql.DriverManager类是 JDK 自带的,被 BootstrapClassLoader 加载(住在核心库 rt.jar 里)。 -
com.mysql.cj.jdbc.Driver是第三方 MySQL 驱动包里的类,躺在 classpath 下,本该由 AppClassLoader 加载。
DriverManager 的职责是管理所有数据库驱动。它需要加载并注册 MySQL 驱动类。
按照双亲委派的规矩:子加载器可以向上委托给父加载器,但父加载器绝对不能向下委派给子加载器 。
这就导致了 BootstrapClassLoader 根本不知道去哪里找 com.mysql.cj.jdbc.Driver------标准的双亲委派机制在这里直接死锁!
9.2 破局之匙:线程上下文类加载器(Thread Context ClassLoader)
Java 设计者为了处理这种"父类调用子类实现"的窘境,引入了一个"后门"------线程上下文类加载器。
DriverManager 不再自己亲自去加载,而是通过 SPI(Service Provider Interface)规范,结合当前线程的上下文类加载器去加载:
java
// DriverManager 静态初始化块中的核心简化逻辑
static {
loadInitialDrivers();
}
private static void loadInitialDrivers() {
// 关键点:获取当前线程的上下文类加载器(默认就是 AppClassLoader)
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 通过 ServiceLoader 加载,传入 AppClassLoader 去扫描 classpath
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class, cl);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
while (driversIterator.hasNext()) {
try {
// 这里会触发 AppClassLoader 加载 MySQL Driver,并执行其静态块
driversIterator.next();
} catch (Throwable t) {
// ...
}
}
}
破坏的本质: 父加载器(Bootstrap)通过获取子加载器(App)的引用,逆向调用子加载器去干活,彻底打破了"只能向上委托"的铁律。
9.3 为什么以前必须写 Class.forName("com.mysql.cj.jdbc.Driver")?
在 JDBC 4.0(Java 6)之前 ,DriverManager 并没有集成 SPI 自动发现机制。为了强制让 JVM 加载驱动类并执行其静态代码块,我们必须手动触发类加载:
java
Class.forName("com.mysql.cj.jdbc.Driver");
当这行代码执行时,AppClassLoader 会去加载 com.mysql.cj.jdbc.Driver 类。类加载完毕后,JVM 会立刻执行其内部的静态初始化块:
java
// com.mysql.cj.jdbc.Driver 源码中的静态块
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
静态块调用了 DriverManager.registerDriver(),于是 MySQL 驱动实例被成功注册进了 DriverManager 的 registeredDrivers 列表中。
9.4 为什么现在(JDBC 4.0+)不用写 Class.forName 了?
从 Java 6 开始,JDK 在 java.sql.Driver 包中内置了 SPI 机制。
MySQL 驱动 JAR 包的 META-INF/services/java.sql.Driver 文件里,清清楚楚写着一行字:com.mysql.cj.jdbc.Driver。
当 DriverManager 初始化时,它会使用 ServiceLoader 配合 Thread.currentThread().getContextClassLoader(),自动扫描 classpath 下所有 JAR 包中 META-INF/services/ 目录下的配置文件,并自动加载其中配置的驱动类。
所以现在我们的代码可以清爽到只剩一行:
java
// 不需要 Class.forName,不需要注册,直接拿连接
Connection conn = DriverManager.getConnection(url, user, password);
9.5 终极总结(面试可直接背诵)
| 阶段 | 方式 | 原理 |
|---|---|---|
| JDBC 4.0 以前 | 手动 Class.forName |
手动触发类加载,执行静态块手动注册驱动 |
| JDBC 4.0 以后 | SPI 自动加载 | DriverManager 利用 线程上下文类加载器 逆向调用 AppClassLoader,自动扫描 META-INF/services 加载驱动 |
| 核心破坏点 | 父加载器调用子加载器 | Bootstrap/Ext 通过 Thread.currentThread().getContextClassLoader() 拿到 AppClassLoader,实现"子加载器加载父加载器所需的类" |
一句话浓缩:
写 Class.forName 是为了在 SPI 出现前手动触发静态块;现在不写是因为 SPI 配合线程上下文类加载器自动完成了加载,而这个动作本身就是对双亲委派机制的一次经典破坏。
如果这三篇系列文章对你有帮助,点赞、收藏、转发支持一下!有任何疑问欢迎评论区交流探讨~