【问题标题】:How to load JAR files dynamically at Runtime in JDK11+?如何在 JDK11+ 的运行时动态加载 JAR 文件?
【发布时间】:2022-01-01 02:57:38
【问题描述】:

我有一个使用以下解决方案在运行时动态加载 jar 文件的应用程序:

File file = ...
URL url = file.toURI().toURL();

URLClassLoader classLoader = (URLClassLoader)ClassLoader.getSystemClassLoader();
Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
method.setAccessible(true);
method.invoke(classLoader, url);

这是使用How to load JAR files dynamically at Runtime?的答案完成的

我现在想要一个适用于 JDK11+ 的解决方案,它与我使用的原始解决方案等效。因此,无需第三方库/框架或加载/调用单个类即可以编程方式进行。

我尝试了以下方法:

  1. 创建了一个扩展 UrlClassLoader 的 DynamicClassLoader:
public final class DynamicClassLoader extends URLClassLoader {


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

    public DynamicClassLoader(String name, ClassLoader parent) {
        super(name, new URL[0], parent);
    }
    
    public DynamicClassLoader(ClassLoader parent) {
        this("classpath", parent);
    }

    public void addURL(URL url) {
        super.addURL(url);
    }
    
}
  1. 然后我用 java.system.class.loader 标志启动我的应用程序:

java -Djava.system.class.loader=com.example.DynamicClassLoader

然后我有一个 jar 文件作为路径对象,我使用以下方法调用它:

public void loadJar(Path path) throws Exception {
        
        URL url = path.toUri().toURL();
        
        DynamicClassLoader classLoader = (DynamicClassLoader)ClassLoader.getSystemClassLoader();
        Method method = DynamicClassLoader.class.getDeclaredMethod("addURL", URL.class);
        method.setAccessible(true);
        method.invoke(classLoader, url);        
    }

调用此方法时,我得到以下强制转换类异常:

class jdk.internal.loader.ClassLoaders$AppClassLoader cannot be cast to class com.example.classloader.DynamicClassLoader (jdk.internal.loader.ClassLoaders$AppClassLoader is
 in module java.base of loader 'bootstrap'; com.example.classloader.DynamicClassLoader is in unnamed module of loader 'app')

我在 OpenJDK11(内部版本 11.0.10+9)上使用 Spring Boot (2.4)。

【问题讨论】:

  • 您在启动时是否收到有关自定义类加载器的任何输出?
  • 不特别。唯一记录的是这样的参数:arg=-Dfile.encoding=UTF-8 arg=-Djava.system.class.loader=DynamicClassLoader,但没有进一步的记录。在 dev 上有一条消息“org.springframework.boot.devtools.restart.classloader.RestartClassLoader - Created Restart ClassLoader”,但这不是在 prod 上(并且对工作/错误没有影响)。
  • 我刚试过,这个系统属性仍然有效。您可以添加日志语句以查看您的类是否被加载+初始化,以及构造函数是否被调用等。但是您为什么要使用反射来调用您自己的public 方法DynamicClassLoader .addURL?并且真的有必要将jar添加到系统类加载器中,你不能通过新的URLClassLoader加载类吗?
  • 当然,要使java.system.class.loader 工作,指定的类必须能够被标准类加载器加载。您没有提到您的类加载器是以非标准方式存储的,但是这应该如何工作呢?除此之外,你没有回答这个问题。是的,使用URLClassLoader 是一种常见的方式。如果你以官方的方式使用它,通过创建一个新的URLClassLoader,你不需要反射,它仍然可以工作。所以,还有一个悬而未决的问题是,你是否真的需要将jar添加到系统类加载器,而不能使用官方的方式。

标签: java jar classloader java-11


【解决方案1】:

更改正在运行的应用程序的ClassLoader 是一个糟糕的主意。根据您的尝试,专门更改系统ClassLoader

JVM 在执行时延迟加载类,然后在第一次加载时缓存它们。干扰该过程可能会对您的应用程序稳定性造成严重问题。

使用反射访问URLClassLoader 的内部方法,正如您所做的那样,只会导致您的应用程序绑定到您的代码恰好可以工作的特定 JDK 版本,但不能保证在您升级到时继续工作较新的 JDK 版本,甚至是来自不同供应商的 JDK。

虽然有一些方法可以做到这一点,但它远非微不足道。

例如,OSGi 是一个将这个特性添加到 JVM 的系统,但是您必须更改您的应用程序以适应 OSGi 模型,这不是很容易。它的主要吸引力之一就是它允许您安装包(带有 OSGi 元数据的 jar)、删除它们,甚至升级它们,而无需重新启动应用程序。

它通过将所有 jar 隔离到它们自己的 ClassLoaders 中(为了简化一点)并计算每个包导出/导入哪些类,然后确保每个 ClassLoader 可以从其他必要的(这很难!)。

不过,您可以拥有一个简单的动态加载机制,方法是为要在运行时加载的每组 jar 创建 URLClassLoader 的新实例。

