40 classpath中存在多个jar存在同限定名的class classloader会如何加载

Posted 蓝风9

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了40 classpath中存在多个jar存在同限定名的class classloader会如何加载相关的知识,希望对你有一定的参考价值。

前言

呵呵 最近碰到一些 老项目, 使用的传统的 libs 文件夹管理依赖, 显然存在的问题就是 jar 的依赖的问题 

然后 之前呢, 也碰到了一些 依赖的相关问题 

1. 异常:Class net.sf.cglib.core.DebuggingClassWriter overrides final method visit

2. tomcat启动内存堆栈溢出ASN1EncodableVector,DEREncodableVector循环依赖

然而 这些问题都需要确定一些事情, 才能够继续理解, 解决问题 

那就是 如果 classpath 中存在多个jar存在 相同限定名的类, classloader 会选择怎么加载类呢 ?

呵呵 我们这里就来展开, 主要是会围绕着以下几个问题  

1. URLClassloader 会如何加载 ?

2. AppClassloader 会如何加载 ?

3. BootstrapClassloader 会如何加载 ?

4. WebappClassloader 如何加载 ? [补充于 2021.05.30]

以下调试 java 层面的调试基于 jdk8, jvm 层面的调试基于 openjdk9 

jvms 关于 BootstrapClassloader, UserDefinedClassloader 的约束?

首先是看一下规范对于 这些不同场景的 classloader 加载 class 的描述 

可以看到主要是描述了需要做什么, 拿到了不同的结果应该做什么, 但是并没有固定需要怎么做, 因此 这个就取决于具体的实现了, 换句话说 无论怎么实现都可以, 只要满足已经提到的这些约束就行 

关于 UserDefinedClassloader.loadClass 方法 这里是有一些明确的约束, 但是与我们这里所关心的东西来说 是不重要的  

相关准备 

请提前下载好相关依赖, 依赖如下 asm-3.3.1.jar, asm-5.0.4.jar 这两个 jar 中有一部分相同的全限定名的类型, 并且类型的定义有一些区别 

我们这里的参照主要是 visit(int var1, int var2, String var3, String var4, String var5, String[] var6) 方法, 这个方法在 两个不同版本的 jar 中有一些区别 

asm-3.3.1 里面声明如下  

asm-5.0.4 里面声明如下  

可以看到明显的区别是 asm-5.0.4.jar 里面的 visit 方法是 final 的, 而 asm-3.3.1.jar 里面的 visit 方法没有 final 标记 

URLClassloader 测试用例

新建一个 HelloWorld02 的项目, 创建如下 class 

package com.hx.test;

import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * Test28UrlClassLoader
 *
 * @author Jerry.X.He <970655147@qq.com>
 * @version 1.0
 * @date 2021-05-29 10:26
 */
public class Test28UrlClassLoader 

    // Test28UrlClassLoader
    public static void main(String[] args) throws Exception 

        ClassLoader classLoader = new URLClassLoader(new URL[]
                new URL("file:///Users/jerry/.m2/repository/asm/asm/3.3.1/asm-3.3.1.jar"),
                new URL("file:///Users/jerry/.m2/repository/org/ow2/asm/asm/5.0.4/asm-5.0.4.jar")
        );

        Class classWriterClazz = classLoader.loadClass("org.objectweb.asm.ClassWriter");
        Method method = classWriterClazz.getMethod("visit", int.class, int.class, String.class, String.class,
                                                   String.class, String[].class);
        System.out.println(method.toString());

    


配置启动参数如下[去掉了一部分无关的 jdk 依赖, idea 调试相关依赖]  

/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/bin/java -Dfile.encoding=UTF-8 -classpath "/Users/jerry/IdeaProjects/HelloWorld02/target/classes:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar" com.hx.test.Test28UrlClassLoader

当我们吧 asm-3.3.1.jar 放在 classloader 的 classpath 前面的时候, 结果如下 

当我们吧 asm-5.0.4.jar 放在 classloader 的 classpath 前面的时候, 结果如下 

我们来看一下 具体的 lookUp 的代码体现, 可以看到是根据 索引0 开始的查询 

主要的查询方式 那就取决于这里的 getNextLoader 的方式了 

我们来看一下 getNextLoader 相关的的具体的实现 

上面的 this.getLookupCache(classname) 返回的是 null, 这里是直接带着参数来到了 getLoader 方法 

再看一下 getLoader 的实现, 是从 urls.pop 的查询路径, 那具体的顺序又依赖于 urls 的初始化了 

urls 的初始化来自于 URLClassPath 初始化的时候, 从最后一个 classpath push 到 urls 里面 

然后 getLoader 里面是从 urls 中 pop classpath 进行查询, 因此 遍历的 classpath 的方式 和 classpath原有的顺序一致 

比如我们这里的 asm-3.3.1.jar 在前面, asm-5.0.4.jar 在后面, 那么遍历的顺序为 asm-3.3.1.jar, asm-5.0.4.jar 

因此会先加载 asm-3.3.1.jar 里面的 org.objectweb.asm.ClassWriter 

同理调整顺序之后, asm-5.0.4.jar 放在前面, 加载的 asm-5.0.4.jar 里面的 org.objectweb.asm.ClassWriter 

AppClassloader 测试用例

用例和之前一样, 不过启动参数 稍微调整一下[去掉了一部分无关的 jdk 依赖, idea 调试相关依赖] 

/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/bin/java -classpath /Users/jerry/IdeaProjects/HelloWorld02/target/classes:/Users/jerry/.m2/repository/asm/asm/3.3.1/asm-3.3.1.jar:/Users/jerry/.m2/repository/org/ow2/asm/asm/5.0.4/asm-5.0.4.jar -Dfile.encoding=UTF-8 com.hx.test.Test28UrlClassLoader

当 classpath 参数中的 asm-3.3.1.jar 放在前面的时候, 加载的是 asm-3.3.1.jar 中的 org.objectweb.asm.ClassWriter 

当 classpath 参数中的 asm-5.0.4.jar 放在前面的时候, 加载的是 asm-5.0.4.jar 中的 org.objectweb.asm.ClassWriter 

我们来大致看一下, AppClassloader 继承自 URLClassloader 

然后我们这里 加载 org.objectweb.asm.ClassWriter, 也是直接走的 super.load, 那就和 URLClassloader 完全一致了 

最终加载的是 classpath 中靠前的 asm-5.0.4.jar 中的 org.objectweb.asm.ClassWriter

BootstrapClassloader 测试用例

用例和之前一样, 不过启动参数 稍微调整一下[去掉了一部分无关的 jdk 依赖, idea 调试相关依赖] 

/Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/bin/java -Xbootclasspath/a:/Users/jerry/.m2/repository/asm/asm/3.3.1/asm-3.3.1.jar:/Users/jerry/.m2/repository/org/ow2/asm/asm/5.0.4/asm-5.0.4.jar -Dfile.encoding=UTF-8 -classpath "/Users/jerry/IdeaProjects/HelloWorld02/target/classes:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar" com.hx.test.Test28UrlClassLoader

当 classpath 参数中的 asm-3.3.1.jar 放在前面的时候, 加载的是 asm-3.3.1.jar 中的 org.objectweb.asm.ClassWriter 

当 classpath 参数中的 asm-5.0.4.jar 放在前面的时候, 加载的是 asm-5.0.4.jar 中的 org.objectweb.asm.ClassWriter 

我们来看一下 这里的类加载的方式, URLClassloader 委托给 AppClassloader, AppClassloader 委托给 ExtClassloader, ExtClassloader 委托给 BootstrapClassloader 

再往下, 我们就需要来到 vm 这一层了 

可以看到这里是从 first_append_entry 开始遍历, 第一个元素是 asm-3.3.1.jar, 优先取的 asm-3.3.1.jar 中的 org.objectweb.asm.ClassWriter

我们再来看一下 ClassLoader.first_append_entry 的初始化 

可以看到 BootstrapClasspath 默认有一个 jdk/modules/java.base, 后面才是我们手动配置添加的 "/Users/jerry/.m2/repository/asm/asm/3.3.1/asm-3.3.1.jar:/Users/jerry/.m2/repository/org/ow2/asm/asm/5.0.4/asm-5.0.4.jar" 

然后这里是 class_path 来自于 Arguments::get_sysclasspath(), -Xbootclasspath/a 选项便是添加到了 Arguments::get_sysclasspath() 

这里是迭代 classpath, 这里是以 ":" 为分隔符[mac上面], 可以判断的是会先处理 /Users/jerry/.m2/repository/asm/asm/3.3.1/asm-3.3.1.jar, 然后再会处理 /Users/jerry/.m2/repository/org/ow2/asm/asm/5.0.4/asm-5.0.4.jar 

Classloader::update_path_entry_list 实现如下, 有一个 check_for_duplicates 这里为 false, 有多少添加多少 

Classloader::add_to_list 的实现如下

从这里的实现我们可以大致判断, 我们这里的场景下处理完了之后 first_append_entry 会是 asm-3.3.1.jar, last_append_entry 会是 asm-5.0.4.jar 

呵呵 我们来验证一下 

足够清晰了吗 ? 

WebappClassloader 如何加载 ? [补充于 2021.05.30]

这个请参见 关于tomcat启动org.bouncycastle.asn1.ASN1EncodableVector->org.bouncycastle.asn1.DEREncodableVector循环依赖 

WebappClassloader 的 classpath 基于其依赖的 StandardRoot, 大体上按照如下几类资源查询 preResource, mainResource, classResource, jarResources, postResources 

我们这里着重关注 jarResources 

jarResources 来自于 File.list("WEB-INF/lib"), 取决于 File.list 的顺序 

更详细的相关代码的初始化 等等, 请参见 关于tomcat启动org.bouncycastle.asn1.ASN1EncodableVector->org.bouncycastle.asn1.DEREncodableVector循环依赖 

完 

参考

Chapter 5. Loading, Linking, and Initializing

Chapter 5. Loading, Linking, and Initializing

关于tomcat启动org.bouncycastle.asn1.ASN1EncodableVector->org.bouncycastle.asn1.DEREncodableVector循环依赖

以上是关于40 classpath中存在多个jar存在同限定名的class classloader会如何加载的主要内容,如果未能解决你的问题,请参考以下文章

43 多个相同限定名类型同时存在导致的继承结构混乱的情况

43 多个相同限定名类型同时存在导致的继承结构混乱的情况

软件包javax.servlet不存在?为啥我把“servlet-api.jar”加入到classPath中还是不行啊?

SpringBoot-读取classpath下文件

Spring高级注解

Tomact的配置