【问题标题】:Java 9, compatability issue with ClassLoader.getSystemClassLoaderJava 9,与 ClassLoader.getSystemClassLoader 的兼容性问题
【发布时间】:2018-03-23 12:07:48
【问题描述】:

以下代码将 jar 文件添加到构建路径,它适用于 Java 8。但是,它会抛出 Java 9 异常,该异常与 URLClassLoader 的转换有关。有什么想法可以解决这个问题吗?最佳解决方案将对其进行编辑以与 Java 8 和 9 一起使用。

private static int AddtoBuildPath(File f) {
    try {
        URI u = f.toURI();
        URLClassLoader urlClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
        Class<URLClassLoader> urlClass = URLClassLoader.class;
        Method method = urlClass.getDeclaredMethod("addURL", URL.class);
        method.setAccessible(true);
        method.invoke(urlClassLoader, u.toURL());
    } catch (NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException | MalformedURLException | IllegalAccessException ex) {
        return 1;
    }

    return 0;
}

【问题讨论】:

  • 来自 JDK 9 发行说明:“应用程序类加载器不再是 java.net.URLClassLoader 的实例(以前版本中从未指定的实现细节)。假定 ClassLoader 的代码。 getSytemClassLoader() 返回一个需要更新的 URLClassLoader 对象。请注意,Java SE 和 JDK 不为应用程序或库提供 API 以在运行时动态增加类路径。”。所以我认为你应该解释你真正需要做什么,以便提出替代方案。
  • 你有一个接口和许多潜在的实现——这听起来像是服务和 ServiceLoader 的一个很好的候选者,不需要动态调整类路径来做到这一点。
  • @AlanBateman 但是如果我想让用户在运行时选择实现,那么我仍然必须使用 URLClassLoader 将其动态添加到类路径中,对吧?
  • 另外,我注意到使用 ServiceLoader 需要提供者添加具有特定配置的 Meta-INF,这在我的场景中是不可能的,因为我无法控制实现提供者。

标签: java classloader java-9 urlclassloader


【解决方案1】:

您遇到了the system class loader is no longer a URLClassLoader 的事实。正如ClassLoader::getSystemClassLoader 的返回类型所表明的那样,这是一个实现细节,尽管依赖于不可忽略的代码量。

从 cmets 来看,您正在寻找一种在运行时动态加载类的方法。如Alan Bateman points out,这在Java 9 中无法通过附加到类路径来完成。

您应该考虑为此创建一个新的类加载器。这还有一个额外的好处,即您将能够摆脱新类,因为它们没有加载到应用程序类加载器中。如果您正在针对 Java 9 进行编译,您应该阅读 layers - 它们为您提供了一个干净的抽象来加载一个全新的模块图。

