【问题标题】:Instantiating classes from a jar that implement a common interface, and then assigning the instance to the interface causes ClassCastException从实现通用接口的 jar 中实例化类,然后将实例分配给接口会导致 ClassCastException
【发布时间】:2016-01-29 01:05:04
【问题描述】:

这是我正在努力解决的类加载器问题。我了解问题的根本原因(不同的类加载器),但我不确定修复它的最佳方法。

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

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

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

由于module.jar 真的只需要来自api 的类,我想我可以创建一个新的URLClassLoader(没有父级),它只有来自api.jar 的类,然后将其用作父类加载器当我加载模块时。这就是我开始遇到麻烦的地方:

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

我知道我最初的问题是由于类加载器在尝试加载它之前首先检查类是否已经加载,这意味着当它使用来自module.jar 的名称解决时,它正在链接不兼容的类版本。

有什么好的方法来处理这个问题?与其创建仅包含来自 api 的类的 URL 类加载器,不如创建我自己的实现,仅当请求的类是来自 api 的类时才委托给父级?

【问题讨论】:

    标签: java classpath classloader


    【解决方案1】:

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

    如果你做一个

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

    你会发现这打印出false

    您创建类加载器的方式:

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

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

    你可以试试:

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

    但是根据您的描述(它是一个包含其自身依赖项的“胖”jar)和网络类加载器的复杂性(孩子优先),这可能无法解决您的问题。

    在这种情况下,唯一的解决方案是让你的模块 jar “精简”,以确保你只用一个类加载器加载每个类一次。

    【讨论】:

      【解决方案2】:

      我忘了用我的解决方案更新这个问题。我能够通过创建一个扩展 URLClassLoader 的自定义类加载器来解决这个问题。这个类加载器没有父类。

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

      基本方法是这样的:

      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);
              }
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-10-04
        • 2014-03-12
        • 2017-08-21
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多