ASM

ASM是一个Java字节码操控框架,用于动态生成类或增强已有类的功能。文章详细介绍了字节码的结构,包括魔数、版本号、常量池等,并讲解了JVM的类加载机制和字节码执行引擎。ASM提供核心API和树形API,可在类加载前修改字节码,常用于AOP、热部署等场景。文中还展示了如何直接利用ASM实现AOP,以及如何使用ASM Bytecode Outline辅助字节码操作。

1,简介

ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

BCEL 和 SERL 不同(ASM 提供了与 BCEL和SERP相似的功能,只有22K的大小,比起350K的BCEL和150K的SERP来说,是相当小巧的,并且它有更高的执行效率,  是BCEL 的7倍,SERP的11倍以上),ASM 提供了更为现代的编程模型。对于 ASM 来说,Java class 被描述为一棵树;使用 “Visitor” 模式遍历整个二进制结构;事件驱动的处理方式使得用户只需要关注于对其编程有意义的部分,而不必了解 Java 类文件格式的所有细节:ASM 框架提供了默认的 “response taker”处理这一切。

 

2,字节码&JVM

2.1什么是字节码

Java之所以可以“一次编译,到处运行”,一是因为JVM针对各种操作系统、平台都进行了定制,二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用。因此,也可以看出字节码对于Java生态的重要性。之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取。在Java中一般是用javac命令编译源代码为字节码文件,一个.java文件从编译到运行的示例如图1所示。

图1 Java运行示意图

                                                                         图1 Java运行示意图

对于开发人员,了解字节码可以更准确、直观地理解Java语言中更深层次的东西,比如通过字节码,可以很直观地看到Volatile关键字如何在字节码上生效。另外,字节码增强技术在Spring AOP、各种ORM框架、热部署中的应用屡见不鲜,深入理解其原理对于我们来说大有裨益。除此之外,由于JVM规范的存在,只要最终可以生成符合规范的字节码就可以在JVM上运行,因此这就给了各种运行在JVM上的语言(如Scala、Groovy、Kotlin)一种契机,可以扩展Java所没有的特性或者实现各种语法糖。理解字节码后再学习这些语言,可以“逆流而上”,从字节码视角看它的设计思路,学习起来也“易如反掌”。

2.2字节码结构

.java文件通过javac编译后将得到一个.class文件,比如编写一个简单的ByteCodeDemo类,如下图2的左侧部分:

图2 示例代码(左侧)及对应的字节码(右侧)

                                                      图2 示例代码(左侧)及对应的字节码(右侧)

编译后生成ByteCodeDemo.class文件,打开后是一堆十六进制数,按字节为单位进行分割后展示如图2右侧部分所示。上文提及过,JVM对于字节码是有规范要求的,那么看似杂乱的十六进制符合什么结构呢?JVM规范要求每一个字节码文件都要由十部分按照固定的顺序组成,整体结构如图3所示。接下来我们将一一介绍这十部分:

图3 JVM规定的字节码结构

                                                图3 JVM规定的字节码结构

(1) 魔数(Magic Number)

所有的.class文件的前四个字节都是魔数,魔数的固定值为:0xCAFEBABE。魔数放在文件开头,JVM可以根据文件的开头来判断这个文件是否可能是一个.class文件,如果是,才会继续进行之后的操作。

有趣的是,魔数的固定值是Java之父James Gosling制定的,为CafeBabe(咖啡宝贝),而Java的图标为一杯咖啡。

(2) 版本号

版本号为魔数之后的4个字节,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)。上图2中版本号为“00 00 00 34”,次版本号转化为十进制为0,主版本号转化为十进制为52,在Oracle官网中查询序号52对应的主版本号为1.8,所以编译该文件的Java版本号为1.8.0。

(3) 常量池(Constant Pool)

紧接着主版本号之后的字节为常量池入口。常量池中存储两类常量:字面量与符号引用。字面量为代码中声明为Final的常量值,符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。常量池整体上分为两部分:常量池计数器以及常量池数据区,如下图4所示。

