ASM字节码操纵框架实现AOP

2024-06-16 03:36
文章标签 实现 框架 字节 aop asm 操纵

本文主要是介绍ASM字节码操纵框架实现AOP,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

使用ASM改写字节码实现Aop,是最快的Aop实现方式。

我猜你肯定懂AOP

凡是学习Spring框架,必然会深入了解AOP的原理以及实现。这里做下简单总结

Spring默认采取的是动态代理机制实现AOP,当动态代理不可用时(代理类无接口)会使用CGlib机制。这里的CGlib机制的底层就是基于ASM来实现的

但是Spring的AOP有一定的缺点,它只能对方法进行切入,不能对接口,字段,静态代码块进行切入(切入接口的某个方法,则该接口下所有实现类的该方法将被切入)。并且同类中的互相调用方法将不会使用代理类

但你有没有尝试过,不依赖Spring框架,自己来实现AOP编程呢?

那么接下来,我将自己最近学习以及了解的ASM字节码操纵框架来实现AOP编程。

当然了,如果你对JVM还没有较深入的了解或认识,这篇文章读起来会比较吃力。

如果你想快速了解认识ASM字节码框架,首先必须要了解熟悉JVM中类文件结构部分。

好了,咖啡宝贝(CAFEBABE),我要开始发车了,系好安全带哦!


一、ASM是什么?

  • ASM 是一个 Java 字节码操纵和分析框架。它可以直接以二进制形式动态地生成 stub 类或其他代理类,或者在装载时动态地修改类。
  • ASM 提供类似于 BCEL 和 SERP 之类的工具包的功能,但是被设计得更小巧、更快速,这使它适用于实时代码插装。
  • ASM提供了一些常见的字节码转换和分析算法,可以从中构建自定义复杂转换和代码分析工具。
  • ASM提供与其他Java字节码框架类似的功能,但专注于性能。 因为它的设计和实现尽可能小而且快,所以它非常适合在动态系统中使用(但当然也可以以静态方式使用,例如在编译器中)。

接下来简单介绍ASM编程模型

  1. Core API : 提供了基于事件形式的编程模型。该模型不需要一次性将整个类的结构读取到内存中,因此这种方式更快,需要更少的内存,但这种编程方式难度较大(咱们接下来演示就采用该种模型)
  2. Tree API:提供了基于树形的编程模型。该模型需要一次性将一个类的完整结构全部读取到内存当中,所以这种方法需要更多的内存,这种编程方式较简单

进一步介绍Core API:

  1. Core API中操纵字节码的功能是基于ClassVisitor接口。而A这个接口中的每个方法对应了class文件中的每一项
  2. 当然ASM提供了三个基于ClassVisitor接口的类来实现class文件的生成和转换。
    • ClassReader : ClassReader解析一个类的class字节码
    • ClassAdapter : ClassAdapter是ClassVisitor 的实现类,实现要变化的功能。或者说切入的功能代码
    • ClassWriter : ClassWriter 也是ClassVisitor的实现类,可以用来输出变化后的字节码,给予JVM运行处理
  3. ASM给我们提供了ASMifier工具来帮助开发,可使用ASMifier工具生成ASM结构来对比
  4. 如果没有ASMifier工具,自己去构建是非常吃力的,接下来代码实现的时候,我将使用该工具进行演示
  5. 好了,直接进入正题了。咖啡BABE,准备好了没?

二、代码实现

1.环境设置

这里为什么叫环境设置呢?随便称呼的,哈哈哈哈

因为ASMifier的使用需要借助里面的org.objectweb.asm.util.ASMifier辅助我们操作

所以起初大家可以安装一个插件

ASM Bytecode Outline 0.3.5


在这里插入图片描述
重启Idea之后
在这里插入图片描述
这就说明咱们插件安装成功了,这里也就可以直接明了的看到字节码指令了

2.分析对比

我给定的测试类文件代码

package com.guanbo.asm;public class Test01 {public  void test() {System.out.println("location:com.guanbo.asm.Test01");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}
}

原字节码方法区:

常量池:

借助ASMified生成的:

在ASMified中可以清晰的发现,众多JVM虚拟机的指令集,如果还不清楚什么意思,建议复习一下哦

并且通过ASMified均是通过visit去访问的,具体的访问细节咱们晚点说

3.增加需求(输出运行时间)

假如我们不考虑修改字节码文件的方式

直接在改Test01.java类中操作

那么我们必然要加入以上两条代码输出时间差

在测试类中实现的代码(需要增加的方法):

package com.guanbo.asm;public class Test01 {public void test() {Long a1 = System.currentTimeMillis();System.out.println("location:com.guanbo.asm.Test01");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}Long a2 = System.currentTimeMillis();System.out.println("invoke method total time ==" + (a2 - a1));}
}
对比

