插桩java_我的开源项目|Java Agent与字节码改写实现业务代码插桩的BeeMite

这个项目只是开源给大家学习的,编写于2018年年底,那时候我刚学完JVM和字节码。这个周末我主要是想学习Attach API,所以我把这个项目改了一下,也遇到了点问题,然后我从Arthas的源码中寻找到了答案。

BeeMite

javaagent+asm+动态字节码插桩实现的业务代码调用链监控项目。

旧版本:master分支

在类加载之前改写字节码,插入埋点。

新版本:agentmain分支

使用Attach API,在Java程序运行期间改写字节码,重新加载类。但有个缺点,就是不能新增或移除字段。旧版本我是通过添加字段实现的,所以改的时候要改很多地方。

该项目编写于2018年年底,当时已经实现的功能有:

业务代码调用链插桩,在方法执行之前拦截获取类名、方法名,方法调用的参数,在方法执行异常时,获取到异常信息;

为统计方法执行时间插入埋点,在方法执行之前和返回之前获取系统时间。

使用方法

旧版本

1、将bee-mite模块执行maven package进行打包,获取jar包的绝对路径。

2、以项目中提供的demo为例,在IDEA中,在bee-mite-webdemo项目下,点击锤子->Edit Config....-> VM options ->输入下面内容

-javaagent:/MyProjects/asm-aop/insert-pile/target/bee-mite-1.2.0-jar-with-dependencies.jar=com.wujiuye

复制代码

等号后面是参数,意思是改写哪个包下的类。

如果报如下异常:

java.lang.VerifyError: Expecting a stackmap frame at branch target 18

复制代码

jdk1.8可以添加参数:-noverify解决

-noverify -javaagent:/MyProjects/asm-aop/insert-pile/target/insert-pile-1.0-SNAPSHOT.jar=com.wujiuye

复制代码

新版本

1、先将bee-mite模块执行maven package进行打包。

2、将bee-mite-boot模块打包,或者直接在idea中启动,这个模块只有一个类,就是BeemiteBoot,它的作用就是查询系统当前有哪些java进程,获取到进程的id,然后根据进程id,将上一步编译的bee-mite的jar包加载到目标进程。

需要告诉BeemiteBoot,bee-mite的jar包放在哪里,就是bee-mite的jar包的绝对路径。目前我是写死在代码中的,可以通过修改代码替换。

try {

vm.loadAgent("/Users/wjy/MyProjects/beemite/bee-mite/target/bee-mite-1.2.0-jar-with-dependencies.jar", pageckName + "`" + plus);

} finally {

vm.detach();

}

复制代码

3、BeemiteBoot启动起来之后,就可以根据提示一步步操作了。

找到如下Java进程,请选择:

[1] 2352

[2] 3818com.wujiuye.ipweb.DemoAppcliction

[3] 3595org.jetbrains.idea.maven.server.RemoteMavenServer36

[4] 3821org.jetbrains.jps.cmdline.Launcher

复制代码

选择对应进程之后,会提示输入目标进程的应用包名,目的是过滤只改写指定包名下的类。

应用的包名:

com.wujiuye

复制代码

输入包名后会提示选择插件,目前就两个插件,一个是监控调用链的,另一个是打印方法执行耗时的,可多选。

选择插件:

1打印调用链路插件

2打印方法执行耗时插件

结束请输入:0

复制代码

完成后,就已经成功将bee-mite的jar包加载到目标进程了。

结果展示

bee-mite-boot是一个web项目,在浏览器中输入http://127.0.0.1:8080/user/wujiuye/25,即可看到插桩后输出的日记

[接收到事件,打印日记]savaFuncStartRuntimeLog[null,com/wujiuye/ipweb/handler/UserHandler,queryUser,1585486646788]

[接收到事件,打印日记]savaBusinessFuncCallLog[null,com/wujiuye/ipweb/handler/UserHandler,queryUser]

[接收到事件,打印日记]savaFuncStartRuntimeLog[null,com/wujiuye/ipweb/service/impl/UserServiceImpl,queryUser,1585486646790]

[接收到事件,打印日记]savaBusinessFuncCallLog[null,com/wujiuye/ipweb/service/impl/UserServiceImpl,queryUser]