图4 常量池的结构

                                                                                图4 常量池的结构

  • 常量池计数器(constant_pool_count):由于常量的数量不固定,所以需要先放置两个字节来表示常量池容量计数值。图2中示例代码的字节码前10个字节如下图5所示,将十六进制的24转化为十进制值为36,排除掉下标“0”,也就是说,这个类文件中共有35个常量。
  • 图5 前十个字节及含义
  •                                             图5 前十个字节及含义
  • 常量池数据区:数据区是由(constant_pool_count-1)个cp_info结构组成,一个cp_info结构对应一个常量。在字节码中共有14种类型的cp_info(如下图6所示),每种类型的结构都是固定的。
  • 图6 各类型的cp_info
  •                                                                  图6 各类型的cp_info

具体以CONSTANT_utf8_info为例,它的结构如下图7左侧所示。首先一个字节“tag”,它的值取自上图6中对应项的Tag,由于它的类型是utf8_info,所以值为“01”。接下来两个字节标识该字符串的长度Length,然后Length个字节为这个字符串具体的值。从图2中的字节码摘取一个cp_info结构,如下图7右侧所示。将它翻译过来后,其含义为:该常量类型为utf8字符串,长度为一字节,数据为“a”。

 图7 CONSTANT_utf8_info的结构(左)及示例(右)图7 CONSTANT_utf8_info的结构(左)及示例(右)

                                               图7 CONSTANT_utf8_info的结构(左)及示例(右)

  • 其他类型的cp_info结构在本文不再赘述,整体结构大同小异,都是先通过Tag来标识类型,然后后续n个字节来描述长度和(或)数据。先知其所以然,以后可以通过javap -verbose ByteCodeDemo命令,查看JVM反编译后的完整常量池,如下图8所示。可以看到反编译结果将每一个cp_info结构的类型和值都很明确地呈现了出来。

图8 常量池反编译结果

                             图8 常量池反编译结果

(4) 访问标志

常量池结束之后的两个字节,描述该Class是类还是接口,以及是否被Public、Abstract、Final等修饰符修饰。JVM规范规定了如下图9的访问标志(Access_Flag)。需要注意的是,JVM并没有穷举所有的访问标志,而是使用按位或操作来进行描述的,比如某个类的修饰符为Public Final,则对应的访问修饰符的值为ACC_PUBLIC | ACC_FINAL,即0x0001 | 0x0010=0x0011。

图9 访问标志

                   图9 访问标志

(5) 当前类名

访问标志后的两个字节,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。

(6) 父类名称

当前类名后的两个字节,描述父类的全限定名,同上,保存的也是常量池中的索引值。

(7) 接口信息

父类名称后为两字节的接口计数器,描述了该类或父类实现的接口数量。紧接着的n个字节是所有接口名称的字符串常量的索引值。

(8) 字段表

字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的局部变量。字段表也分为两部分,第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息fields_info。字段表结构如下图所示:

图10 字段表结构

                                                       图10 字段表结构

以图2中字节码的字段表为例,如下图11所示。其中字段的访问标志查图9,0002对应为Private。通过索引下标在图8中常量池分别得到字段名为“a”,描述符为“I”(代表int)。综上,就可以唯一确定出一个类中声明的变量private int a。

图11 字段表示例

                       图11 字段表示例

(9)方法表

字段表结束后为方法表,方法表也是由两部分组成,第一部分为两个字节描述方法的个数;第二部分为每个方法的详细信息。方法的详细信息较为复杂,包括方法的访问标志、方法名、方法的描述符以及方法的属性,如下图所示:

图12 方法表结构

                                       图12 方法表结构

