基于 JavaAgent 代理技术实现 class 字节码插桩(bytebuddy)

先描述一个场景,生产有一个正在运行的java项目,以某 springboot-service.jar 为例,项目发布后发现了某个http接口响应较慢,此时你希望定位这个http接口执行过程中依次调用的几个主要方法的分别执行耗时,用来作为进一步解决问题的依据。你应该怎么做?

Java Agent 技术常被用于加载class文件之前进行拦截并修改字节码,以实现对Java应用的无侵入式增强。

所以本文基于 javaagent,在 JVM 加载 class 的时候,使用 bytebuddy 库动态修改 class 的方式来实现上述场景,比如我们给主要方法增加统计方法执行时间的逻辑

当然这个 bytebuddy 和 spring的 AOP 切面是有本质区别的,AOP 切面是基于 java 实例运行时进行的动态代理,bytebuddy 是直接在 class 被加载到 JVM 之前修改了 class 字节码的原理。所以使用 bytebuddy 处理过的类,对于程序中本来就存在互相依赖调用的逻辑,在执行时也是包含修改后的行为的。

应用实例

创建一个普通的 Gradle Java 工程、编写用于模拟日常项目业务的 Java 代码、添加主要依赖 shadow 和 bytebuddy 和对应配置、编写 bytebuddy 方法代理业务处理类 和 agent 入口类并使用 bytebuddy 代理 Instrumentation,最后整体实例代码工程截图和批注介绍如下:

在这里插入图片描述

代码如下,可以逐个拷贝整理到工程中执行运行查看效果:

1、build.gradle

plugins {id "java"// Shadow是一个Gradle插件,用于将项目的依赖类和资源组合到单个输出Jar中// https://imperceptiblethoughts.com/shadow/introduction/#benefits-of-shadowid "com.github.johnrengelman.shadow" version "8.1.1"
}group = "com.shanhy.example"
version = "1.0-SNAPSHOT"repositories {mavenCentral()
}// Set the JVM compatibility versions
tasks.withType(JavaCompile).configureEach {sourceCompatibility = JavaVersion.VERSION_1_8targetCompatibility = JavaVersion.VERSION_1_8compileJava.options.encoding = "UTF-8"compileTestJava.options.encoding = "UTF-8"
}dependencies {// 运行时依赖implementation 'net.bytebuddy:byte-buddy:1.12.6'implementation 'net.bytebuddy:byte-buddy-agent:1.12.6'
}shadowJar {manifest {attributes('Premain-Class': 'com.shanhy.example.agent.PreAgent')}
}

2、Hello.java

package com.shanhy.example.agent;public class Hello {private static final String NAME = "Tom";public static String hello(String name) {System.out.println("world");return "Hello ".concat(name);}public void show() {System.out.println("Hello Show, " + NAME);}public String show2() {System.out.println("Hello Show2, " + NAME);return "秀er";}}

3、ProbeTest.java

package com.shanhy.example.agent;/*** 使用方法,运行时添加VM配置* VM options:* -javaagent:工程的路径\build\libs\probe-agent-1.0-SNAPSHOT-all.jar** @author 单红宇* @date 2024-03-07 19:12:18*/
public class ProbeTest {public static void main(String[] args) {System.out.println("Probe test");new Hello().show();System.out.println(new Hello().show2());System.out.println(Hello.hello("Jack"));}}

4、PreAgent.java

