ClassLoader在项目中的应用

本文介绍了JavaClassLoader在项目中的应用,特别是如何处理用户jar包的反序列化,以及为何需要反双亲委派机制以避免类冲突。还探讨了如何自定义ClassLoader,如使用URLClassLoader和AppClassLoader,并揭示了Tomcat类加载机制中对双亲委派的突破,以及JSP热加载的实现原理。
摘要由CSDN通过智能技术生成

ClassLoader在项目中的应用

项目背景

数据资产需要引入kafka数据源,将kafka的topic当成一张表进行资产权限管理,同时给下游的实时flinksql使用。这里当用户的topic中的数据是以DTO的方式进行反序列化时,需要用户传一个jar包将数据传给服务器,里面包含了反序列化的DTO类。

目标:

加载用户的Jar包,可以使用原生KafkaClient进行反序列化,且Jar不会发生冲突。

为了实现目标,我们明显需要使用Java的ClassLoader,同时这个ClassLoader还需要实现反双亲委派。

ClassLoader双亲委派的作用

在这里插入图片描述

双亲委派相信大家都知道是干啥的,简而言之就是类加载器在加载类的时候会把类先丢给父类去加载,只有父类加载不了的时候才会由子类加载。

应用举例:maven依赖中发生了依赖冲突,实际上只会有一个版本的包会被加载。
在这里插入图片描述

优势:

1、避免类的重复加载

2、保护程序的安全,放置核心API被随意篡改

为什么要反双亲委派

反双亲委派,就是先加载自己的类,自己加载不了再从父类加载

在这里插入图片描述

举例1:用户传的Jar包中需要使用的日志的实现类版本为V3.0,我们自己的系统版本为V1.0,按照双亲委派的原则,V1.0的日志实现类肯定已经在我们的系统中先加载了,这个时候用户传的Jar包使用就可能会出问题(明明需要使用V3.0的,却只能使用V1.0的日志)

举例2:我们系统要兼容用户使用2.X和1.X版本的KafkaClient(或者其他的Client),这两种Client的包名啥的全都一样,按照双亲委派的规范肯定只能加载一个。

自定义ClassLoader的使用

理解ClassLoader中的方法

要理解ClassLoader如何实现双亲委派的,我们实际上只需要了解类中四个方法即可

1.findClass(String name)

查找并加载具有指定名称的类

2.loadClass(String name)

加载Class,它和findClass都可以根据全类名加载类,有什么区别呢?

区别就是,loadClass是实际上保证双亲委派规则的类,正常情况下,所有继承了ClassLoader的类都不会重写这个方法(有一些会增加一些安全校验),当需要反双亲委派时才需要重写。loadClass中会调用findClass,如果不同的自定义类加载器需要不同的加载方式时,重写findClass即可。

3.findLoadedClass(String name)

调用这个方法,查看这个Class是否已经别加载

4.definclass(String name, byte[] b, int off, int len)

把字节码转化为Class

(看代码,先看ClassLoader的loadClass方法,再看我自定义的JarByteLoader,讲述一个类加载的全部过程)

通过URLClassLoader进行自定义

在java.net包中,JDK提供了一个更加易用的类加载器URLClassLoader,它扩展了ClassLoader,能够从本地或者网络上指定的位置加载类。我们可以使用该类作为自定义的类加载器使用。

构造方法:

public URLClassLoader(URL[] urls):指定要加载的类所在的URL地址,父类加载器默认为系统类加载器。

public URLClassLoader(URL[] urls, ClassLoader parent):指定要加载的类所在的URL地址,并指定父类加载器。

案例1:加载磁盘上的类

public static void main(String[] args) throws Exception{
		File file = new File("d:/");
		URI uri = file.toURI();
		URL url = uri.toURL();
        URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
        System.out.println(classLoader.getParent());
        Class aClass = classLoader.loadClass("com.leon.Demo");
        Object obj = aClass.newInstance();
    }

案例2:加载网络上的类

public static void main(String[] args) throws Exception{
		URL url = new URL("http://localhost:8080/examples/");
        URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
        System.out.println(classLoader.getParent());
        Class aClass = classLoader.loadClass("com.leon.Demo");
        aClass.newInstance();
}