方法的权限修饰符依然可以通过图9的值查询得到,方法名和方法的描述符都是常量池中的索引值,可以通过索引值在常量池中找到。而“方法的属性”这一部分较为复杂,直接借助javap -verbose将其反编译为人可以读懂的信息进行解读,如图13所示。可以看到属性中包括以下三个部分:

  • “Code区”:源代码对应的JVM指令操作码,在进行字节码增强时重点操作的就是“Code区”这一部分。
  • “LineNumberTable”:行号表,将Code区的操作码和源代码中的行号对应,Debug时会起到作用(源代码走一行,需要走多少个JVM指令操作码)。
  • “LocalVariableTable”:本地变量表,包含This和局部变量,之所以可以在每一个方法内部都可以调用This,是因为JVM将This作为每一个方法的第一个参数隐式进行传入。当然,这是针对非Static方法而言。

图13 反编译后的方法表

                             图13 反编译后的方法表

(10)附加属性表

字节码的最后一部分,该项存放了在该文件中类或接口所定义属性的基本信息。

2.3 字节码操作集合

在上图13中,Code区的红色编号0~17,就是.java中的方法源代码编译后让JVM真正执行的操作码。为了帮助人们理解,反编译后看到的是十六进制操作码所对应的助记符,十六进制值操作码与助记符的对应关系,以及每一个操作码的用处可以查看Oracle官方文档进行了解,在需要用到时进行查阅即可。比如上图中第一个助记符为iconst_2,对应到图2中的字节码为0x05,用处是将int值2压入操作数栈中。以此类推,对0~17的助记符理解后,就是完整的add()方法的实现。

2.4 操作数栈和字节码

JVM的指令集是基于栈而不是寄存器,基于栈可以具备很好的跨平台性(因为寄存器指令集往往和硬件挂钩),但缺点在于,要完成同样的操作,基于栈的实现需要更多指令才能完成(因为栈只是一个FILO结构,需要频繁压栈出栈)。另外,由于栈是在内存实现的,而寄存器是在CPU的高速缓存区,相较而言,基于栈的速度要慢很多,这也是为了跨平台性而做出的牺牲。

我们在上文所说的操作码或者操作集合,其实控制的就是这个JVM的操作数栈。为了更直观地感受操作码是如何控制操作数栈的,以及理解常量池、变量表的作用,将add()方法的对操作数栈的操作制作为GIF,如下图14所示,图中仅截取了常量池中被引用的部分,以指令iconst_2开始到ireturn结束,与图13中Code区0~17的指令一一对应:

图14 控制操作数栈示意图

                                           图14 控制操作数栈示意图

 

在这篇文章中有详细的介绍 认识 .class 文件的字节码结构,这篇文章以一个简单的例子,手把手的分析十六进制符合表示的 .class 文件

 

认识 .class 文件的字节码结构

2.5 Java 虚拟机类加载机制

上面一小节介绍了 .class 文件的结构,但是 .class 文件是静态的,它最终是会被虚拟机加载才能执行的,那么问题来了,.class 文件是什么时候会被加载呢?

 

一般来说,一个 .class 文件就包含一个 Java 类,.class 文件和 Java 类是息息相关的。要说 .class 文件的加载时机,就不得不提到 Java 类的生命周期了。

 

想必大家都知道,Java 类的生命周期包含加载、验证、准备、解析、初始化、使用、卸载七个步骤,在 Java 虚拟机规范中并没有规定 Java 类的加载时机,但是却规定了 Java 类 初始化 的时机,而加载又一定是在初始化的前面,所以也可以说是间接地规定了 .class 文件的加载的时机。

 

有五种情况,是必须初始化一个类的,这五种情况被称为对 Java 类的主动引用,除了 主动引用 之外,其他的对 Java 类的引用称为 被动引用。

 

上面也提到了 Java 类的生命周期总共分为加载、验证、准备、解析、初始化、使用、卸载,其中最重要的是前五个步骤加载、验证、准备、解析、初始化,那在这五个步骤中都发生了什么事情呢?

 

举一个简单的例子,如下所示。下面的 Constant 类中,有一个静态 static 代码块,和一个静态 static 变量, 是什么时候给 value 赋值的呢?什么时候会执行 static 代码块呢?答案是在类的 初始化 阶段。

 

public class Constant {

    static {
        System.out.println("Constant init!");
    }

