拜托,别在 Agent 中依赖 Fastjson 了
创始人
2025-06-28 05:20:04
0

一、背景

最近因为增加了一个在 agent 中上报异常的功能,agent 为了在 http 请求时方便把对象转换为 json 格式,增加了一个 fastjson 的依赖,结果搞出来各种问题。

环境:

  • JDK 1.8
  • SpringBoot 2.0.0.RELEASE
  • skywalking agent 8.14.0

二、初现问题

2.1 初步定位

有同事反馈应用在本地能启动,但是到了测试环境(带 agent 启动)就起不来,报错如下:

图片图片

首先还是要确认下是不是应用的依赖冲突问题,GenericHttpMessageConverter这个类是在 spring-web 这个包下面的, 因为本地打包环境和测试环境有可能不一致,需要确认最终部署到测试环境的包里是否包含了 spring-web 包。经确认包里有 spring-web,排除这个可能。

然后怀疑是 agent 和应用的依赖冲突,临时让这个应用的 agent 下线后重新部署,发现能正常启动,基本确认是 agent 带来的问题。

2.2 进一步排查

为了方便定位问题,我把发现问题时应用部署的包下载到本地,并在本地挂载 agent 启动,问题重现,报错和测试环境一致。至此我就可以在本地 debug 了。

顺便说一下,我一开始用 idea 启动应用(挂载 agent)是没问题的,至于为什么没问题下面会说到。

本地我在java.net.URLClassLoader#findClass方法的入口处打了一个条件断点(类名为GenericHttpMessageConverter的才会进来),启动应用后一会儿进入断点。

idea 这个工具就是好用,从 debug 界面一下子就能看出来,这个 findClass 是调用了 3 次,并且能看到每一次 findClass 是加载的哪个类:

图片图片

从上面的图的最后一行也能看出来,这个类加载最开始的触发是在内部的一个二方库的类WebAutoConfig中触发的。

这 3 次 findClass 的顺序可以看出, 类的加载顺序为:

BootMessageConverter (二方包)

-> FastJsonHttpMessageConverter (fastjson)

-> GenericHttpMessageConverter (spring-web)

再来看看WebAutoConfig触发类加载的那段代码:

@Configuration
public class WebAutoConfig implements WebMvcConfigurer {
  
  @Bean
    @ConditionalOnMissingBean
    public HttpMessageConverters httpMessageConverter() {
        BootMessageConverter converter = new BootMessageConverter(); //这一行触发了类加载
    ...
    }
}

public class BootMessageConverter extends FastJsonHttpMessageConverter {
 ...
}

public class FastJsonHttpMessageConverter extends AbstractHttpMessageConverter
        implements GenericHttpMessageConverter {
 ...        
}

从上面的代码能看出最开始是因为BootMessageConverter的实例化进行了类加载, 而BootMessageConverter因为继承了FastJsonHttpMessageConverter, 又接着触发了FastJsonHttpMessageConverter的类加载, 然后FastJsonHttpMessageConverter因为实现了GenericHttpMessageConverter接口, 又进一步触发了GenericHttpMessageConverter的类加载, 这样来看源码和上面 debug 得出的结论是一致的。

分析到这一步,如果你对类加载机制以及 agent 的运行方式非常熟悉的话,基本已经能得出“为什么会报GenericHttpMessageConverter类找不到的错误”结论了。

那么接下来,我会基于类加载的机制来详细分析一下,为什么会找不到GenericHttpMessageConverter

三、类加载机制

3.1 双亲委派机制

图片图片

上一层类加载器是下一层类加载器的父加载器,除了 Bootstrap ClassLoader 之外,所有的加载器都是有父加载器的。

所谓的双亲委派机制,指的就是:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。

开个玩笑:这样说来,双亲委派这种说法似乎并不准确,因为有父无母,准确来说应该是“单亲委派”...

3.1.1 类中依赖的其他类是怎么加载的

----------------接下来是重点----------------

我们定义的类一般还会依赖其他类,因此在被类加载器加载时,类加载机制中除了双亲委派机制之外,还有一个重要的机制是:

假设类 A 依赖类 B,那么哪个 ClassLoader 找到了类 A,这个 ClassLoader 也会尝试去加载类 B(当然类 B 的加载过程也遵循双亲委派)。

3.2 springboot 的类加载机制

springboot 项目打包之后的 jar 目录结构如下:

├─BOOT-INF
│  ├─classes
│  │  ├─应用代码
│  └─lib
│     ├─应用依赖的jar包
├─META-INF
│  ├─MANIFEST.MF
└─org
    └─springframework
        └─boot
            └─loader
                │  JarLauncher.class
                │  LaunchedURLClassLoader.class
                │  Launcher.class
                │  ...

