从实现通用接口的 jar 中实例化 类,然后将实例分配给接口会导致 ClassCastException

Instantiating classes from a jar that implement a common interface, and then assigning the instance to the interface causes ClassCastException

这是我一直在努力解决的 class 加载程序问题。我了解问题的根本原因(不同的 class 加载器),但我不确定解决问题的最佳方法。

我有一些通用接口的项目;我们称它为 api。我还有另外两个名为 runnermodule 的项目,它们都使用 api 作为依赖项。

runner 的工作是动态加载一个 module 工件(从一个 jar;它是一个包含其依赖项的胖子)然后执行它。 runner 期望 module 提供来自 api 的某些具体实现。为了确保来自不同版本 module.jar 的 classes 不会互相破坏,我创建了一个新的 classloader,其中包含 URL 到 module.jar,并将父 classloader 设置为加载和处理 module.jar 的 class 的 classloader。这没有任何问题。

当我使用 runner 作为网络应用程序(具体来说是 spring 启动应用程序)中的依赖项时,问题出现了,并且很快发现我无法加载一些 classes 来自 module.jar 因为它们与当前 class 路径中已经存在的 classes 冲突(来自 webapp 中的其他依赖项)。

因为 module.jar 实际上只需要来自 api 的 classes,我认为我可以创建一个新的 URLClassLoader(没有父项)只有 classes 来自 api.jar,然后在我加载模块时将其用作父 classloader。这就是我开始 运行 麻烦的地方:

CommonInterface commonInterface = null;
Class<CommonInterface> commonInterfaceClass = null;

ClassLoader myClassLoader = URLClassLoader.newInstance(moduleJarURL, apiClassesClassLoader);

//...
//...

//clazz is a concrete implementation from module.jar
if(myClassLoader.loadClass(CommonInterface.class.getName()).isAssignableFrom(clazz)) {
    commonInterfaceClass = clazz; 
}

commonInterface = commonInterfaceClass.newInstance(); //ClassCastException

我知道我最初的问题是由于 classloader 在尝试加载它之前首先检查 class 是否已经加载,这意味着当它使用 module.jar 中的名称解析,它链接到 class.

的不兼容版本

处理这个问题的好方法是什么?与其创建一个只有 api 中的 class 的 URL classloader,创建我自己的实现是否有意义,只有在请求 class 是 api?

中的一个

您已经从两个不同的 class 加载器加载了 CommonInterface。 类 同名但不同的 class 加载程序与 JVM 不同 class。 (即使 classes 在 .class 文件中 100% 相同 - 问题不是不兼容,而是它们来自不同的 class 加载器)

如果你做

System.out.println(CommonInterface.class == myClassLoader.loadClass(CommonInterface.class.getName()));

你会发现这会打印出 false.

您创建 classloader 的方式:

ClassLoader myClassLoader = URLClassLoader.newInstance(moduleJarURL, apiClassesClassLoader);

.. 仅当 apiClassesClassLoader 也是包含此代码的 class 的父 class 加载程序时才有效。

你可以试试:

ClassLoader myClassLoader = URLClassLoader.newInstance(moduleJarURL, 
                                                       getClass().getClassLoader());

但是根据您的描述(它是一个 "fat" jar,包含自己的依赖项)和网络 classloader 的复杂性(儿童优先)这可能无法解决您的问题。

在那种情况下,唯一的解决办法是制作你的模块 jar "lean" 以确保你只用一个 class 加载器加载每个 class 一次。

我忘记用我的解决方案更新这个问题。我能够通过创建一个扩展 URLClassLoader 的自定义 class-loader 来解决这个问题。此 classloader 没有父级。

然后我覆盖 loadClass 以控制 classes 的加载方式。我首先检查 class 是否存在于 module.jar 中。如果是这样,我从那里加载它。否则,我使用当前的 classloader 加载它。由于我的自定义 classloader 没有父级,它可以从 module.jar 加载 classes 即使它们已经被主 classloader 加载,因为它们有在我的自定义 classloader 层次结构中不存在。

基本方法是这样的:

public class MyClassLoader extends URLClassLoader {

    private final ClassLoader mainClassLoader = MyClassLoader.class.getClassLoader();
    private final Set<String> moduleClasses;

    private MyClassLoader(URL url) {
        super(new URL[]{ url });
        try {
            JarURLConnection connection = (JarURLConnection) url.openConnection();

            this.moduleClasses = connection.getJarFile().stream()
                .map(JarEntry::getName)
                .filter(name -> name.endsWith(".class"))
                .map(name -> name.replace(".class", "").replaceAll("/", "."))
                .collect(Collectors.toSet());
        } catch(IOException e) {
            throw new IllegalArgumentException(String.format("Unexpected error while reading module jar: %s", e.getMessage()));
        }
    }

    public static MyClassLoader newInstance(JarFile libraryJar) {
        try {
            return new MyClassLoader(new URL(String.format("jar:file:%s!/", libraryJar.getName())));
        } catch(MalformedURLException e) {
            throw new IllegalArgumentException(String.format("Path to module jar could not be converted into proper URL: %s", e.getMessage()));
        }
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if(moduleClasses.contains(name)) {
            Class<?> clazz = findLoadedClass(name);
            if(clazz != null) {
                return clazz;
            } else {
                return findClass(name);
            }
        } else {
            return mainClassLoader.loadClass(name);
        }
    }
}