ClassLoader
关于动态加载Jar并卸载
URLClassLoader本身具有动态加载的特性,可以自动按需加载Class,但是当执行URLClassLoader.close()
方法后,则不再具有该特性,因此最好可以一次性地将所有的Class全部加载到内存后,而后使用URLClassLoader.close()
方法释放资源。
代码如下:
String path = "C:\\xxx.jar";
JarFile jarFile = new JarFile(path);
URL url = new URL("file:" + path);
URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { url }, Thread.currentThread().getContextClassLoader());
Enumeration<JarEntry> entryEnumeration = jarFile.entries();
while(entryEnumeration.hasMoreElements()) {
JarEntry entry = entryEnumeration.nextElement();
if (entry.getName().endsWith(".class")) {
String className = entry.getName().replace('/', '.').replace(".class", "");
Class<?> clazz = urlClassLoader.loadClass(className);
}
}
jarFile.close();
urlClassLoader.close();
这样就将所有的Class都加载完成并且释放了资源。
如果需要同时读取资源文件,则仍然可以使用URLClassLoader,代码如下:
URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { url }, Thread.currentThread().getContextClassLoader());
InputStream propertiesStream = urlClassLoader.getResourceAsStream("config.properties");
Properties properties = new Properties();
properties.load(propertiesStream);
String value = properties.get("key");
注意资源文件仍然需要带有properties的尾缀
自定义ClassLoader隔离加载
有了上面的基础,就可以通过自己实现ClassLoader
来实现隔离加载了。
所谓隔离加载,即可以使用同类名、不同字节码加载,并且可在JVM中正常运行。
双亲委派机制
要实现隔离加载,就需要破坏类加载器的双亲委派机制,而ClassLoader
中是通过loadClass
实现双亲委派机制的:
不难看出首先调用了parent
的loadClass
再去调用的findClass
,因此如果需要打破双亲委派机制,只需要将loadClass
进行覆盖即可。
覆盖loadClass
但是覆盖loadClass
还有一个很严重的问题,双亲委派机制可以使得类不被重复加载,并且核心类不被改写,这里覆盖后破坏了双亲委派机制,也会导致核心类加载失败,因为核心类来自Java,因此在写loadClass
时务必考虑到此点。
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
byte[] classBytes = this.getClassBytes(name);
if (classBytes != null && classBytes.length != 0) {
return this.defineClass(classBytes, 0, classBytes.length);
} else {
try {
return this.defaultClassLoader.loadClass(name);
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
}
private byte[] getClassBytes(String name) {
for (JarFile jar : this.jars) {
JarEntry entry = jar.getJarEntry(name.replaceAll("\\.", "/") + ".class");
if (entry != null) {
try {
InputStream inputStream = jar.getInputStream(entry);
return inputStream.readAllBytes();
} catch (IOException e) {
return new byte[] {};
}
}
}
return new byte[]{};
}
对上面代码做出一部分解释,getClassBytes
用于从Jar文件中读取出对应的字节码文件;而loadClass
则是对于需要加载的类,首先先尝试在Jar中寻找是否有对应的字节码文件,若没有,则通过默认的类加载器进行加载,保证核心类可加载(但如果在Jar中也存在核心类的字节码文件,核心类则是被修改的)。
完整ClassLoader
package top.evalexp.tools.common.classloader;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
public class PluginClassLoader extends ClassLoader {
private ClassLoader defaultClassLoader;
private List<JarFile> jars = new ArrayList<>();
private void construct(File[] jars) throws IOException {
for (File jar : jars) {
if (!jar.exists()) throw new FileNotFoundException();
this.jars.add(new JarFile(jar));
}
this.defaultClassLoader = Thread.currentThread().getContextClassLoader();
}
public PluginClassLoader(File[] jars) throws IOException {
this.construct(jars);
}
public PluginClassLoader(File jar) throws IOException {
this.construct(new File[] {jar});
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
byte[] classBytes = this.getClassBytes(name);
if (classBytes != null && classBytes.length != 0) {
return this.defineClass(classBytes, 0, classBytes.length);
} else {
try {
return this.defaultClassLoader.loadClass(name);
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
}
private byte[] getClassBytes(String name) {
for (JarFile jar : this.jars) {
JarEntry entry = jar.getJarEntry(name.replaceAll("\\.", "/") + ".class");
if (entry != null) {
try {
InputStream inputStream = jar.getInputStream(entry);
return inputStream.readAllBytes();
} catch (IOException e) {
return new byte[] {};
}
}
}
return new byte[]{};
}
}
效果如图: