在使用javaagent
实现微服务间调用关系时,难点之一就是类加载问题:不同的classLoader
(类加载器)加载父子class类时所产生的问题,如
ClassNotFoundException问题
NoClassDefFoundError问题
本质都是class loader加载class的问题`
通过本文可以获得以下答案
AppClassLoader
的classpath
指的是什么IllegalAccessError: tried to access field
的原因这篇文章通过实例详细分析下问题3场景,即 tried to access field x.xx.XXX from class x.yy.ZZZ
问题的根本原因,以及原因的原因。
本文力求简单,所以只专注一个点,其他不做扩展
这里我们有两个jar包,一个是xx-agent.jar
,一个是yy-app.jar
。我们使用slf4j jar
演示问题,所以,我们的两个jar都要依赖org.slf4j
包
org.slf4j
slf4j-api
1.7.25
复制代码
当xx-agent.jar
与 yy-app.jar
通过maven package
后,结构如下
在应用代码中,我们自定义了一个类:TTLMDCAdapter,他实现了slf4j
中MDCAdapter.class
package org.slf4j;
public class TTLMDCAdapter implements MDCAdapter {
private static TTLMDCAdapter ttlMDCAdapter;
static {
ttlMDCAdapter = new TTLMDCAdapter();
MDC.mdcAdapter = ttlMDCAdapter;
}
public static MDCAdapter getInstance() {
return ttlMDCAdapter;
}
复制代码
简单回顾下以上的操作: 我们使用了两个jar包:xx-agent.jar
与 yy-app.jar
。其中都引入了slf4j-api jar
,我们在yy-app.jar
中定义了类:TTLMDCAdapter
,他implements MDCAdapter
。
我们通过一张图阐述以上的现场:
现在,现场已经搭建完毕了。
我们开始执行java命令:以运行起来服务
java -Dspring.profiles.active=dev -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5009 javaagent:/xx/xx-agent.jar=config -jar /xx/yy-app.jar
复制代码
说明下:agentlib用于远程debug代码,以清楚的看到问题的本质
运行过程中,问题出现
可以看到,程序报错了 java.lang.IllegalAccessError: tried to access field org.slf4j.MDC.mdcAdapter from class org.slf4j.TTLMDCAdapter
。
从日志我们看到了错误,那么原因是什么呢?我们接下来通过debug
的方式深入代码报错点,以求得找到问题的本质
我们重新运行java -Dspring.profiles.active=dev -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5009 javaagent:/xx/xx-agent.jar=config -jar /xx/yy-app.jar
, 这次启动远程debug监听,并在URLClassLoader.findClass
方法和ClassLoader.loadClass
方法内分别打上条件断点
,
URLClassLoader.findClass
方法
ClassLoader.loadClass
方法
当代码执行到 TTLMDCAdapter.getInstance()
时,由于是第一次使用 TTLMDCAdapter
类,所以会触发这个类的classLoader
的加载过程,由于 TTLMDCAdapter
定义在我们的app.jar中,所以它首次被 org.springframework.boot.loader.LaunchedURLClassLoader
类加载,而 LaunchedURLClassLoader
是继承 java.net.URLClassLoader
,所以对于TTLMDCAdapter
的类加载一直通过parent走过了整个 class loader体系关系
。(参见 class loader体系关系
) 因为每个class loader都有自己的加载范围,即都有URLClassPath
类型的属性ucp(它自身又拥有ArrayList
属性),所以本质加载为:将传入的权限定名的类名与每个class loader
的ucp.path
组合到一起,形成一个绝对路径String串,然后通过new File(String串)
,如果成功,那么就可以加载形成文件流,即说明加载成功。这个过程也是 Resource res = ucp.getResource(path, false)
方法的实现原理。
此时由于TTLMDCAdapter的绝对路径是jar:file:/xx/yy-app.jar!BOOT-INF/classes!/org/slf4j/TTLMDCAdapter.class
,所以LaunchedURLClassLoader
会加载成功。(这里如果没明白可以结合本文开头的yy-app.jar
结构图和LaunchedURLClassLoader.ucp
的值温习下)
前面我们已经说过 TTLMDCAdapter
implements
MDCAdapter
,java规定一个类的加载先加载它的父类或接口,所以此时 MDCAdapter
类会开始class loader
加载,同样走一边class loader
加载流程。从文章开头我们知道,MDCAdapter
所在的jar被xx-agent.jar
与 yy-app.jar
分别引入, 所以 MDCAdapter
的绝对路径有两个,分别为:
jar:file:/xx/yy-app.jar!BOOT-INF/classes!/org/slf4j/spi/MDCAdapter.class
file:/xx/xx-agent.jar!/org/slf4j/spi/MDCAdapter.class
那么疑问来了,两个绝对路径,以哪个为准呢?
这时候我们回到class loader
的加载体系,很多网上的文章都讲了:双亲委派模型,即父类优先加载。又 LaunchedURLClassLoader
的parent属性值为 sun.misc.Launcher$AppClassLoader
,所以AppClassLoader
先加载,加上AppClassLoader
的加载绝对路径为:jar的绝对路径+类的权限定名,所以 file:/xx/xx-agent.jar!/org/slf4j/spi/MDCAdapter.class
这个绝对路径加载成功。所以,MDCAdapter
的类加载器是 sun.misc.Launcher$AppClassLoader
。
我们小结下以上的流程结果 org.slf4j.TTLMDCAdapter.class
被 org.springframework.boot.loader.LaunchedURLClassLoader
加载 org.slf4j.spi.MDCAdapter.class
被 sun.misc.Launcher$AppClassLoader
加载
org.slf4j.MDC
类与 org.slf4j.spi.MDCAdapter.class
相同,都是 slf4j-api jar
的class,所以 org.slf4j.MDC
被 sun.misc.Launcher$AppClassLoader
加载。
所以,当程序执行 TTLMDCAdapter
的 static
代码块的(1)行时,
static {
mtcMDCAdapter = new TTLMDCAdapter();
MDC.mdcAdapter = mtcMDCAdapter; // (1)
}
复制代码
MDC.mdcAdapter = mtcMDCAdapter
就报错 java.lang.IllegalAccessError: tried to access field org.slf4j.MDC.mdcAdapter from class org.slf4j.TTLMDCAdapter
了。
MDC.mdcAdapter
的类加载器是 sun.misc.Launcher$AppClassLoader
mtcMDCAdapter
的类加载器是 org.springframework.boot.loader.LaunchedURLClassLoader
最后,不同类加载器的类不能相互访问
本文通过一个点:java.lang.IllegalAccessError: tried to access field x.xx.XXX from class x.yy.ZZZ问题
来分析class loader的体系以及加载过程。以点来慢慢扩散,带出问题的本质以及对应的技术点和原理
AppClassLoader
的classpath
指的是什么: answer: jar的首层的权限定名classIllegalAccessError: tried to access field
的原因: answer: 每个 class loader
有自己的加载范围,同时,不同类加载器的类不能相互访问附录 class loader体系关系
如下
org.springframework.boot.loader.LaunchedURLClassLoader
-> parent sun.misc.Launcher$AppClassLoader
-> parent sun.misc.Launcher$ExtClassLoader
-> parent BootstrapClassloader