【讨论】:

    【解决方案2】:

    不久前我偶然发现了这个问题。一样多,我使用了类似于问题中的方法

    private static int AddtoBuildPath(File f)
    

    在运行时动态添加路径到类路径。问题中的代码可能在多个方面都是不好的风格:1)假设ClassLoader.getSystemClassLoader()返回URLClassLoader是一个未记录的实现细节,2)使用反射使addURL公开可能是另一个。

    更简洁的动态添加类路径

    如果您需要使用额外的类路径 URL 通过“Class.forName”加载类,一个干净、优雅且兼容(Java 8 到 10)的解决方案如下:

    1) 通过扩展 URL 类加载器编写自己的类加载器,具有公共 addURL 方法

    public class MyClassloader extends URLClassLoader {
    
        public MyClassloader(URL[] urls, ClassLoader parent) {
            super(urls, parent);
        }
    
        public void addURL(URL url) {
            super.addURL(url);
        }
    }
    

    2) 声明类加载器的(单例/应用范围)对象

    private final MyClassloader classLoader;
    

    并通过

    实例化它
    classLoader = new MyClassloader(new URL[0], this.getClass().getClassLoader());
    

    注意:系统类加载器是父类。通过classLoader 加载的类知道可以通过this.getClass().getClassLoader() 加载的类,但反之则不然。

    3) 在需要时(动态)添加额外的类路径:

    File file = new File(path);
    if(file.exists()) {
        URL url = file.toURI().toURL();
        classLoader.addURL(url);
    }
    

    4) 通过您的单例类加载器实例化对象或您的应用程序

    cls = Class.forName(name, true, classLoader);
    

    注意:由于类加载器在加载类(以及父类到其父类)之前尝试委托给父类加载器,因此您必须确保要加载的类对父类加载器不可见,以确保它是通过给定的类加载器加载的。为了更清楚地说明这一点:如果您的系统类路径上有ClassPathB,然后将ClassPathB 和一些ClassPathA 添加到您的自定义classLoader,那么ClassPathB 下的类将通过系统类加载器和下的类加载ClassPathA 他们不知道。但是,如果您从系统类路径中删除ClassPathB,这些类将通过您自定义的classLoader 加载,然后ClassPathA 下的类将被ClassPathB 下的那些知道。

    5) 您可以考虑通过

    将您的类加载器传递给线程
    setContextClassLoader(classLoader)
    

    如果线程使用getContextClassLoader

    【讨论】:

    【解决方案3】:

    如果您只是想读取当前的类路径,例如因为您想启动另一个具有与当前类路径相同的类路径的 JVM,您可以执行以下操作:

    object ClassloaderHelper {
      def getURLs(classloader: ClassLoader) = {
        // jdk9+ need to use reflection
        val clazz = classloader.getClass
    
        val field = clazz.getDeclaredField("ucp")
        field.setAccessible(true)
        val value = field.get(classloader)
    
        value.asInstanceOf[URLClassPath].getURLs
      }
    }
    
    val classpath =
      (
        // jdk8
        // ClassLoader.getSystemClassLoader.asInstanceOf[URLClassLoader].getURLs ++
        // getClass.getClassLoader.asInstanceOf[URLClassLoader].getURLs
    
        // jdk9+
        ClassloaderHelper.getURLs(ClassLoader.getSystemClassLoader) ++
        ClassloaderHelper.getURLs(getClass.getClassLoader)
      )
    

    默认情况下,$AppClassLoader 类中的 final 字段无法通过反射访问,需要向 JVM 传递一个额外的标志:

    --add-opens java.base/jdk.internal.loader=ALL-UNNAMED
    

    【讨论】:

    • 这是 Scala 吗?我知道它适合回复,但考虑到这是一个 Java 问题,也许将其转换为 Java 语法?
    【解决方案4】:

    我得到了一个在 Java 8 中运行的 Spring Boot 应用程序。我的任务是将其升级到 Java 11 版本。

    面临的问题:

    原因:java.lang.ClassCastException:jdk.internal.loader.ClassLoaders$AppClassLoader (in module: java.base) 无法转换为 java.net.URLClassLoader (in module: java.base)

    使用方式:

    创建一个类:

    import java.net.URL;
    
    /**
     * This class has been created to make the code compatible after migration to Java 11
     * From the JDK 9 release notes: "The application class loader is no longer an instance of
     * java.net.URLClassLoader (an implementation detail that was never specified in previous releases).
     * Code that assumes that ClassLoader.getSytemClassLoader() returns a URLClassLoader object will
     * need to be updated. Note that Java SE and the JDK do not provide an API for applications or
     * libraries to dynamically augment the class path at run-time."
     */
    
    public class ClassLoaderConfig {
    
        private final MockClassLoader classLoader;
    
        ClassLoaderConfig() {
            this.classLoader = new MockClassLoader(new URL[0], this.getClass().getClassLoader());
        }
    
        public MockClassLoader getClassLoader() {
            return this.classLoader;
        }
    }
    

    创建另一个类:

    import java.net.URL;
    import java.net.URLClassLoader;
    
    public class MockClassLoader extends URLClassLoader {
    
        public MockClassLoader(URL[] urls, ClassLoader parent) {
            super(urls, parent);
        }
    
        public void addURL(URL url) {
            super.addURL(url);
        }
    }
    

    现在在你的主类的当前线程中设置它(就在你的应用程序的开头)

    Thread.currentThread().setContextClassLoader(new ClassLoaderConfig().getClassLoader());
    

    希望此解决方案对您有用!!!

    【讨论】:

    • MockClassLoader 解决了类转换异常,但未找到加载的 jar 中的类。 mockClassLoader.loadClass 可以工作,但是当类路径中的实用程序 jar 调用此运行时加载的 jar 类时,依赖项在运行时似乎不可用。有什么想法吗?
    【解决方案5】:

    Shadov 指向oracle community 处的线程。有正确答案:

    Class.forName("nameofclass", true, new URLClassLoader(urlarrayofextrajarsordirs));
    

    其中提到的注意事项也很重要:

    注意事项:

    java.util.ServiceLoader 使用线程的 ClassLoader 上下文 Thread.currentThread().setContextClassLoader(specialloader);

    java.sql.DriverManager 确实尊重调用类的 ClassLoader,而不是线程的 ClassLoader。使用 Class.forName("drivername", true, new URLClassLoader(urlarrayofextrajarsordirs).newInstance(); 直接创建驱动程序;

    javax.activation 使用线程的 ClassLoader 上下文(对 javax.mail 很重要)。

    【讨论】:

      【解决方案6】:

      参考 Edi 的解决方案,这对我有用:

      public final class IndependentClassLoader extends URLClassLoader {
      
          private static final ClassLoader INSTANCE = new IndependentClassLoader();
      
          /**
           * @return instance
           */
          public static ClassLoader getInstance() {
      
              return INSTANCE;
          }
      
          private IndependentClassLoader() {
      
              super(getAppClassLoaderUrls(), null);
          }
      
          private static URL[] getAppClassLoaderUrls() {
      
              return getURLs(IndependentClassLoader.class.getClassLoader());
          }
      
          private static URL[] getURLs(ClassLoader classLoader) {
      
              Class<?> clazz = classLoader.getClass();
      
              try {
                  Field field = null;
                  field = clazz.getDeclaredField("ucp");
                  field.setAccessible(true);
      
                  Object urlClassPath = field.get(classLoader);
      
                  Method method = urlClassPath.getClass().getDeclaredMethod("getURLs", new Class[] {});
                  method.setAccessible(true);
                  URL[] urls = (URL[]) method.invoke(urlClassPath, new Object[] {});
      
                  return urls;
      
              } catch (Exception e) {
                  throw new NestableRuntimeException(e);
              }
      
          }
      }
      

      在 Eclipse 中运行,您需要将 VM Arguments 设置为 JUnit Launch/Debug Configuration。 通过命令行使用 maven 运行,您有两个选择:

      选项 1
      将以下行添加到 pom.xml :

                  <plugin>
                      <groupId>org.apache.maven.plugins</groupId>
                      <artifactId>maven-surefire-plugin</artifactId>
                      <version>2.16</version>
                      <configuration>
                          <argLine>--add-opens java.base/jdk.internal.loader=ALL-UNNAMED</argLine>
                      </configuration>
                  </plugin>
      

      选项 2

      运行mvn test -DargLine="-Dsystem.test.property=--add-opens java.base/jdk.internal.loader=ALL-UNNAMED"

      【讨论】:

        【解决方案7】:

        我找到了这个,并为我工作。

        String pathSeparator = Syste .getProperty("path.separator"); String[] classPathEntries = System.getProperty("java.class.path") .split(pathSeparator);

        来自网站https://blog.codefx.org/java/java-11-migration-guide/#Casting-To-URL-Class-Loader

        【讨论】:

          【解决方案8】:

          还有这篇文章对我有帮助。 我找不到这篇文章,但是……在这里:https://github.com/CGJennings/jar-loader

          这是指南的一部分,里面有一个罐子,你可以阅读他的指南并进行设置。

          我自己试了一下,下载了包含类文件的jar文件

          import java.io.File;
          import java.io.FileNotFoundException;
          import java.io.IOException;
          import java.lang.instrument.Instrumentation;
          import java.lang.reflect.Method;
          import java.net.URL;
          import java.net.URLClassLoader;
          import java.util.jar.JarFile;
          
          public final class classname{
          
              public static void premain(String agentArgs, Instrumentation instrumentation) {
                  loadedViaPreMain = true;
                  agentmain(agentArgs,instrumentation);
              }
          
              public final static void addToClassPath(File jarfile)throws IOException{inst.appendToSystemClassLoaderSearch(new JarFile(jarfile));}
              public final static void agentmain(String agentArgs, Instrumentation instrumentation) {
                if (instrumentation == null){throw new NullPointerException("instrumentation");}
                if (inst == null) {inst = instrumentation;}
              }
              private static Instrumentation inst;
              private static boolean loadedViaPreMain = false;
          }
          

          我只是自己尝试将这些代码打包为一个包,然后使用 -javaagent:plugin......jar 选项启动应用程序类,然后调用此函数。它不会更改我的类路径。我可能丢失了这里有一些细节。

          希望你可以让它工作。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2019-03-01
            • 1970-01-01
            • 2018-10-15
            • 2014-08-22
            • 2016-08-15
            • 2011-05-04
            • 1970-01-01
            相关资源
            最近更新 更多