Java Agent与字节码改写实现业务代码插桩的BeeMite

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

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

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

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

业务代码调用链插桩,在方法执行之前拦截获取类名、方法名,方法调用的参数,在方法执行异常时,获取到异常信息;
为统计方法执行时间插入埋点,在方法执行之前和返回之前获取系统时间。

1
使用方法

旧版本的使用

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] 3818 com.wujiuye.ipweb.DemoAppcliction
[3] 3595 org.jetbrains.idea.maven.server.RemoteMavenServer36
[4] 3821 org.jetbrains.jps.cmdline.Launcher

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

应用的包名:
com.wujiuye

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

选择插件:
1 打印调用链路插件
2 打印方法执行耗时插件
结束请输入:0

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

4、结果展示

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]

2
BeeMite是怎么改写Class的

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

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

源代码

public class UserServiceImpl {

public Map<String, Object> queryMap(String username,Integer age) {
    Map<String, Object> 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<String, Object> 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了。

3
源码导读(agentmain分支)

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

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

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

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

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

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

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值