为什么在Java中这么难做到这一点?如果您希望拥有任何类型的模块系统,您需要能够动态加载JAR文件。有人告诉我,有一种方法可以通过编写自己的ClassLoader来实现,但这需要做很多工作,因为(至少在我看来)应该像调用以JAR文件作为参数的方法一样简单。

对实现这一功能的简单代码有什么建议吗?


当前回答

困难的原因在于安全。类加载器是不可变的;您不应该在运行时随意地向它添加类。我很惊讶它能与系统类加载器一起工作。下面是如何创建你自己的子类加载器:

URLClassLoader child = new URLClassLoader(
        new URL[] {myJar.toURI().toURL()},
        this.getClass().getClassLoader()
);
Class classToLoad = Class.forName("com.MyClass", true, child);
Method method = classToLoad.getDeclaredMethod("myMethod");
Object instance = classToLoad.newInstance();
Object result = method.invoke(instance);

很痛苦,但事实就是这样。

其他回答

如果你在Android上工作,下面的代码可以工作:

String jarFile = "path/to/jarfile.jar";
DexClassLoader classLoader = new DexClassLoader(jarFile, "/data/data/" + context.getPackageName() + "/", null, getClass().getClassLoader());
Class<?> myClass = classLoader.loadClass("MyClass");

虽然这里列出的大多数解决方案要么是hack(在JDK 9之前)难以配置(代理),要么就是不再工作(在JDK 9之后),但我发现没有人提到一个清晰的文档化方法,这真的很令人惊讶。

您可以创建一个自定义系统类装入器,然后您可以自由地做任何您想做的事情。不需要反射,所有类共享相同的类加载器。

当启动JVM时添加这个标志:

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

类加载器必须有一个接受类加载器的构造函数,必须将类加载器设置为父类加载器。构造函数将在JVM启动时调用,真正的系统类加载器将被传递,主类将由自定义加载器加载。

要添加jar,只需调用ClassLoader.getSystemClassLoader()并将其转换为您的类。

请查看此实现,以获得精心制作的类装入器。请注意,您可以将add()方法更改为public。

以防将来有人搜索这个,这种方法对我来说适用于OpenJDK 13.0.2。

我有许多需要在运行时动态实例化的类,每个类都可能具有不同的类路径。

在这段代码中,我已经有了一个名为pack的对象,它保存了一些关于我试图加载的类的元数据。getObjectFile()方法返回类的类文件的位置。getObjectRootPath()方法返回bin/目录的路径,其中包含我试图实例化的类的类文件。getLibPath()方法返回一个目录的路径,该目录包含构成该类所在模块类路径的jar文件。

File object = new File(pack.getObjectFile()).getAbsoluteFile();
Object packObject;
try {
    URLClassLoader classloader;

    List<URL> classpath = new ArrayList<>();
    classpath.add(new File(pack.getObjectRootPath()).toURI().toURL());
    for (File jar : FileUtils.listFiles(new File(pack.getLibPath()), new String[] {"jar"}, true)) {
        classpath.add(jar.toURI().toURL());
    }
    classloader = new URLClassLoader(classpath.toArray(new URL[] {}));

    Class<?> clazz = classloader.loadClass(object.getName());
    packObject = clazz.getDeclaredConstructor().newInstance();

} catch (Exception e) {
    e.printStackTrace();
    throw e;
}
return packObject;

我以前使用Maven依赖项:org.xeustechnologies:jcl-core:2.8来做这件事,但在JDK 1.8之后,它有时会冻结,并且永远不会返回在Reference::waitForReferencePendingList()中卡住“等待引用”。

我还保留了类加载器的映射,以便在我试图实例化的类与我已经实例化的类在同一个模块中时可以重用它们,我建议这样做。

对于jar文件的动态上传,您可以使用我对URLClassLoader的修改。这种修改对于在应用程序操作期间更改jar文件没有问题,就像标准的URLClassloader一样。所有加载的jar文件都加载到RAM中,因此独立于原始文件。

内存中的jar和JDBC类装入器

我需要在运行时为java 8和java 9+加载一个jar文件(上面的注释对这两个版本都不起作用)。下面是实现它的方法(如果相关的话,使用Spring Boot 1.5.2)。

public static synchronized void loadLibrary(java.io.File jar) {
    try {            
        java.net.URL url = jar.toURI().toURL();
        java.lang.reflect.Method method = java.net.URLClassLoader.class.getDeclaredMethod("addURL", new Class[]{java.net.URL.class});
        method.setAccessible(true); /*promote the method to public access*/
        method.invoke(Thread.currentThread().getContextClassLoader(), new Object[]{url});
    } catch (Exception ex) {
        throw new RuntimeException("Cannot load library from jar file '" + jar.getAbsolutePath() + "'. Reason: " + ex.getMessage());
    }
}