簡述
最近在看阿里的ARouter的源碼,從git上clone下來之后,run起來發(fā)現(xiàn)項(xiàng)目運(yùn)行的效果和源碼有明顯區(qū)別。打個(gè)比方,源碼是這樣
boolean b = true;
System.out.println(b);
但是當(dāng)你跑起來之后去發(fā)現(xiàn)打印出來的false,打開編譯好的class文件卻發(fā)現(xiàn)編譯出來的class的代碼和源碼不一樣。經(jīng)過翻看ARouter的工程源碼,發(fā)現(xiàn)其實(shí)ARouter是利用了Gradle的 Transform API和ASM共同完成的編譯時(shí)修改源碼的功能。
Transform API的功能是讓你在java文件編譯成class文件之后對這些class文件進(jìn)行讀寫,發(fā)生在編譯時(shí),是Android的gradle打包插件自帶的功能,這里不詳細(xì)展開。本片文章主要是講解ASM的基本使用方法。有機(jī)會(huì)會(huì)出一個(gè)Transform + ASM插件教程。
ASM簡介
ASM 是一個(gè) Java 字節(jié)碼操控框架。它能夠以二進(jìn)制形式修改已有類或者動(dòng)態(tài)生成類。ASM 可以直接產(chǎn)生二進(jìn)制 class 文件,也可以在類被加載入 Java 虛擬機(jī)之前動(dòng)態(tài)改變類行為。ASM 從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據(jù)用戶要求生成新類。許多AOP框架以及動(dòng)態(tài)修改字節(jié)碼的庫的底層都是由ASM實(shí)現(xiàn)的,例如Spring AOP,cglib等等
一句話概括:ASM可以動(dòng)態(tài)的修改創(chuàng)建class文件,達(dá)到動(dòng)態(tài)修改java代碼的效果。
ASM 核心API
public abstract class ClassVisitor {
// 實(shí)現(xiàn)的ASM的API版本。該字段的值必須為如下幾個(gè)之一:Opcodes.ASM4,ASM5,ASM6,ASM7
protected final int api;
// 該類的方法可以委托給子類
protected ClassVisitor cv;
// 構(gòu)造器
public ClassVisitor(final int api) {
this(api, null);
}
// 構(gòu)造器
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
* 類訪問標(biāo)識(shí)符public等
* @param name
* 類名稱
* @param signature
* 類簽名(非泛型為NUll)
* @param superName
* 類的父類
* @param interfaces
* 類實(shí)現(xiàn)的接口
*/
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
* 附加的驗(yàn)證信息,可以為空
*/
public void visitSource(final String source, final String debug) {
if (cv != null) {
cv.visitSource(source, debug);
}
}
/**
* 訪問與類對應(yīng)的模塊. 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);
}
}
/**
* 這個(gè)其實(shí)并不是訪問外部類的回調(diào),而是訪問方法體中含有匿名內(nèi)部類的方法
*
* @param owner 為創(chuàng)建匿名類的類,當(dāng)然其也是一個(gè)enclosing class類型的類
* @param name 創(chuàng)建匿名類的方法。
* @param desc 創(chuàng)建匿名類的方法描述信息。
* @return 返回一個(gè)注解值訪問器
*/
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時(shí)期注解是否可以被訪問
* @return 返回一個(gè)注解值訪問器
*/
public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) {
if (cv != null) {
return cv.visitAnnotation(descriptor, visible);
}
return null;
}
/**
* 訪問標(biāo)注在類型上的注解
*
* @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;
}
/**
* 訪問一個(gè)類的屬性
*
* @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);
}
}
/**
* 訪問內(nèi)部類信息
* @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;
}
/**
* 訪問類結(jié)束
*/
public void visitEnd() {
if (cv != null) {
cv.visitEnd();
}
}
}
ClassVisitor 的調(diào)用必須是遵循下面的調(diào)用順序的:
visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*
( visitInnerClass | visitField | visitMethod )*
visitEnd
圍繞著ClassVisitor ,還有兩個(gè)核心類: 后續(xù)的例子代碼中可以看到,我們必須先調(diào)用visit方法,這就因?yàn)閏lass是字節(jié)流的二進(jìn)制文件,而我們解析和生成也是要遵循一定的順序。ClassVisitor定義了我們需要操作的所有接口,并且ClassVisitor也可以接收一個(gè)ClassVisitor實(shí)例來構(gòu)造,有點(diǎn)類似于一個(gè)事件的filter,可以套很多層的filter來一層層處理邏輯。
1、ClassReader 將class解析成byte 數(shù)組,然后會(huì)通過accept方法去按順序調(diào)用綁定對象(繼承了ClassVisitor的實(shí)例)的方法。可以視為一個(gè)事件的生產(chǎn)者。
2、ClassWriter 是ClassVisitor 的子類。直接可以通過toByteArray()方法以返回的byte數(shù)組形式構(gòu)建編譯后的class。可以視為一個(gè)事件的消費(fèi)者。
但是需要注意的是雖然ClassReader和ClassWriter 看起來像是對稱類例如InputStream和OutputStream但其實(shí)類結(jié)構(gòu)上并無關(guān)聯(lián),ClassWriter 繼承于ClassVisitor,而ClassReader 直接繼承于Object,只是提供解析class,并依次調(diào)用ClassVisitor對象。也就是說ClassReader的api和ClassWriter 的api基本沒有相關(guān)性。
另外補(bǔ)充一下,ASM中常見參數(shù)desc直譯是描述,但是作用其實(shí)時(shí)限定方法的輸入?yún)?shù)和返回參數(shù)類型,比如"()V"是無輸入無輸出,"(I)Ljava/lang/String;"是輸入int,返回String。
無中生有 ——利用ASM動(dòng)態(tài)創(chuàng)建一個(gè)類
由于是憑空創(chuàng)建,所以只需要ClassWriter 即可。
先上目標(biāo)代碼,我們的目的是創(chuàng)造一個(gè)下面的類
public class Student{
public int age = 11;
public int getAge() {
return age;
}
}
一個(gè)特別簡單的javabean類。
簡單說一下創(chuàng)建流程:
- 創(chuàng)建一個(gè)類需要先調(diào)用visit創(chuàng)建類的頭部信息。
- 分別調(diào)用visitMethod或visitField生成需要的創(chuàng)建的方法或者字段。
- 調(diào)用visitEnd結(jié)束類的創(chuàng)建
- 調(diào)用ClassWriter 的toByteArray將動(dòng)態(tài)生成的class轉(zhuǎn)為byte[]數(shù)組,可以用ClassLoader動(dòng)態(tài)載入,或者寫出成.class文件
完整代碼:
public byte[] createNewClass() {
//創(chuàng)建ClassWriter ,構(gòu)造參數(shù)的含義是是否自動(dòng)計(jì)算棧幀,操作數(shù)棧及局部變量表的大小
//0:完全手動(dòng)計(jì)算 即手動(dòng)調(diào)用visitFrame和visitMaxs完全生效
//ClassWriter.COMPUTE_MAXS=1:需要自己計(jì)算棧幀大小,但本地變量與操作數(shù)已自動(dòng)計(jì)算好,當(dāng)然也可以調(diào)用visitMaxs方法,只不過不起作用,參數(shù)會(huì)被忽略;
//ClassWriter.COMPUTE_FRAMES=2:棧幀本地變量和操作數(shù)棧都自動(dòng)計(jì)算,不需要調(diào)用visitFrame和visitMaxs方法,即使調(diào)用也會(huì)被忽略。
//這些選項(xiàng)非常方便,但會(huì)有一定的開銷,使用COMPUTE_MAXS會(huì)慢10%,使用COMPUTE_FRAMES會(huì)慢2倍。
ClassWriter cw = new ClassWriter(0);
//創(chuàng)建類頭部信息:jdk版本,修飾符,類全名,簽名信息,父類,接口集
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "asm/Student", null, "java/lang/Object", null);
//創(chuàng)建字段age:修飾符,變量名,類型,簽名信息,初始值(不一定會(huì)起作用后面會(huì)說明)
cw.visitField(Opcodes.ACC_PUBLIC , "age", "I", null, new Integer(11))
.visitEnd();
//創(chuàng)建方法:修飾符,方法名,類型,描述(輸入輸出類型),簽名信息,拋出異常集合
// 方法的邏輯全部使用jvm指令來書寫的比較晦澀,門檻較高,后面會(huì)介紹簡單的方法
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "getAge", "()I", null, null);
// 創(chuàng)建方法第一步
mv.visitCode();
// 將索引為 #0 的本地變量列表加到操作數(shù)棧下。#0 索引的本地變量列表永遠(yuǎn)是 this ,當(dāng)前類實(shí)例的引用。
mv.visitVarInsn(ALOAD, 0);
// 獲取變量的值,
mv.visitFieldInsn(GETFIELD, "asm/Student", "age", "I");
// 返回age
mv.visitInsn(IRETURN);
// 設(shè)置操作數(shù)棧和本地變量表的大小
mv.visitMaxs(1, 1);
//結(jié)束方法生成
mv.visitEnd();
//結(jié)束類生成
cw.visitEnd();
//返回class的byte[]數(shù)組
return cw.toByteArray();
}
通過以上代碼可以看出其實(shí)類以及字段的創(chuàng)建還是比較簡單的,難點(diǎn)在于方法的創(chuàng)建上。如果對于jvm指令集不熟悉基本抓瞎。這里介紹一個(gè)方法,先手寫目標(biāo)類,即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
}
可以看到j(luò)vm編譯時(shí)幫助Student補(bǔ)全了構(gòu)造方法Student(),著重看getAge的指令代碼
0: aload_0
1: getfield #2 // Field age:I
4: ireturn
一共三條正好和生成方法的代碼對應(yīng)上
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "asm/ASMDemo", "age", "I");
mv.visitInsn(IRETURN);
當(dāng)沒有思路時(shí),可以用這參考這種辦法。
好了現(xiàn)在已經(jīng)生成了新的class的byte[],剩下的就是加載,驗(yàn)證了。
加載的代碼:
/**
*用來加載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);
}
}
點(diǎn)擊運(yùn)行,然后你就會(huì)發(fā)現(xiàn)——華麗麗的報(bào)錯(cuò)了
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>()這個(gè)方法沒有找到,熟悉jvm的可能會(huì)知道其實(shí)<init>就是構(gòu)造函數(shù),構(gòu)造函數(shù)在jvm中會(huì)被重新命名成<init>。但是我們手寫的java文件時(shí)也沒有寫構(gòu)造函數(shù),為什么就可以呢?翻到上面貼出的用javac編譯出的Student文件,可以看到編譯時(shí)編譯器自動(dòng)幫我們加好了構(gòu)造函數(shù)。然后再把咱們自己生成的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里的確沒有構(gòu)造方法。ASM還是要比編譯器懶一些的,哈。既然沒有,咱們加上就行了。
先參考一下上面由java編譯成的class文件,其實(shí)構(gòu)造函數(shù)的代碼:
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變量入棧
- 執(zhí)行父類的<init>方法
- this再次入棧
- byte變量10入棧
- 給對象字段age賦值
- 方法結(jié)束
如此可以看到其實(shí)構(gòu)造函數(shù)最核心的指令就會(huì)調(diào)用父類的<init>方法(暫時(shí)不考慮字段賦值的事情)。現(xiàn)在基本能夠確定,我們手寫的構(gòu)造函數(shù)必須包含這三條指令
aload_0
invokespecial
return
然后和上面生成getAge方法類似的生成一個(gè)<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);
// 結(jié)束
mv.visitInsn(IRETURN);
// 設(shè)置操作數(shù)棧和本地變量表的大小
mv.visitMaxs(1, 1);
//結(jié)束方法生成
mv.visitEnd();
然后再次運(yùn)行,發(fā)現(xiàn)已經(jīng)可以正常運(yùn)行,輸出如下
Field age: 0
Method method : 0
說好的11呢???哈,其實(shí)通過查看java文件編譯后的class就能發(fā)現(xiàn)全局變量的默認(rèn)值賦值其實(shí)是在構(gòu)造函數(shù)中進(jìn)行的,也就是說我們通過ASM創(chuàng)建字段時(shí)設(shè)置的默認(rèn)值沒起效果,WTF!再次修改<init>方法(類似getAge,不在添加詳細(xì)注釋)
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();
再次運(yùn)行
Field age: 10
Method method : 10
哈,完美運(yùn)行。那么可能有同學(xué)會(huì)問了,那么設(shè)置字段默認(rèn)值卵用沒有,為什么還有這個(gè)參數(shù)呢,其實(shí)也并不是一點(diǎn)用沒有,當(dāng)生成的字段時(shí)static時(shí),就會(huì)起作用。這里邊又會(huì)涉及到類的靜態(tài)變量加載時(shí)機(jī),<cinit>函數(shù)等等,這里就不展開細(xì)講了,否則篇幅該hold不住了。總結(jié)起來一句話:ASM只是工具,掌握jvm知識(shí)才是硬道理。
偷梁換柱——ASM修改已有的class
其實(shí)除了動(dòng)態(tài)生成class,還有一大部分需求是修改class,這里簡單介紹下最復(fù)雜的修改class的Method。其他的修改照葫蘆畫瓢就可以。
先上目標(biāo)效果,首先原始類還用咱們的Student:
public class Student{
public int age = 11;
public int getAge() {
return age;
}
}
目標(biāo)是在getAge里邊插入一句打印語句,即:
public class Student{
public int age = 11;
public int getAge() {
System.out.println("getAge");
return age;
}
}
思路如下:
- 首先自定義一個(gè)ClassVisitor,重寫visitMethod,這樣就可以收到每個(gè)方法的回調(diào)
- 判斷方法名稱是不是getAge
- 如果是返回一個(gè)自定義的MethodVisitor
- 自定義的MethodVisitor重寫visitCode(訪問方法的第一個(gè)步驟)
- 添加相應(yīng)的邏輯
- 通過重寫visitMaxs修改操作數(shù)棧和局部變量表的大小(添加了邏輯可能會(huì)導(dǎo)致操作數(shù)棧和局部變量表的最大值增大)
代碼如下
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);
}
運(yùn)行:
getAge
Field age: 10
Method method : 10
注入的邏輯完美運(yùn)行!修改方法邏輯不僅僅可以在方法開始插入邏輯,包括方法結(jié)束時(shí),甚至方法體中間都可以,可以利用這種思路很方便的寫出一個(gè)AOP框架。
ASMifier
ASM由于是基于jvm指令集的所以比較晦澀。官方可能是考慮到大家都是比較菜的,提供了很多的工具類,這里只介紹一種我認(rèn)為最有用的:ASMifier。ASMifier最大的功能就是將一個(gè)java文件翻譯成ASM生成此文件的代碼。
ASMifier.main(new String[]{"asm.Student"});
運(yùn)行后,就可以在控制臺(tái)看見如何利用ASM生成Student類了,省了很大力氣。工具類很多就不一 一介紹了,推薦一個(gè)博客有興趣可以去看看:
總結(jié)
ASM相對于一些其他的操作字節(jié)碼的框架偏底層了一些,只提供了一些低級api,要想熟練使用還是需要比較高的jvm知識(shí)的。但是作為其他操作字節(jié)碼的框架的底層實(shí)現(xiàn),還是非常有必要了解一下的。真實(shí)項(xiàng)目中如果對性能要求不是特別高的話,結(jié)合項(xiàng)目需求完全可以用其他高級庫代替ASM,例如cglib javassist。
突然想起來前兩年做過的一個(gè)需求:拿到一個(gè)類序列化之后的文件,然后在本地沒有這個(gè)類的情況下反序列化它。
當(dāng)時(shí)覺得這個(gè)需求真是扯淡,現(xiàn)在想想做反序列化時(shí)報(bào)出ClassNotFound這個(gè)錯(cuò)誤之前,其實(shí)已經(jīng)可以獲取類的包名,類名,簽名,以及字段詳情了。其實(shí)完全可以重寫反序列化方法,然后獲取到類的信息后動(dòng)態(tài)生成class文件,然后再加載到內(nèi)存中,之后再做正常的反序列化操作。兩年前的需求現(xiàn)在想出了解決方案,哈!
代碼地址:
參考:
https://blog.csdn.net/lijingyao8206/article/category/3276863