一、ASM库概述
ASM 是一个Java 字节码(.class)操控框架,它可以用来动态生成类的字节码或者改变现有类的字节码。借由ASM可以直接创建或修改字节码文件,也就能在类被加载到JVM执行之前动态改变原有的类行为。其目的是生成、转换和分析以字节数组来表示的已编译 Java 类。因为无论是在磁盘的存储形式还是JVM的加载皆采用这种字节数组形式,Java 字节码文件按照Java 虚拟机规范中的格式进行组织并存储。为此,ASM把Java class 抽象为一棵树,使用 “Visitor” 模式遍历整个二进制结构,基于事件驱动的处理方式使得用户只需要关注于对其编程有意义的部分,而不必深入Java 类文件格式的所有细节。ASM 从字节码文件中读取所有相关信息(包括类名称、方法、属性、数值常数、 字符串、 Java 标识符、 Java类型、 Java 类结构元素以及 Java 字节码指令等)并提供了字节码级别的接口来读写和转换这些字节数组且在访问到对应的信息时提供对应的回调方法。通俗来说就是 ASM提供了一系列的API,能够让我们通过Java 字节码指令去处理Java字节码 (因为字节码文件就是主要就是由16进制形式的字节码指令组成的),欲了解更多请自己参阅官网
ASM 的名字没有任何含义,就是任性引用了C库中的一些可以操作汇编语言函数名称中的asm,而且ASM 的使用范围仅限于
对类(.class)文件的读、写、转换和分析(即加载前的过程进行干预),至于类的加载过程就超出了其能力范围。
二、ASM库的架构模型概述
ASM 库提供了两个用于生成和转换已编译类的 API:核心 API(,以基于事件的形式来表示类,)和树 API,以基于对象的形式来表示类
1、核心API概述
核心 API基于事件的形式来表示类,把类抽象为一系列事件,每个事件表示类的一个元素(比如它的一个标头、一个字段、一个方法声明、一条指令等等)。基于事件的 API 定义了一组可能事件,以及这些事件必须遵循的发生顺序,还提供了一个类分析器,为每个被分析元素生成一个事件,还提供一个类写入器,由这些事件的序列生成经过编译的类。其组织结构是围绕事件生成器(类分析器)、事件使用器(类写入器)和各种预定义的事件筛选器进行的,在这一结构中可以添加用户定义的生成器、使用器和筛选器,将事件生成器(负责执行生成或转换过程)、筛选器和使用器组件组装为可能很复杂的体系结构,
‰ 然后启动事件生成器,以执行生成或转换过程。
2、树 API概述
树 API基于对象模型来表示类, 把类抽象为对象树, 每个对象表示类的一部分( 比如类本身、一个字段、一个方法、一条指令等等,每个对象都有一些引用,指向表示其组成部分的对象)。基于对象的 API 提供了一种方法,可以将表示一个类的事件序列转换为表示同一个类的对象树,也可以反过来,将对象树表示为等价的事件序列。换言之,基于对象的 API 构建在基于事件的API 之上,用于操作类树的类生成器或转换器组件是可以组成链的,它们之间的链接代表着转换的顺序,使用 “Visitor” 模式遍历整个二进制结构,基于事件驱动的处理方式使得用户只需要关注于对其编程有意义的部分,而不必深入Java 类文件格式的所有细节。如下所示的复杂体系结构:
其中的箭头表示在类分析器、写入器或转换器之间进行的基于事件或基于对象的通信, 在整个链中的任何位置, 都可能会在基于事件与基于对象的表示之间进行转换。
三、ASM库核心组件和接口类
ASM通过树这种数据结构来抽象复杂的字节码结构并利用 Push 模型来对树进行遍历,在遍历过程中对字节码进行修改。所谓的 Push 模型类似于简单的访问者(Visitor) 设计模式,因为字节码结构是固定的,所以不需要专门抽象出一种 Vistable 接口,而只需要提供 Visitor 接口来遍历一些复杂的数据结构,其中Visitor 相当于用户派出的代表,深入到算法内部,由算法安排访问行程,而Visitor 代表可以更换,但对算法流程无法干涉,因此是被动的。
1、ClassVisitor
ASM用于生成和变转字节码文件的API是基于 ClassVisitor 抽象类的,该类中的每个方法都对应于同名的类文件结构部分。对于字节码中简单的部分只需调用一个方法就能完成对应部分的字节码构建并返回 void;有些复杂部分的内容(例如visitAnnotation、 visitField 和 visitMethod 方法,它们分别返AnnotationVisitor、 FieldVisitor 和 MethodVisitor)则用一个初始方法调用来访问并返回一个辅助的访问者类。ClassVisitor作为Java类的访问者角色,封装了在读取Class字节码时会触发的一系列事件,如类头解析完成、注解解析、字段解析、方法解析等并按照以下顺序调用: visit() >visitSource() >visitModule() >visitNestHost() >visitOuterClass() > visitAnnotation() > visitTypeAnnotation() > visitAttribute()> visitNestMember() > visitInnerClass() > visitField()> visitMethod()> visitEnd(),当分析到方法时就会回调visitMethod方法,进入到方法体内部时也会回调对应的方法。简而言之,ClassVisitor (包含其子类)在ASM 访问到对应类的元素时回自动回调对应的方法。
类/接口 | 说明 |
---|---|
AnnotationVisitor | 定义在解析注解时会触发的一系列的事件,解析到一个基本值类型的注解、enum值类型的注解、Array值类型的注解、注解值类型的注解时,会调用对应的方法,下同。 |
FieldVisitor | 定义在解析字段时触发的事件,如解析到字段上的注解、解析到字段相关的属性等。 |
MethodVisitor | 定义在解析方法时触发的事件,如方法上的注解、属性、代码等。 |
SignatureVisitor | 定义在解析Signature时会触发的事件,如正常的Type参数、类或接口的边界等。 |
各个 ClassVisitor通过责任链 (Chain-of-responsibility) 模式,可以非常简单的封装对字节码的各种修改,而无须关注字节码的内部字节偏移,通过这些对应的Visitor可以访问字节码文件中对应的组成部分,而且ClassVisitor会自己去控制这些过程,用户要做的只是覆写相应的 visit 方法,比如 visitMethod会返回一个实现 MethordVisitor接口的实例,visitField会返回一个实现 FieldVisitor接口的实例,完成子过程后控制返回到父过程,继续访问下一节点。
部分方法 | 说明 |
---|---|
void visit(int version, int access, String name, String signature, String superName, String[] interfaces) | 访问class的头部信息时,version为class版本(编译版本),access为访问修饰符,name为类名称,signature为class的签名,可能是null,superName为超类名称,interfaces为接口的名称 |
AnnotationVisitor visitAnnotation(String descriptor, boolean visible) | 访问class的注解信息时,descriptor为签名描述信息,visible为是否运行时可见 |
void visitAttribute(Attribute attribute) | 访问该类的非标准属性。 |
void visitInnerClass(String name, String outerName, String innerName, int access) | 访问class中内部类的信息,而且这个内部类不一定是被访问类的成员(有可能是一段方法中的匿名内部类或者声明在一个方法中的类等等)。name为内部类的名称,outerName为内部类所在类的名称,innerName为内部类的名称 |
void visitOuterClass(String owner, String name, String descriptor) | 访问该类的外部类,仅当类具有封闭类时,才必须调用此方法。owner为拥有该类的class名称,name为包含该类的方法的名称,如果该类未包含在其封闭类的方法中,则返回null,descriptor为签名描述信息 |
void visitEnd() | 结束访问class时 |
FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) | 访问class中字段的信息,返回一个FieldVisitor用于操作字段相关的信息,access为访问修饰符,name为类名称,signature为class的签名,可能是null,descriptor为描述信息 |
MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) | 访问class中方法的信息,返回一个MethodVisitor用于操作字段相关的信息,access为访问修饰符,name为方法名称,signature为方法的签名,可能是null,descriptor为描述信息,exceptions为异常 |
ModuleVisitor visitModule(String name, int access, String version) | 访问对应的模块。 |
AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) | 访问类签名中类型的注释。 |
2、ClassWriter
ClassWriter类是ASM中主要用于生成一个类的字节码文件的类并继承了ClassVisitor抽象类,通过toByteArray()方法返回生成的字节码的字节流,将字节流写回文件即可生产调整后的 class 文件(所有 visit 事件的时间上先后调用,最终转换成字节码的空间位置前后的调整)。可以通过ClassWriter中的visitXxxx方法返回对应各部分的访问者去创建字节码的文件中的各个部分,传入对应的字节码指令也是通过visitXxxxxx方法,如果要创建则传入对应的创建类型的字节码指令。
2.1、ClassWriter 核心方法
部分方法 | 说明 |
---|---|
public ClassWriter(final int flags) | 构造ClassWriter对象,flag取值为0、1、2(0时表示需要手动计算最大操作数栈、局部变量表、桢变化;ClassWriter.COMPUTE_MAXS表示自动计算局部变量表和操作数栈,但是必须要调用visitMaxs,方法参数会被忽略。桢变化需要手动计算ClassWriter.COMPUTE_FRAMES表示全自动计算,但是必须要调用visitMaxs,方法参数会被忽略。但ClassWriter.COMPUTE_MAXS比0慢10%,比COMPUTE_FRAMES慢一倍。) |
public final void visit(final int version, final int access,final String name,final String signature,final String superName,final String[] interfaces) | 构造class文件的头部信息,version为指定的JDK版本(取值为Opcodes定义的常量),access为类的修饰符(同version),name为类的名称,signature与泛型相关的,若传入null则表示该字段不是泛型的签名,superName为要继承父类的全限定名,Interfaces为要实现的接口全限定名 |
public final FieldVisitor visitField(final int access,final String name,final String descriptor,final String signature,final Object value) | 构造class文件的成员属性,name为成员属性名,descriptor为属性的类型签名,value为属性的值,只适用于静态字段,若当前要生成的字段不是静态的则传入null |
public final MethodVisitor visitMethod(final int access,final String name,final String descriptor,final String signature,final String[] exceptions) | **构造class文件的方法“签名”**并返回都构造方法体的对象(即方法的修饰符、方法名、返回值及全限定的参数) |
public final AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) | 构造class 的注解对象,descriptor为注解的描述名,visible为运行时是否可见 |
public byte[] toByteArray() | 返回生成的字节码的字节流,将字节流写回文件即可生产调整后的 class 文件 |
2.2、AnnotationWriter、FieldWriter、MethodWriter、SignatureWriter
类/接口 | 说明 |
---|---|
AnnotationWriter | 实现了AnnotationVisitor,用于创建注解相关字节码指令来创建注解部分字节码,下类似。 |
FieldWriter | 实现了FieldVisitor,用于创建字段相关字节码。 |
MethodWriter | 实现了MethodVisitor,用于创建方法相关字节码。 |
SignatureWriter | 实现了SignatureVisitor,用于创建泛型相关字节码。 |
AnnotationWriter | 实现了AnnotationVisitor,用于创建注解相关字节码。 |
3、FieldVisitor 、MethodVisitor 、AnnotationVisitor 等
FieldVisitor 、MethodVisitor 、AnnotationVisitor 虽然名称形式有点类似,但是三者在继承关系除了都是继承自Object的抽象类之外并无其他类之间的关系,正如名称所示,它们都是ASM 提供出来供我们传入对应字节码指令来生成对应部分的方法,比如说MethodVisitor 用于创建方法体,不同类型的字节码指令对应不同的方法。
4、ClassReader和SignatureReader
ClassReader类可以从字节数组直接或 class 文件间接的获得字节码数据(因为它是按照Java虚拟机规范中定义的方式来解析class文件中的内容),调用accept方法时开始分析字节码并构建出字节码文件在内存中的抽象的结构树,是字节码的读取与分析引擎,它采用类似SAX的事件读取机制,每当有事件发生时,调用注册的ClassVisitor、AnnotationVisitor、FieldVisitor、MethodVisitor做相应的处理(当然也可以不通过 ClassReader类,自行手工控制这个流程,只要确保各个 visit 事件被先后正确的调用,最后就能生成可以被正确加载的字节码),调用accep方法之后就解析字节码中常量池之后的所有元素并构造成Attribute链,如果attribute名称符合在accept中attribute数组中指定的attribute名,则替换传入的attribute数组对应的项,其中 accept(final ClassVisitor classVisitor, final int parsingOptions)方法中的parsingOptions参数代表用于解析class的选项,有以下取值:
- ClassReader.SKIP_CODE——过代码属性的标志
- ClassReader.SKIP_FRAMES——跳过StackMap和StackMapTable属性的标志,跳过MethodVisitor.visitFrame方法,对于我们开发者来说最好选这个。
- ClassReader.SKIP_DEBUG——跳过SourceFile,SourceDebugExtension,LocalVariableTable,LocalVariableTypeTable和LineNumberTable属性的标志,跳过ClassVisitor.visitSource, MethodVisitor.visitLocalVariable, MethodVisitor.visitLineNumber方法。
- ClassReader.EXPAND_FRAMES——用于展开堆栈映射帧的标志,这会大大降低性能,不过建议使用这个标志。
简而言之,ClassReader字节码分析器就是读取并解析class文件中的内容,在遇到合适的字段时调用ClassVisitor中相对应的方法。而SignatureReader类负责对类定义、字段定义、方法定义、本地变量定义的签名的解析,当范型被引入时,用于存储范型定义时的元数据(因为元数据在运行时会被擦除)。剩下的还有对字节码中属性的类进行抽象的Attribute;用于存储字节码二进制存储的容器ByteVector;字节码指令的一些常量定义的Opcodes接口以及类型相关的常量定义以及一些基于其上的操作的Type类。
四、查看ASM 需要传入的字节码指令
ASM 需要传入的字节码指令有两种方法:
- 可以借助ASM Bytecode Outline插件通过.java文件转为使用ASM 时所用到的字节码指令,其中第一栏Bytecode为直接调入ASM提供的工具类传入的原始的字节码,第二栏ASMified为使用ASM 生成class的代码,第三栏Groovified为使用Groovy 语言生成class的代码。
- 使用javap反编译.class文件查看
javap -c是用于把.class文件反编译为字节码,可以不用带上后缀,javap -verbose 可输出更多完整的信息。
一、ASM 使用前的准备
-
需要引入两个库:核心库org.ow2.asm:asm:7.1和工具类库org.ow2.asm:asm-common:7.1
-
安装ASM Bytecode Viewer 插件,便于快速查看字节码指令
二、ASM的核心流程图
ASM操作字节码核心步骤只有2步:
- 拿到操作对应的字节码指令
- 调用对应的方法传入字节码指令,根据需求进行I/O操作。
三、ASM的基本操作
1、ASM创建新的Java class
使用ASM 创建全新的Java class文件很简单,只需要几步:
- 通过ASM Bytecode Viewer插件直接查看生成代码(当然也可以直接传入对应的字节码指令)
- 创建ClassWriter对象
- 调用ClassWriter对应的visit方法创建各部分
- 执行并保存到class文件中
package com.crazymo.asm.basic;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
/**
* @author : Crazy.Mo
*/
public class BasicASM{
public static void generateClass() {
//生成一个类只需要ClassWriter组件即可
ClassWriter writer = new ClassWriter(0);
//通过visit方法确定类的头部信息
writer.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC + Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT + Opcodes.ACC_INTERFACE,
"com/asm3/Comparable", null, "java/lang/Object", new String[]{"com/asm3/Mesurable"});
//定义类的属性
writer.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC,
"LESS", "I", null, 1000);//.visitEnd();
writer.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC,
"EQUAL", "I", null, new Integer(0));//.visitEnd();
writer.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC,
"GREATER", "I", null, new Integer(1)).visitEnd();
//定义类的方法
writer.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT, "compareTo",
"(Ljava/lang/Object;)I", null, null).visitEnd();
writer.visitEnd(); //使writer类已经完成
//将writer转换成字节数组写到文件里面去
byte[] data = writer.toByteArray();
File file = new File("D://ASMDemo.class");
FileOutputStream fout = null;
try {
fout = new FileOutputStream(file);
fout.write(data);
fout.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 利用ASM 自动生成HelloASM.class
* public class HelloASM {
* public static final String USR = "CrazyMo_";
* public static void main(String[] args){
* System.out.println("Hello ASM From CrazyMo_!");
* }
* }
*/
public static void generateHelloASM() {
//生成一个类只需要ClassWriter组件即可
ClassWriter writer = new ClassWriter(0);
//通过visit方法确定类的头部信息,使用的Java 版本号定义类的头部信息
writer.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "HelloASM", null,
"java/lang/Object", null);
//定义类的属性
writer.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC,
"USR", "Ljava/lang/String;", null, "CrazyMo_").visitEnd();
//默认的构造方法=>public <init>()V
MethodVisitor methodVisitor = writer.visitMethod(Opcodes.ACC_PUBLIC, "<init>",
"()V", null, null);
//传入生成构造方法方法体的字节码指令,将索引值为0的本地变量入栈
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
"<init>", "()V", false);
methodVisitor.visitInsn(Opcodes.RETURN);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
//生成main方法的签名
methodVisitor = writer.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC,
"main","([Ljava/lang/String;)V",
null, null);
//生成main方法中的字节码指令
methodVisitor.visitFieldInsn(Opcodes.GETSTATIC,
"java/lang/System",
"out","Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("Hello ASM From CrazyMo_!");
methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/io/PrintStream",
"println","(Ljava/lang/String;)V", false);
methodVisitor.visitInsn(Opcodes.RETURN);
methodVisitor.visitMaxs(2, 2);
//字节码生成完成
methodVisitor.visitEnd();
writer.visitEnd();
// 获取生成的class文件对应的二进制流
byte[] code = writer.toByteArray();
//将class 二进制流保存到本地磁盘上
saveClassfile(code);
//直接将二进制流加载到内存中
invokeClzBytecode(code);
}
public static void saveClassfile(byte[] code) {
FileOutputStream fos = null;
try {
fos = new FileOutputStream("HelloASM.class");
fos.write(code);
fos.close();
}catch (IOException e) {
e.printStackTrace();
}
}
private static void invokeClzBytecode(byte[] code) {
ByteClassLoader loader = new ByteClassLoader("HelloASM",code);
//通过反射调用main方法
try {
Class<?> exampleClass = loader.loadClass("HelloASM");
if(loader.classLoaded()) {
exampleClass.getMethods()[0].invoke(null, new Object[]{null});
}else{
throw new AssertionError("Class HelloASM" + " loaded from wrong source");
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/** A simple ClassLoader to test that a class can be loaded in the JVM. */
public static class ByteClassLoader extends ClassLoader {
/**
* 传入全类名"com.crazymo.asmdemo.HelloASM"
*/
private final String className;
private final byte[] classContent;
private boolean classLoaded;
ByteClassLoader(final String className, final byte[] classContent) {
this.className = className;
this.classContent = classContent;
}
boolean classLoaded() {
return classLoaded;
}
@Override
protected Class<?> loadClass(final String name, final boolean resolve)
throws ClassNotFoundException {
if (name.equals(className)) {
classLoaded = true;
return defineClass(className, classContent, 0, classContent.length);
} else {
return super.loadClass(name, resolve);
}
}
}
}
定义一个用于把Class 字节码加载到虚拟机的ClassLoader
/** A simple ClassLoader to test that a class can be loaded in the JVM. */
private static class ByteClassLoader extends ClassLoader {
/**
* 要载入HelloASM.class直接传入"HelloASM"
*/
private final String className;
private final byte[] classContent;
private boolean classLoaded;
ByteClassLoader(final String className, final byte[] classContent) {
this.className = className;
this.classContent = classContent;
}
boolean classLoaded() {
return classLoaded;
}
@Override
protected Class<?> loadClass(final String name, final boolean resolve)
throws ClassNotFoundException {
if (name.equals(className)) {
classLoaded = true;
return defineClass(className, classContent, 0, classContent.length);
} else {
return super.loadClass(name, resolve);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
2、修改方法体(函数插桩)
ClassReader 读取class文件时以类似事件驱动的形式,以回调的形式告知给我们(和xml的解析思想有点类似)。
2.1、函数插桩的主要流程图
2.2、函数插桩的实现
如上图所示,实现函数插桩需要以下几个步骤:
2.2.1、读取要进行插桩的函数所在的class
FileInputStream inputStream=new FileInputStream("E:\\ASM\\basicUse\\build\\intermediates\\javac\\debugUnitTest\\classes\\com\\crazymo\\asm\\basic\\LogInject.class");
2.2.2、 创建ClassReader 字节码分析器并通过class文件实例化
ClassReader 是用于解析class文件的,拥有很多重写的构造函数。
//可以直接传入fis也可以传入String name=HelloASM.class.getName()创建字节码分析器
ClassReader classReader=new ClassReader(inputStream);
2.2.3、 继承org.objectweb.asm.ClassVisitor实现具体的字节码分析器并重写对应的方法
要修改class 成员(成员指的是类的方法、属性、类的注解、类的修饰符、内部类),就需要通过ClassVisitor。
/**
* 定义ClassVisitor(代理模式)类访问者,可用于访问类的各种成员,比如类的方法、属性、注解等,
* 但无法进入到方法的内部,解析时就会自动回调ClassVisitor里的方法
*/
static class InjectClassVisitor extends ClassVisitor{
public InjectClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
///return super.visitMethod(access, name, descriptor, signature, exceptions);
MethodVisitor methodVisitor=super.visitMethod(access, name, descriptor, signature, exceptions);
return new InjectMethodVisitor(api,methodVisitor,access, name, descriptor);
}
}
2.2.4、通过ClassWriter构造方法创建并实例化
ClassWriter是用于获取插桩后的class字节码的数据的
//自动计算栈帧
ClassWriter classWriter=new ClassWriter(ClassWriter.COMPUTE_FRAMES);
2.2.5、字节码分析器调用accept方法,开始解析class文件。
//引入的是7就可以传递4、5、6,都是向下兼容的,而ClassReader.EXPAND_FRAMES需要打包进APK就必须传入这个值
classReader.accept(new InjectClassVisitor(Opcodes.ASM7,classWriter),ClassReader.EXPAND_FRAMES);
2.2.6、继承org.objectweb.asm.commons.AdviceAdapter实现具体的MethodVisitor并重写对应的方法
org.objectweb.asm.commons.AdviceAdapter最终继承自MethodVisitor,是工具类库org.ow2.asm:asm-common:7.1提供给我们的便捷访问方法体内部的类,通过这个类可以更加便捷的操作字节码,因为它几乎是提供了与JVM 字节码指令同名或类似名称的方法,在我们修改字节码的时候直接从ASM 插件中拿到对应的字节码传入指定的方法即可。
static class InjectMethodVisitor extends AdviceAdapter{
private int start;
boolean isInject=false;
/**
* Constructs a new {@link AdviceAdapter}.
*
* @param api the ASM API version implemented by this visitor. Must be one of {@link
* Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}.
* @param methodVisitor the method visitor to which this adapter delegates calls.
* @param access the method's access flags (see {@link Opcodes}).
* @param name the method's name.
* @param descriptor the method's descriptor (see {@link Type Type}).
*/
protected InjectMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
System.out.println(descriptor);
if("Lcom/crazymo/asm/basic/Inject;".equals(descriptor)){
isInject=true;
}else {
isInject=false;
}
return super.visitAnnotation(descriptor, visible);
}
@Override
protected void onMethodEnter() {
super.onMethodEnter();
if(!isInject){
return;
}
//插入一个执行静态方法的指令
invokeStatic(Type.getType("Ljava/lang/System;"),new Method("currentTimeMillis","()J"));
//接收返回值局部变量,先创建一个局部变量
//慎用 storeLocal(1),这代表的是接收索引为1的局部变量,因为局部变量是按照索引顺序加载的,第一个局部变量的索引为0,第二个为1
start = newLocal(Type.LONG_TYPE);
//接收一个局部变量
storeLocal(start);
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
if(!isInject){
return;
}
invokeStatic(Type.getType("Ljava/lang/System;"),new Method("currentTimeMillis","()J"));
int end = newLocal(Type.LONG_TYPE);
storeLocal(end);
///若存在与指令码相同的方法就直接调用
getStatic(Type.getType("Ljava/lang/System;"),"out",Type.getType("Ljava/io/PrintStream;"));
newInstance(Type.getType("Ljava/lang/StringBuilder;"));
dup();
invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),new Method("<init>","()V"));
//压栈
visitLdcInsn("cmo:");
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
//减法
loadLocal(end);
loadLocal(start);
math(SUB,Type.LONG_TYPE);
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(J)Ljava/lang/StringBuilder;"));
visitLdcInsn("ms.");
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("toString","()Ljava/lang/String;"));
invokeVirtual(Type.getType("Ljava/io/PrintStream;"),new Method("println","(Ljava/lang/String;)V"));
}
}
- ASM修改方法体本质上都是通过MethodVisitor来创建和修改方法。
要想完全掌握方法插桩,就需要完全掌握字节码指令以及MethodVisitor的相关知识,当然如果想要修改内部类的思路也类似。
3、删除类的字段、方法等
删除类的字段、方法等的本质是在职责链传递过程中中断委派,不访问相应的 visit 方法即可,比如删除方法时只需直接返回 null,而不是返回由 visitMethod方法返回的 MethodVisitor对象。
- 继承ClassVisitor,实现自定义的ClassVisitor
- 构造ClassReader对象
- 调用ClassReader对象的accept方法并传入自定义的ClassVisitor
public class DeleMethodVisitor extends ClassVisitor {
public DeleMethodVisitor(ClassVisitor classVisitor) {
///指定使用ASM7的api版本
super(Opcodes.ASM7,classVisitor);
}
public MethodVisitor visitMethod(final int access,final String name,final String desc,final String signature,final String[] exceptions){
if(name.equals("main")){
///访问到main方法时,就中断委托即删除了
return null;
}
return cv.visitMethod(access,name,desc,signature,exceptions);
}
}
public static void deleMethod(){
ClassReader reader = null;
try {
String fullName = HelloASM.class.getName();
String fullNameType = fullName.replace(".", "/");
reader = new ClassReader(fullNameType);
} catch (IOException e) {
e.printStackTrace();
}
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor = new DeleMethodVisitor(classWriter);
reader.accept(visitor, ClassReader.SKIP_DEBUG);
byte[] data = classWriter.toByteArray();
File file = new File("HelloASM.class");
FileOutputStream fout = null;
try {
fout = new FileOutputStream(file);
fout.write(data);
fout.close();
} catch (IOException e) {
e.printStackTrace();
}
}
4、修改类、字段、方法的名字或修饰符等完善
修改类、字段、方法的名字或修饰符等的本质就是在职责链传递过程中替换调用参数
package com.crazymo.asmdemo.modif.asm;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/**
* @author : Crazy.Mo
*/
public class ModifyMethodVisitor extends ClassVisitor {
public ModifyMethodVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM7, classVisitor);
}
//修改方法名
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
String newName=name;
if("main".equals(name)) {
newName = "TestMain";
}
return cv.visitMethod(access, newName, descriptor, signature, exceptions);
}
}