前言

很早之前就写过面向切面的编程思想,主要学习了AOP的思想(参考:AOP简介)以及使用 AspectJ 实现简单的切面编程(参考:AspectJ之切点语法)。

其他常见的AOP编程框架还有 Cglib,Hibernate 和 Spring 等等,而这些目前流行的AOP框架绝大多数底层实现都是直接或间接地通过 ASM 来实现字节码操作。

因此,如果你想实现一些简单的切面编程,直接采用上面提及的AOP框架是绝对可以实现的,但是这些框架相对于 ASM 来说重了许多,在你进行代码切入的时候,可能会为你引入许多其他包的代码,导致生成的class文件体积增大不少,因此,对于一些简单的代码切片,推荐使用 ASM 字节码操作库直接对class文件动态进行代码切入。

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

简单的说,ASM 可以读取解析class文件内容,并提供接口让你可以对class文件字节码内容进行CRUD操作······

注: class文件存储的是java字节码,ASM 是对java字节码操作的一层封装,因此,如果你很了解 class文件格式的话,你甚至可以通过直接使用文本编辑器(eg:Vim)来改写class文件。

知道了 ASM 的作用后,接下来我们就来看下 ASM 的执行模式,了解它的执行模式后,我们才能更好地使用。

ASM 框架执行流程

ASM 提供了两组API:Core和Tree:

Core是基于访问者模式来操作类的

Tree是基于树节点来操作类的

本文我们主要讨论的是 ASM 的 CoreAPI。

ASM 内部采用 访问者模式 将 .class 类文件的内容从头到尾扫描一遍,每次扫描到类文件相应的内容时,都会调用ClassVisitor内部相应的方法。

比如:

扫描到类文件时,会回调ClassVisitor的visit()方法;

扫描到类注解时,会回调ClassVisitor的visitAnnotation()方法;

扫描到类成员时,会回调ClassVisitor的visitField()方法;

扫描到类方法时,会回调ClassVisitor的visitMethod()方法;

······

扫描到相应结构内容时,会回调相应方法,该方法会返回一个对应的字节码操作对象(比如,visitMethod()返回MethodVisitor实例),通过修改这个对象,就可以修改class文件相应结构部分内容,最后将这个ClassVisitor字节码内容覆盖原来.class文件就实现了类文件的代码切入。

具体关系如下:

树形关系

使用的接口

Class

ClassVisitor

Field

FieldVisitor

Method

MethodVisitor

Annotation

AnnotationVisitor

整个具体的执行时序如下图所示:

ASM执行流程时序图

通过时序图可以看出ASM在处理class文件的整个过程。ASM通过树这种数据结构来表示复杂的字节码结构,并利用 Push模型 来对树进行遍历。

ASM 中提供一个ClassReader类,这个类可以直接由字节数组或者class文件间接的获得字节码数据。它会调用accept()方法,接受一个实现了抽象类ClassVisitor的对象实例作为参数,然后依次调用ClassVisitor的各个方法。字节码空间上的偏移被转成各种visitXXX方法。使用者只需要在对应的的方法上进行需求操作即可,无需考虑字节偏移。

这个过程中ClassReader可以看作是一个事件生产者,ClassWriter继承自ClassVisitor抽象类,负责将对象化的class文件内容重构成一个二进制格式的class字节码文件,ClassWriter可以看作是一个事件的消费者。

至此,相信读者已经对 ASM 框架的执行过程有一定了解了。接下来我们还剩的一点内容就是如何实现class文件字节码的修改。

ASM 字节码修改

由于 ASM 是直接对class文件的字节码进行操作,因此,要修改class文件内容时,也要注入相应的java字节码。

所以,在注入字节码之前,我们还需要了解下class文件的结构,JVM指令等知识。

class文件结构

Java源文件经过javac编译器编译之后,将会生成对应的二进制.class文件,如下图所示:

ASM – Javac 流程

Java类文件是 8 位字节的二进制流。数据项按顺序存储在class文件中,相邻的项之间没有间隔,这使得class文件变得紧凑,减少存储空间。在Java类文件中包含了许多大小不同的项,由于每一项的结构都有严格规定,这使得 class 文件能够从头到尾被顺利地解析。

每个class文件都是有固定的结构信息,而且保留了源码文件中的符号。下图是class文件的格式图。其中带 * 号的表示可重复的结构。

class文件结构图

类结构体中所有的修饰符、字符常量和其他常量都被存储在class文件开始的一个常量堆栈(Constant Stack)中,其他结构体通过索引引用。

每个类必须包含headers(包括:class name, super class, interface, etc.)和常量堆栈(Constant Stack)其他元素,例如:字段(fields)、方法(methods)和全部属性(attributes)可以选择显示或者不显示。

