有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

java如何在JDK11+中运行时动态加载JAR文件?

我有一个应用程序,它使用以下解决方案在运行时动态加载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启动我的应用程序。系统班加载程序标志:

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

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

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

调用此方法时,我得到以下cast类异常:

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(build 11.0.10+9)上使用Spring Boot(2.4)


共 (2) 个答案

  1. # 1 楼答案

    更改正在运行的应用程序的ClassLoader是一个糟糕的主意。特别是当你试图改变系统时

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

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

    尽管有很多方法可以做到这一点,但这绝非小事

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

    它通过将所有JAR隔离到它们自己的ClassLoader中(简化一些事情),并计算每个包导出/导入哪些类,然后确保每个ClassLoader都可以在必要时从其他类中看到必要的类(这很难!)

    不过,通过为要在运行时加载的每组JAR创建^{}的新实例,您可以有一个简单的动态加载机制

    它的工作原理大致如下:

    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. # 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组件,我需要将其转换并添加到这个框架中。这些类是您在第二步中检索到的,scheme是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;
        }
    

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