双亲委派模型是Java类加载体系的核心设计,其默认逻辑是当一个类加载器收到类加载请求时,会先把这个请求委托给父加载器处理,只有父加载器无法完成加载时,子加载器才会尝试自己加载。但在实际开发中,双亲委派并非不可打破,存在多种场景需要主动破坏该机制。

双亲委派模型的基本运行逻辑
Java默认的类加载器分为三层,从顶到底分别是启动类加载器、扩展类加载器、应用程序类加载器。启动类加载器负责加载<JAVA_HOME>/lib目录下的核心类库,扩展类加载器加载<JAVA_HOME>/lib/ext目录下的扩展类,应用程序类加载器加载用户类路径上的类。正常情况下,类加载的委派链会一直向上传递,直到启动类加载器,若父加载器无法加载才会向下回退。
双亲委派的核心价值是避免核心类被重复加载或篡改,比如用户自定义一个名为java.lang.String的类,启动类加载器会优先加载核心库中的String类,不会加载用户自定义的同名类,保障了Java核心类的安全性。
双亲委派被破坏的常见场景
1. 自定义类加载器重写loadClass方法
双亲委派的实现逻辑在ClassLoader类的loadClass方法中,如果自定义类加载器重写了该方法,不遵循先委托父加载器的逻辑,就会直接破坏双亲委派。以下是一个简单的自定义类加载器示例,它优先加载自己路径下的类,不委托父加载器:
import java.io.FileInputStream;
import java.lang.reflect.Method;
// 自定义类加载器,破坏双亲委派
public class CustomClassLoader extends ClassLoader {
private String classPath;
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
// 重写loadClass方法,不遵循双亲委派逻辑
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 如果是java.lang包下的类,还是走双亲委派,避免破坏核心类
if (name.startsWith("java.lang")) {
return super.loadClass(name);
}
// 其他类优先自己加载
try {
byte[] classData = loadClassData(name);
if (classData != null) {
return defineClass(name, classData, 0, classData.length);
}
} catch (Exception e) {
// 自己加载失败再走父加载器逻辑
}
return super.loadClass(name);
}
// 读取类文件的字节数据
private byte[] loadClassData(String className) throws Exception {
String path = classPath + className.replace(".", "/") + ".class";
FileInputStream fis = new FileInputStream(path);
byte[] data = new byte[fis.available()];
fis.read(data);
fis.close();
return data;
}
public static void main(String[] args) throws Exception {
// 指定自定义类加载器的加载路径
CustomClassLoader loader = new CustomClassLoader("D:/custom_classes/");
// 加载自定义路径下的Test类
Class<?> clazz = loader.loadClass("com.test.Test");
Object instance = clazz.newInstance();
Method method = clazz.getMethod("sayHello");
method.invoke(instance);
}
}
上述代码中,自定义类加载器重写了loadClass方法,对于非核心包的类优先自己加载,跳过了父加载器的委托过程,这就是典型的破坏双亲委派的行为。
2. SPI机制的实现
SPI(Service Provider Interface)是Java提供的一种服务发现机制,比如JDBC的驱动加载就是典型的SPI场景。JDBC的核心接口java.sql.Driver定义在核心库中,由启动类加载器加载,但具体的驱动实现类(比如MySQL驱动)是第三方提供的,放在应用程序的类路径下,由应用程序类加载器加载。启动类加载器无法加载应用程序类路径下的类,因此SPI机制会主动打破双亲委派,通过线程上下文类加载器来加载第三方实现类。
以下是JDBC加载驱动的简化逻辑示例:
import java.sql.Driver;
import java.util.ServiceLoader;
public class SpiDemo {
public static void main(String[] args) {
// ServiceLoader会通过线程上下文类加载器加载第三方Driver实现
ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);
for (Driver driver : drivers) {
System.out.println("加载到驱动:" + driver.getClass().getName());
}
}
}
这里ServiceLoader.load方法会使用当前线程的上下文类加载器(默认是应用程序类加载器)去加载Driver的实现类,而Driver接口本身是由启动类加载器加载的,相当于子加载器反向去加载父加载器管辖范围的类,破坏了双亲委派的单向委托逻辑。
3. 热部署、热替换场景
在需要对运行中的应用进行热部署、热替换的场景下,比如OSGi模块化框架、Tomcat的热部署功能,都需要破坏双亲委派。以Tomcat为例,它部署多个Web应用时,每个Web应用都有自己的类加载器,优先加载自己Web应用下的类,而不是先委托父加载器,这样可以保证不同Web应用的类相互隔离,修改某个Web应用的类后不需要重启整个Tomcat,实现热更新。
4. 模块化系统的影响
Java 9引入的模块化系统(JPMS)也对双亲委派模型做了调整,模块之间的类加载不再完全遵循传统的三层委派逻辑,模块可以声明自己的依赖,类加载的委派关系会根据模块定义来走,这也是双亲委派被破坏的一种场景。
破坏双亲委派的注意事项
破坏双亲委派虽然能满足特定场景的需求,但也会带来风险。如果随意加载自定义的核心类同名类,可能会导致核心类被篡改,引发不可预期的问题。因此自定义类加载器时,一般建议只重写findClass方法,而不是loadClass方法,findClass方法是双亲委派逻辑中父加载器加载失败后才会调用的方法,这样既能实现自定义加载逻辑,又不会完全破坏双亲委派的基本规则。
以下是重写findClass方法的正确示例:
import java.io.FileInputStream;
public class SafeCustomClassLoader extends ClassLoader {
private String classPath;
public SafeCustomClassLoader(String classPath) {
this.classPath = classPath;
}
// 重写findClass方法,不破坏双亲委派的整体逻辑
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
String path = classPath + name.replace(".", "/") + ".class";
FileInputStream fis = new FileInputStream(path);
byte[] data = new byte[fis.available()];
fis.read(data);
fis.close();
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
throw new ClassNotFoundException(name);
}
}
}
这种写法下,类加载请求还是会先走双亲委派的流程,只有父加载器无法加载时才会调用findClass方法加载自定义路径的类,既满足了自定义加载的需求,又保留了双亲委派的核心优势。