核心是重写loadclass方法,绕过父加载器优先查找逻辑,实现类隔离;常见于插件化、热部署等场景,需有选择地委派(如java.*仍委托,插件类自行加载)并配合tccl。

Java 类加载器打破双亲委派,核心是**重写 loadClass 方法,绕过父加载器优先查找的逻辑**,从而实现类的隔离。这不是“破坏”机制本身,而是有明确目的的定制行为,常见于插件化、热部署、多租户等场景。
为什么需要打破双亲委派?
双亲委派保证了核心类(如 java.lang.Object)不会被用户自定义类覆盖,提升了安全性和稳定性。但它的副作用是:同一类名、不同版本的类无法共存;插件或模块间容易因类冲突而失败。例如:
- 两个插件都依赖不同版本的
fastjson,按双亲委派会加载到同一个版本,导致兼容性问题; - 应用想在运行时动态加载并卸载某个业务模块,但该模块的类已被系统类加载器加载,无法单独卸载。
如何手动打破双亲委派?
关键是在自定义类加载器中**不调用 super.loadClass(name, resolve)**,而是直接调用 findClass(name),由自己完成字节码加载和定义。典型写法如下:
public class PluginClassLoader extends ClassLoader {
private final String pluginPath;
public PluginClassLoader(String pluginPath, ClassLoader parent) {
super(parent); // 仍可持有 parent,但不强制委派
this.pluginPath = pluginPath;
}
@Override
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 1. 先尝试自己加载(打破委派:不先调用 parent.loadClass)
Class> clazz = findLoadedClass(name);
if (clazz == null) {
try {
clazz = findClass(name); // 自己从插件路径加载
} catch (ClassNotFoundException ignored) {
// 加载失败,再委托给父加载器(可选:实现“逆向委派”或“仅委托基础包”)
}
}
if (clazz == null && getParent() != null) {
// 2. 显式委托——但只对特定包(如 java.*、javax.*)才交出去
if (name.startsWith("java.") || name.startsWith("javax.")) {
clazz = getParent().loadClass(name);
}
}
if (clazz == null) {
throw new ClassNotFoundException(name);
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
byte[] bytes = loadByteCodeFromPlugin(name); // 自行读取 .class 文件
return defineClass(name, bytes, 0, bytes.length);
}
private byte[] loadByteCodeFromPlugin(String name) throws ClassNotFoundException {
String path = pluginPath + "/" + name.replace('.', '/') + ".class";
try (InputStream is = new FileInputStream(path)) {
return is.readAllBytes();
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
}
更安全的隔离策略:过滤委派范围
完全不委派可能引发 NoClassDefFoundError(比如找不到 java.util.List)。推荐做法是“**有选择地委派**”:
- 基础类(
java.*、javax.*、sun.*等)必须由启动类加载器提供,始终委派; - 应用公共库(如
slf4j-api)可由共享类加载器提供,也建议委派; - 插件私有类(如
com.myplugin.*)一律自己加载,不委派; - 避免重复加载日志、JSON 等易冲突的第三方包,可统一由容器层提供 API 接口,插件只依赖接口而非实现。
注意线程上下文类加载器(TCCL)的配合
很多框架(如 JDBC、JAX-WS、Spring)依赖 Thread.currentThread().getContextClassLoader() 加载资源。若插件中调用了这类 API,需在执行前临时设置 TCCL:
Thread thread = Thread.currentThread();
ClassLoader oldCl = thread.getContextClassLoader();
thread.setContextClassLoader(pluginClassLoader);
try {
someFrameworkMethod(); // 此时会用 pluginClassLoader 加载其依赖类
} finally {
thread.setContextClassLoader(oldCl);
}
否则即使你打破了委派,框架仍可能用错类加载器,导致隔离失效。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











