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

相关文章

Python+PyQt5实现MySQL数据库备份神器

《Python+PyQt5实现MySQL数据库备份神器》在数据库管理工作中,定期备份是确保数据安全的重要措施,本文将介绍如何使用Python+PyQt5开发一个高颜值,多功能的MySQL数据库备份工具... 目录概述功能特性核心功能矩阵特色功能界面展示主界面设计动态效果演示使用教程环境准备操作流程代码深度解

golang float和科学计数法转字符串的实现方式

《golangfloat和科学计数法转字符串的实现方式》:本文主要介绍golangfloat和科学计数法转字符串的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望... 目录golang float和科学计数法转字符串需要对float转字符串做处理总结golang float

linux lvm快照的正确mount挂载实现方式

《linuxlvm快照的正确mount挂载实现方式》:本文主要介绍linuxlvm快照的正确mount挂载实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux lvm快照的正确mount挂载1. 检查快照是否正确创建www.chinasem.cn2.

利用Python实现时间序列动量策略

《利用Python实现时间序列动量策略》时间序列动量策略作为量化交易领域中最为持久且被深入研究的策略类型之一,其核心理念相对简明:对于显示上升趋势的资产建立多头头寸,对于呈现下降趋势的资产建立空头头寸... 目录引言传统策略面临的风险管理挑战波动率调整机制:实现风险标准化策略实施的技术细节波动率调整的战略价

使用Python和Tkinter实现html标签去除工具

《使用Python和Tkinter实现html标签去除工具》本文介绍用Python和Tkinter开发的HTML标签去除工具,支持去除HTML标签、转义实体并输出纯文本,提供图形界面操作及复制功能,需... 目录html 标签去除工具功能介绍创作过程1. 技术选型2. 核心实现逻辑3. 用户体验增强如何运行

SpringBoot实现Kafka动态反序列化的完整代码

《SpringBoot实现Kafka动态反序列化的完整代码》在分布式系统中,Kafka作为高吞吐量的消息队列,常常需要处理来自不同主题(Topic)的异构数据,不同的业务场景可能要求对同一消费者组内的... 目录引言一、问题背景1.1 动态反序列化的需求1.2 常见问题二、动态反序列化的核心方案2.1 ht

Python实现文件批量重命名器

《Python实现文件批量重命名器》在日常工作和学习中,我们经常需要对大量文件进行重命名操作,本文将介绍一个使用Python开发的文件批量重命名工具,提供了多种重命名模式,有需要的小伙伴可以了解下... 目录前言功能特点模块化设计1.目录路径获取模块2.文件列表获取模块3.重命名模式选择模块4.序列号参数配

golang实现延迟队列(delay queue)的两种实现

《golang实现延迟队列(delayqueue)的两种实现》本文主要介绍了golang实现延迟队列(delayqueue)的两种实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的... 目录1 延迟队列:邮件提醒、订单自动取消2 实现2.1 simplChina编程e简单版:go自带的time

Python使用python-docx实现自动化处理Word文档

《Python使用python-docx实现自动化处理Word文档》这篇文章主要为大家展示了Python如何通过代码实现段落样式复制,HTML表格转Word表格以及动态生成可定制化模板的功能,感兴趣的... 目录一、引言二、核心功能模块解析1. 段落样式与图片复制2. html表格转Word表格3. 模板生

SpringBoot实现多环境配置文件切换

《SpringBoot实现多环境配置文件切换》这篇文章主要为大家详细介绍了如何使用SpringBoot实现多环境配置文件切换功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 示例代码结构2. pom文件3. application文件4. application-dev文