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

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        cv.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public 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);
    }

    @Override
    public 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);
    }

    @Override
    public 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);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        cv.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public 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);
    }

    @Override
    public void visitCode() {
        //导入需要植入的指令
        mv.visitCode();
        //
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/guanbo/asm/MyTimeLogger", "start", "()V", false);
    }

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

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

未来工牌:蓝牙智联的彩色墨水屏工牌

在快节奏的现代职场中&#xff0c;传统的工牌已无法满足人们对于个性化和智能化的需求。为此&#xff0c;我们创新研发了一款4寸电子墨水屏工牌&#xff0c;它不仅仅是一个身份的象征&#xff0c;更是一个集蓝牙通信、智能显示、节能环保于一体的未来工具。 这款工牌拥有600*4…

通过噪声扰动缓解多模态大型语言模型的幻觉问题

摘要 该论文提出了一种名为NoiseBoost的方法&#xff0c;通过噪声扰动来缓解多模态大语言模型(MLLM)中的幻觉问题。论文分析指出&#xff0c;幻觉主要源于大语言模型固有的总结机制&#xff0c;导致对语言符号的过度依赖&#xff0c;而忽视了视觉信息。NoiseBoost通过在视觉特…

嵌入式学习记录6.17(qss练习)

一思维导图 二.练习 widget.h #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);this->setWindowFlag(Qt::FramelessWindowHint);this->setAttribute(Qt:…

分类预测 | Matlab实现GA-XGBoost遗传算法优化XGBoost的多特征分类预测

分类预测 | Matlab实现GA-XGBoost遗传算法优化XGBoost的多特征分类预测 目录 分类预测 | Matlab实现GA-XGBoost遗传算法优化XGBoost的多特征分类预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 Matlab实现GA-XGBoost遗传算法优化XGBoost的多特征分类预测&#xff0c;…

自动化测试 —— ReadyAPI赋能API性能测试,助力应对高峰期流量挑战!

在当今数字驱动的市场中&#xff0c;API的完美性能对于企业在高峰期提升营业收入至关重要。随着消费者越来越依赖于在线购物和移动App购物&#xff0c;任何与API相关的故障或减速都可能导致顾客体验变差和交易流失&#xff0c;从而造成销售损失。因此&#xff0c;企业需要优先考…

优思学院|怎么选择精益生产培训才不会被坑?

在选择精益生产培训公司时&#xff0c;我们需要从多个角度去思考。企业若只是盲目地跟风&#xff0c;这样的做法无异于缘木求鱼。精益生产的核心在于发现和消除那些不增值的活动&#xff0c;从而提升产品的质量和生产效率&#xff0c;但要知道的是&#xff0c;发现和改进的人就…

zookeeper学习、配置文件参数详解

zookeeper学习、配置文件参数详解 zookeeper 配置文件参数详解tickTime 、session 的过期时间、maxSessionTimeout 三者之间的关系initLimit&#xff0c;syncLimit什么区别minSessionTimeout 默认值,**他的单位是ms** zookeeper 配置文件参数详解 ZooKeeper 是一个分布式协调服…

Java实现一个解析CURL脚本小工具

该工具可以将CURL脚本中的Header解析为KV Map结构&#xff1b;获取URL路径、请求类型&#xff1b;解析URL参数列表&#xff1b;解析Body请求体&#xff1a;Form表单、Raw Body、KV Body、XML/JSON/TEXT结构体等。 使用示例 获取一个http curl脚本&#xff1a; curl --locatio…

玩转OurBMC第八期:OpenBMC webui之通信交互

栏目介绍&#xff1a;“玩转OurBMC”是OurBMC社区开创的知识分享类栏目&#xff0c;主要聚焦于社区和BMC全栈技术相关基础知识的分享&#xff0c;全方位涵盖了从理论原理到实践操作的知识传递。OurBMC社区将通过“玩转OurBMC”栏目&#xff0c;帮助开发者们深入了解到社区文化、…

CentOS 7x 使用Docker 安装oracle11g完整方法

1.安装docker-ce 安装依赖的软件包 yum install -y yum-utils device-mapper-persistent-data lvm2添加Docker的阿里云yum源 yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo更新软件包索引 yum makecache fast查看docker…