    public static String value = "lijiankun24!";
}

 

在 Java 类中,如果有静态 static 代码块、静态 static 变量的话,编译器会为这个类自动生成一个类构造器(注意,不是实例构造器),在 类构造器 中会执行静态 static 代码块,初始化静态 static 变量,类构造器 就是在类的 初始化 阶段执行的

 

提到 Java 类的加载,就不得不说起 Java 中的类加载器 ClassLoader 了,双亲委派模型及其好处也是必须要清楚的。

深入理解jvm类加载机制

2.6 Java 虚拟机字节码执行引擎

 

Java 内存模型中,非常重要的一个区域就是 Java 虚拟机栈。Java 中每一个方法执行的时候都会在 Java 虚拟机栈中压入一个栈帧,方法执行完成之后,也会将该栈帧出栈。


栈帧中最主要的是局部变量表、操作数栈这两个概念,在执行一个 Java 方法的字节码时,其实就是调用 Java 字节码指令操纵局部变量表、操作数栈,最后将执行的结果返回。如果想学习 Java 字节码指令的话,推荐一篇文章。

 

除了方法的执行过程,还需要了解一下 Java 中的方法调用。方法调用就是指通过 .class 文件中方法的符号引用,确认方法的直接引用的过程,这个过程有可能发生在加载阶段,也有可能发生在运行阶段。


有一些方法是在加载阶段就已经确定了方法的直接引用,比如:静态方法、私有方法、实例构造器方法,这类方法的调用称为 解析;除了解析,方法的 静态分派 也是在加载阶段就确定了方法的直接引用,这类方法常见的就是 重载 的方法。


有一些方法是在运行阶段确认方法的直接引用的,比如:重写 的方法,调用重写 的方法时,需要具体到对象的实际类型,所以需要特定的 Java 字节码 invokevirtual 去确定合适的方法。

 

Java 虚拟机是基于栈的解释执行的,这里所说的栈 就是 Java 虚拟机栈,解释执行时相对于编译执行而言的,解释执行就是指:代码通过编译生成字节码指令集之后,通过解释器解释执行的。这个不用了解的太深,明白这几个定义就好。

上面介绍了 Java 虚拟机栈中的 栈帧、方法调用、解析、静态分派、动态分派 和 Java 虚拟机基于栈的解释执行,详细的内容可以参考  虚拟机字节码执行引擎。

虚拟机字节码执行引擎

3,字节码增强

在上文中,着重介绍了字节码的结构,这为我们了解字节码增强技术的实现打下了基础。字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术。接下来,我们将从最直接操纵字节码的实现方式开始深入进行剖析。

图16 字节码增强技术

                                                         图16 字节码增强技术

ASM

asm是字节码增强技术,通过asm可以生成新的class文件,也可以动态的修改即将要装载入jvm的类信息。

ASM是一个Java字节码操控框架,它被用来动态生成类或者增强已有类的功能。

ASM可以直接生产二进制class文件,也可以在类被加载到Java虚拟机之前动态改变类。Java类存储在.class文件中,ASM就从这些类文件中读入信息,然后可以动态改变类行为、分析类行为、或者生成新类。

ASM字节码增强技术主要是用反射的时候提升性能,如果单纯用jdk的反射调用,性能是比较低的,而使用字节码增强技术后反射的调用时间已经基本可以与直接调用相当了。

反射为什么性能低:

1、Class.forname()调用本地方法,比较耗时
2、Class.getMethod会遍历类的方法。
ASM框架的核心类有以下几个:

ClassReader:该类用来解析编译过的class字节码
ClassWriter:该类用来重新构建编译后的类,比如修改类名,属性以及方法,甚至可以生成新的类的字节码文件。
ClassAdapter:该类实现了ClassVisitor接口,它将对它的方法调用委托给另一个ClassVisitor对象。

因此可以将ASM理解为对类文件的CRUD,经过CRUD的字节码可以转换为类。