其中/META-INF/MANIFEST.MF 是 jar 包运行的关键, 来看一下里面的内容:

...

Main-Class: org.springframework.boot.loader.JarLauncher

Start-Class: com.xxxxxx.DemoApplication

Spring-Boot-Classes: BOOT-INF/classes/

Spring-Boot-Lib: BOOT-INF/lib/

...

首先 jar 包运行都有一个入口类定义了 main 方法,可以看到 springboot 项目打包出来的 jar 定义的入口运行类并不是应用代码中的XxxApplication,而是 springboot 中的一个类JarLauncher,那么应用代码中的XxxApplication是怎么运行的呢?

当你运行 java -jar 命令的时候,JarLauncher会加载 /BOOT-INF/classes 下的类和 /BOOT-INF/lib 下的 jar 包。最后调用 MANIFEST.MF 文件的 Start-Class 属性指定的类的 main 方法来完成应用程序的启动。

问题是 /BOOT-INF/ 并不是标准的 classpath 路径,系统内置的 ClassLoader 是加载不到这些目录的类的,那么这些类是谁来加载的呢?答案就是 springboot 自定义的类加载器:LaunchedURLClassLoader

图片图片

也就是说应用代码中的类以及应用依赖的 jar 都是LaunchedURLClassLoader负责加载的。

3.3 fastjson 的类到底是怎么找到的

再说回来在第 2.2 节中说到的类加载顺序:

BootMessageConverter (二方包)

-> FastJsonHttpMessageConverter (fastjson)

-> GenericHttpMessageConverter (spring-web)

这里我们重点来分析一下中间那个FastJsonHttpMessageConverter到底是怎么被加载的。

已知应用依赖了 fastjson 和 spring-web,agent 也依赖了 fastjson 但不依赖 spring-web。

从 Oracle 官方的文档看到,Java 8 的 agent 的 jar 包里的类会添加到 classpath 中,因此会用AppClassLoader来加载。

图片图片

而二方包的BootMessageConverter是应用依赖的 jar, 放在/BOOT-INF/lib 下, 因此是被LaunchedURLClassLoader加载的。整体类加载流程如下图:

图片图片

上图说明:

当BootMessageConverter被LaunchedURLClassLoader加载时, 发现依赖了FastJsonHttpMessageConverter, 因此LaunchedURLClassLoader会继续尝试去加载FastJsonHttpMessageConverter。由于类加载的双亲委派机制,LaunchedURLClassLoader会委派它的父加载器AppClassLoader来尝试加载,当然AppClassLoader会继续往上找父加载器,一直到Bootstrap ClassLoader。

很显然,Bootstrap ClassLoader和ExtClassLoader都无法找到FastJsonHttpMessageConverter,但是AppClassLoader可以找到(因为 agent 包中存在 fastjson 的类)。然后,这一步是关键,AppClassLoader找到了FastJsonHttpMessageConverter之后发现它依赖了GenericHttpMessageConverter,因此由找到了FastJsonHttpMessageConverter的AppClassLoader继续尝试加载GenericHttpMessageConverter,但是GenericHttpMessageConverter只有应用依赖的 spring-web.jar 中才有,而这个 jar 在/BOOT-INF/lib 下,只能被LaunchedURLClassLoader加载。双亲委派机制只能由子加载器往父加载器委托而反过来是不行的,而GenericHttpMessageConverter没办法被AppClassLoader以及它的父加载器加载到,因此AppClassLoader抛出了找不到GenericHttpMessageConverter的错误。

这里的关键就在于LaunchedURLClassLoader本身是能找到 fastjson 类的(在/BOOT-INF/lib), 但是因为双亲委派机制, 在加载 fastjson 类的时候, 被AppClassLoader截胡了,以至于丧失了后面依赖的类加载主动权。

说到这里,就可以回答之前的那个问题了:为什么用 idea 启动应用(挂载 agent)是没问题的?因为 idea 是直接运行应用的 XxxApplication 类的 main 方法,不是通过 springboot 的JarLauncher启动的,而在运行时所有的依赖都是通过指定 classpath 来做的,因此 idea 运行过程中,所有的类都能通过AppClassLoader加载到,也就不存在上面这种冲突问题了。

四、解决方案一:maven-shade-plugin

知道问题的根因了,那么思路就是怎么样可以让 fastjson 类被LaunchedURLClassLoader找到而不要被AppClassLoader找到。这里的思路是把 agent 中依赖的 fastjson 的 package 给重命名一下。

maven-shade-plugin在 maven 官方网站中提供的一个插件,官方文档中定义其功能如下:

This plugin provides the capability to package the artifact in an uber-jar, including its dependencies and to shade - i.e. rename - the packages of some of the dependencies.