【日记】被客户一顿输出该怎么办(431 字)

正文 上午有个客户在电话里对着我一顿输出&#xff0c;说他们没有发票财务账务没法处理怎么怎么的。话里话外满满一股 “全是你们的错” 的味道。 当时我很想笑&#xff0c;大姐&#xff0c;你对我输出有啥用啊。票是上级行开的&#xff0c;我们又没有开票权限&#xff0c;对我…

给电脑bios主板设置密码

增强安全性&#xff1a;防止未经授权的人员更改 BIOS 中的重要设置&#xff0c;如启动顺序、硬件配置等。这有助于保护计算机系统的稳定性和数据的安全性。防止恶意篡改&#xff1a;阻止可能的攻击者或恶意软件通过修改 BIOS 设置来破坏系统或获取敏感信息。数据保护&#xff1…

Redis 主从同步

主从同步 很多企业没有使用Redis的集群&#xff0c;但是至少都做了主从。有了主从&#xff0c;当master挂掉的时候&#xff0c;运维让从库过来接管&#xff0c;服务就可以继续&#xff0c;否则master需要经过数据恢复和重启的过程&#xff0c;可能会拖很长时间&#xff0c;影响…

男士内裤品牌哪个好?2024公认好穿的五款男士内裤分享

男士内裤作为大家每天都要长时间穿着的贴身衣物&#xff0c;它的重要性不言而喻。为了确保健康与卫生&#xff0c;专家和医生建议您每三个月更换一次内裤&#xff0c;避免细菌滋生&#xff0c;让身体更加清爽自在。而一款优质的内裤&#xff0c;不仅要有舒适的弹性&#xff0c;…

TikTok电商带货指南:策略、技巧与流量获取全解析

随着短视频平台的迅猛发展&#xff0c;TikTok已成为品牌和个人进行带货营销的主要阵地之一。通过有创意的内容、有效的互动方式和精准的流量获取策略&#xff0c;品牌和个人都能在TikTok上取得显著的带货效果。本文Nox聚星将和大家探讨在TikTok上进行带货营销的有效策略和技巧&…

图形化编程:解锁数字创意的新钥匙

在这个日新月异的数字时代&#xff0c;编程已不再局限于专业人士的小圈子&#xff0c;它正逐渐成为一项基础技能&#xff0c;融入我们的日常生活与工作中。而对于那些对传统代码望而生畏的人来说&#xff0c;6547网认为图形化编程犹如一股清流&#xff0c;以其直观、易学的特点…

芝麻文件重命名 一键批量重命名 支持批量修改图片 文档 文件夹名称

芝麻文件重命名是一款专业的文件批量重命名软件&#xff0c;它提供了丰富的功能和灵活的命名规则&#xff0c;可以大大提高文件管理的效率。以下是关于芝麻文件重命名的详细介绍&#xff1a; 一、软件特点 支持批量重命名&#xff1a;芝麻文件重命名支持文件和文件夹的批量重命…

学生课程信息管理系统

摘 要 目前&#xff0c;随着科学经济的不断发展&#xff0c;高校规模不断扩大&#xff0c;所招收的学生人数越来越 多&#xff1b;所开设的课程也越来越多。随之而来的是高校需要管理更多的事务。对于日益增 长的学生相关专业的课程也在不断增多&#xff0c;高校对其管理具有一…

【机器学习】机器学习重要方法——无监督学习:理论、算法与实践

文章目录 引言第一章 无监督学习的基本概念1.1 什么是无监督学习1.2 无监督学习的主要任务 第二章 无监督学习的核心算法2.1 聚类算法2.1.1 K均值聚类2.1.2 层次聚类2.1.3 DBSCAN聚类 2.2 降维算法2.2.1 主成分分析&#xff08;PCA&#xff09;2.2.2 t-SNE 2.3 异常检测算法2.3…

扩散模型在时间序列预测中的兴起

摘要 本文探讨了扩散模型在时间序列预测中的应用。扩散模型在生成式人工智能的各个领域展示了最先进的成果。本文包括扩散模型的全面背景资料&#xff0c;详细说明了它们的调节方法&#xff0c;并回顾了它们在时间序列预测中的应用。分析涵盖了11个具体的时间序列实现&#xf…