ASM的解析类似于SAX解析XML文件,相比于其它方式比如CGLIB,它的优势在于性能更高,在Spring中都是使用的cglib动态代理,而cglib本身就是使用ASM。

利用ASM可以获得class文件的详细信息:比如类名、父类名、接口、成员名、方法参数名、局部变量名、元数据等。
还可以对class文件进行动态修改,比如增加、删除、修改某个类的方法。

CGLIB是对ASM的封装,简化ASM的操作,降低ASM的使用门槛。
 

对于需要手动操纵字节码的需求,可以使用ASM,它可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为(如下图17所示)。ASM的应用场景有AOP(Cglib就是基于ASM)、热部署、修改其他jar包中的类等。当然,涉及到如此底层的步骤,实现起来也比较麻烦。接下来,本文将介绍ASM的两种API,并用ASM来实现一个比较粗糙的AOP。但在此之前,为了让大家更快地理解ASM的处理流程,强烈建议读者先对访问者模式和 ASM进行了解。简单来说,访问者模式主要用于修改或操作一些数据结构比较稳定的数据,而通过第一章,我们知道字节码文件的结构是由JVM固定的,所以很适合利用访问者模式对字节码文件进行修改。

图17 ASM修改字节码

                                图17 ASM修改字节码

ASM API

3.1.1 ASM核心API

ASM Core API可以类比解析XML文件中的SAX方式,不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用Core API。在Core API中有以下几个关键类:

  • ClassReader:用于读取已经编译好的.class文件。
  • ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。
  • 各种Visitor类:如上所述,CoreAPI根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的Visitor,比如用于访问方法的MethodVisitor、用于访问类变量的FieldVisitor、用于访问注解的AnnotationVisitor等。为了实现AOP,重点要使用的是MethodVisitor。

3.1.2 树形API

ASM Tree API可以类比解析XML文件中的DOM方式,把整个类的结构读取到内存中,缺点是消耗内存多,但是编程比较简单。TreeApi不同于CoreAPI,TreeAPI通过各种Node类来映射字节码的各个区域,类比DOM节点,就可以很好地理解这种编程方式。

ASM 可以直接生产二进制的 .class 文件,也可以在类被加载入 JVM 之前动态修改类行为。ASM 库的介绍和使用 文章介绍了 ASM 库的结构和几个重要的 Core Api,包括 ClassVisitor、ClassReader、ClassWriter、MethodVisitor 和 AdviceAdapter 等

 

4.实战

4.1直接利用ASM实现AOP 直接生成.calss文件

利用ASM的CoreAPI来增强类。这里不纠结于AOP的专业名词如切片、通知,只实现在方法调用前、后增加逻辑,通俗易懂且方便理解。首先定义需要被增强的Base类:其中只包含一个process()方法,方法内输出一行“process”。增强后,我们期望的是,方法执行前输出“start”,之后输出”end”。

public class Base {
    public void process(){
        System.out.println("process");
    }
}

 

为了利用ASM实现AOP,需要定义两个类:一个是MyClassVisitor类,用于对字节码的visit以及修改;另一个是Generator类,在这个类中定义ClassReader和ClassWriter,其中的逻辑是,classReader读取字节码,然后交给MyClassVisitor类处理,处理完成后由ClassWriter写字节码并将旧的字节码替换掉。Generator类较简单,我们先看一下它的实现,如下所示,然后重点解释MyClassVisitor类。

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;

public class Generator {
    public static void main(String[] args) throws Exception {
		//读取
        ClassReader classReader = new ClassReader("meituan/bytecode/asm/Base");
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        //处理
        ClassVisitor classVisitor = new MyClassVisitor(classWriter);
        classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
        byte[] data = classWriter.toByteArray();
        //输出
        File f = new File("operation-server/target/classes/meituan/bytecode/asm/Base.class");
        FileOutputStream fout = new FileOutputStream(f);
        fout.write(data);
        fout.close();
        System.out.println("now generator cc success!!!!!");
    }
}

 

