ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

【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的无参构造器源码javaprotected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader()); }getSystemClassLoader()返回的是AppClassLoader。所以结论非常明确如果你自定义类加载器时构造方法里没有传入parent那么它的默认父加载器就是 AppClassLoader系统类加载器。java// 默认情况父加载器是 AppClassLoader public class MyClassLoader extends ClassLoader { public MyClassLoader() { super(); // 隐式调用无参构造parent AppClassLoader } }7.2 如何手动指定父加载器通过ClassLoader的带参构造器传入parent对象javapublic 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方法强行打破双亲委派javapublic 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 ClassLoaderJava 设计者为了处理这种“父类调用子类实现”的窘境引入了一个“后门”——线程上下文类加载器。DriverManager不再自己亲自去加载而是通过 SPIService Provider Interface规范结合当前线程的上下文类加载器去加载java// DriverManager 静态初始化块中的核心简化逻辑 static { loadInitialDrivers(); } private static void loadInitialDrivers() { // 关键点获取当前线程的上下文类加载器默认就是 AppClassLoader ClassLoader cl Thread.currentThread().getContextClassLoader(); // 通过 ServiceLoader 加载传入 AppClassLoader 去扫描 classpath ServiceLoaderDriver loadedDrivers ServiceLoader.load(Driver.class, cl); IteratorDriver 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.0Java 6之前DriverManager并没有集成 SPI 自动发现机制。为了强制让 JVM 加载驱动类并执行其静态代码块我们必须手动触发类加载javaClass.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(Cant 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 配合线程上下文类加载器自动完成了加载而这个动作本身就是对双亲委派机制的一次经典破坏。如果这三篇系列文章对你有帮助点赞、收藏、转发支持一下有任何疑问欢迎评论区交流探讨
返回列表