package com.shanhy.example.agent;import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;import java.lang.instrument.Instrumentation;/*** 代理入口类** @author 单红宇* @date 2024-03-06 17:22:31*/
public class PreAgent {/*** premain** @param agentArgs 代理参数* @param inst      Instrumentation 是Java提供的一个接口,提供了类定义和类加载相关的服务,*                  允许在类加载期间动态地修改类文件字节码,也就是所谓的字节码增强或字节码操作*/public static void premain(String agentArgs, Instrumentation inst) {// listener 用来监听扫描和处理的class情况,比如哪些处理了,哪些发生异常了// 开发调试阶段 onError 方法很重要,可以方法签名和参数不匹配等错误问题AgentBuilder.Listener listener = new AgentBuilder.Listener() {@Overridepublic void onDiscovery(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {// System.out.println("onDiscovery: " + typeName);}@Overridepublic void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded, DynamicType dynamicType) {// System.out.println("onTransformation: " + typeDescription.getName());}@Overridepublic void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded) {// System.out.println("onIgnored: " + typeDescription.getName());}@Overridepublic void onError(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded, Throwable throwable) {System.out.println("onError: " + typeName);throwable.printStackTrace();}@Overridepublic void onComplete(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {// System.out.println("onComplete: " + typeName);}};// transform 指定的转换器来对匹配到的class进行操作AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {return builder
//                    .method(ElementMatchers.named("executeInternal")) // 拦截指定的方法executeInternal
//                    .method(ElementMatchers.any()) // 拦截任意方法.method(ElementMatchers.not(ElementMatchers.isStatic())) // 拦截非静态方法.intercept(MethodDelegation.to(com.shanhy.example.agent.MonitorMethod.class)); // 将拦截到的方法委托给目标类处理};// type 通过ElementMatcher 来匹配我们加载的classnew AgentBuilder.Default()
//                .type(ElementMatchers.nameStartsWith("com.mysql.cj.jdbc.ClientPreparedStatement")).type(ElementMatchers.nameStartsWith("com.shanhy.example.agent"))
//                .type(ElementMatchers.any()).transform(transformer)
//                .with(listener).installOn(inst);// 第二种匹配和增强规则new AgentBuilder.Default().type(ElementMatchers.nameStartsWith("com.shanhy.example.agent.Hello")).transform((builder, typeDescription, classLoader, module) ->// 所有静态方法并且排除掉静态的构造方法、排除掉void方法、排除到static构造方法builder.visit(Advice.to(com.shanhy.example.agent.MonitorStaticMethod.class).on(ElementMatchers.isStatic().and(ElementMatchers.not(ElementMatchers.returns(TypeDescription.VOID))).and(ElementMatchers.not(ElementMatchers.isTypeInitializer()))))).with(listener).installOn(inst);}}

5、MonitorMethod.java

package com.shanhy.example.agent;import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.implementation.bind.annotation.This;import java.lang.reflect.Method;
import java.util.concurrent.Callable;/*** 监听拦截方法** @author 单红宇* @date 2024-03-07 15:25:50*/
public class MonitorMethod {/*** 方法拦截处理器。* 方法使用注解 @RuntimeType 告诉 Byte Buddy 不要进行严格的参数类型检测,* 在参数匹配失败时,尝试使用类型转换方式(runtime type casting)进行类型转换,匹配相应方法** @param obj      使用 @This 注入被拦截的目标对象* @param method   使用 @Origin 注入目标方法对应的 Method 对象* @param callable 我们要在方法中调用目标方法的话,需要通过 @SuperCall 注入* @param args     使用 @AllArguments 注入目标方法的全部参数* @return 方法执行结果* @throws Exception Exception*/@RuntimeTypepublic static Object intercept(@This Object obj, @Origin Method method, @SuperCall Callable<?> callable,@AllArguments Object[] args) throws Exception {long start = System.currentTimeMillis();Object resObj = null;try {resObj = callable.call();return resObj;} finally {System.out.println("所属类名:" + method.getDeclaringClass());System.out.println("方法名称:" + method.getName());System.out.println("入参个数:" + method.getParameterCount());for (int i = 0; i < method.getParameterCount(); i++) {System.out.println("入参-" + (i + 1) + ":类型:" + method.getParameterTypes()[i].getTypeName() + " 内容:" + args[i]);}System.out.println("出参类型:" + method.getReturnType().getName());System.out.println("出参结果:" + resObj);System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms");}}}

6、MonitorStaticMethod.java

package com.shanhy.example.agent;import net.bytebuddy.asm.Advice;import java.lang.reflect.Method;/*** 监听拦截static方法。* ByteBuddy的MethodDelegation不能直接应用于静态方法。但可以使用ByteBuddy的Advice API来拦截静态方法。** @author 单红宇* @date 2024-03-07 15:25:50*/
public class MonitorStaticMethod {@Advice.OnMethodEnterpublic static void beforeStatic(@Advice.Origin Method method, @Advice.AllArguments Object[] args) {// 在方法执行前的处理逻辑System.out.print("beforeStatic >>> " + method.getName() + ", args = ");System.out.println(args != null && args.length > 0 ? args[0] : null);}/*** static方法执行结束* 注意:在使用@Advice.Return时,需要准确区分void方法或者有返回值的方法,* void方法不能包含@Advice.Return参数,并且对应的返回值类型要和方法返回值类型对应** @param method    method* @param result    result* @param throwable throwable*/@Advice.OnMethodExit(onThrowable = Throwable.class)public static void afterStatic(@Advice.Origin Method method,@Advice.Return(readOnly = false) String result,@Advice.Thrown Throwable throwable) {// 在方法执行后的处理逻辑System.out.println("afterStatic >>> " + method.getName() + ", result = " + result);// 想要修改返回值,需要设定readOnly = falseresult = "[New] " + result;System.out.println("返回值被修改为:" + result);}}

最后执行 gradle 任务 shadowJar 进行打包。

运行测试

为要运行的 Java 程序添加 VM options 参数,设置 -javaagent 指向打包生成的 probe-agent-1.0-SNAPSHOT-all.jar,例如下图所示:

在这里插入图片描述

如果是命令行运行,则示例如下(注意将 javaagent 放在前面)

java -javaagent:目录\probe-agent-1.0-SNAPSHOT-all.jar -jar demo-hello.jar

下面执行日志为本例的对 Hello 中方法拦截插桩处理后的日志:

> Task :ProbeTest.main()
Probe test
Hello Show, Tom
所属类名:class com.shanhy.example.agent.Hello
方法名称:show
入参个数:0
出参类型:void
出参结果:null
方法耗时:1ms
所属类名:class com.shanhy.example.agent.Hello
方法名称:show
入参个数:0
出参类型:void
出参结果:null
方法耗时:2ms
Hello Show2, Tom
所属类名:class com.shanhy.example.agent.Hello
方法名称:show2
入参个数:0
出参类型:java.lang.String
出参结果:秀er
方法耗时:0ms
所属类名:class com.shanhy.example.agent.Hello
方法名称:show2
入参个数:0
出参类型:java.lang.String
出参结果:秀er
方法耗时:0ms
秀er
beforeStatic >>> hello, args = Jack
beforeStatic >>> hello, args = Jack
world
afterStatic >>> hello, result = Hello Jack
返回值被修改为:[New] Hello Jack
afterStatic >>> hello, result = [New] Hello Jack
返回值被修改为:[New] [New] Hello Jack
[New] [New] Hello Jack

(END)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/520914.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

开启AI绘画新纪元:让创意在指尖绽放

文章目录 一、了解AI绘画的基本原理二、选择合适的AI绘画工具三、掌握AI绘画的基本技巧四、借鉴与创新&#xff1a;从模仿到创作五、参与社区交流&#xff0c;共同成长《AI绘画教程&#xff1a;Midjourney使用方法与技巧从入门到精通》亮点推荐内容简介作者简介目录 在科技日新…

MySQL--优化(索引--聚簇和非聚簇索引)

MySQL–优化&#xff08;索引–聚簇和非聚簇索引&#xff09; 定位慢查询SQL执行计划索引 存储引擎索引底层数据结构聚簇和非聚簇索引索引创建原则索引失效场景 SQL优化经验 一、聚簇索引 聚簇索引&#xff1a;将数据存储与索引放到了一块&#xff0c;索引结构的叶子节点保存…

测试一下 Anthropic 宣称超过 GPT-4 的 Claude 3 Opus

测试一下 Anthropic 宣称超过 GPT-4 的 Claude 3 Opus 0. 引言1. 测试 Claude 3 Opus 0. 引言 今天测试一下 Anthropic 发布的 Claude 3 Opus。 3月4日&#xff0c;Anthropic 宣布推出 Claude 3 型号系列&#xff0c;该系列在广泛的认知任务中树立了新的行业基准。该系列包括…

NLP_文本数据分析_3(代码示例)

目标 了解文本数据分析的作用.掌握常用的几种文本数据分析方法. 一、 文件数据分析介绍 文本数据分析的作用: 文本数据分析能够有效帮助我们理解数据语料, 快速检查出语料可能存在的问题, 并指导之后模型训练过程中一些超参数的选择. 常用的几种文本数据分析方法: 标签数量分…

Web——HTML

一.HTML概述 超文本标记语言&#xff08;英语&#xff1a;HyperText Markup Language&#xff0c;简称&#xff1a;HTML&#xff09;是一种用于创建网页的标准标记语言。可以使用 HTML 来建立自己的 WEB 站点&#xff0c;HTML 运行在浏览器上&#xff0c;由浏览器来解析。 二.…

图书推荐|Windows Server 2022 系统与网站配置实战

讲述桌面体验、Server Core/Nano Server&#xff0c;容器与云系统的配置 1 本书内容 《Windows Server 2022 系统与网站配置实战》秉持作者一贯理论兼具实践的写作风格&#xff0c;以新版的Windows Server 2022系统与网站配置实践为主题&#xff0c;辅以大量的实例演示&#x…

finallShell上传文件失败?

上传了一部分就直接报错&#xff0c;导致centos服务器还连接不上了&#xff0c; 后来我用网上的破解版finallshell&#xff0c;也是出现同样的情况&#xff0c; 最后没办法我用的网上推荐的另外一款ssh的客户端工具才可以上传成功。 有遇到同样情况的小伙伴&#xff0c;可以…

【小黑送书—第十一期】>>如何阅读“计算机界三大神书”之一 ——SICP(文末送书)

《计算机程序的构造和解释》&#xff08;Structure and Interpretation of Computer Programs&#xff0c;简记为SICP&#xff09;是MIT的基础课教材&#xff0c;出版后引起计算机教育界的广泛关注&#xff0c;对推动全世界大学计算机科学技术教育的发展和成熟产生了很大影响。…

git revert 撤回之前的几个指定的提交

文章目录 Intro操作命令-n 选项 参考 Intro 在开发过程中&#xff0c;有的时候一开始只是一个小需求&#xff0c;可以改着改着事情超出了控制&#xff0c;比如说我一开始只是想调整一个依赖包的版本&#xff0c;可是改到后来类库不兼容甚至导致项目无法启动。 这个时候我就想&…

音频库及分析软件介绍

搞音频的兄弟必须要看一下的&#xff0c;俗话说&#xff0c;工欲善其事必先利其器&#xff0c;好的音频分析软件&#xff0c;对于音频分析工程师来讲&#xff0c;可谓是非常重要的&#xff0c;下面由小编介绍一下&#xff1a;

Gafana Redis Overview dashboard

1. 简介 根据提供的 Redis 监控仪表盘 JSON 文件,包含的监控指标及其简要描述如下: redis_uptime_in_seconds: Redis 实例的运行时间(秒)。 redis_connected_clients: 当前连接到 Redis 实例的客户端数量。 redis_memory_used_bytes: Redis 实例使用的内存量(字节)。 redis_m…

利用CesiumJS开发模拟飞机飞行的应用(三、飞行动画)

上一节介绍了利用CesiumJS开发模拟飞机飞行的应用(添加飞行轨迹),本节介绍如何在上节基础上添加模拟飞行 飞行动画效果实现 我们将创建一个 SampledPositionProperty 来存储每个位置以及时间戳。源数据不包含每个样本的时间戳&#xff0c;但我们知道航班号为 SK936&#xff…