背景 byte-buddy-agent 是 ByteBuddy 的一个组件, 用于快速构建出一个 JavaAgent. SkyWalking-java 是一个 SkyWalking 为 Java 准备的运行时代码生成器, 适配了众多中间件以提供定制的监控能力.
SkyWalking-java 本质是一个 java agent ,并在其中使用了 byte-buddy-agent.
本文会介绍 SkyWalking-agent 如何使用 byte-buddy-agent 来做到代码增强的.
ByteBuddy 基础用法 在深入 SkyWalking-agent 和 byte-buddy-agent 之前, 我们先来看下 ByteBuddy 是如何在调用一个方法前后插入日志的.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class MemoryDatabase { public List<String> load (String info) { return Arrays.asList(info + ": foo" , info + ": bar" ); } } class LoggerInterceptor { public static List<String> log (@SuperCall Callable<List<String>> zuper) throws Exception { System.out.println("Calling database" ); try { return zuper.call(); } finally { System.out.println("Returned from database" ); } } } MemoryDatabase loggingDatabase = new ByteBuddy () .subclass(MemoryDatabase.class) .method(named("load" )) .intercept(MethodDelegation.to(LoggerInterceptor.class)) .make() .load(getClass().getClassLoader()) .getLoaded() .newInstance();
上面是一个简单的 ByteBuddy 例子. 它增强了 MemoryDatabase 类, 会在调用 MemoryDatabase.load 前后,打印输出一些信息. ByteBuddy API 每行代码的含义都已经写在注释中了. 可以想象, 在SkyWalking agent 中, 也是构造了类似的代码增强, 在调用前后加入了 SkyWalking 埋点.
SkyWalking-java 简要分析 我们会先分析中间件如何接入 SkyWalking-java , 然后分析 SkyWalking-java 如何接入 byte-buddy-agent.
插件定义 SkyWalking-java 提供了一种比较简单的 API ,来供中间件接入. 我们以 SOFA RPC 为例子看下.
一个埋点的接入代码只有两个类,以及一个配置文件,直接上代码:
SofaRpcConsumerInstrumentation.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class SofaRpcConsumerInstrumentation extends ClassInstanceMethodsEnhancePluginDefine { private static final String ENHANCE_CLASS = "com.alipay.sofa.rpc.filter.ConsumerInvoker" ; private static final String INTERCEPT_CLASS = "org.apache.skywalking.apm.plugin.sofarpc.SofaRpcConsumerInterceptor" ; @Override protected ClassMatch enhanceClass () { return NameMatch.byName(ENHANCE_CLASS); } @Override public ConstructorInterceptPoint[] getConstructorsInterceptPoints() { return null ; } @Override public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() { return new InstanceMethodsInterceptPoint [] { new InstanceMethodsInterceptPoint () { @Override public ElementMatcher<MethodDescription> getMethodsMatcher () { return named("invoke" ); } @Override public String getMethodsInterceptor () { return INTERCEPT_CLASS; } @Override public boolean isOverrideArgs () { return false ; } } }; } }
SofaRpcConsumerInstrumentation 继承了 ClassInstanceMethodsEnhancePluginDefine ,定义了 SOFA RPC 插件如何工作,它通过接口指定了三个属性:
enhanceClass: 需要增强哪个类, 这里是增强 com.alipay.sofa.rpc.filter.ConsumerInvoker
getConstructorsInterceptPoints: 指定构造函数如何增强: 这个插件不需要构造函数增强
getInstanceMethodsInterceptPoints: 指定方法如何增强, 这里返回了一个 InstanceMethodsInterceptPoint
数组, 数组中每个元素代表一个增强点
接着我们看下 InstanceMethodsInterceptPoint
是怎么定义的:
getMethodsMatcher: 指定了如何找到需要增强的方法, 这里指定的是名为 invoke
的方法
getMethodsInterceptor: 指定如何增强刚刚指定的invoke
方法, 这里指定的是 org.apache.skywalking.apm.plugin.sofarpc.SofaRpcConsumerInterceptor
类, 这个类下文会分析.
isOverrideArgs: 这个属性指定了是否需要修改修改 invoke
的入参,这个例子中我们不需要修改,所以返回 false
SofaRpcConsumerInterceptor.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class SofaRpcConsumerInterceptor implements InstanceMethodsAroundInterceptor { public static final String SKYWALKING_PREFIX = "skywalking." ; @Override public void beforeMethod (EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, MethodInterceptResult result) throws Throwable { final ContextCarrier contextCarrier = new ContextCarrier (); final String operationName = generateOperationName(providerInfo, sofaRequest); AbstractSpan span = ContextManager.createExitSpan(operationName, contextCarrier, host + ":" + port); span.setComponent(ComponentsDefine.SOFARPC); SpanLayer.asRPCFramework(span); } @Override public Object afterMethod (EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Object ret) throws Throwable { SofaResponse result = (SofaResponse) ret; if (result != null && result.isError()) { dealException((Throwable) result.getAppResponse()); } ContextManager.stopSpan(); return ret; } @Override public void handleMethodException (EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) { dealException(t); } }
SofaRpcConsumerInterceptor
会重载三个方法, 分别在 com.alipay.sofa.rpc.filter.ConsumerInvoker#invoke()
调用之前,之后,以及发生异常时被回调.
配置文件为:skywalking-plugin.def
1 sofarpc=org.apache.skywalking.apm.plugin.sofarpc.SofaRpcConsumerInstrumentation
这里面配置了 SofaRpcConsumerInstrumentation
类.
适配 ByteBuddy 通过上文的配文件,SkyWalking-java 可以获取到所有的 PluginDefine 类. SkyWalking-java 会加载这些类, 并将他们统一交给 PluginFinder
管理.
PluginFinder
的 buildMatch()
方法生成一个 ElementMatcher 来匹配到所有需要被加载的类型. 以 SOFA RPC 为例就是 com.alipay.sofa.rpc.filter.ConsumerInvoker
类.
PluginFinder
会被传递给 org.apache.skywalking.apm.agent.SkyWalkingAgent.Transformer
来构造一个 AgentBuilder.Transformer
. 可以被上面 ElementMatcher 匹配的类,都会被 Transformer 增强.
增强逻辑如下:
处理每个匹配 ElementMatcher 的类
通过 PluginFinder
找到这个类对应的 PluginDefine
调用 PluginDefine
的 define
方法对方法进行增强:
获取 PluginDefine
定义的 InstanceMethodsInterceptPoints
遍历 InstanceMethodsInterceptPoints
, 通过 MethodsMatcher
确定需要增强的方法, 通过 getMethodsInterceptor
获取如何增强方法. 并将 Interceptor 注入到 InstMethodsInter
中.
将 MethodsMatcher
和 InstMethodsInter
传递给 ByteBuddy 就完成了 JavaAgent 的配置.
下面我们看下 InstMethodsInter
是什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 public class InstMethodsInter { private static final ILog LOGGER = LogManager.getLogger(InstMethodsInter.class); private InstanceMethodsAroundInterceptor interceptor; public InstMethodsInter (String instanceMethodsAroundInterceptorClassName, ClassLoader classLoader) { try { interceptor = InterceptorInstanceLoader.load(instanceMethodsAroundInterceptorClassName, classLoader); } catch (Throwable t) { throw new PluginException ("Can't create InstanceMethodsAroundInterceptor." , t); } } @RuntimeType public Object intercept (@This Object obj, @AllArguments Object[] allArguments, @SuperCall Callable<?> zuper, @Origin Method method) throws Throwable { EnhancedInstance targetObject = (EnhancedInstance) obj; MethodInterceptResult result = new MethodInterceptResult (); try { interceptor.beforeMethod(targetObject, method, allArguments, method.getParameterTypes(), result); } catch (Throwable t) { LOGGER.error(t, "class[{}] before method[{}] intercept failure" , obj.getClass(), method.getName()); } Object ret = null ; try { if (!result.isContinue()) { ret = result._ret(); } else { ret = zuper.call(); } } catch (Throwable t) { try { interceptor.handleMethodException(targetObject, method, allArguments, method.getParameterTypes(), t); } catch (Throwable t2) { LOGGER.error(t2, "class[{}] handle method[{}] exception failure" , obj.getClass(), method.getName()); } throw t; } finally { try { ret = interceptor.afterMethod(targetObject, method, allArguments, method.getParameterTypes(), ret); } catch (Throwable t) { LOGGER.error(t, "class[{}] after method[{}] intercept failure" , obj.getClass(), method.getName()); } } return ret; } }
InstMethodsInter
与上文 ByteBuddy 基础用法 中的 LoggerInterceptor
是同一类型的东西, ByteBuddy 会将需要增强的类代理给 intercept
方法. 可以看 intercept
方法的参数上都带上了 ByteBuddy
的注解:
This : 代表当前代理类的实例
AllArguments : 原始调用的参数列表
SuperCall : 代表原始调用
Origin : 代表原始方法
通过 InstMethodsInter
, SkyWalking-java 在需要增强的方法前后及异常处理阶段增加了一些回调, 回调给 PluginDefine
中定义的 Interceptor
.
ByteBuddy Agent 原理简介 在开始社介绍 ByteBuddy 原理之前, 可以先看下 Java Agent 原理简介 来回顾下原生 javaagent 是怎么工作的:
我们向 Instrumentation
注册一些 Transformer
,在类加载期间 Transformer
的 transform
方法就会被调用. transform
方法会修改类的二进制表示(字节码), 从而实现类增强.
那么 ByteBuddy 是如何适配原生 javaagent 这种模式的呢?
ByteBuddy 内部有一个适配器 net.bytebuddy.agent.builder.AgentBuilder.Default.ExecutingTransformer
将原生 transform 方法适配到 net.bytebuddy.agent.builder.AgentBuilder.Transformer
的方法上. 然后通过 net.bytebuddy.dynamic.DynamicType.Builder
构造新的类的字节码, 返回给 Instrumentation
.