案例3:快速实现反双亲委派

public class MyClassLoader extends URLClassLoader {
    public MyClassLoader(File file) throws MalformedURLException {
        super(new URL[] {file.toURI().toURL()});
    }
    // 修改加载顺序,优先自己加载,加载不了再让双新加载
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class<?> c = null;
        try {
            c = findClass(name);
        } catch (Exception e) {
        }
        return c == null ? c = super.loadClass(name, resolve) : c;
    }
}

如何继承AppClassLoader

AppClassLoader就是JDK的Launcher中用来加载CLASSPATH下的Jar包中的类,正常情况下我们都应该继承这个类(毕竟它作用域中的类最全了)。但是我们实际继承的时候时指定不了这个类的。

实际上,我们如果自定义一个类加载器,如果继承了ClassLoader,则默认设置的父类加载器是AppClassLoader,这个原因是在因为在初始化自定义类加载器的时候,会指定其parentClassLoader为AppClassLoader 。

Launcher这个类是jre中用来启动main()方法的入口,在这个类中,我们着重关注的是初始化构造方法。

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
    	// 初始化extClassLoader
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
    	// 初始化appClassLoader,这里的这个赋值是比较重要的
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }

    //线程上下文设置
    Thread.currentThread().setContextClassLoader(this.loader);
	...
}

这个构造方法中,我目前所了解到的,就是初始化了AppClassLoader和ExtClassLoader,并且,我们只需要关心this.loader这个全局变量,这个全局变量存放的是AppClassLoader的对象信息。

而自定义的类解析器对应的parentClassLoader,就是在空参构造函数中被赋值的。因为MyClassLoaderTest继承了ClassLoader,所以,会调用到ClassLoader的空参构造函数 。

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<>();
        domains =
            Collections.synchronizedSet(new HashSet<ProtectionDomain>());
        assertionLock = new Object();
    } else {
        // no finer-grained lock; lock on the classloader instance
        parallelLockMap = null;
        package2certs = new Hashtable<>();
        domains = new HashSet<>();
        assertionLock = this;
    }
}

我们会发现,parentClassLoader就是getSystemClassLoader()返回的

public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader();
    if (scl == null) {
        return null;
    }
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkClassLoaderPermission(scl, Reflection.getCallerClass());
    }
    return scl;
}

// 这个方法中的其他变量我们可以暂时先不关心,我们看到有获取到一个Launcher对象
private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        if (scl != null)
            throw new IllegalStateException("recursive invocation");
        //获取Launcher!!!!
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        if (l != null) {
            Throwable oops = null;
            // 在这里调用其getClassLoader()方法,将返回的值,赋值给scl,而这个scl就是入参中的parent
            scl = l.getClassLoader();
            try {
                scl = AccessController.doPrivileged(
                    new SystemClassLoaderAction(scl));
            } catch (PrivilegedActionException pae) {
                oops = pae.getCause();
                if (oops instanceof InvocationTargetException) {
                    oops = oops.getCause();
                }
            }
            if (oops != null) {
                if (oops instanceof Error) {
                    throw (Error) oops;
                } else {
                    // wrap the exception
                    throw new Error(oops);
                }
            }
        }
        sclSet = true;
    }
}

所以,通过上面的代码,可以发现,我们自定义的类解析器,是在初始化的时候,指定了parent为AppClassLoader 

Class.forName()报错

在KafkaClient实际调用我们自定义类加载器加载的类的时候,会报错。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NKW4WyGi-1691556334751)(d:\user\01412349\Application Data\Typora\typora-user-images\1675131079926.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rZk0YJCa-1691556334754)(d:\user\01412349\Application Data\Typora\typora-user-images\1675131029198.png)]

原因:

kafkaClient是通过Class.forName()+线程上下文加载器对指定全类名进行加载的!
在这里插入图片描述

Class.forNameClassLoader.loadClass都可以用来进行类型加载,而在Java进行类型加载的时刻,一般会有多个ClassLoader可以使用,并可以使用多种方式可以获取类加载器。

​ CurrentClassLoader,称之为当前类加载器,简称CCL,一般可以通过一些native方法拿到,在代码中对应的就是(Class<?> caller = Reflection.getCallerClass())。

