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实现双亲委派机制的:

不难看出首先调用了parentloadClass再去调用的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[]{};  
    }  
}

效果如图: