上一篇:https://blog.csdn.net/fengxianaa/article/details/124450147
1. URLClassLoader
URLClassLoader是ClassLoader的子类,也是ExtClassLoader和AppClassLoader的父类
它可以从指定的 jar 文件和目录中加载类和资源。也就是说,可以动态加载jar包中的类。
常用的构造方法:
- URLClassLoader(URL[] urls, ClassLoader parent):使用指定的父加载器创建对象,从指定的urls路径来查询、并加载类。(到实战,解析Excel的时候,什么场景下使用这个构造方法)
- URLClassLoader(URL[] urls):使用默认的父加载器(AppClassLoader)创建一个ClassLoader对象,从指定的urls路径来查询、并加载类。
2. 实战
新建一个普通的maven项目,
建立完成后,应该是:
建立两个model:parse、loader
在 parse 中新建一个类 ParseExcel
public class ParseExcel {
public void parse(){
System.out.println("执行解析Excel方法...........");
}
}
然后打成jar包:parse.jar。
在 loader 中,新建一个 Test 类,利用 URLClassLoader 运行这个jar包:
public static void main(String[] args) throws Exception {
// 创建一个URL数组
File file = new File("/Users/fengxiansheng/Downloads/parse.jar");
URL[] urls = new URL[]{file.toURI().toURL()};
//这时候 myClassLoader 的 parent 是 AppClassLoader
URLClassLoader myClassLoader = new URLClassLoader(urls);
Class<?> aClass = myClassLoader.loadClass("com.test.ParseExcel");
Object obj = aClass.newInstance();//利用反射创建对象
Method method = aClass.getMethod("parse");//获取parse方法
method.invoke(obj,null);
}
输出:
这样做的好处:
- 修改 parse 不会影响到 loader,只需要把 parse 这个模块打成jar,放在固定的地方
- 这样 loader 可以24小时不间断运行
注意:这时候 myClassLoader 的 parent 是 AppClassLoader,这点非常重要
3. 问题
有些时候,parse中可能引用一些第三方库,比如:jackson-core-2.11.0.jar
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.11.0</version>
</dependency>
并在代码中使用:
package com;
import com.fasterxml.jackson.core.JsonFactory;
public class ParseExcel {
public void parse(){
System.out.println("执行解析Excel方法........");
JsonFactory factory = new JsonFactory();
System.out.println("执行jsonFaction的getFormatGeneratorFeatures方法:"+factory.getFormatGeneratorFeatures());
}
}
这时候,在 loader 中运行这个jar:
public static void main(String[] args) throws Exception {
File file = new File("/Users/fengxiansheng/Downloads/parse.jar");
File file2 = new File("/Users/fengxiansheng/Downloads/jackson-core-2.11.0.jar");
URL[] urls = new URL[]{file.toURI().toURL(),file2.toURI().toURL()};
//这时候 myClassLoader 的 parent 是 AppClassLoader
URLClassLoader myClassLoader = new URLClassLoader(urls);
Class<?> aClass = myClassLoader.loadClass("com.test.ParseExcel");
Object obj = aClass.newInstance();
Method method = aClass.getMethod("parse");
method.invoke(obj,null);
}
这样运行完全没有问题。
但问题是,如果 loader 中也引用了jackson,不过版本是:2.5.4,这时候再次运行:
报错,找不到:getFormatGeneratorFeatures() 方法
原因,执行 :myClassLoader.loadClass("com.test.ParseExcel")
- myClassLoader 的 parent 是 AppClassLoader
- 根据坑爹模式,myClassLoader 加载 JsonFactory 之前,会先让 AppClassLoader 去加载
- 而 AppClassLoader 从自己的 classpath 找到了这个类,加载成功,不过版本是2.5.4
- 但是2.5.4的版本中并没有getFormatGeneratorFeatures方法,所以。。。。
解决:
修改myClassLoader的 parent 为ExtClassLoader,这就要用到 URLClassLoader 的另一个构造方法
加载过程:
- myClassLoader 的父亲是 ExtClassLoader,所以这时候 myClassLoader 跟 appClassLoader 是兄弟关系
- myClassLoader 在加载 ParseExcel 时,会加载 JsonFactory,同时遵循坑爹模式
- 所以 ExtClassLoader 会尝试加载 JsonFactory 这个类,不会加载成功,转而由 myClassLoader 自己去加载 JsonFactory,加载成功,版本是2.11
- 2.11 版本的 JsonFactory 有 getFormatGeneratorFeatures 方法,所以正常运行
4. 又一个问题
新建一个 loader-api 的model ,并创建一个接口:MyInterface
让 ParseExcel 实现 MyInterface 接口
public class ParseExcel implements MyInterface {
public void parse(){
System.out.println("执行解析Excel方法........");
JsonFactory factory = new JsonFactory();
System.out.println("执行jsonFaction的getFormatGeneratorFeatures方法:"+factory.getFormatGeneratorFeatures());
}
}
修改 main 方法,判断 ParseExcel 是否实现了MyInterface
public static void main(String[] args) throws Exception {
// 创建一个URL数组
File file = new File("/Users/fengxiansheng/Downloads/parse.jar");
File file2 = new File("/Users/fengxiansheng/Downloads/jackson-core-2.11.0.jar");
File file3 = new File("/Users/fengxiansheng/Downloads/loader-api.jar");
URL[] urls = new URL[]{file.toURI().toURL(),file2.toURI().toURL(),file3.toURI().toURL()};
URLClassLoader myClassLoader = new URLClassLoader(urls,Test.class.getClassLoader().getParent());
Class<?> aClass = myClassLoader.loadClass("com.test.ParseExcel");
//判断是否实现了MyInterface
if(!MyInterface.class.isAssignableFrom(aClass)){
System.out.println("该类没有实现 MyInterface 接口,不运行");
return;
}
Object obj = aClass.newInstance();
Method method = aClass.getMethod("parse");
method.invoke(obj,null);
}
输出:
为什么会有这样的输出呢?
在java 中,所有的 class 文件都会被加载到 jvm 中的元数据区,而且一个class的唯一标识:类加载器+类名
因为 myClassLoader 跟 AppClassLoader 是兄弟关系
- 执行:myClassLoader.loadClass("com.test.ParseExcel");
- myClassLoader 会加载 ParseExcel,而 ParseExcel 引用了 MyInterface
- 所以 myClassLoader 会把 MyInterface,加载到内存中
- 执行:MyInterface.class.isAssignableFrom(aClass)
- AppClassLoader 也会把 MyInterface,加载到内存中
所以此时,内存中有两个 MyInterface 的 class 对象
再看代码, ParseExcel 实现的是 myClassLoader.MyInterface 接口,所以。。。。。。
解决:把 myClassLoader 的 parent ,设置为 AppClassLoader,
但是会产生找不到:getFormatGeneratorFeatures() 方法
5. 自定义ClassLoader
这时候应该自定义类加载器,打破双亲委派模式
在 loader 中新增 CustomClassLoader
package com;
import java.net.URL;
import java.net.URLClassLoader;
/**
* 自定义类加载器,打破双亲委派模式
*/
public class CustomClassLoader extends URLClassLoader {
private URL[] urls;//扫描的jar包路径
public CustomClassLoader(URL[] urls) {
//1. 指定搜索路径和父加载器:AppClassLoader
super(urls, CustomClassLoader.class.getClassLoader());
this.urls = urls;
}
/**
* 重载loadClass方法
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (urls != null && urls.length>0) {
Class<?> c = null;
try {
//2. 先在自己的扫描路径中找class
c = findClass(name);//该方法存在于URLClassLoader中,如果加载不到指定类,会报ClassNotFoundException
}catch (ClassNotFoundException e){
//3. 找不到就让 parent 去加载
c = this.getParent().loadClass(name);
}
return c;
}
return super.loadClass(name);
}
}
坑爹模式是:自己加载在class之前,先让 parent 加载
我们自定义的类加载器,先在自己的扫描路径中找,找不到会报:ClassNotFoundException
catch到这个异常,然后让 parent 去找
修改代码:
public static void main(String[] args) throws Exception {
// 创建一个URL数组
File file = new File("/Users/fengxiansheng/Downloads/parse.jar");
File file2 = new File("/Users/fengxiansheng/Downloads/jackson-core-2.11.0.jar");
URL[] urls = new URL[]{file.toURI().toURL(),file2.toURI().toURL()};
CustomClassLoader myClassLoader = new CustomClassLoader(urls);
Class<?> aClass = myClassLoader.loadClass("com.test.ParseExcel");
//判断是否实现了MyInterface
if(!MyInterface.class.isAssignableFrom(aClass)){
System.out.println("该类没有实现 MyInterface 接口,不运行");
return;
}
Object obj = aClass.newInstance();
Method method = aClass.getMethod("parse");
method.invoke(obj,null);
}
输出:
加载过程:
customClassLoader 父亲是 AppClassLoader
- 执行:myClassLoader.loadClass("com.test.ParseExcel");
- customClassLoader 在加载 ParseExcel 时,会加载 MyInterface,并拒绝遵循坑爹模式
- 先在自己的搜索路径中查找MyInterface,找不到(因为已经去掉了)
- 所以让它的 parent,也就是 AppClassLoader 去加载,加载成功
- 执行:MyInterface.class.isAssignableFrom(aClass)
- AppClassLoader尝试去加载MyInterFace,因为已经加载成功,所以不需要重复加载
- 所以 MyInterFace.class.isAssignableFrom(aClass) 返回 true
5. 总结
类加载器可以做什么
- 插件功能
- 热加载
- 解决类冲突,一个应用可以使用多个版本的依赖
- 隔离,同级别中不同的类加载器之间隔离,比如Tomcat容器,每个WebApp有自己的ClassLoader,加载每个WebApp的ClassPath路径上的类。