前言
最近做项目,有需求需要运行过程动态加载jar,然后执行后加载别的jar,来达到实现不同的动态能力,下面实现了2种方式,各有各的优势。
1. bean

随手写了一个bean,打个jar,扔在了resources下

2. 新建classloader方式
public class LoadMain {
public static void main(String[] args) {
loadByNewClassLoader();
}
public static void loadByNewClassLoader() {
try {
File file = new File("/Users/huahua/IdeaProjects/classloader-demo/classloader-load/src/main/resources/classloader-common-1.0-SNAPSHOT.jar");
URL[] urls = new URL[]{file.toURI().toURL()};
URLClassLoader urlClassLoader = new URLClassLoader(urls);
Class<?> c = urlClassLoader.loadClass("com.feng.classloader.common.Cat");
Object o = c.newInstance();
Method setName = c.getDeclaredMethod("setName", new Class[]{String.class});
Method getName = c.getDeclaredMethod("getName", new Class[]{});
setName.invoke(o, "Tom");
System.out.println(getName.invoke(o));
} catch (Exception e) {
e.printStackTrace();
}
}
}
这种方式classloader

我们自定义了classloader,装载,卸载很方便,直接操作class loader即可,缺陷是
URLClassLoader加载的class,只能当前classloader能访问;由于双亲委派,APPClassLoader加载的是可以直接访问的。
2.1 双亲委派原理
核心方法urlClassLoader.loadClass("com.feng.collections.Cat"),实际上是调用父类ClassLoader类的loadClass方法
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//加锁
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//检查是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//有父加载器,优先父加载器加载
c = parent.loadClass(name, false);
} else {
//Bootstrap去找
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//自己去找了
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//解析class
resolveClass(c);
}
return c;
}
}
优先父加载器加载,比如URLClassLoader就是默认使用父加载器加载的。只是这里我们加载的是jar,没有依赖;所以只能URLClassLoader自己去加载,也只能在URLClassLoader中访问。
parent来源getSystemClassLoader()
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
assertionLock = this;
}
}
看看我们加载的类

2.2 覆盖loadClass方法
打破双亲委派机制
public class LoadMain {
public static void main(String[] args) {
loadByNewClassLoader();
}
public static void loadByNewClassLoader() {
try {
File file = new File("/Users/huahua/IdeaProjects/classloader-demo/classloader-load/src/main/resources/classloader-common-1.0-SNAPSHOT.jar");
URL[] urls = new URL[]{file.toURI().toURL()};
URLClassLoader urlClassLoader = new DefURLClassLoader(urls);
Class<?> c = urlClassLoader.loadClass("com.feng.classloader.common.Cat");
Object o = c.newInstance();
Method setName = c.getDeclaredMethod("setName", new Class[]{String.class});
Method getName = c.getDeclaredMethod("getName", new Class[]{});
setName.invoke(o, "Tom");
System.out.println(getName.invoke(o));
} catch (Exception e) {
e.printStackTrace();
}
}
private static class DefURLClassLoader extends URLClassLoader{
public DefURLClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public DefURLClassLoader(URL[] urls) {
super(urls);
}
public DefURLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
super(urls, parent, factory);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//加锁
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//检查是否已加载
Class<?> c = findLoadedClass(name);
try {
if (this.getParent() != null && name.startsWith("java.")) {
c = super.loadClass(name, false);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
//自己去找
c = findClass(name);
}
if (resolve) {
//解析class
resolveClass(c);
}
return c;
}
}
}
}
可以看见,我们自己实现的类不让父加载器优先加载了,直接自己干活,这就打破了双亲委派
测试ok

这就会造成一个严重的问题,由于我们没有按照JDK官方的加载顺序,很容易出现同一个类的对象不能转换的问题
2.3 不能转换异常
在load的module依赖common的module

随意写一个类
public class Dog {
public String getCatName(Cat cat){
return cat.getName();
}
}
写个main方法,就在上面的改一改
public static void loadByNewClassLoader() {
try {
File file = new File("/Users/huahua/IdeaProjects/classloader-demo/classloader-load/src/main/resources/classloader-common-1.0-SNAPSHOT.jar");
URL[] urls = new URL[]{file.toURI().toURL()};
URLClassLoader urlClassLoader = new DefURLClassLoader(urls);
Class<?> c = urlClassLoader.loadClass("com.feng.classloader.common.Cat");
Object o = c.newInstance();
Method setName = c.getDeclaredMethod("setName", new Class[]{String.class});
Method getName = c.getDeclaredMethod("getName", new Class[]{});
setName.invoke(o, "Tom");
System.out.println(getName.invoke(o));
//就加这一句代码
System.out.println(new Dog().getCatName((Cat) o));
} catch (Exception e) {
e.printStackTrace();
}
}
报错了:是不是很有意思,相同的类居然不能转换

这是因为,这个对象是我们自定义的classloader加载的,且打破双亲委派机制, Class是由我们的classloader自己干活加载的,而当前线程的Class是APPClassLoader加载器加载的,所以不能转换。
如果不覆写loadClass,双亲委派生效,则符合JDK的标准,代码运行正常,因为是父加载器去加载的。

直接看也是这样

3. 使用当前的classloader加载
public static void loadByCurrentClassLoader(){
Method method = null;
try {
File file = new File("/Users/huahua/IdeaProjects/classloader-demo/classloader-load/src/main/resources/classloader-common-1.0-SNAPSHOT.jar");
URL[] urls = new URL[]{file.toURI().toURL()};
//use current classloader load jar
URLClassLoader urlClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
//set method reach
method.setAccessible(true);
method.invoke(urlClassLoader, urls);
Class<?> c = Class.forName("com.feng.classloader.common.Cat", false, urlClassLoader);
Object o = c.newInstance();
Method setName = c.getDeclaredMethod("setName", new Class[]{String.class});
Method getName = c.getDeclaredMethod("getName", new Class[]{});
setName.invoke(o, "Tom");
System.out.println(getName.invoke(o));
} catch (Exception e) {
e.printStackTrace();
} finally {
if (method != null) {
method.setAccessible(false);
}
}
}
这里不能直接调用addURL方法,因为是protected修饰,但反射可以

运行后结果

使用系统的classloader,加载jar,当前classloader均能访问,缺陷很明显,卸载这些class loader加载的class异常麻烦。在ASM或者JDK的动态代理就是使用这种方式生成字节码加载class的。
总结
各有各的好处吧,通过新创建classloader适合热加载,加载卸载方便;通过当前classloader加载方便业务的调用,常在AOP中使用。
本文深入探讨了Java中动态加载类的两种方法:通过自定义ClassLoader和使用当前ClassLoader。详细讲解了自定义ClassLoader如何打破双亲委派机制,并讨论了由此可能引发的类转换异常。同时,对比了两种方法的优缺点,为读者提供了实际项目中动态加载类的选择依据。
3455

被折叠的 条评论
为什么被折叠?



