一、使用
1、plugin
在这种模式下,one-java-agent在加载plugin时,会从以下几个方面进行,分别是
描述 | |
---|---|
plugin.properties | 这个文件是一个引导文件,里面的配置内容引导plugin加载 |
plugin-jar | plugin的引导包,内有实现PluginActivator,用于引导和管理plugin的初始化和销毁 |
instrument-lib | 这个目录存放plugin的代理逻辑和Instrument类 |
在项目中有demo,以dubbo-test-plugin为例,在${oneagent.home}路径下新建目录,例如dubbo,该目录的内容如下:
plugin.properties文件内容如下:
在运行dubbo应用时添加相应的启动参数:
java -javaagent:${oneagent.home}/one-java-agent.jar=oneagent.home=${oneagent.home} org.apache.dubbo.springboot.demo.provider.ProviderApplication
如果是在idea则是在run/debug configurations 添加运行配置:
Dubbo consumer应用也是一样的,运行发起调用后,会输出instrument-jar中添加的增强逻辑。
在dubbo-test-plugin中是对org.apache.dubbo.rpc.Invoker 接口的实现类在invoke()方法中都添加日志,内容如下:
2、agent
one-java-agent 对于原生agent jar的支持也是非常简单直接的,在插件目录下面放一个plugin.properties,并且放上原生的agent jar文件。
plugin.properties配置内容例如:
type=traditional
name=demo-agent
version=1.0.0
agentJarPath=demo-agent.jar
则 one java agent会启动这个demo-agent。
二、设计
One java agent的代码量并不多,设计也非常巧妙,模块示意图如下:
one-java-agent的引导就是Bootstrap,会先初始化三个component,分别管理classloader, class share, 和class file transformer, 然后再从oneagent.home目录加载plugin的信息,找到PluginActivator,逐个激活plugin,在这个过程中会找到plugin中的instrument信息(增强类),创建InstrumentTransformer,然后就交由Java的Instrumentation完成后面的转换逻辑。
这里提一下,InstrumentTransformer中关于字节码的处理是通过alibaba-bytekit实现的。
三、alibaba-bytekit与bytebuddy
ByteKit和byte-buddy都是基于ASM提供更高层的字节码处理能力, 两者都提供丰富的注入点支持。
1、两者比较
ByteKit是阿里开源的字节码处理插件,在其开源的介绍内容中有对比byte-buddy。
功能 | 函数Enter/Exit注入点 | 绑定数据 | inline | 防止重复增强 | 避免装箱/拆箱开销 | origin调用替换 | @ExceptionHandler |
---|---|---|---|---|---|---|---|
ByteKit | @AtEnter @AtExit @AtExceptionExit @AtFieldAccess @AtInvoke @AtInvokeException @AtLine @AtSyncEnter @AtSyncExit @AtThrow |
this/args/return/throw field locals 子调用入参/返回值/子调用异常 line number |
✓ | ✓ | ✓ | ✓ | ✓ |
ByteBuddy | OnMethodEnter @OnMethodExit @OnMethodExit#onThrowable() |
this/args/return/throw field locals |
✓ | ✗ | ✓ | ✓ | ✓ |
传统AOP | Enter Exit Exception |
this/args/return/throw | ✗ | ✗ | ✗ | ✗ | ✗ |
2、实验-入参替换
对于ByteKit和bytebuddy进行了一次在agent场景下修改方法入参的实验,结合两者使用,就编码、调试方面进行对比总结。
2.1、 ByteKit
ByteKit的是直接在原方法中修改字节码,场景如下:
原方法:
public String invokeList(List<String> list) {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (String s : list) {
if(sb.length()>1){
sb.append(", ");
}
sb.append(s);
}
sb.append("]");
return sb.toString();
}
apm增强方法:
@Instrument(Interface = "org.apache.dubbo.springboot.demo.provider.Caller")
public abstract class Caller {
public String invokeList(List<String> list) {
if(list==null || list.size()<2){
String result = InstrumentApi.invokeOrigin();
System.err.println(result);
return "changed: " + result;
}
List<String> ls = list;
String results = "";
for (String s : ls) {
list = Arrays.asList(s);
String result = InstrumentApi.invokeOrigin();
results += result + ",";
}
System.err.println(results);
return "changed: " + results;
}
}
ByteKit处理后新的方法(反编译后的代码):
public String invokeList(List<String> list) {
if (list == null || list.size() < 2) {
List<String> list2 = list;
InvokeOriginDemo invokeOriginDemo = this;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("[");
for (String string : list2) {
if (stringBuilder.length() > 1) {
stringBuilder.append(", ");
}
stringBuilder.append(string);
}
stringBuilder.append("]");
String result = stringBuilder.toString();
System.err.println(result);
return "changed: " + result;
}
List<String> ls = list;
String results = "";
for (String s : ls) {
List<String> list3 = list = Arrays.asList(s);
InvokeOriginDemo invokeOriginDemo = this;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("[");
for (String string : list3) {
if (stringBuilder.length() > 1) {
stringBuilder.append(", ");
}
stringBuilder.append(string);
}
stringBuilder.append("]");
String result = stringBuilder.toString();
results = results + result + ",";
}
System.err.println(results);
return "changed: " + results;
}
idea调试:
2.2、bytebuddy
本次实验中采用bytebuddy的intercept Implementation 方式,实验代码如下:
进行插桩处理:
private static void bytebuddyTransform(Instrumentation instrumentation) {
new AgentBuilder.Default()
.type(ElementMatchers.named("com.saleson.learn.java.agent.Caller"))
.transform(
(builder, type, classLoader, module) -> {
DynamicType.Builder.MethodDefinition.ImplementationDefinition tImplementationDefinition = null;
tImplementationDefinition = builder.method(ElementMatchers.any());
return tImplementationDefinition.intercept(MethodDelegation.to(TimeInterceptor.class));
}
).installOn(instrumentation);
}
增强代码:
public static class TimeInterceptor {
/**
* 进行方法拦截, 注意这里可以对所有修饰符的修饰的方法(包含private的方法)进行拦截
*
* @param method 待处理方法
* @param callable 原方法执行
* @param args 方法入参
* @return 执行结果
*/
@RuntimeType
public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable, @AllArguments Object[] args) throws Exception {
List<String> list = (List<String>) args[0];
Field argField1 = callable.getClass().getDeclaredFields()[1];
argField1.setAccessible(true);
boolean argsEquals = list.equals(argField1.get(callable));
System.out.println("call arg equals args[0]: " + argsEquals);
String results = "";
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
List<String> l = Arrays.asList(s);
argField1.set(callable, l);
String res = (String) callable.call();
results += res + ", ";
System.out.println(String.format("$d/%d $s -> %s", i + 1, list.size(), s, res));
}
return "changed: " + results;
}
}
Idea 调试:
2.3、入参替换对比总结
本次实验主要测试ByteKit和bytebuddy关于方法入参替换的场景,仅针对这个场景而言,个人觉得bytebuddy设计的更优雅,能够将增强的代码和原代码做到有效隔离,而ByteKit是直接修改了原方法的字节码,对于增强代码无法调试,同时增强代码和原代码相当于是揉合在一块。
对于bytebuddy个人也觉得对于该场景仍有可优化的地方,比如添加修改方法入参的方法,对方法入参的处理更优雅。