ASM 简介
ASM 是一个通用的 Java 字节码操作和剖析框架,它能够用来批改现有的类或间接以二进制模式动静生成类。ASM 提供了一些常见的字节码转换和剖析算法,从中能够构建定制的简单转换和代码剖析工具。ASM 提供了与其余 Java 字节码框架相似的性能,但侧重于性能。因为它的设计和实现都尽可能小和快,所以它非常适合在动静零碎中应用(当然也能够以动态形式应用,例如在编译器中)。
ASM 被用在很多我的项目中,包含如下:
- OpenJDK,生成 lambda 调用站点,以及 Nashorn 编译器;
- Groovy 编译器和 Kotlin 编译器;
- Cobertura 和 Jacoco,以工具化类来度量代码覆盖率;
- CGLIB,用于动静生成代理类;
- Gradle,在运行时生成一些类;
更多参考官网:https://asm.ow2.io/
IDE 插件
ASM 是间接对字节码进行操作,如果不相熟字节码操作汇合的话,写起来会很吃力,所以 ASM 为支流的 IDE 专门提供了开发插件 BytecodeOutline:
- IDEA:ASM Bytecode Outline
- Eclipse:BytecodeOutline
以 IDEA 为例,只须要对应的类中右击 ->Show Bytecode outline 即可,大抵如下图所示:
面板中蕴含三个页签:
Bytecode
:类对应的字节码文件;ASMified
:利用ASM
生成字节码对应的代码;Groovified
:类对应的字节码指令;
ASM API
ASM
库提供了两个用于生成和转换已编译类的 API
,一个是外围API
,以基于事件的模式来示意类;另一个是树API
,以基于对象的模式来示意类;能够比照XML
文件解析的形式:SAX
模式和 DOM
模式;外围 API 对应 SAX
模式,树 API
对应 DOM
模式;每种模式都有本人的优缺点:
- 基于事件的 API 要快于基于对象的 API,所须要的内存也较少,但在应用基于事件的 API 时,类转换的实现可能要更难一些;
- 基于对象的 API 会把整个类加载到内存中;
ASM
库组织在几个包中,这些包散布在几个独自的 JAR 文件中:
org.objectweb.asm
和org.objectweb.asm.signature
包:定义基于事件的 API 并提供类解析器和编写器组件,它们蕴含在 asm.jar 中;org.objectweb.asm.util
包:提供基于外围 API 的各种工具,这些工具可在 ASM 应用程序的开发和调试过程中应用,蕴含在asm-util.jar
中;org.objectweb.asm.commons
包:提供了几个有用的预约义类转换器,次要基于外围 API,蕴含在asm-commons.jar
中;org.objectweb.asm.tree
包:定义基于对象的 API,并提供用于在基于事件的示意和基于对象的示意之间进行转换的工具,蕴含在asm-tree.jar
中;org.objectweb.asm.tree.analysis
包:包提供了一个基于树 API 的类剖析框架和几个预约义的类分析器,蕴含在asm-analysis.jar
中;
外围 API
在学习外围 API
之前,倡议理解一下访问者模式,因为 ASM
对字节码的操作和剖析都是基于访问者模式来实现的;
访问者模式
访问者模式倡议将新行为放入一个名为 访问者 的独立类中,而不是试图将其整合到已有类中。当初,须要执行操作的原始对象将作为参数被传递给访问者中的办法,让办法能拜访对象所蕴含的所有必要数据;常见的利用场景:
- 如果你须要对一个简单对象构造(例如对象树)中的所有元素执行某些操作,可应用访问者模式;
- 可应用访问者模式来清理辅助行为的业务逻辑;
- 当某个行为仅在类层次结构中的一些类中有意义,而在其余类中没有意义时,可应用该模式;
字节码其实就是一个简单的对象构造,还有像 Sharding-Jdbc
中对 sql
的解析也用到访问者模式,能够发现都是一些数据结构比较稳定的数据,固定的语法;
更多参考:访问者模式
类
访问者模式有两个外围类别离是:独立的访问者、接管访问者事件产生器;对应的 ASM
外面就是两个外围类:ClassVisitor
和ClassReader
,上面别离进行介绍;
ClassVisitor
用于生成和转换编译类的 ASM API
基于 ClassVisitor
抽象类,这个类中的每个办法都对应于同名的类文件构造:
public abstract class ClassVisitor {public ClassVisitor(int api);
public ClassVisitor(int api, ClassVisitor cv);
public void visit(int version, int access, String name,String signature, String superName, String[] interfaces);
public void visitSource(String source, String debug);
public void visitOuterClass(String owner, String name, String desc);
AnnotationVisitor visitAnnotation(String desc, boolean visible);
public void visitAttribute(Attribute attr);
public void visitInnerClass(String name, String outerName,String innerName, int access);
public FieldVisitor visitField(int access, String name, String desc,String signature, Object value);
public MethodVisitor visitMethod(int access, String name, String desc,String signature, String[] exceptions);
void visitEnd();}
内容能够具备任意长度和复杂性的部件将通过返回辅助访问者类,次要包含:AnnotationVisitor
、FieldVisitor
、MethodVisitor
;更多能够参考Java 虚拟机标准
;
以上所有办法都会被事件产生器 ClassReader
调用,所有办法中的参数都是 ClassReader
提供的,当然调用每个办法是有程序的:
visit visitSource? visitOuterClass? (visitAnnotation | visitAttribute)* (visitInnerClass | visitField |visitMethod)* visitEnd
首先调用 visit
,而后是对visitSource
的最多一个调用,接下来是对visitOuterClass
的最多一个调用,而后是可按任意程序对 visitAnnotation
和visitAttribute
的任意多个拜访,接下来是可按任意程序对 visitInnerClass
、visitField
和 visitMethod
的任意多个调用,最初以一个 visitEnd
调用完结。
ClassReader
此类次要性能就是读取字节码文件,而后把读取的数据告诉ClassVisitor
,字节码文件能够多种形式传入:
public ClassReader(final InputStream inputStream)
:字节流的形式;public ClassReader(final String className)
:文件全门路;public ClassReader(final byte[] classFile)
:二进制文件;
常见应用形式如下所示:
ClassReader classReader = new ClassReader("com/zh/asm/TestService");
ClassWriter classVisitor = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classReader.accept(classVisitor, 0);
ClassReader
的 accept
办法解决接管一个访问者,还包含另外一个 parsingOptions
参数,选项包含:
SKIP_CODE
:跳过已编译代码的拜访(如果您只须要类构造,这可能很有用);SKIP_DEBUG
:不拜访调试信息,也不为其创立人工标签;SKIP_FRAMES
:跳过堆栈映射帧;EXPAND_FRAMES
:解压缩这些帧;
ClassWriter
以上实例中应用了ClassWriter
,其继承于ClassVisitor
,次要用来生成类,能够独自应用,如下所示:
ClassWriter cw = new ClassWriter(0);
cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,"pkg/Comparable", null, "java/lang/Object",new String[]{"pkg/Mesurable"});
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS","I", null, new Integer(-1)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL","I", null, new Integer(0)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER","I", null, new Integer(1)).visitEnd();
cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo","(Ljava/lang/Object;)I", null, null).visitEnd();
cw.visitEnd();
byte[] b = cw.toByteArray();
// 输入
FileOutputStream fileOutputStream = new FileOutputStream(new File("F:/asm/Comparable.class"));
fileOutputStream.write(b);
fileOutputStream.close();
以上通过 ClassWriter
生成一个字节码文件,而后转换成字节数组,最初通过 FileOutputStream
输入到文件中,反编译后果如下:
package pkg;
public interface Comparable extends Mesurable {
int LESS = -1;
int EQUAL = 0;
int GREATER = 1;
int compareTo(Object var1);
}
在实例化 ClassWriter
须要提供一个参数flags
,选项包含:
COMPUTE_MAXS
:将为你计算局部变量与操作数栈局部的大小;还是必须调用visitMaxs
,但能够应用任何参数:它们将被疏忽并从新计算;应用这一选项时,依然必须自行计算这些帧;COMPUTE_FRAMES
:一切都是主动计算;不再须要调用visitFrame
,但依然必须调用visitMaxs
(参数将被疏忽并从新计算);- 0:不会主动计算任何货色;必须自行计算帧、局部变量与操作数栈的大小;
以上只是对 ClassWriter
的独自应用,但更有意义的其实是把以上三个外围类整合起来应用,上面重点看看转换操作;
转换操作
在类读取器和类写入器之间引入一个 ClassVisitor
,把三者整合起来,大抵代码构造如下所示:
ClassReader classReader = new ClassReader("com/zh/asm/TestService");
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
// 解决
ClassVisitor classVisitor = new AddFieldAdapter(classWriter...);
classReader.accept(classVisitor, 0);
上述代码绝对应的体系结构如下图所示:
这里提供了一个增加属性的适配器,能够重写 visitEnd
办法,而后写入新的属性,代码如下:
public class AddFieldAdapter extends ClassVisitor {
private int fAcc;
private String fName;
private String fDesc;
// 是否曾经有雷同名称的属性
private boolean isFieldPresent;
public AddFieldAdapter(ClassVisitor cv, int fAcc, String fName,
String fDesc) {super(ASM4, cv);
this.fAcc = fAcc;
this.fName = fName;
this.fDesc = fDesc;
}
@Override
public FieldVisitor visitField(int access, String name, String desc,
String signature, Object value) {
// 判断是否有雷同名称的字段,不存在才会在 visitEnd 中增加
if (name.equals(fName)) {isFieldPresent = true;}
return cv.visitField(access, name, desc, signature, value);
}
@Override
public void visitEnd() {if (!isFieldPresent) {FieldVisitor fv = cv.visitField(fAcc, fName, fDesc, null, null);
if (fv != null) {fv.visitEnd();
}
}
cv.visitEnd();}
}
依据 ClassVisitor
的每个办法被调用的程序,如果类中有多个属性,那么 visitField
会被调用屡次,每次都会查看要增加的字段是否曾经有了,而后保留在 isFieldPresent
标识中,这样在拜访最初的 visitEnd
中判断是否须要增加新属性;
ClassVisitor classVisitor = new AddFieldAdapter(classWriter,ACC_PUBLIC + ACC_FINAL + ACC_STATIC,"id","I");
这里增加了一个public static final int id
;能够把字节数组写入 class 类文件中,而后反编译查看:
public class TestService {
public static final int id;
......
}
工具类
除了下面几个外围类之外,ASM
也提供了一些工具类,不便用户应用:
- Type
Type
对象示意一种Java
类型,既能够由类型描述符结构,也能够由Class
对象构建;Type
类还蕴含示意基元类型的动态变量; - TraceClassVisitor
扩大了ClassVisitor
类,并构建了所拜访类的文本示意;应用TraceClassVisitor
以便取得理论生成内容的可读跟踪; - CheckClassAdapter
ClassWriter
类并不会核实对其办法的调用程序是否失当,以及参数是否无效;因而有可能会生成一些被 Java 虚拟机验证器回绝的有效类。为了尽可能提前检测出局部此类谬误,能够应用CheckClassAdapter
类; - ASMifier
这个类为TraceClassVisitor
工具提供了一个可选的后端(默认状况下,它应用一个Textifier
后端,产生下面显示的输入类型)。这个后端使TraceClassVisitor
类的每个办法打印用于调用它的 Java 代码。
办法
在介绍下面的 ClassVisitor
在拜访复杂性的部件将通过返回辅助访问者类,其中包含:AnnotationVisitor
、FieldVisitor
、MethodVisitor
;在介绍 MethodVisitor
之前理解一下 Java 虚拟机执行模型;
执行模型
每个办法被执行的时候,Java 虚拟机都会同步创立一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动静连贯、办法进口等信
息。每一个办法被调用直至执行结束的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程;
- 局部变量表:蕴含可由其索引以随机程序拜访的变量;
- 操作数栈:字节码指令用作操作数的值堆栈;
看一个具备 3 帧的执行栈:
第一帧:蕴含 3 个局部变量,操作数栈最大值为 4,蕴含 2 个值;
第二帧:蕴含 2 个局部变量,操作数栈最大值为 3,蕴含 2 个值;
第三帧:蕴含 4 个局部变量,操作数栈最大值为 2,蕴含 2 个值;
字节代码指令
字节码指令由标识该指令的操作码和固定数量的参数组成:
- 操作码:是一个无符号字节值,由助记符号标识。例如,操作码值 0 由助记符 NOP 设计,并对应于不执行任何操作的指令。
- 参数:是动态值,确定了准确的指令行为。它们紧跟在操作码之后给出。
字节码指令分为两类:
- 一小部分指令用于将值从局部变量转移到操作数堆栈;
- 其余指令只作用于操作数堆栈:它们从堆栈中弹出一些值,依据这些值计算结果,而后将其推回堆栈;
局部变量指令:
ILOAD
:用于加载一个 boolean、byte、char、short 或 int 局部变量;LLOAD, FLOAD, DLOAD
:别离用于加载 long、float 或 double 值;ALOAD
:用于加载任意非基元值,即对象和数组援用;
操作数栈指令:
ISTORE
:从操作数栈中弹出一个 boolean、byte、char、short 或 int 局部变量值,并将它存储在由其索引 i 指定的局部变量中;LSTORE,FSTORE,DSTORE
:别离弹出 long、float 或 double 值;ASTORE
:用于弹出任意非基元值;GETFIELD
、PUTFIELD
:GETFIELD owner name desc
弹出一个对象援用,并推送其name
字段的值;PUTFIELD owner name desc
弹出一个值和一个对象援用,并将该值存储在其name
字段中;
在这两种状况下,对象必须是owner
类型,其字段必须是desc
类型。GETSTATIC
和PUTSTATIC
是相似的指令,然而对于动态字段。INVOKEVIRTUAL、INVOKESTATIC、INVOKESPECIAL、INVOKEINTERFACE、INVOKEDYNAMIC
:INVOKEVIRTUAL owner name desc
调用类owner
中定义的name
办法,其办法描述符为desc
。INVOKESTATIC
用于静态方法,INVOKESPECIAL
用于公有办法和构造函数,INVOKEINTERFACE
用于接口中定义的办法。最初,对于 java7 类,INVOKEDYNAMIC
用于新的动静办法调用机制。
MethodVisitor
用于生成和转换已编译办法的 ASM API
是基于 MethodVisitor
抽象类的;它由 ClassVisitor
的visitMethod
办法返回;此类还依据这些指令的参数数量和类型为每个字节码指令类别定义了一个办法;必须按以下顺序调用这些办法:
visitAnnotationDefault? (visitAnnotation | visitParameterAnnotation | visitAttribute)*(visitCode( visitTryCatchBlock | visitLabel | visitFrame | visitXxx Insn |visitLocalVariable | visitLineNumber)*visitMaxs )?visitEnd
上面看一个对现有办法进行转换实例,给办法增加开始和完结日志;
-
筹备须要被转换的实例,在
query
办法解决前和解决后增加日志;public class TestService {public void query(int param) {System.out.println("service handle..."); } }
-
重写
ClassVisitor
中的visitMethod
public class MyClassVisitor extends ClassVisitor implements Opcodes {public MyClassVisitor(ClassVisitor cv) {super(ASM5, cv); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions); if (!name.equals("<init>") && methodVisitor != null) {methodVisitor = new MyMethodVisitor(methodVisitor); } return methodVisitor; } }
过滤掉 <init>
办法,其余办法都会被 MyMethodVisitor
包装,而后重写 MethodVisitor
的办法;
-
重载 MethodVisitor
public class MyMethodVisitor extends MethodVisitor implements Opcodes {public MyMethodVisitor(MethodVisitor mv) {super(Opcodes.ASM4, 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); } }
visitCode
办法拜访之前调用,visitInsn
须要判断操作符是不是办法返回,个别办法在返回之前会执行 mv.visitInsn(RETURN)
操作,这时候能够通过 opcode
来判断;
-
查看生成的新的字节码文件
public class TestService {public TestService() { } public void query(int var1) {System.out.println("start"); System.out.println("service handle..."); System.out.println("end"); } }
工具类
在办法上面也同样提供了一些工具类:
LocalVariablesSorter
:此办法适配器将一个办法中应用的局部变量依照它们在这个办法中的呈现程序从新进行编号,同时能够应用newLocal
办法创立一个新的局部变量;AdviceAdapter
:此办法适配器是一个抽象类,可用于在办法的结尾以及任何RETURN
或ATHROW
指令之前插入代码;其次要长处是它也实用于构造函数,其中代码不能仅插入构造函数的结尾,而是在调用超级构造函数之后插入。
应用场景
ASM 被用在很多我的项目中,这里介绍两种常见的应用场景:AOP 和代替反射;
AOP
面向切面编程,在程序开发中次要用来解决一些零碎层面上的问题,比方日志,事务,权限期待;其中关键技术就是代理,代理包含动静代理和动态代理,实现的形式也有多种:
- AspectJ:属于动态织入,原理是动态代理;
- JDK 动静代理:
JDK
动静代理两个外围类:Proxy
和InvocationHandler
; - Cglib 动静代理:封装了
ASM
,能够再运行期动静生成新的Class
;性能上比JDK 动静代理
更弱小;
其中的 Cglib
动静代理形式就依赖 ASM
,下面的实例中咱们也看到了ASM
的字节码加强性能;
代替反射
FastJson
以速度快著称,其中有一项就是应用 ASM
代替了 Java
反射;另外还有 ReflectASM
包专门用来代替 Java
反射;
ReflectASM 是一个十分小的 Java 类库,通过代码生成来提供高性能的反射解决,主动为 get/set 字段提供拜访类,拜访类应用字节码操作而不是 Java 的反射技术,因而十分快。
看一段 ReflectASM
简略应用形式:
TestBean testBean = new TestBean(1, "zhaohui", 18);
MethodAccess methodAccess = MethodAccess.get(TestBean.class);
String[] mns = methodAccess.getMethodNames();
for (int i = 0; i < mns.length; i++) {System.out.println(methodAccess.invoke(testBean, mns[i]));
}
这里失常打印 TestBean
中的属性值,为什么速度快,因为外部会通过 ASM
生成一个长期的TestBeanMethodAccess
,外部重写了 invoke 办法,反编译之后如下所示:
public Object invoke(Object var1, int var2, Object... var3) {TestBean var4 = (TestBean)var1;
switch(var2) {
case 0:
return var4.getName();
case 1:
return var4.getId();
case 2:
return var4.getAge();
default:
throw new IllegalArgumentException("Method not found:" + var2);
}
}
能够发现 invoke 外面其实就是一般的调用,速度必定比应用 java 反射快。
参考文档
asm4-guide.pdf
ASM4 手册中文版
感激关注
能够关注微信公众号「回滚吧代码」,第一工夫浏览,文章继续更新;专一 Java 源码、架构、算法和面试。