笔者考试测验从字节码增强的底层逻辑的角度来剖析该问题的症结。
挂载多个JavaAgent的增强冲突问题引入SkyWalking的初衷,是希望自研JavaAgent对业务的增强和SkyWalking的链路追踪能力都能正常在业务运用上生效。-javaagent参数是支持多次实行的,以是因此在启动运用时在JAVA_TOOL_OPTIONS中加上了-javaagent:/xxx/my-agent.jar和-javaagent:/xxx/skywalking-agent.jar参数。
在测试时首先把自研JavaAgent放在前面,SkyWalking放在后面, 即-javaagent:/xxx/my-agent.jar -javaagent:/xxx/SkyWalking-agent.jar。运用启动前实行的逻辑如下图所示。按照参数的配置顺序,该当是自研JavaAgent先对业务运用的jar包中字节码进行增强,然后再由SkyWalking进行增强,末了再实行业务运用的main()方法启动运用。

然而启动后创造日志中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,运用启动前实行的逻辑如下图所示
经由调度后,创造两个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启动过程中被加载的。是不是被加载过后的类再进行增强就会冲突呢?接着往下看。
剖析源码可知SkyWalking利用的是Byte Buddy字节码增强工具,AgentBuilder作为其供应字节码增强的接口,SkyWalking中利用到的是如下的默认的AgentBuilder$Default,个中的RedefinitionStrategy规定了已加载的类如何被构建的JavaAgent修正字节码,RedefinitionStrategy.DiscoveryStrategy则规定了创造哪些类来进行字节码的重定义,该默认策略利用的是RedefinitionStrategy.DiscoveryStrategy.SinglePass
/ Creates a new agent builder with default settings. By default, Byte Buddy ignores any types loaded by the bootstrap class loader, any type within a {@code net.bytebuddy} package and any synthetic type. Self-injection and rebasing is enabled. In order to avoid class format changes, set {@link AgentBuilder#disableClassFormatChanges()}. All types are parsed without their debugging information ({@link PoolStrategy.Default#FAST}). @param byteBuddy The Byte Buddy instance to be used. / public Default(ByteBuddy byteBuddy) { this(byteBuddy, Listener.NoOp.INSTANCE, DEFAULT_LOCK, PoolStrategy.Default.FAST, TypeStrategy.Default.REBASE, LocationStrategy.ForClassLoader.STRONG, NativeMethodStrategy.Disabled.INSTANCE, WarmupStrategy.NoOp.INSTANCE, TransformerDecorator.NoOp.INSTANCE, new InitializationStrategy.SelfInjection.Split(), RedefinitionStrategy.DISABLED, RedefinitionStrategy.DiscoveryStrategy.SinglePass.INSTANCE, RedefinitionStrategy.BatchAllocator.ForTotal.INSTANCE, RedefinitionStrategy.Listener.NoOp.INSTANCE, RedefinitionStrategy.ResubmissionStrategy.Disabled.INSTANCE, InjectionStrategy.UsingReflection.INSTANCE, LambdaInstrumentationStrategy.DISABLED, DescriptionStrategy.Default.HYBRID, FallbackStrategy.ByThrowableType.ofOptionalTypes(), ClassFileBufferStrategy.Default.RETAINING, InstallationListener.NoOp.INSTANCE, new RawMatcher.Disjunction( new RawMatcher.ForElementMatchers(any(), isBootstrapClassLoader().or(isExtensionClassLoader())), new RawMatcher.ForElementMatchers(nameStartsWith("net.bytebuddy.") .and(not(ElementMatchers.nameStartsWith(NamingStrategy.BYTE_BUDDY_RENAME_PACKAGE + "."))) .or(nameStartsWith("sun.reflect.").or(nameStartsWith("jdk.internal.reflect."))) .<TypeDescription>or(isSynthetic()))), Collections.<Transformation>emptyList()); }
RedefinitionStrategy.DiscoveryStrategy.SinglePass源码中的resolve()方法返回的是instrumentation.getAllLoadedClasses(),也便是说,该方法将返回JVM当前加载的所有类的凑集。由此可以看出,AgentBuilder$Default将会对所有在JVM中已加载的类进行筛选(也包括其内部类)。上文提到com.google.common.eventbus.Dispatcher和其内部类都在个中。RedefinitionStrategy作为字节码redefine的策略将浸染于字节码增强的retransform过程。
/ A strategy for discovering types to redefine. /public interface DiscoveryStrategy { / Resolves an iterable of types to retransform. Types might be loaded during a previous retransformation which might require multiple passes for a retransformation. @param instrumentation The instrumentation instance used for the redefinition. @return An iterable of types to consider for retransformation. / Iterable<Iterable<Class<?>>> resolve(Instrumentation instrumentation); / A discovery strategy that considers all loaded types supplied by {@link Instrumentation#getAllLoadedClasses()}. / enum SinglePass implements DiscoveryStrategy { / The singleton instance. / INSTANCE; / {@inheritDoc} / public Iterable<Iterable<Class<?>>> resolve(Instrumentation instrumentation) { return Collections.<Iterable<Class<?>>>singleton(Arrays.<Class<?>>asList(instrumentation.getAllLoadedClasses())); } }
在AgentBuilder中,retransform过程如下图进行。首先AgentBuilder在构建过程中会根据重定义策略来对JVM中当前已加载的所有类来进行筛选处理,实行到Dispatcher#retransformClasses()时已经筛选出JVM已加载的类和SkyWalking声明要增强的类的交集,终极将通过反射调用到字节码增强的底层实现逻辑Instrumentation#retransformClasses(),通过native方法retransformClasses0()来完成末了的处理。
上文所述产生冲突的类com.google.common.eventbus.Dispatcher$LegacyAsyncDispatcher就在Instrumentation#retransformClasses()要处理的类的凑集中。
根因探究
剖析到这一步,可以初步看出该当是retransformClasses()方法的某些限定作成冲突的类碰着前面的的java.lang.UnsupportedOperationException非常的抛出。因此接下来剖析下Instrumentation的实现逻辑。
transform在利用java.lang.instrument.Instrumentation接口进行字节码增强操作时,我们必要利用的方法便是:
void addTransformer(ClassFileTransformer transformer, boolean canRetransform)
通过此方法,我们可以为我们想要操作的类添加一个ClassFileTransFormer,顾名思义其为类文件转换器,其官方描述如下:
All future class definitions will be seen by the transformer, except definitions of classes upon which any registered transformer is dependent. The transformer is called when classes are loaded, when they are redefined. and if canRetransform is true, when they are retransformed.
大略来讲,在对一个类注册了该转换器后,未来该类的每一次redefine以及retransform,都会被该转换器检讨到,并且实行该转换器的操作。
由上述描述可以知道,我们想要做的字节码增强操作便是通过向JVM中添加转换器并且通过转换器将JVM中的类转换为我们想要的结果(Transform a class by transfomer.)流程如下:
首先通过premain方法运行JavaAgent,此时在premain参数中我们可以获取到Instrumentation,第二步通过Instrumentation接口将实现的ClassFileTransfomer注册到JVM上,当JVM去加载类的时候,ClassFileTransfomer会得到类的字节数组,并对其进行transform后再返回给JVM,此后该类在Java程序中的表现便是转换之后的结果。
retransform上述为类加载时Instrumentation在个中所做的事情,但是如果类以及被加载完成后,想要再次对其做转换(适用于多个JavaAgent场景及通过agentmain办法运行JavaAgent),就须要利用到Instrumentation接口为我们供应的如下方法:
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException
其官方描述如下:
This function facilitates the instrumentation of already loaded classes. When classes are initially loaded or when they are redefined, the initial class file bytes can be transformed with the ClassFileTransformer. This function reruns the transformation process (whether or not a transformation has previously occurred)
这个方法将用于对已经加载的类进行插桩,并且是从最初类加载的字节码开始重新运用转换器,并且每一个被注册到JVM的转换器都将会被实行。
通过这个方法,我们就可以对已经被加载的类进行transform,实行该方法后的流程如下,实在便是重新触发ClassFileTransformer中的transform方法:
值得把稳的是,reTransformClasses 功能很强大,但是其也有一系列的限定,在官方文档描述中,其限定如下:
The retransformation must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance.
重转换过程中,我们不能新增、删除或者重命名字段和方法,不能变动方法的署名,不能变动类的继续。
字节码剖析上述reTransformClasses方法的限定是否是问题产生的根因呢?
在反编译经由SkyWalking增强后的字节码文件后,缘故原由底细毕露。类经由Skywalking增强之后的继续关系上多了implements EnhancedInstance。这显然改变了类的继续关系,而这一点恰好是官网接口文档中明确描述的限定行为。正是由于这个接口的实现导致了本文开头描述的多个JavaAgent的类冲突增强失落败的问题。
该问题在SkyWalking的社区中也有一个干系issue,社区阐明为了减少链路追踪过程中的反射调用确实冲破了reTransformClasses()的限定,类增强后新增实现了一个接口。
final class Dispatcher$LegacyAsyncDispatcher extends Dispatcher implements EnhancedInstance { private final ConcurrentLinkedQueue<com.google.common.eventbus.Dispatcher.LegacyAsyncDispatcher.EventWithSubscriber> queue; private volatile Object _$EnhancedClassField_ws; private Dispatcher$LegacyAsyncDispatcher() { this.queue = Queues.newConcurrentLinkedQueue(); } void dispatch(Object var1, Iterator<Subscriber> var2) { delegate$51c0bj0.intercept(this, new Object[]{var1, var2}, cachedValue$P524FzM0$7gcbrk1, new JKwtdbN5(this)); } public void setSkyWalkingDynamicField(Object var1) { this._$EnhancedClassField_ws = var1; } public Object getSkyWalkingDynamicField() { return this._$EnhancedClassField_ws; } static { ClassLoader.getSystemClassLoader().loadClass("net.bytebuddy.dynamic.Nexus").getMethod("initialize", Class.class, Integer.TYPE).invoke((Object)null, Dispatcher$LegacyAsyncDispatcher.class, -1207479570); cachedValue$P524FzM0$7gcbrk1 = Dispatcher$LegacyAsyncDispatcher.class.getDeclaredMethod("dispatch", Object.class, Iterator.class); }}
现在JavaAgent技能越来越受到各大厂商和开源社区的青睐,呈现出不少精良的JavaAgent框架。开拓者或厂商在利用JavaAgent的时候难免会碰着同时挂载多个JavaAgent的场景,如果JavaAgent开拓方能够对其他同类框架做到良好的兼容性,将会给利用者带来更少的麻烦,毕竟利用者未必能透彻的理解字节增强的底层事理。
上文经由剖析已经找到多个JavaAgent类增强冲突的根因,那么该如何避免此类问题涌现呢?这里给出两点较为通用的建议。
谨慎安排JavaAgent的挂载顺序前面我们提到SkyWalking和自研JavaAgent加载顺序会有不同的结果。SkyWalking增强时对类的继续关系有修正,而自研JavaAgent则没有,那么该场景将兼容性相对较低的SkyWalking放在前面,兼容性相对较高的自研JavaAgent放在后面,可以暂时规避类增强的冲突问题。
严格遵守字节码增强的利用哀求和限定但是如果我们须要利用3个乃至更多的JavaAgent,上面的方法是治标不治本的。
无论是Byte Buddy、Javassist还是ASM,底层实现都离不开JDK1.5之后引入的Instrumentation接口。既然官方接口的设计理念是reTransformClasses()增强类时不能新增、删除或者重命名字段和方法,不能变动方法的署名,也不能变动类的继续关系,那作为JavaAgent的框架开拓者,该当不要做出超越上述限定的设计,否则极易导致JavaAgent之间的兼容性问题涌现。不仅仅是这个接口,JavaAgent框架的开拓者也须要遵照所有的字节码增强的底层接口的设计理念,毕竟有规则才有秩序。
Sermant避免类增强冲突的实践首先,在自身字节码增强生效的问题上,Sermant严格遵守了上述的字节码增强的官方限定,未改变类的原始继续关系或类方法的署名等,在利用中都未碰着因多个JavaAgent兼容性导致Sermant的字节码增强失落效的问题。只须要把Sermant放在末了挂载,基本可以杜绝上文范例的类增强的冲突问题发生。
其次,Sermant不仅要保护自身增强不受其他JavaAgent影响,也考虑到避免Sermant对其他JavaAgent的影响。Sermant操持将premain方法中对第三方依赖的利用进行
目前市情上和社区的JavaAgent大都是定位于链路追踪或者运用监控领域,Sermant基于做事管理的自身定位,和其他主流JavaAgent不是相互替代的关系,而是友好共存的关系。利用者挂载多个JavaAgent的场景大概并不少见,Sermant避免JavaAgent类增强冲突的做法不仅可以担保客户的业务做事可以不受滋扰地利用Sermant供应的限流降级、做事注册、负载均衡、标签路由、优雅高下线、动态配置这些微做事管理能力,也能不滋扰客户利用的其他JavaAgent按部就班的事情。