一、ASM介绍
1、ASM 是什么
ASM是一个通用的Java字节码操作和分析框架。它可以用于修改现有类或直接以二进制形式动态生成类。ASM提供了一些常见的字节码转换和分析算法,可以从中构建定制的复杂转换和代码分析工具。ASM提供了与其他Java字节码框架类似的功能,但侧重于性能。由于它的设计和实现尽可能小和快,因此非常适合在动态系统中使用(但当然也可以以静态方式使用,例如在编译器中)。
一个.java文件经过Java编译器(javac)编译之后会生成一个.class文件。在.class文件中,存储的是字节码(ByteCode)数据。ASM所操作的对象是字节码(ByteCode),而在许多情况下,字节码(ByteCode)的具体表现形式是.class文件。
ASM处理字节码(ByteCode)的方式是“拆分-修改-合并”。
字节码工具 | 类创建 | 实现接口 | 方法调用 | 类扩展 | 父类方法调用 | 优点 | 缺点 | 常见使用 | 学习成本 |
---|---|---|---|---|---|---|---|---|---|
java-proxy | 支持 | 支持 | 支持 | 不支持 | 不支持 | 简单动态代理首选 | 功能有限,不支持扩展 | spring-aop,MyBatis | 1星 |
asm | 支持 | 支持 | 支持 | 支持 | 支持 | 任意字节码插入,几乎不受限制 | 学习难度大,编写代码多 | cglib | 5星 |
javaassit | 支持 | 支持 | 支持 | 支持 | 支持 | java原始语法,字符串形式插入,写入直观 | 不支持jdk1.5以上的语法,如泛型,增强for | Fastjson,MyBatis | 2星 |
cglib | 支持 | 支持 | 支持 | 支持 | 支持 | 与bytebuddy看起来差不多 | 正在被bytebuddy淘汰 | EasyMock,jackson-databind | 3星 |
bytebuddy | 支持 | 支持 | 支持 | 支持 | 支持 | 支持任意维度的拦截,可以获取原始类、方法,以及代理类和全部参数 | 不太直观,学习理解有些成本,API非常多 | SkyWalking,Mockito,Hibernate,powermock | 3星 |
•ASM官网:https://asm.ow2.io/
•ASM源码:https://gitlab.ow2.org/asm/asm
•开发者指南:https://asm.ow2.io/developer-guide.html
2、ASM能做什么
生成、修改、删除(接口、类、字段、方法…)ASM能够对字节码数据进行analyze、generate、transformation,ASM可以形象的理解为“Java语言世界”边缘上一扇大门,通过这扇大门,可以帮助我们进入到“字节码的世界”。
3、ASM实际的使用场景
3.1、Spring当中的ASM
第一个应用场景,是Spring框架当中的AOP。 在很多Java项目中,都会使用到Spring框架,而Spring框架当中的AOP(Aspect Oriented Programming)是依赖于ASM的。具体来说,Spring的AOP,可以通过JDK的动态代理来实现,也可以通过CGLIB实现。其中,CGLib (Code Generation Library)是在ASM的基础上构建起来的,所以,Spring AOP是间接的使用了ASM。(参考自 Spring Framework Reference Documentation的 8.6 Proxying mechanisms)。
3.2、JDK当中的ASM
第二个应用场景,是JDK当中的Lambda表达式。 在Java 8中引入了一个非常重要的特性,就是支持Lambda表达式。Lambda表达式,允许把方法作为参数进行传递,它能够使代码变的更加简洁紧凑。但是,我们可能没有注意到,其实,在现阶段(Java 8版本),Lambda表达式的调用是通过ASM来实现的。
在rt.jar文件的jdk.internal.org.objectweb.asm包当中,就包含了JDK内置的ASM代码。在JDK 8版本当中,它所使用的ASM 5.0版本。
如果我们跟踪Lambda表达式的编码实现,就会找到InnerClassLambdaMetafactory.spinInnerClass()方法。在这个方法当中,我们就会看到:JDK会使用jdk.internal.org.objectweb.asm.ClassWriter来生成一个类,将lambda表达式的代码包装起来。
LambdaMetafactory.metafactory() 第一步,找到这个方法 InnerClassLambdaMetafactory.buildCallSite() 第二步,找到这个方法
InnerClassLambdaMetafactory.spinInnerClass() 第三步,找到这个方法
4、 ASM的两个组成部分
从组成结构上来说,ASM分成两部分,一部分为Core API,另一部分为Tree API。
其中,Core API包括asm.jar、asm-util.jar和asm-commons.jar;其中,Tree API包括asm-tree.jar和asm-analysis.jar。
asm.jar内核心类:ClassReader、ClassVisitor、ClassWriter、FieldVisitor、FieldWriter、MethodVisitor、MethodWriter、Label、Opcodes、Type
ClassReader类,负责读取.class文件里的内容,然后拆分成各个不同的部分。ClassVisitor类,负责对.class文件中某一部分里的信息进行修改。ClassWriter类,负责将各个不同的部分重新组合成一个完整的.class文件。
asm-util.jar内核心类
以Check开头的类,主要负责检查(Check)生成的.class文件内容是否正确。以Trace开头的类,主要负责将.class文件的内容打印成文字输出。根据输出的文字信息,可以探索或追踪(Trace).class文件的内部信息。
5、ClassFile
我们都知道,在.class文件中,存储的是ByteCode数据。但是,这些ByteCode数据并不是杂乱无章的,而是遵循一定的数据结构。
这个.class文件遵循的数据结构就是由 Java Virtual Machine Specification中定义的 The class File Format
6、常见的字节码类库
Apache Commons BCEL:其中BCEL为Byte Code Engineering Library首字母的缩写。
Javassist:Javassist表示Java programming assistant
ObjectWeb ASM:本课程的研究对象。
Byte Buddy:在ASM基础上实现的一个类库。
二、无中生有
1、生成新的接口
预期目标:
生成一个正常接口结构定义的.class文件
public interface ASMInterface {
byte byteType = 1;
short shortType = 1;
int intType = 1;
char charType = 's';
float floatType = 1.1F;
double doubleType = 1.2;
long longType = 1L;
boolean booleanType = false;
Byte ByteType = 1;
Short ShortType = Short.valueOf((short)1);
Integer IntegerType = 1;
String StringType = "s";
Float FloatType = 1.1F;
Double DoubleType = 1.1;
Long LongType = 1L;
Boolean BooleanType = true;
void function();
default String defaultFunction(Integer integer) {
System.out.println("param = " + integer);
return String.valueOf(integer);
}
static Integer getInteger(String str) {
return Integer.valueOf(str);
}
}
编码实现:
public class InterfaceGenerateCore {
public static void main(String[] args) throws Exception {
String relative_path = "sample/ASMGenerateInterface.class";
String filepath = FileUtils.getFilePath(relative_path);
// (1) 生成byte[]内容
byte[] bytes = dump();
// (2) 保存byte[]到文件
FileUtils.writeBytes(filepath, bytes);
}
public static byte[] dump() throws Exception {
// (1) 创建ClassWriter对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// (2) 调用visitXxx()方法,调用顺序和说明如下
/*
* visit
* [visitSource][visitModule][visitNestHost][visitPermittedSubclass][visitOuterClass]
* (visitAnnotation |
* visitTypeAnnotation |
* visitAttribute)*
* (visitNestMember |
* visitInnerClass |
* visitRecordComponent |
* visitField |
* visitMethod)*
* visitEnd
* []: 表示最多调用一次,可以不调用,但最多调用一次。
* ()和|: 表示在多个方法之间,可以选择任意一个,并且多个方法之间不分前后顺序。
* *: 表示方法可以调用0次或多次。
* */
//定义接口
/*
*visit(version, access, name, signature, superName, interfaces)
*version: 表示当前类的版本信息。在下述示例代码中,其取值为Opcodes.V1_8,表示使用Java 8版本。
*access: 表示当前类的访问标识(access flag)信息。在下面的示例中,access的取值是ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,也可以写成ACC_PUBLIC | ACC_ABSTRACT | ACC_INTERFACE。如果想进一步了解这些标识的含义,可以参考Java Virtual Machine Specification的Chapter 4. The class File Format部分。
*name: 表示当前类的名字,它采用的格式是Internal Name的形式。在.java文件中,我们使用Java语言来编写代码,使用类名的形式是Fully Qualified Class Name,例如java.lang.String;将.java文件编译之后,就会生成.class文件;在.class文件中,类名的形式会发生变化,称之为Internal Name,例如java/lang/String。因此,将Fully Qualified Class Name转换成Internal Name的方式就是,将.字符转换成/字符。
*signature: 表示当前类的泛型信息。因为在这个接口当中不包含任何的泛型信息,因此它的值为null。
*superName: 表示当前类的父类信息,它采用的格式是Internal Name的形式。
*interfaces: 表示当前类实现了哪些接口信息。
**/
cw.visit(
V1_8, // version
ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, // access
"sample/ASMGenerateInterface", // name
null, // signature
"java/lang/Object", // superName
null // interfaces
);
//定义字段-基本类型
/*
* visitField(access, name, descriptor, signature, value)
*access参数:表示当前字段或方法带有的访问标识(access flag)信息,例如ACC_PUBLIC、ACC_STATIC和ACC_FINAL等。
*name参数:表示当前字段或方法的名字。
*descriptor参数:表示当前字段或方法的描述符。这些描述符,与我们平时使用的Java类型是有区别的。byte-B、short-S、int-I、char-C、具体可以参考如下示例代码
*signature参数:表示当前字段或方法是否带有泛型信息。换句话说,如果不带有泛型信息,提供一个null就可以了;如果带有泛型信息,就需要给它提供某一个具体的值。
*value参数:是visitField()方法的第5个参数。这个参数的取值,与当前字段是否为常量有关系。如果当前字段是一个常量,就需要给value参数提供某一个具体的值;如果当前字段不是常量,那么使用null就可以了。
* */
{
FieldVisitor fv1 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "byteType", "B", null, new Integer(1));
fv1.visitEnd();
}
{
FieldVisitor fv2 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "shortType", "S", null, new Integer(1));
fv2.visitEnd();
}
{
FieldVisitor fv3 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "intType", "I", null, new Integer(1));
fv3.visitEnd();
}
{
FieldVisitor fv4 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "charType", "C", null, 's');
fv4.visitEnd();
}
{
FieldVisitor fv5 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "floatType", "F", null, new Float("1.1"));
fv5.visitEnd();
}
{
FieldVisitor fv6 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "doubleType", "D", null, new Double("1.2"));
fv6.visitEnd();
}
{
FieldVisitor fv7 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "longType", "J", null, new Long(1L));
fv7.visitEnd();
}
{
FieldVisitor fv8 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "booleanType", "Z", null, new Integer(0));
fv8.visitEnd();
}
//定义变量-包装类型
{
FieldVisitor fv11 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "ByteType", "Ljava/lang/Byte;", null, null);
fv11.visitEnd();
}
{
FieldVisitor fv12 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "ShortType", "Ljava/lang/Short;", null,null);
fv12.visitEnd();
}
{
FieldVisitor fv13 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "IntegerType", "Ljava/lang/Integer;", null,null);
fv13.visitEnd();
}
{
FieldVisitor fv14 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "StringType", "Ljava/lang/String;", null, "s");
fv14.visitEnd();
}
{
FieldVisitor fv15 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "FloatType", "Ljava/lang/Float;", null,null);
fv15.visitEnd();
}
{
FieldVisitor fv16 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "DoubleType", "Ljava/lang/Double;", null,null);
fv16.visitEnd();
}
{
FieldVisitor fv17 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "LongType", "Ljava/lang/Long;", null, null);
fv17.visitEnd();
}
{
FieldVisitor fv18 = cw.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "BooleanType", "Ljava/lang/Boolean;", null, null);
fv18.visitEnd();
}
//定义方法-抽象方法
/*
* visitMethod(access, name, descriptor, signature, exceptions)
*access参数:表示当前字段或方法带有的访问标识(access flag)信息,例如ACC_PUBLIC、ACC_STATIC和ACC_FINAL等。
*name参数:表示当前字段或方法的名字。
*descriptor参数:表示当前字段或方法的描述符。这些描述符,与我们平时使用的Java类型是有区别的。()内为入参,后面为反参
*signature参数:表示当前字段或方法是否带有泛型信息。换句话说,如果不带有泛型信息,提供一个null就可以了;如果带有泛型信息,就需要给它提供某一个具体的值。
*exceptions参数:是visitMethod()方法的第5个参数。这个参数的取值,与当前方法声明中是否具有throws XxxException相关。
* */
{
MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "function", "()V", null, null);
mv1.visitEnd();
}
//定义方法-默认方法
{
MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "defaultFunction", "(Ljava/lang/Integer;)Ljava/lang/String;", null, null);
mv2.visitCode();
mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv2.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv2.visitInsn(DUP);
mv2.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv2.visitLdcInsn("param = ");
mv2.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv2.visitVarInsn(ALOAD, 1);
mv2.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/Object;)Ljava/lang/StringBuilder;", false);
mv2.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv2.visitVarInsn(ALOAD, 1);
mv2.visitMethodInsn(INVOKESTATIC, "java/lang/String", "valueOf", "(Ljava/lang/Object;)Ljava/lang/String;", false);
mv2.visitInsn(ARETURN);
mv2.visitMaxs(3, 2);
mv2.visitEnd();
}
//定义方法-静态方法
{
MethodVisitor mv3 = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "getInteger", "(Ljava/lang/String;)Ljava/lang/Integer;", null, null);
mv3.visitCode();
mv3.visitVarInsn(ALOAD, 0);
mv3.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(Ljava/lang/String;)Ljava/lang/Integer;", fal