
在使用javaagent实现微服务间调用关系时,难点之一就是类加载问题:不同的classLoader(类加载器)加载父子class类时所产生的问题,如
- 问题场景1、
ClassNotFoundException问题 - 问题场景2、
NoClassDefFoundError问题 - 问题场景3、`java.lang.IllegalAccessError: tried to access field x.xx.XXX from class x.yy.ZZZ问题
本质都是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包
1 | <dependency> |
当xx-agent.jar 与 yy-app.jar通过maven package后,结构如下
xx-agent.jar结构,关注点已经被圈中

yy-app.jar结构

在应用代码中,我们自定义了一个类:TTLMDCAdapter,他实现了slf4j中MDCAdapter.class1
2
3
4
5
6
7
8
9
10
11
12
13
14package 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命令:以运行起来服务1
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<URL> path属性),所以本质加载为:将传入的全限定名的类名与每个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.classfile:/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)行时,1
2
3
4static {
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$AppClassLoadermtcMDCAdapter 的类加载器是 org.springframework.boot.loader.LaunchedURLClassLoader

最后,不同类加载器的类不能相互访问
it`s time to sumiray
本文通过一个点: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体系关系 如下1
2
3
4org.springframework.boot.loader.LaunchedURLClassLoader
-> parent sun.misc.Launcher$AppClassLoader
-> parent sun.misc.Launcher$ExtClassLoader
-> parent BootstrapClassloader