这里我们对比通过ASMified自动生成对应的ASM代码来分析
我们只需对比分析test()方法内的字节码即可

增加需求之前的ASMified自动解析生成的ASM代码:

增加需求之后

补充:其实在这里就可以看到,程序在编译期时,JDK已经做了相应的JVM优化
这里String,StringBuffer,StringBuilder的区别以及性能的分析,以及在多线程中的适用性、安全性是目前我了解到的面试题或者笔试题中会经常出现的,不晓得咖啡BABE你是不是已经了如指掌了呢?

另外再配上增加需求之后对应的字节码文件:
我们可以先来看一下字节码文件中的本地变量表

test()方法

对比之后,不考虑变量的情况下
那么


进而输出了不一样的字节码的文件

接下来咱们开始创建自己的Visitor

这里会涉及到asm包下的org.objectweb.asm.Opcodes类


在该类中基本涵盖了各种JVM虚拟机的字节码指令以及操作码常量,后面的所有方法的执行,均需要调用该类中的字节码指令属性

咱们接下来所涉及的只重写vistMethond以及visit方法(其他方法均类似,可自由测试,欢迎大家跟我一起探讨交流)

植入代码(重写ClassVisitor)

代码如下(示例):

package com.guanbo.asm;import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;public class MyClassVisitor extends ClassVisitor {public MyClassVisitor(ClassVisitor classVisitor) {super(Opcodes.ASM7, classVisitor);}@Overridepublic void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {cv.visit(version, access, name, signature, superName, interfaces);}@Overridepublic MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);//这里需要过滤掉<init>JVM执行时初始化的方法if (!"<init>".equals(name) && mv != null) {//这里便开始植入所需要的功能需求代码mv = new MyMethodVistor(mv);}return mv;}
}class MyMethodVistor extends MethodVisitor {public MyMethodVistor(MethodVisitor methodVisitor) {super(Opcodes.ASM7, methodVisitor);}@Overridepublic void visitCode() {//导入需要植入的指令mv.visitCode();//mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);mv.visitVarInsn(Opcodes.LSTORE, 1);Label l4 = new Label();mv.visitLabel(l4);mv.visitLineNumber(7, l4);}@Overridepublic void visitInsn(int opcode) {if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN || opcode == Opcodes.ATHROW) {mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);mv.visitVarInsn(Opcodes.LSTORE, 3);Label l7 = new Label();mv.visitLabel(l7);mv.visitLineNumber(14, l7);mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");mv.visitInsn(Opcodes.DUP);mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);mv.visitLdcInsn("invoke method total time ==");mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);mv.visitVarInsn(Opcodes.LLOAD, 3);mv.visitVarInsn(Opcodes.LLOAD, 1);mv.visitInsn(Opcodes.LSUB);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);Label l8 = new Label();mv.visitLabel(l8);mv.visitLineNumber(16, l8);}mv.visitInsn(opcode);}
}

到目前为止,我们已经将功能写入了,但是我们怎么去用呢?

这里我们需要一个Generator把这些功能作用到我们类上并且输出出去!

构建Generator

到这了就只需要关注咱们之前提到的三部曲了

  • ClassReader : ClassReader解析一个类的class字节码
  • ClassAdapter : ClassAdapter是ClassVisitor 的实现类,实现要变化的功能。或者说切入的功能代码
  • ClassWriter : ClassWriter 也是ClassVisitor的实现类,可以用来输出变化后的字节码,给予JVM运行处理

关于:ClassWriter.COMPUTE_MAXS):表示交给ASM 自动帮你计算局部变量表和操作数栈的大小

代码如下(示例):

package com.guanbo.asm;import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;public class Generator {public static void main(String[] args) throws IOException {ClassReader cr = new ClassReader("com/guanbo/asm/Test01");ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);ClassVisitor cv = new MyClassVisitor(cw);cr.accept(cv, ClassReader.SKIP_DEBUG);byte[] data = cw.toByteArray();File file = new File("G:\WorkSpace\myProject\jvm_test\asm\out\production\asm\com\guanbo\asm");FileOutputStream fo = new FileOutputStream(file);fo.write(data);fo.close();System.out.println("Generator run success!");}
}

我们运行看看输出
Generator run success!
运行成功
我们看下生成的class字节码文件
javap -v -p -s -sysinfo -constants Test01.class在这里插入图片描述
在这里插入图片描述

我们可以清晰的发现在常量池中已经注入了我们所需要的方法

IDEA解析后的
在这里插入图片描述

因此我们的需求 我们的需求已经被植入并生成了Test01.class文件

创建Tset01对象,调用test()方法

我们直接创建一个测试类即可

package com.guanbo.asm;public class MyTest {public static void main(String[] args) {Test01 myTest01 = new Test01();myTest01.test();}}

我们执行以下看下输出结果
在这里插入图片描述

这里我们会发这样一个错误

