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();}}

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

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

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

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

相关文章

Mac M3 Pro安装Hadoop-3.3.6

1、下载Hadoop安装包 可以到官方网站下载&#xff0c;也可以使用网盘下载 官网下载地址&#xff1a;Hadoop官网下载地址 网盘地址&#xff1a;https://pan.baidu.com/s/1p4BXq2mvby2B76lmpiEjnA?pwdr62r提取码: r62r 2、解压并添加环境变量 # 将安装包移动到指定目录 mv …

FPGA - 数 - 加减乘除

一&#xff0c;数的表示 首先&#xff0c;将二进制做如下解释&#xff1a; 2的0次方1 2的1次方2 2的2次方4 2的3次方8 ..... 以此类推&#xff0c;那么任何整数&#xff0c;或者说任意一个自然数均可以采用这种方式来表示。 例如&#xff0c;序列10101001&#xff0c;根据上述…

Unity贪吃蛇改编【详细版】

Big and small greedy snakes 游戏概述 游戏亮点 通过对称的美感&#xff0c;设置两条贪吃蛇吧&#xff0c;其中一条加倍成长以及加倍减少&#xff0c;另一条正常成长以及减少&#xff0c;最终实现两条蛇对整个界面的霸占效果。 过程中不断记录两条蛇的得分情况&#xff0c…

【Unity】RPG2D龙城纷争(二)关卡、地块

更新日期&#xff1a;2024年6月12日。 项目源码&#xff1a;后续章节发布 索引 简介地块&#xff08;Block&#xff09;一、定义地块类二、地块类型三、地块渲染四、地块索引 关卡&#xff08;Level&#xff09;一、定义关卡类二、关卡基础属性三、地块集合四、关卡初始化五、关…

VCG显示——汉字,数字,图像

详细的介绍资料&#xff1a; 【从零开始走进FPGA】 玩转VGA http://www.cnblogs.com/spartan/archive/2011/08/16/2140546.html 【FPGA实验】基于DE2-115平台的VGA显示_vga接口实验 de2-115-CSDN博客 【FPGA】VGA显示文字、彩条、图片——基于DE2-115-CSDN博客 一.VCG原理 1.1…

时序预测 | MATLAB实现TCN-Transformer时间序列预测

时序预测 | MATLAB实现TCN-Transformer时间序列预测 目录 时序预测 | MATLAB实现TCN-Transformer时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.MATLAB实现TCN-Transformer时间序列预测&#xff1b; 2.运行环境为Matlab2023b及以上&#xff1b; 3.data为数…

Python **运算符(python**kwargs:参数解包)(kwargs:keyword arguments)

文章目录 Python中的 ** 运算符&#xff1a;参数解包参数解包基础语法和示例 在函数定义中使用 **示例代码 使用场景和好处1. 灵活性&#xff1a;使用 **kwargs 允许函数设计得更加灵活&#xff0c;可以接受未来可能增加的新参数而无需修改函数定义。2. 可读性和可维护性&#…

Kali中安装和使用docker的学习笔记

一、常见命令 ctrl 、shift、 &#xff1a; 窗口变大&#xff1b; ctrl 、- &#xff1a;窗口变小&#xff1b; ctrl L&#xff1a; 清屏 &#xff1b; sudo su : 切换root 用户&#xff1b; ip addr / ifconfig: 获取IP地址&#xff1b; systemctl start ssh…

B端颜值无所谓?麻痹自己可以,麻痹业务人员和客户试一试。

很多老铁觉得B端系统颜值和体验无所谓&#xff0c;功能好就行了&#xff0c;我不认同这种说法&#xff0c;我觉得优秀的B端系统应该是内外兼修的&#xff0c;而不是偏科的。你想一想你费尽研发的系统&#xff0c;就是因为颜值问题&#xff0c;你的业务人员没信息推销&#xff0…

北方工业大学24计算机考研情况,学硕专硕都是国家线复试!

北方工业大学&#xff08;North China University of Technology&#xff0c;NCUT&#xff09;&#xff0c;简称“北方工大”&#xff0c;位于北京市&#xff0c;为一所以工为主、文理兼融&#xff0c;具有学士、硕士、博士培养层次的多科性高等学府&#xff0c;是中华人民共和…