简单来说就是将依赖的包在 package 阶段一起打入 jar 包中,以及对依赖的 jar 包进行重命名从而达到隔离的作用。接下来就把这个 maven 插件引入 agent 中。

maven 配置:


    org.apache.maven.plugins
    maven-shade-plugin
    3.2.1
    
        
            package
            
                shade
            
            
                false
                true
                true
                true
                
                    
                        
                            xxxxxx.AgentStarter
                            true
                            true
                        
                    
                
                
                
                    
                        com.alibaba.fastjson
                        shade.com.alibaba.fastjson
                    
                
            
        
    

package 之后的效果:

图片图片

可以看到在 agent 包中,fastjson 类的 package 都已经加上了一个前缀shade.,这样的话,应用中加载正常的 fastjson 类的时候,肯定不会找到 agent 里面来了,以此避免了类加载被AppClassLoader截胡的情况。

用重新 package 的 agent 包启动之前应用, 应用正常启动, 至此问题解决。

五、再现问题

本以为问题已经解决,没想到几天后另一个应用又报了类找不到的错误:

图片图片

有了上次的经验, 这次还算顺利, 排查过程跟上次的差不多。

最后发现是应用依赖的 jersey 这个三方库,而 jersey 通过 SPI 的方式会去找所有 classpath 中\META-INF\services\目录下的javax.ws.rs.ext.MessageBodyReader这个文件,由于 agent 依赖了 fastjson,而 fastjson 也实现了这个 SPI 的扩展,结果 jersey 就找到了 agent 包的\META-INF\services\目录下的javax.ws.rs.ext.MessageBodyReader文件,而javax.ws.rs.ext.MessageBodyReader文件中的内容如下:

图片图片

可以看到 maven-shade-plugin 把这里的类 package 也改掉了。然后 jersey 读取到这个文件后,根据类名去加载了shade.com.alibaba.fastjson.support.jaxrs.FastJsonProvider这个类,结果肯定是找到了 agent 包里的这个类,而这个类依赖的MessageBodyReader类是在 jsr311-api.jar 里的, 这个 jar 包只在应用中依赖, agent 并不依赖这个 jar 包, 因此就抛出了找不到类的错误。

依赖冲突真是让人防不胜防~

六、决定:干掉 fastjson

本来我查了下 maven-shade-plugin 似乎是可以在 agent 打包时把\META-INF\services\这个目录排除掉的,这样的话上面的问题也能解决掉,但是连续两次踩了这个坑还是让我静下来好好思考了一下。

这两次的依赖冲突从根本上来看,都是因为 fastjson 做的太重,第一次是因为 fastjson 依赖了 spring,第二次是因为 fastjson 实现了 jsr311-api,而在 agent 中去依赖 fastjson 并没有那么多的需求,只是为了做一个纯粹的转换工作:Java 对象和 Json 串之间的互相转换。所以找一个纯粹的轻量级的 Json 转换库是我的本质需求。否则 fastjson 下次可能又遇到其他的依赖冲突问题,我还得改。

如何考量是否轻量级呢?我主要从两方面着手:

  1. 看这个三方库的 pom.xml 中有没有依赖其他三方库
  2. 看这个三方库的\META-INF\services\目录有没有多余的 SPI 实现

最终我选择了 Google 的 gson 作为 agent 依赖的 Json 转换库。

相关内容

热门资讯

如何允许远程连接到MySQL数... [[277004]]【51CTO.com快译】默认情况下,MySQL服务器仅侦听来自localhos...
如何利用交换机和端口设置来管理... 在网络管理中,总是有些人让管理员头疼。下面我们就将介绍一下一个网管员利用交换机以及端口设置等来进行D...
施耐德电气数据中心整体解决方案... 近日,全球能效管理专家施耐德电气正式启动大型体验活动“能效中国行——2012卡车巡展”,作为该活动的...
20个非常棒的扁平设计免费资源 Apple设备的平面图标PSD免费平板UI 平板UI套件24平图标Freen平板UI套件PSD径向平...
德国电信门户网站可实时显示全球... 德国电信周三推出一个门户网站,直观地实时提供其安装在全球各地的传感器网络检测到的网络攻击状况。该网站...
为啥国人偏爱 Mybatis,... 关于 SQL 和 ORM 的争论,永远都不会终止,我也一直在思考这个问题。昨天又跟群里的小伙伴进行...
《非诚勿扰》红人闫凤娇被曝厕所... 【51CTO.com 综合消息360安全专家提醒说,“闫凤娇”、“非诚勿扰”已经被黑客盯上成为了“木...
2012年第四季度互联网状况报... [[71653]]  北京时间4月25日消息,据国外媒体报道,全球知名的云平台公司Akamai Te...