Exception in thread “main” java.lang.VerifyError: Bad local variable type
错误的本地变量类型
我们接着往下看
Reason:
Type top (current frame, locals[1]) is not assignable to long

错误的本地存储类型 不是long

这里我们继续对比

BUG分析对比

这里是我们原始类的class文件被ASM解析后的代码
在这里插入图片描述
被注入之后的class文件解析结果
在这里插入图片描述
我们发现原class文件中ASTORE[1]的位置被我们强行改为了LSTORE[1]
这就导致对本地变量表造成了影响
那么我们该怎么解决呢?

解决方案

这时候你肯定会在想,那我把原java文件类中的变量删掉不就好了
这是我们的一种解决方案

我们演示一下

package com.guanbo.asm;public class Test01 {public void test() throws InterruptedException {System.out.println("location:com.guanbo.asm.Test01");
//        try {Thread.sleep(100);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }}
}

重新Generator一下

Generator run success!
成功写出

我们启动咱们的测试类

package com.guanbo.asm;public class MyTest {public static void main(String[] args) throws InterruptedException {Test01 myTest01 = new Test01();myTest01.test();}}

控制台输出结果:

location:com.guanbo.asm.Test01
invoke method total time ==104

这里我们切入的功能已经成功写入

这里你会有疑问了?

我这不是拆东墙补西墙吗?UP主你这也太拉了吧,真TM无情啊

别慌,咱们还有解决方案

通过引入类,解决本地变量表冲突

创建MyTimeLogger类

package com.guanbo.asm;public class MyTimeLogger {public static long a1 = 0L;public static void start() {a1 = System.currentTimeMillis();}public static void end() {long a2 = System.currentTimeMillis();System.out.println("new invoke method total time == " + (a2 - a1));}}

我们对原始的Test01.java做下修改,引入MyTimeLogger中的方法

观察ASM自动解析生成的代码

在这里插入图片描述

那我们接下来重新定义一个ClassVisitor

package com.guanbo.asm;import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;public class MyClassVisitor2 extends ClassVisitor {public MyClassVisitor2(ClassVisitor classVisitor) {super(Opcodes.ASM7, classVisitor);}@Overridepublic void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {cv.visit(version, access, name, signature, superName, interfaces);}@Overridepublic MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);//这里需要过滤掉<init>JVM执行时初始化的方法if (!"<init>".equals(name) && mv != null) {//这里便开始植入所需要的功能需求代码mv = new MyMethodVistor2(mv);}return mv;}
}class MyMethodVistor2 extends MethodVisitor {public MyMethodVistor2(MethodVisitor methodVisitor) {super(Opcodes.ASM7, methodVisitor);}@Overridepublic void visitCode() {//导入需要植入的指令mv.visitCode();//mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/guanbo/asm/MyTimeLogger", "start", "()V", false);}@Overridepublic void visitInsn(int opcode) {if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN || opcode == Opcodes.ATHROW) {mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/guanbo/asm/MyTimeLogger", "end", "()V", false);}mv.visitInsn(opcode);}
}

是不是只需要对类文件添加引入,而无需关心本地变量库的存储了?

那我们重新进行Generator

在这里插入图片描述
这里我们把try catch代码块放行
在存在InterruptedException e变量的情况下进行测试

    public void test() throws InterruptedException {System.out.println("location:com.guanbo.asm.Test01");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}

在这里插入图片描述
因此我们可以将增强的方法,全部封装在一个新的类中

这样就完美的解决了变量冲突问题

这篇关于ASM字节码操纵框架实现AOP的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1065372

相关文章

字节面试 | 如何测试RocketMQ、RocketMQ?

字节面试:RocketMQ是怎么测试的呢? 答: 首先保证消息的消费正确、设计逆向用例,在验证消息内容为空等情况时的消费正确性; 推送大批量MQ,通过Admin控制台查看MQ消费的情况,是否出现消费假死、TPS是否正常等等问题。(上述都是临场发挥,但是RocketMQ真正的测试点,还真的需要探讨) 01 先了解RocketMQ 作为测试也是要简单了解RocketMQ。简单来说,就是一个分

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略 1. 特权模式限制2. 宿主机资源隔离3. 用户和组管理4. 权限提升控制5. SELinux配置 💖The Begin💖点点关注,收藏不迷路💖 Kubernetes的PodSecurityPolicy(PSP)是一个关键的安全特性,它在Pod创建之前实施安全策略,确保P

cross-plateform 跨平台应用程序-03-如果只选择一个框架,应该选择哪一个?

跨平台系列 cross-plateform 跨平台应用程序-01-概览 cross-plateform 跨平台应用程序-02-有哪些主流技术栈? cross-plateform 跨平台应用程序-03-如果只选择一个框架,应该选择哪一个? cross-plateform 跨平台应用程序-04-React Native 介绍 cross-plateform 跨平台应用程序-05-Flutte

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、