GitLab教程(四):分支(branch)和合并(merge)

文章目录 1.分支&#xff08;branch&#xff09;&#xff08;1&#xff09;分支的概念&#xff08;2&#xff09;branch命令 2.合并&#xff08;merge&#xff09;&#xff08;1&#xff09;三个命令pullfetchmergegit fetchgit mergegit pull &#xff08;2&#xff09;合并冲…

【计算机网络仿真实验-实验2.6】带交换机的RIP路由协议

实验2.6 带交换机的rip路由协议 1. 实验拓扑图 2. 实验前查看是否能ping通 不能 3. 三层交换机配置 switch# configure terminal switch(config)# hostname s5750 !将交换机更名为S5750 S5750# configure terminal S5750(config)#vlan 10 S5750(config-vlan)#exit S57…

PyTorch 维度变换-Tensor基本操作

以如下 tensor a 为例&#xff0c;展示常用的维度变换操作 >>> a torch.rand(4,3,28,28) >>> a.shape torch.Size([4, 3, 28, 28])view / reshape 两者功能完全相同: a.view(shape) >>> a.view(4,3,28*28) ## a.view(4,3,28,28) 可恢复squeeze…

【LLM】吴恩达『微调大模型』课程完全笔记

Finetuning Large Language Models 版权说明&#xff1a; 『Finetuning Large Language Models』是DeepLearning.AI出品的免费课程&#xff0c;版权属于DeepLearning.AI(https://www.deeplearning.ai/)。 本文是对该课程内容的翻译整理&#xff0c;只作为教育用途&#xff0c;不…

数据分析必备:一步步教你如何用matplotlib做数据可视化(2)

1、Matplotlib Anaconda Anaconda是Python和R编程语言的免费开源发行版&#xff0c;用于大规模数据处理&#xff0c;预测分析和科学计算。 该分发使包管理和部署变得简单容易。 Matplotlib和许多其他有用的(数据)科学工具构成了分发的一部分。 包版本由包管理系统Conda管理。 …

旅游网站(携程旅行网页学习 vue3+element)

旅游网站 1. 创建项目 在你要创建项目的路径下打开vscode&#xff0c;新建终端&#xff0c;然后输入vue ui,进入Vue项目管理器。选择“创建”&#xff0c;确定项目路径&#xff0c;并点击“在此创建新项目”。在项目文件夹中输入项目名称&#xff0c;点击下一步&#xff1b;选…

CMU最新论文:机器人智慧流畅的躲避障碍物论文详细讲解

CMU华人博士生Tairan He最新论文&#xff1a;Agile But Safe: Learning Collision-Free High-Speed Legged Locomotion 代码开源&#xff1a;Code: https://github.com/LeCAR-Lab/ABS B站实际效果展示视频地址&#xff1a;bilibili效果地址 我会详细解读论文的内容,让我们开始吧…

Python读取wps中的DISPIMG图片格式

需求&#xff1a; 读出excel的图片内容&#xff0c;这放在微软三件套是很容易的&#xff0c;但是由于wps的固有格式&#xff0c;会出现奇怪的问题&#xff0c;只能读出&#xff1a;类似于 DISPIMG(“ID_2B83F9717AE1XXXX920xxxx644C80DB1”,1) 【该DISPIMG函数只有wps才拥有】 …

关于使用‘rt-thread-master‘包从新创建对应开发板型号工程遇到相关问题

问题1:裁剪完成后在ENV中使用命令每次使用scons --targetmdk5重新生成工程后在工程中自己选择的单片机型号会变成默认问题 解决: 通过生成的“template.uvprojx”打开工程&#xff0c;在devicezhong 更改成自己要选择的单片机型号&#xff0c;然后保存&#xff1b;再次通过&qu…

IDEA创建SpringBoot项目教程,讲解超详细!!!(2024)

前言 在创建Spring Boot项目时&#xff0c;为了确保项目的顺利构建和运行&#xff0c;我们依赖于JDK&#xff08;Java开发工具包&#xff09;和Maven仓库。 JDK作为Java编程的基础&#xff0c;提供了编译和运行Java应用程序所需的核心类库和工具。 JDK安装配置教程&#xff1…