Java ClassLoader 03 -- URLClassLoader 动态加载


上一篇: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路径上的类。

猜你喜欢

转载自blog.csdn.net/fengxianaa/article/details/124450445