为什么用Java这么难? 如果要使用任何类型的模块系统,则需要能够动态加载jar。 有人告诉我有一种方法可以通过编写自己的ClassLoader来完成,但是对于(至少在我看来)应该像调用带有jar文件作为其参数的方法一样容易的事情来说,这是很多工作。
对执行此操作的简单代码有什么建议吗?
很难的原因是安全性。类加载器是不可变的。您不应在运行时随意向其添加类。实际上,我很惊讶能与系统类加载器一起使用。这是制作自己的子类加载器的方法:
1 2 3 4 5 6 7 8
| 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); |
很痛苦,但确实如此。
以下解决方案有些骇人,因为它使用反射来绕过封装,但是可以完美地工作:
1 2 3 4 5 6 7
| 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); |
您应该看一下OSGi,例如在Eclipse平台中实现。它确实做到了。您可以安装,卸载,启动和停止所谓的捆绑软件,这些捆绑软件实际上是JAR文件。但是它提供了更多功能,例如可以在运行时在JAR文件中动态发现的服务。
或参见Java模块系统的规范。
JCL类加载器框架如何?我不得不承认,我没有使用过,但是看起来很有希望。
用法示例:
1 2 3 4 5 6 7 8 9 10
| JarClassLoader jcl = new JarClassLoader();
jcl.add("myjar.jar"); // Load jar file
jcl.add(new URL("http://myserver.com/myjar.jar")); // Load jar from a URL
jcl.add(new FileInputStream("myotherjar.jar")); // Load jar file from stream
jcl.add("myclassfolder/"); // Load class folder
jcl.add("myjarlib/"); // Recursively load all jar files in the folder/sub-folder(s)
JclObjectFactory factory = JclObjectFactory.getInstance();
// Create object of loaded class
Object obj = factory.create(jcl,"mypackage.MyClass"); |
这是不推荐使用的版本。我修改了原始文件以删除不推荐使用的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| /**************************************************************************************************
* Copyright (c) 2004, Federal University of So Carlos *
* *
* All rights reserved. *
* *
* Redistribution and use in source and binary forms, with or without modification, are permitted *
* provided that the following conditions are met: *
* *
* * Redistributions of source code must retain the above copyright notice, this list of *
* conditions and the following disclaimer. *
* * Redistributions in binary form must reproduce the above copyright notice, this list of *
* * conditions and the following disclaimer in the documentation and/or other materials *
* * provided with the distribution. *
* * Neither the name of the Federal University of So Carlos nor the names of its *
* * contributors may be used to endorse or promote products derived from this software *
* * without specific prior written permission. *
* *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS *
*"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT *
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR *
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR *
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, *
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, *
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR *
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF *
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING *
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS *
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *
**************************************************************************************************/
/*
* Created on Oct 6, 2004
*/
package tools;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
/**
* Useful class for dynamically changing the classpath, adding classes during runtime.
*/
public class ClasspathHacker {
/**
* Parameters of the method to add an URL to the System classes.
*/
private static final Class< ? >[] parameters = new Class[]{URL.class};
/**
* Adds a file to the classpath.
* @param s a String pointing to the file
* @throws IOException
*/
public static void addFile(String s) throws IOException {
File f = new File(s);
addFile(f);
}
/**
* Adds a file to the classpath
* @param f the file to be added
* @throws IOException
*/
public static void addFile(File f) throws IOException {
addURL(f.toURI().toURL());
}
/**
* Adds the content pointed by the URL to the classpath.
* @param u the URL pointing to the content to be added
* @throws IOException
*/
public static void addURL(URL u) throws IOException {
URLClassLoader sysloader = (URLClassLoader)ClassLoader.getSystemClassLoader();
Class< ? > sysclass = URLClassLoader.class;
try {
Method method = sysclass.getDeclaredMethod("addURL",parameters);
method.setAccessible(true);
method.invoke(sysloader,new Object[]{ u });
} catch (Throwable t) {
t.printStackTrace();
throw new IOException("Error, could not add URL to system classloader");
}
}
public static void main(String args[]) throws IOException, SecurityException, ClassNotFoundException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException{
addFile("C:\\dynamicloading.jar");
Constructor< ? > cs = ClassLoader.getSystemClassLoader().loadClass("test.DymamicLoadingTest").getConstructor(String.class);
DymamicLoadingTest instance = (DymamicLoadingTest)cs.newInstance();
instance.test();
}
} |
在Java 9中,URLClassLoader的答案现在会出现如下错误:
1
| java.lang.ClassCastException: java.base/jdk.internal.loader.ClassLoaders$AppClassLoader cannot be cast to java.base/java.net.URLClassLoader |
这是因为使用的类加载器已更改。相反,要添加到系统类加载器,可以通过代理使用Instrumentation API。
创建一个代理类:
1 2 3 4 5 6 7 8 9 10 11
| package ClassPathAgent;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.util.jar.JarFile;
public class ClassPathAgent {
public static void agentmain(String args, Instrumentation instrumentation) throws IOException {
instrumentation.appendToSystemClassLoaderSearch(new JarFile(args));
}
} |
添加META-INF / MANIFEST.MF并将其放在具有代理类的JAR文件中:
1 2
| Manifest-Version: 1.0
Agent-Class: ClassPathAgent.ClassPathAgent |
运行代理:
这使用byte-buddy-agent库将代理添加到正在运行的JVM:
1 2 3 4 5 6 7 8 9 10 11
| import java.io.File;
import net.bytebuddy.agent.ByteBuddyAgent;
public class ClassPathUtil {
private static File AGENT_JAR = new File("/path/to/agent.jar");
public static void addJarToClassPath(File jarFile) {
ByteBuddyAgent.attach(AGENT_JAR, String.valueOf(ProcessHandle.current().pid()), jarFile.getPath());
}
} |
我发现的最好的是org.apache.xbean.classloader.JarFileClassLoader,它是XBean项目的一部分。
这是我过去使用的一种简短方法,可以从特定目录中的所有lib文件创建类加载器
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public void initialize(String libDir) throws Exception {
File dependencyDirectory = new File(libDir);
File[] files = dependencyDirectory.listFiles();
ArrayList<URL> urls = new ArrayList<URL>();
for (int i = 0; i < files.length; i++) {
if (files[i].getName().endsWith(".jar")) {
urls.add(files[i].toURL());
//urls.add(files[i].toURI().toURL());
}
}
classLoader = new JarFileClassLoader("Scheduler CL" + System.currentTimeMillis(),
urls.toArray(new URL[urls.size()]),
GFClassLoader.class.getClassLoader());
} |
然后使用类加载器,只需执行以下操作:
1
| classLoader.loadClass(name); |
如果您使用的是Android,则以下代码适用:
1 2 3
| String jarFile ="path/to/jarfile.jar";
DexClassLoader classLoader = new DexClassLoader(jarFile,"/data/data/" + context.getPackageName() +"/", null, getClass().getClassLoader());
Class< ? > myClass = classLoader.loadClass("MyClass"); |
这是Allain方法使其与Java的较新版本兼容的快速解决方法:
1 2 3 4 5 6 7 8 9 10 11
| ClassLoader classLoader = ClassLoader.getSystemClassLoader();
try {
Method method = classLoader.getClass().getDeclaredMethod("addURL", URL.class);
method.setAccessible(true);
method.invoke(classLoader, new File(jarPath).toURI().toURL());
} catch (NoSuchMethodException e) {
Method method = classLoader.getClass()
.getDeclaredMethod("appendToClassPathForInstrumentation", String.class);
method.setAccessible(true);
method.invoke(classLoader, jarPath);
} |
请注意,它依赖于特定JVM内部实现的知识,因此它不是理想的,也不是通用的解决方案。但是,如果您知道将要使用标准的OpenJDK或Oracle JVM,则这是一个快速简便的解决方法。在将来发布新的JVM版本时,它有时也可能会中断,因此您需要牢记这一点。
jodonnell提出的解决方案很好,但是应该有所增强。我使用这篇文章成功地开发了我的应用程序。
分配当前线程
首先,我们必须添加
1
| Thread.currentThread().setContextClassLoader(classLoader); |
否则您将无法将存储的资源(例如spring / context.xml)加载到jar中。
不包括
您的jar放入父类加载器,否则您将无法理解谁正在加载什么。
另请参见使用URLClassLoader重新加载jar时出现问题
但是,OSGi框架仍然是最好的方法。
使用Instrumentation的另一个有效解决方案对我有效。它具有修改类加载器搜索的优势,避免了相关类的类可见性问题:
创建一个代理类
对于此示例,它必须位于命令行调用的同一jar中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package agent;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.util.jar.JarFile;
public class Agent {
public static Instrumentation instrumentation;
public static void premain(String args, Instrumentation instrumentation) {
Agent.instrumentation = instrumentation;
}
public static void agentmain(String args, Instrumentation instrumentation) {
Agent.instrumentation = instrumentation;
}
public static void appendJarFile(JarFile file) throws IOException {
if (instrumentation != null) {
instrumentation.appendToSystemClassLoaderSearch(file);
}
}
} |
修改MANIFEST.MF
向代理添加引用:
1 2 3
| Launcher-Agent-Class: agent.Agent
Agent-Class: agent.Agent
Premain-Class: agent.Agent |
我实际上使用的是Netbeans,因此本文有助于您更改manifest.mf。
跑步
Launcher-Agent-Class仅在JDK 9+上受支持,并且负责加载代理,而无需在命令行上明确定义它:
在JDK 6+上工作的方式是定义-javaagent参数:
1
| java -javaagent:<your jar> -jar <your jar> |
在运行时添加新的Jar
然后,您可以根据需要使用以下命令添加jar:
1
| Agent.appendJarFile(new JarFile(<your file>)); |
我在文档上没有发现任何问题。
来自Allain的骇客解决方案的另一个版本,也适用于JDK 11:
1 2 3 4 5 6 7
| File file = ...
URL url = file.toURI().toURL();
URLClassLoader sysLoader = new URLClassLoader(new URL[0]);
Method sysMethod = URLClassLoader.class.getDeclaredMethod("addURL", new Class[]{URL.class});
sysMethod.setAccessible(true);
sysMethod.invoke(sysLoader, new Object[]{url}); |
在JDK 11上,它给出了一些弃用警告,但作为在JDK 11上使用Allain解决方案的人的临时解决方案。
这可能是一个较晚的响应,我可以使用DataMelt(http://jwork.org/dmelt)的jhplot.Web类来做到这一点(fastutil-8.2.2.jar的简单示例)
1 2
| import jhplot.Web;
Web.load("http://central.maven.org/maven2/it/unimi/dsi/fastutil/8.2.2/fastutil-8.2.2.jar"); // now you can start using this library |
根据文档,此文件将在" lib / user"中下载,然后动态加载,因此您可以在同一程序中立即使用此jar文件中的类开始。
请看一下我开始的这个项目:proxy-object lib
该库将从文件系统或任何其他位置加载jar。它将为jar专用一个类加载器,以确保没有库冲突。用户将能够从加载的jar创建任何对象,并在其上调用任何方法。
该库旨在从支持Java 7的代码库中加载用Java 8编译的jar。
创建对象:
1 2 3 4 5 6 7 8 9 10
| File libDir = new File("path/to/jar");
ProxyCallerInterface caller = ObjectBuilder.builder()
.setClassName("net.proxy.lib.test.LibClass")
.setArtifact(DirArtifact.builder()
.withClazz(ObjectBuilderTest.class)
.withVersionInfo(newVersionInfo(libDir))
.build())
.build();
String version = caller.call("getLibVersion").asString(); |
ObjectBuilder支持工厂方法,调用静态函数和回调接口实现。
我将在自述页面上发布更多示例。
我个人发现java.util.ServiceLoader做得很好。您可以在此处获得示例。