​ SpecificClassLoader,指定类加载器,简称SCL,一般就是A.getClassLoader()。

​ ThreadContextClassLoader,称之为线程上下文类加载器,简称TCCL,每个线程都会拥有一个ClassLoader引用,而且可以通过**Thread.currentThread().setContextClassLoader(ClassLoader classLoader)**进行切换。

详细说明一下Class.forName()加载器

看Demo --classForNameDemo。

反双亲委派还会Jar包冲突吗

反双亲委派,明明加载顺序是,先加载自己的, 自己作用域没有再加载父类的,还有可能Jar包冲突吗?

在这里插入图片描述
在这里插入图片描述

报错含义: LoggerFactory中的getILoggerFactory()的方法的返回声明对象的ClassLoader和调用方法StaticLoggerBinder.getSingleton().getLoggerFactory()的ClassLoader不一致。

在这里插入图片描述

原因:StaticLoggerBinder没有打到包里面,则使用了AppClassLoader来加载,而LoggerFactory是我们自定义ClassLoader加载的。打包的时候provide了部分,比如一些spi上下游依赖的包,就可能出现上述情况。

看DEMO—classDeclaredMethod()

Tomcat的类加载机制

ExtClassLoader → Bootstrap ClassLoader→WebAppClassLoader→ AppClassLoader

这样的加载顺序有什么好处?

Tomcat要解决什么问题?

作为一个Web容器,Tomcat要解决什么问题 , Tomcat 如果使用默认的双亲委派类加载机制能不能行?

1.我们知道Tomcat可以部署多个应用,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离 。举个例子 假设APP1 使用的是 Spring4 , APP2 使用的是Spring5 , 毫无疑问 Spring4 和 Spring 5 肯定有 类的全路径一样的类吧,如果使用双亲委派 ,父加载器加载谁?

2.web容器 自己依赖的类库 (tomcat lib目录下),不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。

3.部署在同一个web容器中相同的类库相同的版本可以共享, 比如jdk的核心jar包,否则,如果服务器有n个应用程序,那么要有n份相同的类库加载进虚拟机。

反双亲委派步骤

  1. 先在本地cache查找该类是否已经加载过,看看 Tomcat 有没有加载过这个类。

  2. 如果Tomcat 没有加载过这个类,则从系统类加载器的cache中查找是否加载过。

  3. 如果没有加载过这个类,尝试用ExtClassLoader类加载器类加载,重点来了,这里并没有首先使用 AppClassLoader 来加载类。这个Tomcat 的 WebAPPClassLoader 违背了双亲委派机制,直接使用了 ExtClassLoader来加载类。这里注意 ExtClassLoader 双亲委派依然有效,ExtClassLoader 就会使用 Bootstrap ClassLoader 来对类进行加载,保证了 Jre 里面的核心类不会被重复加载。 比如在 Web 中加载一个 Object 类。**ExtClassLoader → Bootstrap ClassLoader→WebAppClassLoader→ AppClassLoader **,这个加载链,就保证了 Object 不会被重复加载。

  4. 如果 BoostrapClassLoader,没有加载成功,就会调用自己的 findClass 方法由自己来对类进行加载,findClass 加载类的地址是自己本 web 应用下的 class。

  5. 加载依然失败,才使用 AppClassLoader 继续加载。

  6. 都没有加载成功的话,抛出异常。

源码如何实现的:

在这里插入图片描述

在这里插入图片描述

总结一下以上步骤,WebAppClassLoader 加载类的时候,故意打破了JVM 双亲委派机制,绕开了 AppClassLoader,直接先使用 ExtClassLoader 来加载类,目的就是不重复加载JAVA基础类。

Jsp热加载如何实现的

web容器要支持jsp的修改, jsp 文件最终也是要编译成class文件才能在虚拟机中运行, web容器需要支持 jsp 修改后不用重启 ,就是热加载的功能。

要怎么实现jsp文件的热加载呢? jsp 文件其实也就是class文件,是调用 Compiler.compile() 重新把 jsp 转换成 servlet,并编译 servlet 成 class 文件。那么如果 jsp修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?可以直接卸载掉这jsp文件的类加载器 .当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件

源码详见: org.apache.jasper.servlet.JasperLoader

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值