MyClassVisitor继承自ClassVisitor,用于对字节码的观察。它还包含一个内部类MyMethodVisitor,继承自MethodVisitor用于对类内方法的观察,它的整体代码如下:

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class MyClassVisitor extends ClassVisitor implements Opcodes {
    public MyClassVisitor(ClassVisitor cv) {
        super(ASM5, cv);
    }
    @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 desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
                exceptions);
        //Base类中有两个方法:无参构造以及process方法,这里不增强构造方法
        if (!name.equals("<init>") && mv != null) {
            mv = new MyMethodVisitor(mv);
        }
        return mv;
    }
    class MyMethodVisitor extends MethodVisitor implements Opcodes {
        public MyMethodVisitor(MethodVisitor mv) {
            super(Opcodes.ASM5, mv);
        }

        @Override
        public void visitCode() {
            super.visitCode();
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("start");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
        @Override
        public void visitInsn(int opcode) {
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
                    || opcode == Opcodes.ATHROW) {
                //方法在返回之前,打印"end"
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitLdcInsn("end");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            }
            mv.visitInsn(opcode);
        }
    }
}

 

利用这个类就可以实现对字节码的修改。详细解读其中的代码,对字节码做修改的步骤是:

  • 首先通过MyClassVisitor类中的visitMethod方法,判断当前字节码读到哪一个方法了。跳过构造方法 <init> 后,将需要被增强的方法交给内部类MyMethodVisitor来进行处理。
  • 接下来,进入内部类MyMethodVisitor中的visitCode方法,它会在ASM开始访问某一个方法的Code区时被调用,重写visitCode方法,将AOP中的前置逻辑就放在这里。
  • MyMethodVisitor继续读取字节码指令,每当ASM访问到无参数指令时,都会调用MyMethodVisitor中的visitInsn方法。我们判断了当前指令是否为无参数的“return”指令,如果是就在它的前面添加一些指令,也就是将AOP的后置逻辑放在该方法中。
  • 综上,重写MyMethodVisitor中的两个方法,就可以实现AOP了,而重写方法时就需要用ASM的写法,手动写入或者修改字节码。通过调用methodVisitor的visitXXXXInsn()方法就可以实现字节码的插入,XXXX对应相应的操作码助记符类型,比如mv.visitLdcInsn(“end”)对应的操作码就是ldc “end”,即将字符串“end”压入栈。

完成这两个visitor类后,运行Generator中的main方法完成对Base类的字节码增强,增强后的结果可以在编译后的target文件夹中找到Base.class文件进行查看,可以看到反编译后的代码已经改变了(如图18左侧所示)。然后写一个测试类MyTest,在其中new Base(),并调用base.process()方法,可以看到下图右侧所示的AOP实现效果:

图18 ASM实现AOP的效果

                                                     图18 ASM实现AOP的效果

 

5.ASM Bytecode Outline

利用ASM手写字节码时,需要利用一系列visitXXXXInsn()方法来写对应的助记符,所以需要先将每一行源代码转化为一个个的助记符,然后通过ASM的语法转换为visitXXXXInsn()这种写法。第一步将源码转化为助记符就已经够麻烦了,不熟悉字节码操作集合的话,需要我们将代码编译后再反编译,才能得到源代码对应的助记符。第二步利用ASM写字节码时,如何传参也很令人头疼。ASM社区也知道这两个问题,所以提供了工具ASM ByteCode Outline

安装后,右键选择“Show Bytecode Outline”,在新标签页中选择“ASMified”这个tab,如图19所示,就可以看到这个类中的代码对应的ASM写法了。图中上下两个红框分别对应AOP中的前置逻辑于后置逻辑,将这两块直接复制到visitor中的visitMethod()以及visitInsn()方法中,就可以了。

图19 ASM Bytecode Outline

                                图19 ASM Bytecode Outline

 

本文参考:字节码增强技术探索

                  换个方式实现hugo | 从 Java 字节码到 ASM 实践

                  Java Agent与ASM字节码介绍

                  史上最通俗易懂的ASM教程

                  Java中ASM框架详解

 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值