大概是这样的:

URL[] jars = { ... };

// you can pass a parent loader to the constructor...
// it will delegate first, so already loaded classes are not tried to reload
var loader = new URLClassLoader(jars);

// once you have a ClassLoader, you may use it to load classes provided
// by the new jars
Class<?> type = loader.loadClass("DynamicClass");

// do something with this class?
// But you must cast it to some type that's known in the current class-path,
// otherwise this class is only usable by reflection!

// For example, if you know this class must implement Runnable and it has
// a default constructor, you may be able to do this:
var runnable = (Runnable) type.newInstance();

// as we now know the type, we can actually use it type-safely
runnable.run();

【讨论】:

    【解决方案2】:

    根据评论部分的讨论,我找到了解决方案。这可能不像我希望的那样通用,并假设您知道要使用的类(相反,只需将 Maven 依赖项添加到您的 pom.xml 或旧的 JDK8 解决方案中)。

    1. 下载 Maven 依赖项(使用 Jeka
    
    List<Path> paths = resolveDependency(groupId, artifactId, version);
    
    public List<Path> resolveDependency(String groupId, String artifactId, String version) throws Exception {
            
            String dependency = groupId + ":" + artifactId + ":" + version;
            
            JkDependencySet deps = JkDependencySet.of()
                    .and(dependency)
                    .withDefaultScopes(COMPILE_AND_RUNTIME);
    
            JkDependencyResolver resolver = JkDependencyResolver.of(JkRepo.ofMavenCentral());
            List<Path> paths = resolver.resolve(deps, RUNTIME).getFiles().getEntries();
    
            return paths;
    
        }
    
    1. 加载 jar 文件
    
    List<Class> classes = loadDependency(paths);
    
    public List<Class> loadDependency(List<Path> paths) throws Exception {
    
            List<Class> classes = new ArrayList<>();
    
            for(Path path: paths){
    
                URL url = path.toUri().toURL();
                URLClassLoader child = new URLClassLoader(new URL[] {url}, this.getClass().getClassLoader());
    
                ArrayList<String> classNames = getClassNamesFromJar(path.toString());
    
                for (String className : classNames) {
                    Class classToLoad = Class.forName(className, true, child);
                    classes.add(classToLoad);
                }
            }
    
            return classes;
    
        }
    
    
        // Returns an arraylist of class names in a JarInputStream
        private ArrayList<String> getClassNamesFromJar(JarInputStream jarFile) throws Exception {
            ArrayList<String> classNames = new ArrayList<>();
            try {
                //JarInputStream jarFile = new JarInputStream(jarFileStream);
                JarEntry jar;
    
                //Iterate through the contents of the jar file
                while (true) {
                    jar = jarFile.getNextJarEntry();
                    if (jar == null) {
                        break;
                    }
                    //Pick file that has the extension of .class
                    if ((jar.getName().endsWith(".class"))) {
                        String className = jar.getName().replaceAll("/", "\\.");
                        String myClass = className.substring(0, className.lastIndexOf('.'));
                        classNames.add(myClass);
                    }
                }
            } catch (Exception e) {
                throw new Exception("Error while getting class names from jar", e);
            }
            return classNames;
        }
    
    
    // Returns an arraylist of class names in a JarInputStream
    // Calls the above function by converting the jar path to a stream
    private ArrayList<String> getClassNamesFromJar(String jarPath) throws Exception {
            return getClassNamesFromJar(new JarInputStream(new FileInputStream(jarPath)));
        }
    
    
    1. 使用类

    就像 Renato 指出的那样,知道你需要知道类才能使用它。在我的例子中,它是一个 Camel 组件,我需要将其转换并添加到这个框架中。这些类是您在第二步中检索到的内容,方案是component 的名称。

    
    Component camelComponent = getComponent(classes, scheme);
    context.addComponent(scheme, camelComponent);
    
    public Component getComponent(List<Class> classes, String scheme) throws Exception {
    
            Component component = null;
            for(Class classToLoad: classes){
                String className = classToLoad.getName().toLowerCase();
                if(className.endsWith(scheme + "component")){
                    Object object =  classToLoad.newInstance();
                    component = (Component) object;
                }
            }
    
            return component;
        }
    

    因此,第二部分是使其动态可用。添加了第一部分和第三部分以提供完整解决方案的示例。

    【讨论】:

    • URLClassLoader 应该接收 JAR 的 URL,而不是像您在这里所做的那样的单个类。应该只创建一个类加载器实例来从一个 jar(或多个)加载类。我在您的 GitHub 存储库中对此发表了评论。
    猜你喜欢
    • 1970-01-01
    • 2010-09-16
    • 2014-07-30
    • 2011-11-26
    • 2015-09-05
    • 2012-05-01
    • 2012-06-16
    • 2012-10-20
    • 1970-01-01
    相关资源
    最近更新 更多