javaagent类加载器加载父子类问题实例分析

20220121213914

在使用javaagent实现微服务间调用关系时,难点之一就是类加载问题:不同的classLoader(类加载器)加载父子class类时所产生的问题,如

  1. 问题场景1、ClassNotFoundException问题
  2. 问题场景2、NoClassDefFoundError问题
  3. 问题场景3、`java.lang.IllegalAccessError: tried to access field x.xx.XXX from class x.yy.ZZZ问题

本质都是class loader加载class的问题`

通过本文可以获得以下答案

  1. AppClassLoaderclasspath指的是什么
  2. 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
2
3
4
5
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>

xx-agent.jaryy-app.jar通过maven package后,结构如下

  • xx-agent.jar结构,关注点已经被圈中
    20220120194310

  • yy-app.jar结构
    20220120204239

在应用代码中,我们自定义了一个类:TTLMDCAdapter,他实现了slf4jMDCAdapter.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.jaryy-app.jar。其中都引入了slf4j-api jar,我们在yy-app.jar中定义了类:TTLMDCAdapter,他implements MDCAdapter

我们通过一张图阐述以上的现场:
20220121152452

现在,现场已经搭建完毕了。

问题出现

我们开始执行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代码,以清楚的看到问题的本质

运行过程中,问题出现
20220120211436

可以看到,程序报错了 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 方法
20220121154834

ClassLoader.loadClass 方法
20220121154956

当代码执行到 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 loaderucp.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.jaryy-app.jar 分别引入, 所以 MDCAdapter 的绝对路径有两个,分别为:

  1. jar:file:/xx/yy-app.jar!BOOT-INF/classes!/org/slf4j/spi/MDCAdapter.class
  2. 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.classorg.springframework.boot.loader.LaunchedURLClassLoader 加载
org.slf4j.spi.MDCAdapter.classsun.misc.Launcher$AppClassLoader 加载

org.slf4j.MDC 类与 org.slf4j.spi.MDCAdapter.class 相同,都是 slf4j-api jar 的class,所以 org.slf4j.MDCsun.misc.Launcher$AppClassLoader 加载。

所以,当程序执行 TTLMDCAdapterstatic 代码块的(1)行时,

1
2
3
4
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

20220121180224

最后,不同类加载器的类不能相互访问

it`s time to sumiray

本文通过一个点:java.lang.IllegalAccessError: tried to access field x.xx.XXX from class x.yy.ZZZ问题 来分析class loader的体系以及加载过程。以点来慢慢扩散,带出问题的本质以及对应的技术点和原理

  1. AppClassLoaderclasspath指的是什么: answer: jar的首层的全限定名class
  2. IllegalAccessError: tried to access field的原因: answer: 每个 class loader 有自己的加载范围,同时,不同类加载器的类不能相互访问

20220121183145

附录
class loader体系关系 如下

1
2
3
4
org.springframework.boot.loader.LaunchedURLClassLoader
-> parent sun.misc.Launcher$AppClassLoader
-> parent sun.misc.Launcher$ExtClassLoader
-> parent BootstrapClassloader