从实现通用接口的 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
。我还有另外两个名为 runner
和 module
的项目,它们都使用 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);
}
}
}
这是我一直在努力解决的 class 加载程序问题。我了解问题的根本原因(不同的 class 加载器),但我不确定解决问题的最佳方法。
我有一些通用接口的项目;我们称它为 api
。我还有另外两个名为 runner
和 module
的项目,它们都使用 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);
}
}
}