双亲委派有哪些缺点?如何打破双亲委派?


双亲委派有哪些缺点?


  1. 双亲委派主要保证 Java 核心类库的安全性和一致性,但也带来了 类冲突、无法隔离模块、扩展性差等问题。
  2. Tomcat、OSGi、Spring Boot 都 修改了类加载机制 以适应自己的需求。
  3. 在 插件化、动态代理、J2EE 服务器 这些场景下,往往需要绕过双亲委派机制,使用自定义类加载器。

如何打破双亲委派?


虽然 JVM 默认使用双亲委派机制 来 保证类加载的安全性和稳定性,但在某些情况下(如 插件隔离、不同版本 JAR 并存、动态代理 等),需要 绕过或改进双亲委派。

可以通过 三种方式 来打破双亲委派:

1. 重写 findClass(),子加载器优先加载
默认的 loadClass() 机制 是先让 父加载器 尝试加载,只有当父加载器加载失败时,子类加载器才会尝试 findClass() 进行自定义加载。

如果我们希望子类加载器优先加载类,而不是先让父加载器尝试加载,可以 直接重写 loadClass() 方法,让它先查找自己加载的类,再委派给父加载器。

示例:重写 findClass(),子加载器优先
java

import java.io.*;

public class CustomClassLoader extends ClassLoader {
    private String classPath;

    public CustomClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        return defineClass(name, classData, 0, classData.length);
    }

    private byte[] loadClassData(String className) {
        String filePath = classPath + className.replace(".", "/") + ".class";
        try (InputStream input = new FileInputStream(filePath);
             ByteArrayOutputStream output = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = input.read(buffer)) != -1) {
                output.write(buffer, 0, bytesRead);
            }
            return output.toByteArray();
        } catch (IOException e) {
            return null;
        }
    }

    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // **打破双亲委派**,先尝试自己加载
        Class<?> clazz = findLoadedClass(name);
        if (clazz == null) {
            try {
                clazz = findClass(name);  // **优先使用自己的 `findClass()`**
            } catch (ClassNotFoundException e) {
                clazz = super.loadClass(name, false);  // **如果找不到,才交给父类**
            }
        }
        if (resolve) {
            resolveClass(clazz);
        }
        return clazz;
    }

    public static void main(String[] args) throws Exception {
        CustomClassLoader loader = new CustomClassLoader("D:/custom_classes/");
        Class<?> clazz = loader.loadClass("com.example.MyClass");
        Object obj = clazz.newInstance();
        System.out.println("类加载器:" + obj.getClass().getClassLoader());
    }
}

说明:

先检查 自己是否已经加载了类(findLoadedClass())。
自己优先加载(调用 findClass())。
如果 找不到,再交给父类加载器。
这样,com.example.MyClass 不会被父类加载器加载,而是由自定义类加载器加载。

2. 通过自定义 ClassLoader 实现模块隔离
场景:
在 插件化开发(Plugin System) 或 J2EE 服务器 中,不同模块可能包含 相同类的不同版本,但由于 双亲委派机制,默认的类加载器会导致 类冲突。

解决方案:

为每个插件或 Web 应用创建一个独立的类加载器,这样不同插件可以加载自己的 JAR 包,避免冲突。
示例:插件化 ClassLoader
java

import java.net.URL;
import java.net.URLClassLoader;

public class PluginClassLoader extends URLClassLoader {

    public PluginClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // 先查找自己加载的类
        Class<?> clazz = findLoadedClass(name);
        if (clazz == null) {
            try {
                clazz = findClass(name); // 先从插件目录加载
            } catch (ClassNotFoundException e) {
                clazz = super.loadClass(name); // 如果找不到,才交给父类
            }
        }
        return clazz;
    }
}

使用示例

java

public class PluginLoaderTest {
    public static void main(String[] args) throws Exception {
        URL jarUrl = new URL("file:D:/plugins/pluginA.jar");
        PluginClassLoader pluginLoader = new PluginClassLoader(new URL[]{jarUrl}, ClassLoader.getSystemClassLoader());
        Class<?> pluginClass = pluginLoader.loadClass("com.plugin.PluginMain");
        Object pluginInstance = pluginClass.newInstance();
        System.out.println("插件类加载器:" + pluginInstance.getClass().getClassLoader());
    }
}

效果:

每个插件都可以加载自己的 JAR,而不会影响主程序。
避免 JAR 冲突,支持不同版本共存。
3. 动态调整 ThreadContextClassLoader,解决 AOP/代理类加载问题
场景:

在 AOP(如 Spring 代理)、动态代理(Proxy)、反射 中,代理类 和 被代理类 可能由不同的类加载器加载,导致 ClassCastException。
解决方案:临时切换线程上下文类加载器 (ThreadContextClassLoader)。
示例:动态调整 ThreadContextClassLoader

public class ThreadContextClassLoaderDemo {
    public static void main(String[] args) {
        ClassLoader originalLoader = Thread.currentThread().getContextClassLoader();
        try {
            ClassLoader customLoader = new CustomClassLoader("D:/custom_classes/");
            Thread.currentThread().setContextClassLoader(customLoader);

            // 使用反射加载类
            Class<?> clazz = Class.forName("com.example.DynamicClass", true, customLoader);
            Object obj = clazz.newInstance();
            System.out.println("动态加载类:" + obj.getClass().getClassLoader());

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 还原原来的类加载器
            Thread.currentThread().setContextClassLoader(originalLoader);
        }
    }
}

效果:

动态切换类加载器,保证代理类和被代理类使用相同的 ClassLoader,避免 ClassCastException。
Spring AOP、JNDI、Hibernate 也常用这个方法 解决 ClassLoader 不匹配问题。

总结

方法 场景 解决方案**

  1. 重写 findClass(),子加载器优先 需要优先加载自己的类,而不是父加载器的类 继承 ClassLoader,
  2. 重写 findClass() 和 loadClass() 自定义 ClassLoader 实现模块隔离 插件化、Web
    应用隔离,不同模块依赖相同类 URLClassLoader 或 自定义 ClassLoader 来加载插件 动态调整
  3. ThreadContextClassLoader 解决 AOP/动态代理 ClassLoader不匹配问题Thread.currentThread().setContextClassLoader(loader)

你可能感兴趣的:(北京JAVA面试,java)