每个字段块(Field section)包括名称、修饰符(public, private, etc.)、描述符号(descriptor)和字段属性。

每个方法区域(Method section)里面的信息与header部分的信息类似,信息关于最大堆栈(max stack)和最大本地变量数量(max local variable numbers)被用于修改字节码。对于非abstract和非native的方法有一个方法指令表,exceptions表和代码属性表。除此之外,还可以有其他方法属性。

每个类、字段、方法和方法代码的属性有属于自己的名称记录在类文件格式的JVM规范的部分,这些属性展示了字节码多方面的信息,例如源文件名、内部类、签名、代码行数、本地变量表和注释。JVM规范允许定义自定义属性,这些属性会被标准的VM(虚拟机)忽略,但是可以包含附件信息。

方法代码表包含一系列对java虚拟机的指令。有些指令在代码中使用偏移量,当指令从方法代码被插入或者移除时,全部偏移量的值可能需要调整。

Java类型与class文件内部类型对应关系

Java类型分为基本类型和引用类型,在 JVM 中对每一种类型都有与之相对应的类型描述,如下表:

Java type

JVM Type descriptor

boolean

Z

char

C

byte

B

short

S

int

I

float

F

long

J

double

D

Object

Ljava/lang/Object;

int[]

[I

Object[][]

[[Ljava/lang/Object;

在 ASM 中要获得一个类的 JVM 内部描述,可以使用org.objectweb.asm.Type类中的getDescriptor(final Class c)方法,如下:

public class TypeDescriptors {

public static void main(String[] args) {

System.out.println(Type.getDescriptor(TypeDescriptors.class));

System.out.println(Type.getDescriptor(String.class));

}

}

运行结果:

Lorg/victorzhzh/core/structure/TypeDescriptors;

Ljava/lang/String;

Java方法声明与class文件内部声明的对应关系

在·Java·的二进制文件中,方法的方法名和方法的描述都是存储在Constant pool 中的,且在两个不同的单元里。因此,方法描述中不含有方法名,只含有参数类型和返回类型。

格式:(参数描述符)返回值描述符

Method declaration in source file

Method descriptor

void m(int i, float f)

(IF)V

int m(Object o)

(Ljava/lang/Object;)I

int[] m(int i, String s)

(ILjava/lang/String;)[I

Object m(int[] i)

([I]Ljava/lang/Object;

String m()

()Ljava/lang/String;

JVM 指令

假设现在我们有如下一个类:

package com.yn.test;

public class Test {

public static void main(String[] agrs){

System.out.println("Hello World!");

}

}

我们先用javac com/yn/test/Test.java编译得到Test.class文件,然后再使用javap -c com/yn/test/Test来查看下这个Test.class文件的字节码,结果如下图所示:

Test.class字节码

上图中第3行到第7行,是类Test的默认构造函数(由编译器默认生成),Code以下部分是构造函数内部代码,其中:

aload_0: 这个指令是LOAD系列指令中的一个,它的意思表示装载当前第 0 个元素到堆栈中。代码上相当于“this”。而这个数据元素的类型是一个引用类型。这些指令包含了:ALOAD,ILOAD,LLOAD,FLOAD,DLOAD。区分它们的作用就是针对不用数据类型而准备的LOAD指令,此外还有专门负责处理数组的指令 SALOAD。

invokespecial: 这个指令是调用系列指令中的一个。其目的是调用对象类的方法。后面需要给上父类的方法完整签名。“#1”的意思是 .class 文件常量表中第1个元素。值为:“java/lang/Object."":()V”。结合ALOAD_0。这两个指令可以翻译为:“super()”。其含义是调用自己的父类构造方法。

第9到14行是main方法,Code以下是其字节码表示:

getstatic: 这个指令是GET系列指令中的一个其作用是获取静态字段内容到堆栈中。这一系列指令包括了:GETFIELD、GETSTATIC。它们分别用于获取动态字段和静态字段。此处表示的意思获取静态成员System.out到堆栈中。

ldc:这个指令的功能是从常量表中装载一个数据到堆栈中。此处表示从常量池中获取字符串"Hello World!"。

invokevirtual:也是一种调用指令,这个指令区别与 invokespecial 的是它是根据引用调用对象类的方法。此处表示调用java.io.PrintStream.println(String)方法,结合前面的操作,这里调用的就是System.out.println("Hello World!")。

return: 这也是一系列指令中的一个,其目的是方法调用完毕返回:可用的其他指令有:IRETURN,DRETURN,ARETURN等,用于表示不同类型参数的返回。

接下来,我们就可以根据上面所讲的内容,将代码字节码注入到class文件中了。

现在假设我们想要在类Test的main方法前后动态插入代码,如下所示:

package com.yn.test;

public class Test {

public static void main(String[] agrs){

System.out.println("asm insert before");

System.out.println("Hello World!");

System.out.println("asm insert after");

}

}

要完成在main方法前后插入输出代码,需要以下几步操作:

读取Test.class文件,可以通过 ASM 提供的ClassReader类进行class文件的读取与遍历。

// 使用全限定名,创建一个ClassReader对象

ClassReader classReader = new ClassReader("com.yn.test.Test");

// 构建一个ClassWriter对象,并设置让系统自动计算栈和本地变量大小

ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);

//创建一个自定义ClassVisitor,方便后续ClassReader的遍历通知

ClassVisitor classVisitor = new TestClassVisitor(classWriter);

//开始扫描class文件

classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);

构造System.out.println(String)的 ASM 代码。

上面我们从javap反编译得到的字节码可以知道,实现System.out.println("Hello World!");的字节码总共需要3步操作:

(1). 获取System静态成员out,其对应的指令为getstatic,对应的 ASM 代码为:

mv.visitFieldInsn(Opcodes.GETSTATIC,

Type.getInternalName(System.class), //"java/lang/System"

"out",

Type.getDescriptor(PrintStream.class) //"Ljava/io/PrintStream;"

);

(2). 获取字符串常量"Hello World!",其对应的指令为ldc,对应的 ASM 代码为:

mv.visitLdcInsn("Hello World!");

(3). 获取PrintStream.println(String)方法,其对应的指令为invokervirtual,对应的 ASM 代码为:

mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,

Type.getInternalName(PrintStream.class), //"java/io/PrintStream"

"println",

"(Ljava/lang/String;)V",//方法描述符

false);

在main方法进入前,进行代码插入,可以通过MethodVisitor.visitCode()方法。

// 在源方法前去修改方法内容,这部分的修改将加载源方法的字节码之前

@Override

public void visitCode() {

mv.visitCode();

System.out.println("method start to insert code");

sop("asm insert before");//this is the insert code

}

在main方法退出前,进行代码插入,可以通关过MethodVisitor.visitInsn()方法,通过判断当前的指令为return时,表明即将执行return语句,此时插入字节码即可。

@Override

public void visitInsn(int opcode) {

//检测到return语句

if (opcode == Opcodes.RETURN) {

System.out.println("method end to insert code");

sop("asm insert after");

}

//执行原本语句

mv.visitInsn(opcode);

}

字节码插入class文件成功后,导出字节码到原文件中。

//获取改写后的class二进制字节码

byte[] classFile = classWriter.toByteArray();

// 将这个类输出到原先的类文件目录下,这是原先的类文件已经被修改

File file = new File("E:/code/Android/Projects/AsmButterknife/sample-java/build/classes/java/main/com/yn/test/Test.class");

FileOutputStream fos = new FileOutputStream(file);

fos.write(classFile);

fos.close();

至此,我们已经完成了对Test.class的代码注入。

详细代码请参见:AsmTest

注: asm-commons 包中提供了一个类AdviceAdapter,使用该类可以更加方便的让我们在方法前后注入代码,因为其提供了方法onMethodEnter()和onMethodExit()。

通过上面介绍的内容,我们已经成功使用 ASM 动态注入字节码到class文件中。但是如果直接采用 ASM 代码注入字节码,还是相对困难的,幸运的是 ASM 给我们提供了 ASMifier 工具,使得我们可以直接通过.class文件反编译为 ASM 代码。

因此,当我们要使用 ASM 框架往class文件注入字节码时,我们通常是将要注入的java源码先写出来,然后通过javac编译出目标.class文件,然后再通过 ASMifier 工具反编译该.class文件,得到所需的 ASM 注入代码。

ASMifier 存在于asm-util.jar中,同时需要依赖asm.jar,幸运的是 ASM 提供了一个asm-all.jar包,可以方便我们直接运行 ASMifier。

asm-all.jar下载地址:asm-all

运行命令如下:

java -classpath "asm-all.jar" org.objectweb.asm.util.ASMifier org/domain/package/YourClass.class

如果还嫌上面的操作麻烦,github 上已经有人写了个前端页面方便我们将源码转变为 ASM 代码操作:asmifier

参考

java asm 中文文档_ASM 简介相关推荐

  1. java asm 中文文档_Java ASM3学习(3)

    MethodVisitor ClassVisitor的visitMethod能够访问到类中某个方法的一些入口信息,那么针对具体方法中字节码的访问是由MethodVisitor来进行的 访问顺序如下,其 ...

  2. JAVA API 中文文档 下载

    JAVA JDK API 1.8 链接: https://pan.baidu.com/s/1mE_O6biq80Z_bCO-ROOWug  密码: m41r JAVA JDK API 1.9 链接:h ...

  3. PlantCV中文文档

    PlantCV中文文档 1. 简介 1. 欢迎来到PlantCV文档 总览 开始 教程 贡献 版本 2. PlantCV Namespace 2.1 PlantCV 2.1.1 分析颜色 2.1.2 ...

  4. javax.servlet-api 简介、中文文档、中英对照文档 下载

    javax.servlet-api 文档 下载链接(含jar包.源码.pom) 组件名称 中文-文档-下载链接 中英对照-文档-下载链接 javax.servlet-api-3.1.0.jar 暂无 ...

  5. easyexcel 简介、中文文档、中英对照文档 下载

    easyexcel 文档 下载链接(含jar包.源码.pom) 组件名称 中文-文档-下载链接 中英对照-文档-下载链接 easyexcel-3.0.5.jar easyexcel-3.0.5-API ...

  6. commons-lang3 简介、中文文档、中英对照文档 下载

    commons-lang3 文档 下载链接(含jar包.源码.pom) 组件名称 中文-文档-下载链接 中英对照-文档-下载链接 commons-lang3-3.10.jar commons-lang ...

  7. itext 简介、中文文档、中英对照文档 下载

    itext 文档 下载链接(含jar包.源码.pom) 组件名称 中文-文档-下载链接 中英对照-文档-下载链接 itext-2.1.7.jar itext-2.1.7-API文档-中文版.zip i ...

  8. docx4j 简介、中文文档、中英对照文档 下载

    docx4j 文档 下载链接(含jar包.源码.pom) 组件名称 中文-文档-下载链接 中英对照-文档-下载链接 docx4j-3.3.5.jar docx4j-3.3.5-API文档-中文版.zi ...

  9. mybatis 简介、中文文档、中英对照文档 下载

    mybatis 文档 下载链接(含jar包.源码.pom) 组件名称 中文-文档-下载链接 中英对照-文档-下载链接 mybatis-3.2.8.jar mybatis-3.2.8-API文档-中文版 ...

  10. snakeyaml 简介、中文文档、中英对照文档 下载

    snakeyaml 文档 下载链接(含jar包.源码.pom) 组件名称 中文-文档-下载链接 中英对照-文档-下载链接 snakeyaml-1.12.jar snakeyaml-1.12-API文档 ...

最新文章

  1. 中国疾控中心回应论文争议:所有病例在论文撰写前已向社会公布
  2. 解决 android 高低版本 webView 里内容 自适应屏幕的终极方法
  3. android view clip,Android 自定义View Clip
  4. sphinx conf 文件模板
  5. Ubuntu搭建JDK环境
  6. WPF性能调试系列 – 内存监测
  7. java外挂源码_2.7 万 Star!Github 项目源码辅助阅读神器
  8. xtrabackup备份mysql“ib_logfile0 is of different”错误分析
  9. 从Java里调用R – JRI的设置方法
  10. python大纲_python学习大纲
  11. MFC工作笔记0007---消息映射处理
  12. 输入三角形的三c语言程序,输入三角形的三边 a,b,c,计算三角形的面积的公式是 C++...
  13. 2017哈尔滨ACM CCPC-final 总结
  14. linux 隐藏字符 h,webpack手动配置
  15. 【车牌识别】基于matlab车辆出入库计时系统【含Matlab源码 469期】
  16. Ubuntu卸载WPS安装Libreoffice
  17. 图解项目绩效考核指标及实例模板
  18. 解决Ionic官方网站打开缓慢问题
  19. python 识别汉字、数字、字母,实现半角及全角之间的转换
  20. 我的创作纪念日的温柔与七夕的浪漫交织了在一起

热门文章

  1. 抽象代数基本概念(一):代数系
  2. eslint 快捷键设置_eslint的妙用和快捷修复
  3. 【时间序列分析】01. 时间序列·平稳序列
  4. 通用文档信息提取模型浅析
  5. win10系统设置护眼色 word2016页面显示失败
  6. mx250显卡天梯图_mx250显卡天梯图_2020最新笔记本显卡天梯图,看看你的显卡排在哪里吧...
  7. 方舟官方服务器怎么显示血量,方舟端游怎么显示血量
  8. 勒索病毒是什么?防勒索病毒我们该怎么做?
  9. php编程常用英语词汇,泰牛程序员 PHP编程掌握的英语词汇(3) 韩顺平整理
  10. Python干货分享+百G资源放送!