ASM字节码操纵框架实现AOP

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 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运行处理
  1. ASM给我们提供了ASMifier工具来帮助开发,可使用ASMifier工具生成ASM结构来对比
  2. 如果没有ASMifier工具,自己去构建是非常吃力的,接下来代码实现的时候,我将使用该工具进行演示
  3. 好了,直接进入正题了。咖啡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();
        }
       
    }


因此我们可以将增强的方法,全部封装在一个新的类中

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

相关文章
|
4月前
|
分布式计算 Java MaxCompute
详解 Java 限流接口实现问题之在Spring框架中使用AOP来实现基于注解的限流问题如何解决
详解 Java 限流接口实现问题之在Spring框架中使用AOP来实现基于注解的限流问题如何解决
|
5月前
|
Java API Android开发
ASM 框架:字节码操作的常见用法(生成类,修改类,方法插桩,方法注入)
ASM 框架:字节码操作的常见用法(生成类,修改类,方法插桩,方法注入)
96 0
|
5月前
|
存储 算法 Java
Android 进阶——代码插桩必知必会&ASM7字节码操作
Android 进阶——代码插桩必知必会&ASM7字节码操作
241 0
|
5月前
|
Java Kotlin
ASM字节码插桩实现点击防抖
ASM字节码插桩实现点击防抖
47 0
|
5月前
|
设计模式 SQL Java
Spring框架第四章(AOP概念及相关术语)
Spring框架第四章(AOP概念及相关术语)
|
6月前
|
安全 Java 开发者
在Spring框架中,IoC和AOP是如何实现的?
【4月更文挑战第30天】在Spring框架中,IoC和AOP是如何实现的?
79 0
|
6月前
|
Oracle 关系型数据库
oracle asm 磁盘显示offline
oracle asm 磁盘显示offline
323 2
|
24天前
|
存储 Oracle 关系型数据库
数据库数据恢复—Oracle ASM磁盘组故障数据恢复案例
Oracle数据库数据恢复环境&故障: Oracle ASM磁盘组由4块磁盘组成。Oracle ASM磁盘组掉线 ,ASM实例不能mount。 Oracle数据库故障分析&恢复方案: 数据库数据恢复工程师对组成ASM磁盘组的磁盘进行分析。对ASM元数据进行分析发现ASM存储元数据损坏,导致磁盘组无法挂载。
|
6月前
|
存储 Oracle 关系型数据库
【数据库数据恢复】Oracle数据库ASM磁盘组掉线的数据恢复案例
oracle数据库ASM磁盘组掉线,ASM实例不能挂载。数据库管理员尝试修复数据库,但是没有成功。
【数据库数据恢复】Oracle数据库ASM磁盘组掉线的数据恢复案例
|
SQL Oracle 关系型数据库
Oracle ASM磁盘和磁盘组的常用SQL语句
Oracle ASM磁盘和磁盘组的常用SQL语句
282 0