前几天遇到一个动态加载jar的问题, 有点绕, 还好解决了, 这里记录一下.
我遇到的问题是, 一个Swing工具中, 在外部类Outer中动态加载一个组件类Inner, 这个组件类Inner需要一个几百兆的jar包, 而外部类Outer其实是不需要的, 所以如果在启动时直接加载jar包会导致外部类启动速度很慢, 过了5秒左右界面才出来. 所以就在研究组件类采用延迟加载的方式, 在Outer执行完以后等Inner动态加载完成然后更新到Outer里去. 动态加载需要通过URLClassLoader实现.
遇到的两个问题是:
①在外部类启动时, 内部类已经一起加载到启动时的ClassLoader里去了, 而启动时ClassLoader是不认识我指定的子级URLClassLoader的, 所以对于Inner需要的类库还是会通过原ClassLoader去查找, 自然是找不到的.
②Inner组件类使用了一个外部类的JFrame对象, 所以不能采用父子分离的加载方式, 即Outer使用原ClassLoader, Inner使用一个新的URLClassLoader, 且URLClassLoader和原ClassLoader没有继承关系. 这样会导致出现两个JFrame class, 分属于两个ClassLoader, 在Outer中用反射去调用Inner中的方法时会出现找不到方法的异常, 因为参数类型的class不完全一样.
后来的解决方法是, 重写URLClassLoader的loadClass方法, 指定了 只在加载Inner类时不使用父ClassLoader中已有的类, 而是通过子URLClassLoader重新加载一个新的Inner类, 这样对于Inner类需要的各种类库, 都能在子URLClassLoader中找到. 而对于JFrame等共通类, 依然使用父ClassLoader加载的方式, 这样可以保持两个ClassLoader参数的一致性.
由于手上没有原始代码, 以下代码只是模拟我遇到的情况.
代码文件如下:
具体java代码如下:
LoadJars.java
package work.drqf;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
public class LoadJars {
public static URLClassLoader loadJars(String path) throws Exception {
Path p = Paths.get(path);
List<URL> urls = new ArrayList<>();
Files.walkFileTree(p, new FileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (file.toFile().getName().toLowerCase().endsWith(".jar")) {
urls.add(file.toUri().toURL());
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
});
// urls.forEach(System.out::println);
URLClassLoader loader = new URLClassLoader(urls.toArray(new URL[urls.size()]),
Thread.currentThread().getContextClassLoader()) {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 对于DynamicClass$InnerClass类, 通过子classLoader加载
if ("work.drqf.DynamicClass$InnerClass".equals(name)) {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
c = findClass(name);
}
return c;
}
} else {
// 其他类, 通过原classLoader加载
return super.loadClass(name);
}
}
};
return loader;
}
}
DynamicClass.java
package work.drqf;
import java.lang.reflect.Method;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Map;
import com.google.gson.Gson;
public class DynamicClass {
public static void main(String[] args) throws Exception {
Map<String, String> map = new HashMap<>();
map.put("aaa", "bbb");
URLClassLoader loader = LoadJars.loadJars("D:\\tmp\\Test\\loadJars");
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(loader);
Class<?> gsonClazz = loader.loadClass("com.google.gson.Gson");
System.out.println(gsonClazz);
Class<?> clazz = loader.loadClass("work.drqf.DynamicClass$InnerClass");
System.out.println(clazz.getClassLoader());
Method m = clazz.getDeclaredMethod("callGson", Map.class);
m.setAccessible(true);
m.invoke(null, map);
}
private static class InnerClass {
private static void callGson(Map<String, String> map) {
Gson gson = new Gson();
System.out.println(gson.toJson(map));
}
}
}
其中Inner类用到了Gson库.
导出项目为Test.jar
测试时文件如图:
run.bat代码如下:
java -cp .\Test.jar work.drqf.DynamicClass
pause
run.bat用于执行Test.jar, loadJars中是待加载的Gson库.
注意的是待加载lib中也要有一份Test.jar代码. 因为待加载的Inner类在其中. 外部的Test.jar中删掉Inner类也是可以的.
执行run.bat
成功在Inner中调用Gson, 即实现了动态加载lib.