【Java类加载器】Java 类加载器完整体系深度拆解(下):自定义加载器实战与 SPI 破坏双亲委派(MySQL 驱动揭秘)

作者介绍: 大家好,我是 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 配合线程上下文类加载器自动完成了加载,而这个动作本身就是对双亲委派机制的一次经典破坏。


如果这三篇系列文章对你有帮助,点赞、收藏、转发支持一下!有任何疑问欢迎评论区交流探讨~

相关推荐
dishugj1 小时前
操作系统四大特征|软考架构师考点梳理
java·linux·服务器
砚底藏山河1 小时前
并发与限频工程:把20只的2秒压到0.5秒不封号(魔码量化实战 #03)
java·开发语言·数据库·python·金融
ly76891 小时前
Spring WebFlux背压机制在生产环境的调优与陷阱
java·spring·microsoft·webflux·project reactor·背压·响应式流
云雀衔光2 小时前
MCP 协议全景:为什么它是 AI 连接工具的「USB-C」
java·开发语言·数据库·人工智能·ai编程
ly76892 小时前
Spring 异步编程的隐藏风险:@Async 线程池耗尽与异常处理详解
java·后端·spring·异常处理·线程池·任务拒绝
用户8181870627462 小时前
第23章 热点Key问题排查与解决:大促场景经典坑
java·后端
geovindu2 小时前
go: Task Scheduler
开发语言·后端·golang
IT枫斗者枫哥2 小时前
Java 接口超时后,重试为什么会多建一条任务?
java
用户3126874877202 小时前
CAS 到底是怎么保证原子性的?从 Unsafe 到 VarHandle 的演进
java