我需要为应用程序的某些部分添加插件功能到现有应用程序。我希望能够在运行时添加一个jar,应用程序应该能够从jar加载一个类而无需重新启动应用程序。到现在为止还挺好。我使用URLClassLoader在线发现了一些样本,它工作正常。
我还希望能够在更新版本的jar可用时重新加载同一个类。我再次找到了一些示例和实现这一点的关键,据我所知,我需要为每个新加载使用一个新的类加载器实例。
我写了一些示例代码但是遇到了NullPointerException。首先让我告诉你们代码:
package test.misc;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import plugin.misc.IPlugin;
public class TestJarLoading {
public static void main(String[] args) {
IPlugin plugin = null;
while(true) {
try {
File file = new File("C:\\plugins\\test.jar");
String classToLoad = "jartest.TestPlugin";
URL jarUrl = new URL("jar", "","file:" + file.getAbsolutePath()+"!/");
URLClassLoader cl = new URLClassLoader(new URL[] {jarUrl}, TestJarLoading.class.getClassLoader());
Class loadedClass = cl.loadClass(classToLoad);
plugin = (IPlugin) loadedClass.newInstance();
plugin.doProc();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
IPlugin是一个简单的界面,只有一个方法doProc:
public interface IPlugin {
void doProc();
}
和jartest.TestPlugin是这个接口的一个实现,其中doProc只打印出一些语句。
现在,我将jartest.TestPlugin类打包到一个名为test.jar的jar中,并将其放在C:\ plugins下并运行此代码。第一次迭代运行顺利,类加载没有问题。
当程序执行sleep语句时,我将C:\ plugins \ test.jar替换为包含同一类的更新版本的新jar,并等待while的下一次迭代。现在这是我不明白的地方。有时更新的类会重新加载而不会出现问题,即下一次迭代运行正常。但有时,我看到一个异常抛出:
java.lang.NullPointerException
at java.io.FilterInputStream.close(FilterInputStream.java:155)
at sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream.close(JarURLConnection.java:90)
at sun.misc.Resource.getBytes(Resource.java:137)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:256)
at java.net.URLClassLoader.access$000(URLClassLoader.java:56)
at java.net.URLClassLoader$1.run(URLClassLoader.java:195)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
at java.lang.ClassLoader.loadClass(ClassLoader.java:307)
at java.lang.ClassLoader.loadClass(ClassLoader.java:252)
at test.misc.TestJarLoading.main(TestJarLoading.java:22)
我在网上搜索过并摸不着头脑,但是无法得出任何结论,为什么会抛出这种异常,而且有时也是如此 - 有时候,并非总是如此。
我需要你的经验和专业知识来理解这一点。这段代码出了什么问题?请帮忙!!
如果您需要更多信息,请与我们联系。谢谢你的期待!
答案 0 :(得分:14)
为了每个人的利益,让我总结一下真正的问题以及对我有用的解决方案。
正如Ryan指出的那样,JVM中有一个bug,它会影响Windows平台。 URLClassLoader
在打开jar文件以加载类之后不会关闭它们,从而有效地锁定jar文件。无法删除或替换jar文件。
解决方案很简单:在读取后打开jar文件。但是,要获得打开的jar文件的句柄,我们需要使用反射,因为我们需要遍历的属性不是公共的。所以我们沿着这条路走下去
URLClassLoader -> URLClassPath ucp -> ArrayList<Loader> loaders
JarLoader -> JarFile jar -> jar.close()
关闭打开的jar文件的代码可以添加到扩展URLClassLoader的类中的close()方法中:
public class MyURLClassLoader extends URLClassLoader {
public PluginClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
/**
* Closes all open jar files
*/
public void close() {
try {
Class clazz = java.net.URLClassLoader.class;
Field ucp = clazz.getDeclaredField("ucp");
ucp.setAccessible(true);
Object sunMiscURLClassPath = ucp.get(this);
Field loaders = sunMiscURLClassPath.getClass().getDeclaredField("loaders");
loaders.setAccessible(true);
Object collection = loaders.get(sunMiscURLClassPath);
for (Object sunMiscURLClassPathJarLoader : ((Collection) collection).toArray()) {
try {
Field loader = sunMiscURLClassPathJarLoader.getClass().getDeclaredField("jar");
loader.setAccessible(true);
Object jarFile = loader.get(sunMiscURLClassPathJarLoader);
((JarFile) jarFile).close();
} catch (Throwable t) {
// if we got this far, this is probably not a JAR loader so skip it
}
}
} catch (Throwable t) {
// probably not a SUN VM
}
return;
}
}
(此代码取自Ryan发布的第二个链接。此代码也发布在错误报告页面上。)
然而,有一个问题:为了使这个代码能够工作并且能够获得打开的jar文件的句柄来关闭它们,加载器用于通过URLClassLoader实现从文件加载类必须是JarLoader
。查看URLClassPath
的{{3}}(方法getLoader(URL url)
),我注意到只有在用于创建URL的文件字符串不以“/”结尾时才使用JARLoader。因此,URL必须如下定义:
URL jarUrl = new URL("file:" + file.getAbsolutePath());
整个类加载代码应如下所示:
void loadAndInstantiate() {
MyURLClassLoader cl = null;
try {
File file = new File("C:\\jars\\sample.jar");
String classToLoad = "com.abc.ClassToLoad";
URL jarUrl = new URL("file:" + file.getAbsolutePath());
cl = new MyURLClassLoader(new URL[] {jarUrl}, getClass().getClassLoader());
Class loadedClass = cl.loadClass(classToLoad);
Object o = loadedClass.getConstructor().newInstance();
} finally {
if(cl != null)
cl.close();
}
}
更新:JRE 7在类source code中引入了close()
方法,可能已解决此问题。我还没有验证过。
答案 1 :(得分:11)
答案 2 :(得分:2)
从Java 7开始,close()
中确实有一个URLClassLoader
方法,但如果直接或间接调用类型为ClassLoader#getResource(String)
的方法,则完全释放jar文件是不够的, ClassLoader#getResourceAsStream(String)
或ClassLoader#getResources(String)
。实际上,默认情况下,JarFile
实例会自动存储到JarFileFactory
的缓存中,以防我们直接或间接调用以前的方法之一,即使我们调用java.net.URLClassLoader#close()
,这些实例也不会被释放
因此,即使使用Java 1.8.0_74,在这种特殊情况下仍然需要hack,这是我在这里https://github.com/essobedo/application-manager/blob/master/src/main/java/com/github/essobedo/appma/core/util/Classpath.java#L83使用的hack https://github.com/essobedo/application-manager/blob/master/src/main/java/com/github/essobedo/appma/core/DefaultApplicationManager.java#L388。即使有了这个黑客攻击,我还是必须明确地调用GC来完全释放jar文件,你可以在这里看到https://github.com/essobedo/application-manager/blob/master/src/main/java/com/github/essobedo/appma/core/DefaultApplicationManager.java#L419
答案 3 :(得分:1)
这是更新在 java 7上成功测试。现在class MyReloaderMain {
...
//assuming ___BASE_DIRECTORY__/lib for jar and ___BASE_DIRECTORY__/conf for configuration
String dirBase = ___BASE_DIRECTORY__;
File file = new File(dirBase, "lib");
String[] jars = file.list();
URL[] jarUrls = new URL[jars.length + 1];
int i = 0;
for (String jar : jars) {
File fileJar = new File(file, jar);
jarUrls[i++] = fileJar.toURI().toURL();
System.out.println(fileJar);
}
jarUrls[i] = new File(dirBase, "conf").toURI().toURL();
URLClassLoader classLoader = new URLClassLoader(jarUrls, MyReloaderMain.class.getClassLoader());
// this is required to load file (such as spring/context.xml) into the jar
Thread.currentThread().setContextClassLoader(classLoader);
Class classToLoad = Class.forName("my.app.Main", true, classLoader);
instance = classToLoad.newInstance();
Method method = classToLoad.getDeclaredMethod("start", args.getClass());
Object result = method.invoke(instance, args);
...
}
对我来说很好用
classLoader.close();
然后更新您的jar并致电
MyReloaderMain.class.getClassLoader()
然后您可以使用新版本重新启动应用程序。
不要将您的jar包含在“MyReloaderMain
”的基类加载器“MyReloaderMain
”中,换句话说,开发2个项目,其中2个jar用于“{{1}}”和另一个用于您的实际应用程序而不依赖于两者,或者您将无法理解我加载了什么。
答案 4 :(得分:0)
jdk1.8.0_2
上的Windows
5中仍然存在错误。虽然@Nicolas的答案很有帮助,但是当我在WildFly上运行它时,ClassNotFound
为sun.net.www.protocol.jar.JarFileFactory
,并且在调试某些盒子测试时有几个vm崩溃......
因此,我最终将处理加载和卸载的代码部分提取到外部jar。从主代码我只需用java -jar....
调用它,现在看起来都很好。
注意:当jvm退出时,Windows会释放已加载的jar文件上的锁定,这就是原因。
答案 5 :(得分:0)
URLClassLoader
有一个问题,那就是jar文件保持打开状态。URLClassLoader
的不同实例从一个jar文件加载的多个类,并且在运行时更改了jar文件,通常会收到以下错误:java.util.zip.ZipException: ZipFile invalid LOC header (bad signature)
。错误可能有所不同。close
上使用URLClassLoader
方法。但这实际上是导致整个应用程序重新启动的解决方案。更好的解决方案是修改URLClassLoader
,以便将jar文件的内容加载到RAM缓存中。这不再影响其他从同一jar文件读取数据的URLClassloader
。然后可以在应用程序运行时自由更改jar文件。例如,您可以为此目的对URLClassLoader
进行以下修改:in-memory URLClassLoader