记一次多个Java Agent同时使用的类增强冲突问题及分析

摘要:Java Agent技术常被用于加载class文件之前进行拦截并修改字节码,以实现对Java应用的无侵入式增强 。
本文分享自华为云社区《记一次多个JavaAgent同时使用的类增强冲突问题及分析》,作者:Vansittart 。
问题背景Java Agent技术常被用于加载class文件之前进行拦截并修改字节码,以实现对Java应用的无侵入式增强 。Sermant是致力于服务治理领域的开源Java Agent框架项目 。某客户在集成Sermant之前已集成了两套Java Agent:用于业务能力增强的自研Java Agent和用于链路采集的SkyWalking 。该客户单独挂载自研Java Agent插件包时,字节码增强可以按照预期生效 。后期引入开源SkyWalking并同时将自研Java Agent插件包和SkyWalking通过-javaagent启动参数挂载至业务应用中 。使用过程中发现 , 两者的加载顺序会对预期的拦截点增强生效与否有直接影响 。为什么会产生这种现象?该客户求助Sermant社区寻求解决多个JavaAgent的增强冲突问题,以避免类似典型问题再次出现以及顺利集成Sermant用于业务的服务治理 。
笔者尝试从字节码增强的底层逻辑的角度来分析该问题的症结 。
挂载多个JavaAgent的增强冲突问题引入SkyWalking的初衷,是希望自研JavaAgent对业务的增强和SkyWalking的链路追踪能力都能正常在业务应用上生效 。-javaagent参数是支持多次执行的,所以因此在启动应用时在JAVA_TOOL_OPTIONS中加上了-javaagent:/xxx/my-agent.jar和-javaagent:/xxx/skywalking-agent.jar参数 。
先加载自研JavaAgent后加载SkyWalking在测试时首先把自研JavaAgent放在前面 , SkyWalking放在后面, 即-javaagent:/xxx/my-agent.jar -javaagent:/xxx/SkyWalking-agent.jar 。应用启动前执行的逻辑如下图所示 。按照参数的配置顺序,应该是自研JavaAgent先对业务应用的jar包中字节码进行增强,然后再由SkyWalking进行增强,最后再执行业务应用的main()方法启动应用 。
记一次多个Java Agent同时使用的类增强冲突问题及分析

文章插图
然而启动后发现日志中SkyWalking抛出java.lang.UnsupportedOperationException异常,该异常对应的目标类是com.google.common.eventbus.Dispatcher$LegacyAsyncDispatcher 。自研JavaAgent无异常抛出 。
ERROR 2022-09-27 15:32:09:546 main SkyWalkingAgent : index=0, batch=[class com.google.common.eventbus.Dispatcher$LegacyAsyncDispatcher], types=[class com.google.common.eventbus.Dispatcher$LegacyAsyncDispatcher]Caused by: java.lang.UnsupportedOperationException: class redefinition failed: attempted to change superclass or interfacesat sun.instrument.InstrumentationImpl.retransformClasses0(Native Method)at sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:144)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at org.apache.SkyWalking.apm.dependencies.net.bytebuddy.agent.builder.AgentBuilder$RedefinitionStrategy$Dispatcher$ForJava6CapableVm.retransformClasses(AgentBuilder.java:6910)... 12 more经过确认自研JavaAgent并没有对这个类有过拦截和增强,而SkyWalking中的apm-guava-eventbus-plugin插件对该类进行了拦截和增强 。两个JavaAgent并没有同时增强同一个类,但是SkyWalking却增强失败了,有点令人费解 。初步猜测可能JavaAgent的加载顺序有关,笔者调整了顺序 , 再次进行了测试 。
先加载SkyWalking后加载自研JavaAgent调整后JAVA_TOOL_OPTIONS配置为-javaagent:/xxx/SkyWalking-agent.jar -javaagent:/xxx/my-agent.jar,应用启动前执行的逻辑如下图所示
记一次多个Java Agent同时使用的类增强冲突问题及分析

文章插图
经过调整后,发现两个JavaAgent都没有错误日志,而且各拦截点的增强也能正常生效 , 没有遇到类增强的冲突问题 。
问题表象给人的直觉是JavaAgent的加载顺序确实对字节码增强有关系 。但是为什么会出现这种现象呢?
冲突根因分析增强失败的类在两个JavaAgent中的角色上面提到,先加载自研JavaAgent后加载SkyWalking的场景中遇到SkyWalking对com.google.common.eventbus.Dispatcher$LegacyAsyncDispatcher增强失败 。Dispatcher$LegacyAsyncDispatcher这个类在SkyWalking的插件中定义为被拦截增强的类 。
经过排查发现Dispatcher$LegacyAsyncDispatcher也被自研JavaAgent中在增强过程中作为第三方依赖引入,但并未对其增强 。
Debug分析鉴于自研JavaAgent没有报错,但SkyWalking出现异常,所以对SkyWalking进行debug分析 。
在premain方法中,可以看到进入到SkyWalkingAgent时``com.google.common.eventbus.Dispatcher`已经被加载了 。观察它的类加载器,可以知道该类是在自研JavaAgent启动过程中被加载的 。是不是被加载过后的类再进行增强就会冲突呢?接着往下看 。

推荐阅读