简述
最近在看阿里的ARouter的源码,从git上clone下来之后,run起来发现项目运行的效果和源码有明显区别。打个比方,源码是这样
boolean b = true;
System.out.println(b);
但是当你跑起来之后去发现打印出来的false,打开编译好的class文件却发现编译出来的class的代码和源码不一样。经过翻看ARouter的工程源码,发现其实ARouter是利用了Gradle的 Transform API和ASM共同完成的编译时修改源码的功能。
Transform API的功能是让你在java文件编译成class文件之后对这些class文件进行读写,发生在编译时,是Android的gradle打包插件自带的功能,这里不详细展开。本片文章主要是讲解ASM的基本使用方法。有机会会出一个Transform + ASM插件教程。
ASM简介
ASM 是一个 Java 字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。许多AOP框架以及动态修改字节码的库的底层都是由ASM实现的,例如Spring AOP,cglib等等
一句话概括:ASM可以动态的修改创建class文件,达到动态修改java代码的效果。
ASM 核心API
public abstract class ClassVisitor {
// 实现的ASM的API版本。该字段的值必须为如下几个之一:Opcodes.ASM4,ASM5,ASM6,ASM7
protected final int api;
// 该类的方法可以委托给子类
protected ClassVisitor cv;
// 构造器
public ClassVisitor(final int api) {
this(api, null);
}
// 构造器
public ClassVisitor(final int api, final ClassVisitor classVisitor) {
if (api != Opcodes.ASM6 && api != Opcodes.ASM5 && api != Opcodes.ASM4 && api != Opcodes.ASM7) {
throw new IllegalArgumentException();
}
this.api = api;
this.cv = classVisitor;
}
/**
* 访问类头部信息
*
* @param version
* 类版本
* @param access
* 类访问标识符public等
* @param name
* 类名称
* @param signature
* 类签名(非泛型为NUll)
* @param superName
* 类的父类
* @param interfaces
* 类实现的接口
*/
public void visit(
final int version,final int access,
final String name, final String signature,
final String superName,final String[] interfaces) {
if (cv != null) {
cv.visit(version, access, name, signature, superName, interfaces);
}
}
/**
* 访问类的源文件.
*
* @param source
* 源文件名称
* @param debug
* 附加的验证信息,可以为空
*/
public void visitSource(final String source, final String debug) {
if (cv != null) {
cv.visitSource(source, debug);
}
}
/**
* 访问与类对应的模块. ASM6之后才有的API
*
* @param name
* 模块名称
* @param access
* 模式 ACC_MANDATED 等
* @param version
* 版本号
*/
public ModuleVisitor visitModule(final String name, final int access, final String version) {
if (api < Opcodes.ASM6) {
throw new UnsupportedOperationException("This feature requires ASM6");
}
if (cv != null) {
return cv.visitModule(name, access, version);
}
return null;
}
public void visitNestHost(final String nestHost) {
if (api < Opcodes.ASM7) {
throw new UnsupportedOperationException("This feature requires ASM7");
}
if (cv != null) {
cv.visitNestHost(nestHost);
}
}
/**
* 这个其实并不是访问外部类的回调,而是访问方法体中含有匿名内部类的方法
*
* @param owner 为创建匿名类的类,当然其也是一个enclosing class类型的类
* @param name 创建匿名类的方法。
* @param desc 创建匿名类的方法描述信息。
* @return 返回一个注解值访问器
*/
public void visitOuterClass(final String owner, final String name, final String descriptor) {
if (cv != null) {
cv.visitOuterClass(owner, name, descriptor);
}
}
/**
* 访问类的注解
*
* @param desc
* 注解类的类描述
* @param visible
* runtime时期注解是否可以被访问
* @return 返回一个注解值访问器
*/
public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) {
if (cv != null) {
return cv.visitAnnotation(descriptor, visible);
}
return null;
}
/**
* 访问标注在类型上的注解
*
* @param typeRef
* @param typePath
* @param desc
* @param visible
* @return
*/
public AnnotationVisitor visitTypeAnnotation(
final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) {
if (api < Opcodes.ASM5) {
throw new UnsupportedOperationException("This feature requires ASM5");
}
if (cv != null) {
return cv.visitTypeAnnotation(typeRef, typePath, descriptor, visible);
}
return null;
}
/**
* 访问一个类的属性
*
* @param attribute
* 类的属性
*/
public void visitAttribute(final Attribute attribute) {
if (cv != null) {
cv.visitAttribute(attribute);
}
}
public void visitNestMember(final String nestMember) {
if (api < Opcodes.ASM7) {
throw new UnsupportedOperationException("This feature requires ASM7");
}
if (cv != null) {
cv.visitNestMember(nestMember);
}
}
/**
* 访问内部类信息
* @param name
* @param outerName
* @param innerName
* @param access
*/
public void visitInnerClass(
final String name, final String outerName, final String innerName, final int access) {
if (cv != null) {
cv.visitInnerClass(name, outerName, innerName, access);
}
}
/**
* 访问类的字段
* @param access
* @param name
* @param desc
* @param signature
* @param value
* @return
*/
public FieldVisitor visitField(
final int access,
final String name,
final String descriptor,
final String signature,
final Object value) {
if (cv != null) {
return cv.visitField(access, name, descriptor, signature, value);
}
return null;
}
/**
* 访问类的方法
* @param access
* @param name
* @param desc
* @param signature
* @param exceptions
* @return
*/
public MethodVisitor visitMethod(
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions) {
if (cv != null) {
return cv.visitMethod(access, name, descriptor, signature, exceptions);
}
return null;
}
/**
* 访问类结束
*/
public void visitEnd() {
if (cv != null) {
cv.visitEnd();
}
}
}
ClassVisitor 的调用必须是遵循下面的调用顺序的:
visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*
( visitInnerClass | visitField | visitMethod )*
visitEnd
围绕着ClassVisitor ,还有两个核心类: 后续的例子代码中可以看到,我们必须先调用visit方法,这就因为class是字节流的二进制文件,而我们解析和生成也是要遵循一定的顺序。ClassVisitor定义了我们需要操作的所有接口,并且ClassVisitor也可以接收一个ClassVisitor实例来构造,有点类似于一个事件的filter,可以套很多层的filter来一层层处理逻辑。
1、ClassReader 将class解析成byte 数组,然后会通过accept方法去按顺序调用绑定对象(继承了ClassVisitor的实例)的方法。可以视为一个事件的生产者。
2、ClassWriter 是ClassVisitor 的子类。直接可以通过toByteArray()方法以返回的byte数组形式构建编译后的class。可以视为一个事件的消费者。
但是需要注意的是虽然ClassReader和ClassWriter 看起来像是对称类例如InputStream和OutputStream但其实类结构上并无关联,ClassWriter 继承于ClassVisitor,而ClassReader 直接继承于Object,只是提供解析class,并依次调用ClassVisitor对象。也就是说ClassReader的api和ClassWriter 的api基本没有相关性。
另外补充一下,ASM中常见参数desc直译是描述,但是作用其实时限定方法的输入参数和返回参数类型,比如"()V"是无输入无输出,"(I)Ljava/lang/String;"是输入int,返回String。
无中生有 ——利用ASM动态创建一个类
由于是凭空创建,所以只需要ClassWriter 即可。
先上目标代码,我们的目的是创造一个下面的类
public class Student{
public int age = 11;
public int getAge() {
return age;
}
}
一个特别简单的javabean类。
简单说一下创建流程:
- 创建一个类需要先调用visit创建类的头部信息。
- 分别调用visitMethod或visitField生成需要的创建的方法或者字段。
- 调用visitEnd结束类的创建
- 调用ClassWriter 的toByteArray将动态生成的class转为byte[]数组,可以用ClassLoader动态载入,或者写出成.class文件
完整代码:
public byte[] createNewClass() {
//创建ClassWriter ,构造参数的含义是是否自动计算栈帧,操作数栈及局部变量表的大小
//0:完全手动计算 即手动调用visitFrame和visitMaxs完全生效
//ClassWriter.COMPUTE_MAXS=1:需要自己计算栈帧大小,但本地变量与操作数已自动计算好,当然也可以调用visitMaxs方法,只不过不起作用,参数会被忽略;
//ClassWriter.COMPUTE_FRAMES=2:栈帧本地变量和操作数栈都自动计算,不需要调用visitFrame和visitMaxs方法,即使调用也会被忽略。
//这些选项非常方便,但会有一定的开销,使用COMPUTE_MAXS会慢10%,使用COMPUTE_FRAMES会慢2倍。
ClassWriter cw = new ClassWriter(0);
//创建类头部信息:jdk版本,修饰符,类全名,签名信息,父类,接口集
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "asm/Student", null, "java/lang/Object", null);
//创建字段age:修饰符,变量名,类型,签名信息,初始值(不一定会起作用后面会说明)
cw.visitField(Opcodes.ACC_PUBLIC , "age", "I", null, new Integer(11))
.visitEnd();
//创建方法:修饰符,方法名,类型,描述(输入输出类型),签名信息,抛出异常集合
// 方法的逻辑全部使用jvm指令来书写的比较晦涩,门槛较高,后面会介绍简单的方法
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "getAge", "()I", null, null);
// 创建方法第一步
mv.visitCode();
// 将索引为 #0 的本地变量列表加到操作数栈下。#0 索引的本地变量列表永远是 this ,当前类实例的引用。
mv.visitVarInsn(ALOAD, 0);
// 获取变量的值,
mv.visitFieldInsn(GETFIELD, "asm/Student", "age", "I");
// 返回age
mv.visitInsn(IRETURN);
// 设置操作数栈和本地变量表的大小
mv.visitMaxs(1, 1);
//结束方法生成
mv.visitEnd();
//结束类生成
cw.visitEnd();
//返回class的byte[]数组
return cw.toByteArray();
}
通过以上代码可以看出其实类以及字段的创建还是比较简单的,难点在于方法的创建上。如果对于jvm指令集不熟悉基本抓瞎。这里介绍一个方法,先手写目标类,即Student的java文件,然后用javac编译成class文件(或者用IDE编译),找到编译好的class文件,用javap -c Student打开class文件。输出如下:
public class asm.Student {
public int age;
public asm.Student();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #2 // Field age:I
10: return
public int getAge();
Code:
0: aload_0
1: getfield #2 // Field age:I
4: ireturn
}
可以看到jvm编译时帮助Student补全了构造方法Student(),着重看getAge的指令代码
0: aload_0
1: getfield #2 // Field age:I
4: ireturn
一共三条正好和生成方法的代码对应上
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "asm/ASMDemo", "age", "I");
mv.visitInsn(IRETURN);
当没有思路时,可以用这参考这种办法。
好了现在已经生成了新的class的byte[],剩下的就是加载,验证了。
加载的代码:
/**
*用来加载byte[],由于defineClass不是public修饰的所以只能这样写。
*/
public class MyClassLoader extends ClassLoader {
public Class getClassByBytes(byte[] bytes) {
return defineClass(null, bytes, 0, bytes.length);
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader();
Class classByBytes = myClassLoader.getClassByBytes(create());
Object o = classByBytes.newInstance();
Field field = classByBytes.getField("age");
Object o1 = field.get(o);
Method method = classByBytes.getMethod("getAge");
Object o2 = method.invoke(o);
System.out.println("Field age: " + o1 );
System.out.println("Method method : " + o2);
}
}
点击运行,然后你就会发现——华丽丽的报错了
Exception in thread "main" java.lang.InstantiationException: asm.CreateTest
at java.lang.Class.newInstance(Class.java:427)
at asm.ASMTest.main(ASMTest.java:20)
Caused by: java.lang.NoSuchMethodException: asm.CreateTest.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.newInstance(Class.java:412)
... 1 more
asm.CreateTest.<init>()这个方法没有找到,熟悉jvm的可能会知道其实<init>就是构造函数,构造函数在jvm中会被重新命名成<init>。但是我们手写的java文件时也没有写构造函数,为什么就可以呢?翻到上面贴出的用javac编译出的Student文件,可以看到编译时编译器自动帮我们加好了构造函数。然后再把咱们自己生成的class文件的byte[]通过输出流写成class文件,在通过javap -c 查看:
public class asm.Student{
public int zero;
public int getZero();
Code:
0: aload_0
1: getfield #11 // Field zero:I
4: ireturn
}
果然利用ASM生成的class里的确没有构造方法。ASM还是要比编译器懒一些的,哈。既然没有,咱们加上就行了。
先参考一下上面由java编译成的class文件,其实构造函数的代码:
public asm.Student();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #2 // Field age:I
10: return
简单翻译一下这6条指令:
- this变量入栈
- 执行父类的<init>方法
- this再次入栈
- byte变量10入栈
- 给对象字段age赋值
- 方法结束
如此可以看到其实构造函数最核心的指令就会调用父类的<init>方法(暂时不考虑字段赋值的事情)。现在基本能够确定,我们手写的构造函数必须包含这三条指令
aload_0
invokespecial
return
然后和上面生成getAge方法类似的生成一个<init>方法即可,代码如下:
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
// aload_0
mv.visitVarInsn(ALOAD, 0);
// 获取变量的值,
mv.visitMethodInsn(INVOKESPECIAL,"java/lang/Object", "<init>", "()V", false);
// 结束
mv.visitInsn(IRETURN);
// 设置操作数栈和本地变量表的大小
mv.visitMaxs(1, 1);
//结束方法生成
mv.visitEnd();
然后再次运行,发现已经可以正常运行,输出如下
Field age: 0
Method method : 0
说好的11呢???哈,其实通过查看java文件编译后的class就能发现全局变量的默认值赋值其实是在构造函数中进行的,也就是说我们通过ASM创建字段时设置的默认值没起效果,WTF!再次修改<init>方法(类似getAge,不在添加详细注释)
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitVarInsn(ALOAD, 0);
mv.visitIntInsn(BIPUSH, 10);
mv.visitFieldInsn(PUTFIELD, "asm/Student", "age", "I");
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
再次运行
Field age: 10
Method method : 10
哈,完美运行。那么可能有同学会问了,那么设置字段默认值卵用没有,为什么还有这个参数呢,其实也并不是一点用没有,当生成的字段时static时,就会起作用。这里边又会涉及到类的静态变量加载时机,<cinit>函数等等,这里就不展开细讲了,否则篇幅该hold不住了。总结起来一句话:ASM只是工具,掌握jvm知识才是硬道理。
偷梁换柱——ASM修改已有的class
其实除了动态生成class,还有一大部分需求是修改class,这里简单介绍下最复杂的修改class的Method。其他的修改照葫芦画瓢就可以。
先上目标效果,首先原始类还用咱们的Student:
public class Student{
public int age = 11;
public int getAge() {
return age;
}
}
目标是在getAge里边插入一句打印语句,即:
public class Student{
public int age = 11;
public int getAge() {
System.out.println("getAge");
return age;
}
}
思路如下:
- 首先自定义一个ClassVisitor,重写visitMethod,这样就可以收到每个方法的回调
- 判断方法名称是不是getAge
- 如果是返回一个自定义的MethodVisitor
- 自定义的MethodVisitor重写visitCode(访问方法的第一个步骤)
- 添加相应的逻辑
- 通过重写visitMaxs修改操作数栈和局部变量表的大小(添加了逻辑可能会导致操作数栈和局部变量表的最大值增大)
代码如下
public class MyMethodVisitor extends MethodVisitor {
public MyMethodVisitor(MethodVisitor mv) {
super(ASM5, mv);
}
@Override
public void visitCode() {
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("getAge");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack+1, maxLocals);
}
}
public class MyClassVisitor extends ClassVisitor {
public MyClassVisitor(ClassVisitor cv) {
super(ASM5, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
if(name.equals("getAge")){
return new MyMethodVisitor(methodVisitor);
}else {
return methodVisitor;
}
}
}
//修改测试代码
public static void main(String[] args) throws Exception {
ClassReader classReader = new ClassReader(createNewClass());
ClassWriter classWriter = new ClassWriter(classReader, 0);
ClassVisitor cv = new MyClassVisitor(classWriter);
classReader.accept(cv,0);
MyClassLoader myClassLoader = new MyClassLoader();
Class classByBytes = myClassLoader.getClassByBytes(classWriter.toByteArray());
Object o = classByBytes.newInstance();
Field field = classByBytes.getField("age");
Object o1 = field.get(o);
Method method = classByBytes.getMethod("getAge");
Object o2 = method.invoke(o);
System.out.println("Field age: " + o1);
System.out.println("Method method : " + o2);
}
运行:
getAge
Field age: 10
Method method : 10
注入的逻辑完美运行!修改方法逻辑不仅仅可以在方法开始插入逻辑,包括方法结束时,甚至方法体中间都可以,可以利用这种思路很方便的写出一个AOP框架。
ASMifier
ASM由于是基于jvm指令集的所以比较晦涩。官方可能是考虑到大家都是比较菜的,提供了很多的工具类,这里只介绍一种我认为最有用的:ASMifier。ASMifier最大的功能就是将一个java文件翻译成ASM生成此文件的代码。
ASMifier.main(new String[]{"asm.Student"});
运行后,就可以在控制台看见如何利用ASM生成Student类了,省了很大力气。工具类很多就不一 一介绍了,推荐一个博客有兴趣可以去看看:
总结
ASM相对于一些其他的操作字节码的框架偏底层了一些,只提供了一些低级api,要想熟练使用还是需要比较高的jvm知识的。但是作为其他操作字节码的框架的底层实现,还是非常有必要了解一下的。真实项目中如果对性能要求不是特别高的话,结合项目需求完全可以用其他高级库代替ASM,例如cglib javassist。
突然想起来前两年做过的一个需求:拿到一个类序列化之后的文件,然后在本地没有这个类的情况下反序列化它。
当时觉得这个需求真是扯淡,现在想想做反序列化时报出ClassNotFound这个错误之前,其实已经可以获取类的包名,类名,签名,以及字段详情了。其实完全可以重写反序列化方法,然后获取到类的信息后动态生成class文件,然后再加载到内存中,之后再做正常的反序列化操作。两年前的需求现在想出了解决方案,哈!
代码地址:
参考:
https://blog.csdn.net/lijingyao8206/article/category/3276863