资讯动态

ASM 字节码编程实战:从零用 ClassWriter 动态生成类到无侵入式方法增强

发布时间:2026/9/24 15:44:57 来源:尧图企业网站定制
文档教程后端【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址https://gitcode.com/gh_mirrors/code/CodeGuide点击查看免费下载本文是 CodeGuide 仓库中 ASM 字节码编程入门教程 的完整技术展开。文章以该教程为主线结合仓库内 ASM 使用手册译文、JVM 指令表 以及 JavaAgentASM 字节码插桩续篇 等资料逐层拆解 ASM 的核心 API 与真实用法。读完本文你将掌握用 ASM 动态生成 class 文件、用字节码指令手写 HelloWorld 与两数求和、以及在不改动业务源码的前提下为既有方法植入耗时监控、入参采集和外部调用增强的完整套路。一、为什么说只写 CRUD就碰不到这类技术大多数程序员常年忙于业务 CRUD 开发或奔波于日常维护与修复 Bug 的路上。当无法从中吸取技术营养、无法改变现状时人就像一台恒定运行的机器逃不出限定宇宙速度的一个圈。加班太晚没时间学习、周末琐事太多没精力投入、放假计划太满没空闲安排——学习就这样被一再搁置直到年龄与能力不匹配时才开始后悔。尤其是一线编码的技术人除了在 SSM 等框架里写业务代码往往会遇到一种不知道自己不会什么的学习瓶颈。原教程给出了一份技术自检清单你可以对照看看自己了解多少1. javaagent 2. asm 3. jvmti 4. javaassit 5. netty 6. 算法搜索引擎 7. cglib 8. 混沌工程 9. 中间件开发 10. 高级测试压力测试、链路测试、流量回放、流量染色 11. 故障系列突袭、重现、演练 12. 分布式的数据一致性 13. 文件操作es、hive 14. 注册中心zookeeper、Eureka 15. 互联网工程开发技术栈spring、mybatis、网关、rpc(thrift, grpc, dubbo)、mq、缓存redis、分库分表、定时任务、分布式事务、限流、熔断、降级 16. 数据库binlog解析 17. 架构设计DDD领域驱动设计、微服务、服务治理 18. 容器k8s, docker 19. 分布式存储ceph 20. 服务istio 21. 压测 jmeter 22. Jenkins-部署java代码项目 ansible 23. 全链路监控分布式追踪 24. 语音识别、语音合成 26. lvs nginx haproxy iptables 27. hadoop mapreduce hive sqoop hbase flink kylin druid这些高级技术可能平时开发中看不到却一直以某个支撑服务的形式存在于你使用的框架里。其中ASM就是一个典型的例子它的典型用途包括类的代理如 cglib混沌工程反向工程逆向分析编译后的类结合javaagent做到非侵入式监控方法耗时、日志、机器性能等等破解对字节码进行分析与修改。二、ASM 是什么一个 Java 字节码操控框架ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里这些类文件拥有足够的元数据来解析类中的所有元素类名称、方法、属性以及 Java 字节码指令。ASM 从类文件中读入信息后能够改变类行为分析类信息甚至能够根据用户要求生成新类。这段定义点出了 ASM 的三大能力分析读取并解析已编译的 class、生成直接产出二进制 class 文件、转换在类加载前修改类行为。原教程同时整理了一份 《ASM 使用手册》译文方便随时查阅。结合手册 1.2.2 模型 可以知道ASM 提供两套 API基于事件的 API核心 API类被表示为一串事件每个事件对应类的一个元素类头、字段、方法声明、指令等。类分析器ClassReader为每个被分析的元素产生事件类写入器ClassWriter由事件序列生成编译后的类。它速度快、内存占用小但同一时刻只能看到类中的一个元素转换实现难度更高基于对象的 API树 API类被表示为一棵对象树每个对象代表类的一部分。它构建在事件 API 之上可以在内存中拿到整个类使用更方便但更慢、更占内存。本文所有实战案例都基于事件 API它是 ASM 应用最广、性能最优的用法也是后续 JavaAgent 插桩、中间件监控的底层基石。三、环境配置与工程结构原教程的环境配置如下jdk 1.8idea 2019.3.1asm-commons 6.2.1配套的入门工程分为四个模块每个模块对应一个渐进式的学习目标itstack-demo-asm-01字节码编程HelloWorlditstack-demo-asm-02字节码编程两数之和itstack-demo-asm-03字节码增强输出入参itstack-demo-asm-04字节码增强调用外部方法。关于依赖的组织方式手册 1.3 组织形式 说明 ASM 按 jar 分包分发org.objectweb.asm与org.objectweb.asm.signature包定义事件 API 及分析器/写入器组件位于asm.jarorg.objectweb.asm.util包提供开发调试工具位于asm-util.jarorg.objectweb.asm.commons包提供预定义类转换器本文用到的AdviceAdapter就在这个包里位于asm-commons.jarorg.objectweb.asm.tree包定义树 API位于asm-tree.jarorg.objectweb.asm.tree.analysis包提供类分析框架位于asm-analysis.jar。所以在 Maven 工程中引入asm-commons时通常会连带引入asm、asm-tree、asm-util、asm-analysis等模块以保证ClassWriter、ClassReader、ClassVisitor、AdviceAdapter等类全部可用。四、HelloWorld 还可以这样写你熟悉的 HelloWorld 是这样的public class HelloWorld { public static void main(String[] var0) { System.out.println(Hello World); } }那你有尝试反解析它的类、查看汇编指令吗执行javap -c HelloWorld后可以看到public class org.itstack.demo.test.HelloWorld { public org.itstack.demo.test.HelloWorld(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object.init:()V 4: return public static void main(java.lang.String[]); Code: 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello World 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return }这段字节码只用到四个核心指令指令描述getstatic获取静态字段的值ldc常量池中的常量值入栈invokevirtual运行时方法绑定调用方法returnvoid 函数返回关于更多指令的语义常量入栈、局部变量装载、算术运算、控制流、对象操作、方法调用与返回等可以查阅仓库内的 JVM 指令表其中给出了完整的指令码、助记符与操作数栈行为描述是手写字节码时的案头工具。现在把这段代码用 ASM 的方式写出来import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; private static byte[] generate() { ClassWriter classWriter new ClassWriter(0); // 定义对象头版本号、修饰符、全类名、签名、父类、实现的接口 classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, org/itstack/demo/asm/AsmHelloWorld, null, java/lang/Object, null); // 添加方法修饰符、方法名、描述符、签名、异常 MethodVisitor methodVisitor classWriter.visitMethod(Opcodes.ACC_PUBLIC Opcodes.ACC_STATIC, main, ([Ljava/lang/String;)V, null, null); // 执行指令获取静态属性 methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, java/lang/System, out, Ljava/io/PrintStream;); // 加载常量 load constant methodVisitor.visitLdcInsn(Hello World); // 调用方法 methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, java/io/PrintStream, println, (Ljava/lang/String;)V, false); // 返回 methodVisitor.visitInsn(Opcodes.RETURN); // 设置操作数栈的深度和局部变量的大小 methodVisitor.visitMaxs(2, 1); // 方法结束 methodVisitor.visitEnd(); // 类完成 classWriter.visitEnd(); // 生成字节数组 return classWriter.toByteArray(); }以上所有操作与javap -c XXX反解析出的字节码完全一致只不过是把反编译指令反过来用指令写代码而已。逐段理解定义一个类的生成器ClassWriter设定版本、修饰符、全类名、签名、父类、实现的接口——其实对应的就是那句public class HelloWorld创建方法同样需要设定修饰符、方法名、描述符等。其中有两个固定标识体系类型描述符JVM 内部对 Java 类型的编码Java 类型类型描述符booleanZcharCbyteBshortSintIfloatFlongJdoubleDObjectLjava/lang/Object;int[][IObject[][][[Ljava/lang/Object;方法描述符(参数描述符)返回描述符的形态源文件中的方法声明方法描述符void m(int i, float f)(IF)Vint m(Object o)(Ljava/lang/Object;)Iint[] m(int i, String s)(ILjava/lang/String;)[IObject m(int[] i)([I)Ljava/lang/Object;其中([Ljava/lang/String;)V就等价于void main(String[] args)执行指令GETSTATIC获取静态属性主要是获得System.out加载常量 load constant输出 HelloWorldmethodVisitor.visitLdcInsn(Hello World);最后调用输出方法并设置空返回同时在结尾设置操作数栈的深度visitMaxs(2, 1)中的 2和局部变量的大小其中的 1最后以visitEnd结束方法、toByteArray产出字节码。值得一提的是ClassVisitor.visitXxx的调用顺序在 手册 2.2.1 接口和组件 中有明确规定必须先visit之后是visitSource、visitOuterClass、visitAnnotation/visitAttribute的可选调用然后是visitInnerClass/visitField/visitMethod的任意多个调用最后以visitEnd收尾。手写字节码时遵循这一顺序ClassWriter才能产出合法的 class 文件。如果你看过仓库内 《用 Java 实现 JVM》系列会发现这些指令、描述符和运行时数据区的概念并不陌生——解析 class 是读ASM 生成 class 是写两者互为镜像。五、有插件的帮助字节码开发也不是很难对新人来说直接用字节码指令开发复杂代码块确实有难度。好在 IDEA 有现成插件ASM Bytecode Outline可以帮忙在 IDEA 的插件市场搜索并安装ASM Bytecode Outline打开任意一个 Java 类通过右键菜单或快捷键打开该插件的视图即可同时看到当前代码反编译后的指令码等价于javap -c的输出用 ASM API 生成这段代码的等价 Java 源码。有了这个插件你写增强字节码就有了抓手先写普通的 Java 代码再用插件查看它对应的 ASM 调用序列最后把这段序列搬到自己的增强逻辑里即可准确、高效地完成字节码开发。此外手册 2.3 工具 还介绍了三个配套开发工具同样值得掌握TraceClassVisitor把访问到的类以可读文本形式输出。配合ClassWriter使用可以在生成链的任意位置打印当前类的文本表示便于确认生成结果是否符合预期CheckClassAdapter在把事件委托给下一个访问器之前校验方法调用顺序与参数是否合法提前发现会被 JVM 验证器拒绝的无效类ASMifierTraceClassVisitor的另一种后端访问一个已编译类时会打印用 ASM 生成这个类的 Java 源码。如果你不知道如何用 ASM 生成某个类先写源码、javac 编译再用 ASMifier 访问就能得到对应的 ASM 代码——这与 IDEA 插件的能力是一致的。六、用字节码写出一个两数之和计算有了插件和基础知识我们来开发一个计算两数之和的方法再运行得到计算结果。这是我们的目标public class SumOfTwoNumbers { public int sum(int i, int m) { return i m; } }使用字节码编程方式实现import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; private static byte[] generate() { ClassWriter classWriter new ClassWriter(0); { MethodVisitor methodVisitor classWriter.visitMethod(Opcodes.ACC_PUBLIC, init, ()V, null, null); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, java/lang/Object, init, ()V, false); methodVisitor.visitInsn(Opcodes.RETURN); methodVisitor.visitMaxs(1, 1); methodVisitor.visitEnd(); } { // 定义对象头版本号、修饰符、全类名、签名、父类、实现的接口 classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, org/itstack/demo/asm/AsmSumOfTwoNumbers, null, java/lang/Object, null); // 添加方法修饰符、方法名、描述符、签名、异常 MethodVisitor methodVisitor classWriter.visitMethod(Opcodes.ACC_PUBLIC, sum, (II)I, null, null); methodVisitor.visitVarInsn(Opcodes.ILOAD, 1); methodVisitor.visitVarInsn(Opcodes.ILOAD, 2); methodVisitor.visitInsn(Opcodes.IADD); // 返回 methodVisitor.visitInsn(Opcodes.IRETURN); // 设置操作数栈的深度和局部变量的大小 methodVisitor.visitMaxs(2, 3); methodVisitor.visitEnd(); } // 类完成 classWriter.visitEnd(); // 生成字节数组 return classWriter.toByteArray(); }代码中有两个{}块第一个用于生成一个空的构造函数public AsmSumOfTwoNumbers() { }第二个块的指令比较简单ILOAD 1、ILOAD 2把局部变量表下标 1、2 的两个 int 值压入操作数栈注意下标 0 是this所以方法参数从 1 开始IADD弹出栈顶两个 int 值相加结果入栈IRETURN返回 int 类型结果注意返回的是I类型而不是RETURNvisitMaxs(2, 3)操作数栈最大深度 2压入两个 int局部变量大小 3this 两个参数。这段方法块实现完成。反编译后如下// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.itstack.demo.asm; public class AsmSumOfTwoNumbers { public AsmSumOfTwoNumbers() { } public int doSum(int var1, int var2) { return var1 var2; } }执行代码块public static void main(String[] args) throws Exception { // 生成二进制字节码 byte[] bytes generate(); // 输出字节码 outputClazz(bytes); // 加载AsmSumOfTwoNumbers GenerateSumOfTwoNumbers generateSumOfTwoNumbers new GenerateSumOfTwoNumbers(); Class? clazz generateSumOfTwoNumbers.defineClass(org.itstack.demo.asm.AsmSumOfTwoNumbers, bytes, 0, bytes.length); // 反射获取 main 方法 Method method clazz.getMethod(sum, int.class, int.class); Object obj method.invoke(clazz.newInstance(), 6, 2); System.out.println(obj); }这段执行操作和 Java 反射的用法类似先把generate()产出的字节数组交给继承ClassLoader的工具类通过defineClass加载为Class对象再用反射拿到sum方法并调用入参 6 和 2最后打印结果。同时还可以把字节码输出成文件方便用javap或 IDEA 反编译查看生成的 class 类。到这里我们已完成了运行时凭空生成一个可用的类这一关键动作。七、在原有方法上字节码增强监控耗时非侵入式监控的最基本模型动态生成类只是第一步。实际使用中更多场景是修改一个原有的方法在开始和结尾插入代码监控方法耗时——这正是非侵入式监控的最基本模型业务代码一行不改行为却被悄悄增强。定义一个方法public class MyMethod { public String queryUserInfo(String uid) { System.out.println(xxxx); System.out.println(xxxx); System.out.println(xxxx); System.out.println(xxxx); return uid; } }像这个方法插入监控public class TestMonitor extends ClassLoader { public static void main(String[] args) throws IOException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException { ClassReader cr new ClassReader(MyMethod.class.getName()); ClassWriter cw new ClassWriter(cr, ClassWriter.COMPUTE_MAXS); { MethodVisitor methodVisitor cw.visitMethod(Opcodes.ACC_PUBLIC, init, ()V, null, null); methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, java/lang/Object, init, ()V, false); methodVisitor.visitInsn(Opcodes.RETURN); methodVisitor.visitMaxs(1, 1); methodVisitor.visitEnd(); } ClassVisitor cv new ProfilingClassAdapter(cw, MyMethod.class.getSimpleName()); cr.accept(cv, ClassReader.EXPAND_FRAMES); byte[] bytes cw.toByteArray(); outputClazz(bytes); Class? clazz new TestMonitor().defineClass(org.itstack.demo.asm.MyMethod, bytes, 0, bytes.length); Method queryUserInfo clazz.getMethod(queryUserInfo, String.class); Object obj queryUserInfo.invoke(clazz.newInstance(), 10001); System.out.println(测试结果 obj); } static class ProfilingClassAdapter extends ClassVisitor { public ProfilingClassAdapter(final ClassVisitor cv, String innerClassName) { super(ASM5, cv); } public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { System.out.println(access access); System.out.println(name name); System.out.println(desc desc); if (!queryUserInfo.equals(name)) return null; MethodVisitor mv cv.visitMethod(access, name, desc, signature, exceptions); return new ProfilingMethodVisitor(mv, access, name, desc); } } static class ProfilingMethodVisitor extends AdviceAdapter { private String methodName ; protected ProfilingMethodVisitor(MethodVisitor methodVisitor, int access, String name, String descriptor) { super(ASM5, methodVisitor, access, name, descriptor); this.methodName name; } Override protected void onMethodEnter() { mv.visitMethodInsn(INVOKESTATIC, java/lang/System, nanoTime, ()J, false); mv.visitVarInsn(LSTORE, 2); mv.visitVarInsn(ALOAD, 1); } Override protected void onMethodExit(int opcode) { if ((IRETURN opcode opcode RETURN) || opcode ATHROW) { mv.visitFieldInsn(GETSTATIC, java/lang/System, out, Ljava/io/PrintStream;); mv.visitTypeInsn(NEW, java/lang/StringBuilder); mv.visitInsn(DUP); mv.visitMethodInsn(INVOKESPECIAL, java/lang/StringBuilder, init, ()V, false); mv.visitLdcInsn(方法执行耗时(纳秒)- methodName ); mv.visitMethodInsn(INVOKEVIRTUAL, java/lang/StringBuilder, append, (Ljava/lang/String;)Ljava/lang/StringBuilder;, false); mv.visitMethodInsn(INVOKESTATIC, java/lang/System, nanoTime, ()J, false); mv.visitVarInsn(LLOAD, 2); mv.visitInsn(LSUB); mv.visitMethodInsn(INVOKEVIRTUAL, java/lang/StringBuilder, append, (J)Ljava/lang/StringBuilder;, false); mv.visitMethodInsn(INVOKEVIRTUAL, java/lang/StringBuilder, toString, ()Ljava/lang/String;, false); mv.visitMethodInsn(INVOKEVIRTUAL, java/io/PrintStream, println, (Ljava/lang/String;)V, false); } } } }整体代码块较大可以分块来看ClassReader cr new ClassReader(MyMethod.class.getName());—— 读取原有类这是字节码增强的开始。ClassReader充当事件产生器把 class 文件中的元素逐个转换为访问事件ClassWriter cw new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);—— 注意这里与前面不同构造ClassWriter时传入COMPUTE_MAXS让 ASM 自动计算操作数栈深度与局部变量大小省去手工维护visitMaxs的负担ClassVisitor cv new ProfilingClassAdapter(cw, ...)—— 自定义的ClassVisitor扮演事件过滤器/转换器ProfilingClassAdapter.visitMethod会打印每个方法的 access/name/desc 信息并且只对queryUserInfo方法返回自定义的ProfilingMethodVisitor做增强其他方法返回 null 表示不处理cr.accept(cv, ClassReader.EXPAND_FRAMES);—— 启动事件链EXPAND_FRAMES展开栈映射帧保证方法体插入指令后帧信息依然正确ProfilingMethodVisitor extends AdviceAdapter——AdviceAdapter是asm-commons包提供的便捷适配器它把方法进入和方法退出抽象成onMethodEnter()与onMethodExit(int opcode)两个回调。onMethodEnter里插入System.nanoTime()调用并把结果LSTORE到局部变量 2即 startTimeonMethodExit里判断退出指令是返回类IRETURN..RETURN或抛异常ATHROW时构造StringBuilder拼接方法执行耗时(纳秒)-xxx并相减两次nanoTime()最终println输出。这样字节码层面就等价于给方法首尾各加了一行耗时采集代码。测试结果直接运行 TestMonitor.javaaccess1 nameinit desc()V access1 namequeryUserInfo desc(Ljava/lang/String;)Ljava/lang/String; ASM类输出路径/E:/itstack/git/github.com/itstack-demo-asm/itstack-demo-asm-03/target/classes/AsmTestMonitor.class xxxx xxxx xxxx xxxx 方法执行耗时(纳秒)-queryUserInfo132300 测试结果10001可以看到MyMethod的原始打印逻辑原样执行额外多出了耗时输出而业务源码没有任何改动——非侵入式监控的基本模型就此打通。这一模型的工程化落地就是续篇 JavaAgentASM 字节码插桩 讲解的内容通过premain(String agentArgs, Instrumentation inst)入口把自定义的ClassFileTransformer注册进 JVM在类加载过程中对指定类做同样的ClassReader→ClassWriter(COMPUTE_MAXS)→ClassVisitor→cr.accept(cv, EXPAND_FRAMES)流水线并在MANIFEST.MF中声明Premain-Class与Can-Redefine-Classes: true即可做到对全应用方法的无侵入插桩。仓库内还有一篇 用字节码增强给所有方法加上 TryCatch 捕获异常并输出 的进阶实践展示了如何用Label与tryCatchBlock包裹整个方法体。八、字节码控制打印方法的入参除了监控耗时还可以把方法的入参打印出来这样在异常场景下能留下完整的调用日志。其他代码与上面相同这里只列出修改的地方static class ProfilingMethodVisitor extends AdviceAdapter { private String methodName ; protected ProfilingMethodVisitor(MethodVisitor methodVisitor, int access, String name, String descriptor) { super(ASM5, methodVisitor, access, name, descriptor); this.methodName name; } Override protected void onMethodEnter() { mv.visitFieldInsn(GETSTATIC, java/lang/System, out, Ljava/io/PrintStream;); mv.visitVarInsn(ALOAD, 1); mv.visitMethodInsn(INVOKEVIRTUAL, java/io/PrintStream, println, (Ljava/lang/String;)V, false); } Override protected void onMethodExit(int opcode) { } }这段增强的字节码含义是方法进入时使用指令GETSTATIC获取输出对象java/lang/System.out描述符Ljava/io/PrintStream;使用ALOAD 1从局部变量 1 装载引用类型值入栈——局部变量 1 正是queryUserInfo(String uid)的第一个入参uid调用PrintStream.println(String)输出入参。由于AdviceAdapter.onMethodEnter插入的指令位于原方法第一条业务指令之前执行方法时就会先打印入参。测试结果直接运行 TestMonitor.javaClass? clazz new TestMonitor().defineClass(org.itstack.demo.asm.MyMethod, bytes, 0, bytes.length); Method queryUserInfo clazz.getMethod(queryUserInfo, String.class); Object obj queryUserInfo.invoke(clazz.newInstance(), 10001); System.out.println(测试结果 obj);结果access1 nameinit desc()V access1 namequeryUserInfo desc(Ljava/lang/String;)Ljava/lang/String; ASM类输出路径/E:/itstack/git/github.com/itstack-demo-asm/itstack-demo-asm-04/target/classes/AsmTestMonitor.class 10001 ...输出中的10001就是我们的方法入参。九、用字节码增强调用外部方法把信息打印到控制台还做不了真正的业务。更实际的做法是在增强点把方法名、参数等信息调用外部类发送到服务端比如通过 MQ、日志服务等。下面用MonitorLog模拟这种采集 → 上报的形态。定义日志信息输出类public class MonitorLog { public static void info(String name, int... parameters) { System.out.println(方法 name); System.out.println(参数 [ parameters[0] , parameters[1] ]); } }这个类主要模拟字节码增强后方法调用输出一些信息。增强字节码static class ProfilingMethodVisitor extends AdviceAdapter { private String name; ... Override protected void onMethodEnter() { // 输出方法和参数 mv.visitLdcInsn(name); mv.visitInsn(ICONST_2); mv.visitIntInsn(NEWARRAY, T_INT); mv.visitInsn(DUP); mv.visitInsn(ICONST_0); mv.visitVarInsn(ILOAD, 1); mv.visitInsn(IASTORE); mv.visitInsn(DUP); mv.visitInsn(ICONST_1); mv.visitVarInsn(ILOAD, 2); mv.visitInsn(IASTORE); mv.visitMethodInsn(INVOKESTATIC, org/itstack/demo/asm/MonitorLog, info, (Ljava/lang/String;[I)V, false); } }这段字节码的操作与写代码定义一个 int 数组并填充元素完全对应LDC加载方法名字符串常量ICONST_2把数组长度 2 压栈NEWARRAY T_INT创建int[2]数组DUP复制数组引用——因为数组赋值后还需要再操作一次所以要先复制一份ICONST_0ILOAD 1把下标 0 和第一个入参压栈IASTORE完成arr[0] iICONST_1ILOAD 2把下标 1 和第二个入参压栈IASTORE完成arr[1] mINVOKESTATIC调用静态方法MonitorLog.info(String, int[])。增强后最终的效果等价于public int sum(int i, int m) { Monitor.info(sum, i, m); return i m; }测试结果access1 namesum desc(II)I signaturenull ASM类输出路径/E:/itstack/git/github.com/itstack-demo-asm/itstack-demo-asm-05/target/classes/AsmTestMonitor.class 方法sum 参数[6,2] 结果8通过测试内容可以看到方法名称与参数信息已完整打印。真正的生产级实现里这里会进一步做两件事详见 JavaAgentASM 续篇解析方法描述符面对(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;IJ[I[[Ljava/lang/Object;Lorg/itstack/test/Req;)Ljava/lang/String;这类复杂描述符用正则(L.*?;|\\[{0,2}L.*?;|[ZCBSIFJD]|\\[{0,2}[ZCBSIFJD]{1})拆解出每个入参类型再把 8 种基本类型装箱后填充到Object[]数组中从而支持任意签名的方法为方法生成全局唯一 ID用AtomicInteger自增 AtomicReferenceArrayMethodTag存储每个方法只生成一次 ID运行期通过 ID 反查方法元信息避免在热路径上反复传参损耗性能。十、总结与进阶路线高级编程技术的内容远不止于此。不要只为一时的功能实现而放弃深挖的机会正是不断拓展个人知识技能才让人越来越与众不同ASM这类字节码编程的应用非常广但平时确实很难直接看到——因为它总是与其他框架结合作为支撑服务存在。像这样的技术还有很多比如javassist、netty等。从设计哲学上看ASM 通过事件驱动的方式把读 class和写 class解耦成ClassReader、ClassWriter与各种ClassVisitor过滤器组成的流水线参见 手册 2.2 接口和组件这也是它小而快、适合运行时动态生成与转换的根本原因真正要学透一门技术不能只看爽文。成体系地学习、压榨自己的时间做有意义的事才是 37 年开发人员最正确的事。如果你打算沿着这条路线继续深入仓库内已经沉淀了完整的进阶链路JavaAgent ASM 字节码插桩把本文的增强模型升级为可运行的 agent采集类名、方法名、出入参与耗时用字节码增强给所有方法加上 TryCatch利用Label与异常表对任意方法做异常兜底捕获字节码插桩技术调研系统监控设计从系统监控的全局视角对比多种插桩技术选型IDEA 过期破解的字节码原理分析一个鲜活的真实案例展示字节码修改如何改变程序行为《用 Java 实现 JVM》系列亲手解析 class 文件、实现指令集和解释器从底层彻底理解字节码ASM 使用手册全量译文与 JVM 指令表作为手写字节码与阅读 ASM 源码的常备参考资料。从只会写 CRUD到能操控字节码差的不是天赋而是一套成体系的资料加上亲手敲一遍的实践。本文五个案例生成类、两数求和、耗时监控、入参采集、外部调用正是这条路的起点。赞分享文档教程后端【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址https://gitcode.com/gh_mirrors/code/CodeGuide点击查看免费下载相关推荐CodeGuide 中间件实战非入侵监控设计之 ASM 字节码插桩CodeGuide 中间件实战非入侵监控设计之 ASM 字节码插桩 本文源自小傅哥《SpringBoot 中间件设计和开发》专栏第 16 章。在大型互联网架构文档教程后端macOS与Linux环境下CodeBrowser的编译与配置全攻略macOS与Linux环境下CodeBrowser的编译与配置全攻略 CodeBrowser是一款高效的代码浏览工具专为开发者打造支持快速导航和理解复杂代码开发工具洛雪音乐音源配置完全指南一站式解决全网音乐资源获取难题洛雪音乐音源配置完全指南一站式解决全网音乐资源获取难题 你是否曾经为了寻找高质量的音乐资源而烦恼在不同的音乐平台之间切换只为找到一首歌的无损版本现在通音视频创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价