[接收到事件,打印日记]savaFuncEndRuntimeLog[null,com/wujiuye/ipweb/service/impl/UserServiceImpl,queryUser,1585486646791]

[接收到事件,打印日记]savaFuncEndRuntimeLog[null,com/wujiuye/ipweb/handler/UserHandler,queryUser,1585486646791]

复制代码

源码导读【agentmain分支】

core包:实现字节码插桩,在方法执行之前拦截获取类名、方法名,方法调用的参数,在方法执行异常时,获取到异常信息,以及在方法执行之前和return之前获取当前系统时间,可用于统计方法执行耗时。

tracsformer包:代码插桩过滤器,使用责任连模式,对字节码进行多次插桩,每个插桩器只负责自己想要实现的逻辑。

event包:事件的封装,埋点代码抛出的事件放入事件队列,异步分派事件给监听器进行处理。

logs包:提供事件监听器接口,具体实现可扩展,我这里提供了两个默认的实现类,默认的实现类只是将日记打印,在控制台打印日记信息。

因为字节码是插入到业务代码中的,当执行业务代码的时候会执行埋点代码,如果处理程序也在业务代码中进行,那么这将是个耗时的操作,影响性能,拖慢一次请求的响应速度,所以当埋点代码执行的时候,我是直接抛出一个事件,让线程池异步消费事件,分派事件给相应的监听器处理,这样就可以执行耗时操作,比如将日记输出到硬盘,再存储到ES,便于后期进行项目代码异常排查。

bee-mite是怎么改写class的

我在bee-mite模块中将改写后的class字节码输出到文件了,会在控制台打印输出的文件路径,可以看下输出后的class。

以UserServiceImpl为例,这个类是插桩的目标,来看下对比改写后的代码,到底bee-mite改写字节码都做了什么。

源代码

public class UserServiceImpl{

public Map queryMap(String username,Integer age){

Map map = new HashMap<>();

map.put("username", username);

map.put("age", age);

return map;

}

}

复制代码

bee-mite插桩后

@Service

public class UserServiceImpl implements UserService{

public UserServiceImpl(){

}

public Map queryUser(String var1, Integer var2){

long var3 = System.currentTimeMillis();

FuncRuntimeEvent.sendFuncStartRuntimeEvent(SessionIdContext.getContext().getSessionId(), "com/wujiuye/ipweb/service/impl/UserServiceImpl", "queryUser", var3);

try {

Object[] var8 = new Object[]{var1, var2};

BusinessCallLinkEvent.sendBusinessFuncCallEvent(SessionIdContext.getContext().getSessionId(), "com/wujiuye/ipweb/service/impl/UserServiceImpl", "queryUser", var8);

HashMap var9 = new HashMap();

var9.put("username", var1);

var9.put("age", var2);

long var5 = System.currentTimeMillis();

FuncRuntimeEvent.sendFuncEndRuntimeEvent(SessionIdContext.getContext().getSessionId(), "com/wujiuye/ipweb/service/impl/UserServiceImpl", "queryUser", var5);

return var9;

} catch (Exception var7) {

BusinessCallLinkEvent.sendBusinessFuncCallThrowableEvent(SessionIdContext.getContext().getSessionId(), "com/wujiuye/ipweb/service/impl/UserServiceImpl", "queryUser", var7);

throw var7;

}

}

}

复制代码

因为使用了责任链模式,会对代码进行两次插桩,目的就是为了后面容易扩展功能,其实只是18年时候自己的水平问题,没有想到通过暴露切点的方式实现更好,少写字节码。我简单看了下arthas的部分源码,它的实现就是改写的字节码只插入三个埋点,其它功能不再操作字节码。

相信看了对比你也能知道bee-mite都插入了哪些代码,这些代码都是通过asm写字节码指令插入的。当然也不是很难,要说难就是try-catch代码块的插入了,没有文档看还是很难摸索出来的。

visitTryCatchBlock方法的三个label的位置,以及catch块处理异常算是个难点,我最终通过在源码类中添加try-catch块,然后javap查看字节码的异常处理表。

* Exception table:

* from to target type

* 0 27 30 Class java/lang/Exception

复制代码

那么三个label对应的就是from、to、target了。当type为any的时候就是try-finally了。

END

欢迎大家下载这个开源项目的源码学习,但这个项目后续不再维护更新。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值