ASM簡介
- ASM是一個操作Java字節(jié)碼類庫,其操作的對象是字節(jié)碼數(shù)據(jù),處理字節(jié)碼方式是“拆分-修改-合并”
- 將.class文件拆分成多個部分;
- 對某一個部分的信息進行修改;
- 將多個部分重新組織成一個新的.class文件;
- 版本發(fā)展:Java語言在不斷發(fā)展,那么,ASM版本也要不斷發(fā)展來跟得上Java的發(fā)展;在選擇ASM版本的時候,要注意它支持的Java版本,來確保兼容性;
- 學(xué)習(xí)目標:使用ASM實現(xiàn)1)無狀態(tài)轉(zhuǎn)換:查找方法;替換方法; 2)有狀態(tài)轉(zhuǎn)換:獲取Hybrid使用Action集合,RN使用modules集合數(shù)據(jù);
- 既然要操作字節(jié)碼數(shù)據(jù),我們需要了解字節(jié)碼存儲結(jié)構(gòu):The class File Format
ClassFile
- 既然ASM操作的是.class文件,則我們需要了解class文件結(jié)構(gòu):在.class文件中,存儲的是ByteCode數(shù)據(jù),但是,這些ByteCode數(shù)據(jù)并不是雜亂無章的,而是遵循一定的數(shù)據(jù)結(jié)構(gòu),這些結(jié)構(gòu)定義在 Java Virtual Machine Specification中的The class File Format,如下所示。
ClassFile {
u4 magic; //魔數(shù)
u2 minor_version; //次版本號
u2 major_version; //主版本號
u2 constant_pool_count; //常量池容量:從1開始,0:不引用任何一個常量池數(shù)據(jù)
cp_info constant_pool[constant_pool_count-1]; //常量數(shù)據(jù),數(shù)據(jù)構(gòu)成有17種,以首位u1表示tag類型
u2 access_flags; //訪問標記,識別類或接口的訪問信息如:ACC_PUBLIC;ACC_ABSTRACT,由于每個標記占用二進制位不同,使用|表示交集;
u2 this_class; //當前類索引:常量池中偏移量指向一個類型為CONSTANT_Class_info的類描述符常量
u2 super_class; //當前類的父類索引:java單繼承機制
u2 interfaces_count; //當前類實現(xiàn)的接口計數(shù)器
u2 interfaces[interfaces_count]; //接口索引表:常量池中偏移量
u2 fields_count; //字段計數(shù)器
field_info fields[fields_count]; //字段表:由類級變量static和實例變量(全局),不包括局部變量
u2 methods_count; //方法數(shù)
method_info methods[methods_count]; //方法表
u2 attributes_count; //屬性數(shù)
attribute_info attributes[attributes_count]; //屬性表
}
- u* : 表示占用字節(jié)數(shù),有u1,u2,u4,u8等構(gòu)成;
字段表
- field_info字段表格式如下:
field_info {
u2 access_flags; //字段的訪問標記
u2 name_index; //字段的名稱
u2 descriptor_index; //字段的描述符
u2 attributes_count; //字段屬性
attribute_info attributes[attributes_count];
}
全限定名:類的全限定名是將類全名的
.全部替換為/,如java.lang.String ,全限定名java/lang/String簡單名稱:方法main() 簡單名稱為main,全局變量num為名稱num
-
描述符:基本數(shù)據(jù)類型及void的由大寫字母表示,對象類型有L+全限定名=> Ljava/lang/String;表示String字段描述符;對于數(shù)組類型根據(jù)維度在前面加上“[”,如int[] => [I ; String[] => [Ljava/lang/String;
desc_asm.png attribute_info:字段的屬性表,存儲一些額外信息;
方法表
- 方法表格式如下,方法的描述與字段的描述采用了幾乎完全一致的方式
method_info {
u2 access_flags; //訪問標記
u2 name_index; //方法簡單名稱常量池索引
u2 descriptor_index; //方法描述符索引
u2 attributes_count; //方法屬性表
attribute_info attributes[attributes_count];
}
- 注意class文件中編譯器添加的實例構(gòu)造器<init> 和類構(gòu)造器<clint>兩個方法
屬性表
- Class文件、字段表、方法表都可以攜帶自己的屬性表集合,以描述某些場景專有的信息,參考如下事例表示:
public class HelloWorld {
public void foo() {
int i = 0;
int j = 0;
if (i > 0) {
int k = 0;
}
int l = 0;
}
}
//使用javap -v 獲取foo字節(jié)碼code數(shù)據(jù)如下:
public void foo();
descriptor: ()V //方法描述符
flags: (0x0001) ACC_PUBLIC //方法訪問標記
Code: 033C033D 1B9E0005 033E033E B1
stack=1, locals=4, args_size=1 //stack:操作數(shù)棧深度,locals:局部變量表,args_size:方法參數(shù)的個數(shù),包括方法參數(shù)、this
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_1 // 1B
/**
* ifle 字節(jié)碼 9E -->后面u2類型字段為 0005 表示偏移量,當前5 + 偏移量 = 10
* 10由code字節(jié)碼偏移量6,7位置,code屬性中字節(jié)碼長度由u4表示,
* 但是《Java虛擬機規(guī)范》中明確限制了一個方法不允許超過65535條字節(jié)碼指令,即實際只使用了u2的長度,如果超過這個限制,Javac編譯器就會拒絕編譯,因此這里使用u2表示跳轉(zhuǎn)字節(jié)碼偏移量
*/
5: ifle 10 //9E 0005
8: iconst_0 //03
9: istore_3
10: iconst_0
11: istore_3
12: return
LineNumberTable: //源碼行數(shù)與code中偏移量對應(yīng)關(guān)系,常用于log中輸出日志
line 11: 0
line 12: 2
line 13: 4
line 14: 8
line 18: 10
line 20: 12
LocalVariableTable: //描述棧幀中局部變量表的變量與Java源碼中定義的變量之間的關(guān)系
//start:局部變量的生命周期開始的字節(jié)碼偏移量
//Length:其作用范圍覆蓋的長度,兩者結(jié)合起來就是這個局部變量在字節(jié)碼之中的作用域范圍
//Slot: 局部變量表位置:對應(yīng)上方最大locals=4,根據(jù)字節(jié)碼可以驗證stack最大為1
Start Length Slot Name Signature
0 13 0 this Lsample/HelloWorld;
2 11 1 i I
4 9 2 j I
12 1 3 l I
StackMapTable: number_of_entries = 1 //棧映射幀Stack Map Frame個數(shù):1
//虛擬機類加載的字節(jié)碼驗證階段被新類型檢查驗證器使用,目的代替以前比較消耗性能的基于數(shù)據(jù)流分析的類型推導(dǎo)驗證器;其中記錄的是一個方法中操作數(shù)棧與局部變量區(qū)的類型在一些特定位置的狀態(tài)。
frame_type = 253 /* append */ //基本塊開頭處的狀態(tài):frame_type = 251,表示多了2個局部變量,append 增加變量,chop 減少變量
offset_delta = 10 //棧映射幀的code偏移量為10,記錄的是if跳轉(zhuǎn)語句
locals = [ int, int ] //增量設(shè)置,進入時有默認Frame局部變量區(qū):[this],在10位置變量k已經(jīng)過了作用域局部變量區(qū):[ this, int, int],增量為 locals = [ int, int ]
- 這里我們主要關(guān)注StackMapTable中包含的棧映射楨:它有什么用呢?
- 在使用ASM的classWriter修改字節(jié)碼時構(gòu)造函數(shù)如下,flags屬性一般使用COMPUTE_FRAMES,其于COMPUTE_MAXS,默認0有何區(qū)別呢?
public ClassWriter(ClassReader classReader, int flags)- 默認值0:ASM即不會自動計算max stacks和max locals,也不會自動計算stack map frames;
- COMPUTE_MAXS:ASM只會自動計算max stacks和max locals,但不會自動計算stack map frames;
- COMPUTE_FRAMES:ASM即會自動計算max stacks和max locals,也會自動計算stack map frames;
ASM組成
- 組成結(jié)構(gòu)上來說,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,ClassWrite這三個類,他們的關(guān)系如下:
asm_relation_chart.png
ClassReader拆分
- 主要負責讀取.class文件里的內(nèi)容,然后拆分成各個不同的部分;如何實現(xiàn)呢?
public class ClassReader {
//真實的數(shù)據(jù)部分
final byte[] classFileBuffer;
//數(shù)據(jù)的索引信息:標識了classFileBuffer中數(shù)據(jù)里包含的常量池位置
private final int[] cpInfoOffsets;
//標記訪問標識(access flag)在classFileBuffer中的位置信息
public final int header;
/**
* 全局變量初始化
* classFileOffset :默認為0,起始位置
*/
ClassReader(byte[] classFileBuffer, int classFileOffset, boolean checkClassVersion) {
this.classFileBuffer = classFileBuffer; //class文件數(shù)據(jù)字節(jié)數(shù)組
this.b = classFileBuffer;
if (checkClassVersion && this.readShort(classFileOffset + 6) > 60) {
throw new IllegalArgumentException("Unsupported class file major version " + this.readShort(classFileOffset + 6));
} else {
//讀取第8個字節(jié)位置:常量池大小constant_pool_count
int constantPoolCount = this.readUnsignedShort(classFileOffset + 8);
this.cpInfoOffsets = new int[constantPoolCount];
//當前常量池起始位置:注意ClassFile由1開始,保留0位置用于未指定任何數(shù)據(jù)
int currentCpInfoIndex = 1;
//起始偏移量,首位常量池位置:(魔數(shù)u4,次版本號u2,主版本號u2,常量池大小u2)
int currentCpInfoOffset = classFileOffset + 10; //從第10個字節(jié)開始保存常量
//當前各個常量的偏移量
int cpInfoSize;
for(hasConstantDynamic = false; currentCpInfoIndex < constantPoolCount; currentCpInfoOffset += cpInfoSize) {
/**
* 常量池的數(shù)據(jù):u1:表示當前數(shù)據(jù)類型
CONSTANT_Utf8_info {
u1 tag; // == 1
u2 length;
u1 bytes[length];
}
*/
this.cpInfoOffsets[currentCpInfoIndex++] = currentCpInfoOffset + 1; //去掉u1數(shù)據(jù)類型保存常量數(shù)據(jù)
switch(classFileBuffer[currentCpInfoOffset]) { //currentCpInfoOffset記錄的為當前常量類型tag
case 1: //字符串計算字符串長度作為偏移量cpInfoSize
//tag:u1 length:u2 = 3 加上Short位的length表示bytes數(shù)組長度的
cpInfoSize = 3 + this.readUnsignedShort(currentCpInfoOffset + 1);
...
break;
...
}
/**
* currentCpInfoOffset:常量池數(shù)據(jù)已經(jīng)全部遍歷完存入cpInfoOffsets中,此時位置為:access_flags
*/
this.header = currentCpInfoOffset;
- int[] cpInfoOffsets:由class文件往后讀取8個字節(jié),在classFile中可知(魔數(shù)u4,次版本號u2,主版本號u2)constant_pool_count即常量池大小為cpInfoOffsets數(shù)組大小,數(shù)組中數(shù)據(jù)為當前常量在classFile中的偏移量,用于快速獲取常量;
- header:存儲當前類的access_flags標識位在字節(jié)碼數(shù)組中位置:快速定位到當前類,父類,字段,方法等數(shù)據(jù)內(nèi)容,如何驗證,看一下accept()方法:
public void accept(ClassVisitor classVisitor, Attribute[] attributePrototypes, int parsingOptions) {
...
int currentOffset = this.header; //currentOffset指定位置為:u2 access_flags
int accessFlags = this.readUnsignedShort(currentOffset); //獲取類標識位 Short
String thisClass = this.readClass(currentOffset + 2, charBuffer); // +2獲取當前類索引 this_class
String superClass = this.readClass(currentOffset + 4, charBuffer); // +4 獲取當前父類索引 super_class
String[] interfaces = new String[this.readUnsignedShort(currentOffset + 6)]; //接口集合數(shù)據(jù):大小 + 6
currentOffset += 8; //u2:access_flags, u2:this_class, u2:super_class , u2:interfaces_count
//獲取實現(xiàn)接口數(shù)據(jù)
int innerClassesOffset;
for(innerClassesOffset = 0; innerClassesOffset < interfaces.length; ++innerClassesOffset) {
interfaces[innerClassesOffset] = this.readClass(currentOffset, charBuffer);
currentOffset += 2; //interfaces[] 數(shù)組:數(shù)據(jù)類型u2
}
...
//獲取屬性表位置:attribute_info
int currentAttributeOffset = this.getFirstAttributeOffset();
//屬性表個數(shù):attributes_count
int fieldsCount;
for(fieldsCount = this.readUnsignedShort(currentAttributeOffset - 2); fieldsCount > 0; --fieldsCount) {
String attributeName = this.readUTF8(currentAttributeOffset, charBuffer);
int attributeLength = this.readInt(currentAttributeOffset + 2);
currentAttributeOffset += 6;
...
if ("Signature".equals(attributeName)) { //若當前類屬性有泛型,則讀取其信息
signature = this.readUTF8(currentAttributeOffset, charBuffer);
}
...
currentAttributeOffset += attributeLength;
}
//調(diào)用visit方法,每個類只會調(diào)用一次,參數(shù)為我們讀取到字節(jié)碼數(shù)據(jù)
classVisitor.visit(this.readInt(this.cpInfoOffsets[1] - 7), accessFlags, thisClass, signature, superClass, interfaces);
...
//獲取filed字段個數(shù)
fieldsCount = this.readUnsignedShort(currentOffset);
//readField() 調(diào)用classVisitor.visitField()方法
for(currentOffset += 2; fieldsCount-- > 0; currentOffset = this.readField(classVisitor, context, currentOffset)) {}
//獲取method方法個數(shù)
methodsCount = this.readUnsignedShort(currentOffset);
//readMethod() 調(diào)用classVisitor.visitMethod()方法
for(currentOffset += 2; methodsCount-- > 0; currentOffset = this.readMethod(classVisitor, context, currentOffset)) {}
classVisitor.visitEnd();
- accept()方法調(diào)用:
- 根據(jù)header快速獲取類標識位,當前類索引,父類索引,接口數(shù)據(jù),屬性表等數(shù)據(jù)后調(diào)用classVisitor.visit()方法,這也就解釋了為何classVisitor.visit()及classVisitor.visitEnd()只會調(diào)用一次,且一個在前,一個在最后調(diào)用;
- 根據(jù)字段個數(shù),調(diào)用readField中對字段解析調(diào)用classVisitor.visitField()方法
- 根據(jù)方法個數(shù),調(diào)用readMethod中對字段解析調(diào)用classVisitor.visitMethod()方法
private int readField(ClassVisitor classVisitor, Context context, int fieldInfoOffset) {
//descriptor :字段描述符 int:I ; constantValue :字段默認值
FieldVisitor fieldVisitor = classVisitor.visitField(accessFlags, name, descriptor, signature,constantValue);
...
fieldVisitor.visitEnd();
return currentOffset;
}
private int readMethod(ClassVisitor classVisitor, Context context, int methodInfoOffset) {
//調(diào)用classVisitor.visitMethod()掃描類中每一個方法
MethodVisitor methodVisitor = classVisitor.visitMethod(context.currentMethodAccessFlags, context.currentMethodName, context.currentMethodDescriptor, signatureIndex == 0 ? null : this.readUtf(signatureIndex, charBuffer), exceptions);
...
//獲取方法注解
if (annotationDefaultOffset != 0) {
AnnotationVisitor annotationVisitor = methodVisitor.visitAnnotationDefault();
this.readElementValue(annotationVisitor, annotationDefaultOffset, (String)null, charBuffer);
if (annotationVisitor != null) {
annotationVisitor.visitEnd();
}
}
...
//如果方法存在code屬性
if (codeOffset != 0) {
methodVisitor.visitCode();
this.readCode(methodVisitor, context, codeOffset);
}
methodVisitor.visitEnd();
return currentOffset;
}
- MethodVisitor:調(diào)用順序由classVisitor.accept() -> classVisitor.visitMethod() -> methodVisitor.visitCode() -> readCode() ->methodVisitor.visitEnd()
- methodVisitor.visitCode()方法起始位置調(diào)用,methodVisitor.visitEnd()方法結(jié)束時調(diào)用,且只會調(diào)用一次
- readCode() 讀取code中屬性值調(diào)用MethodVisitor對應(yīng)方法
private void readCode(MethodVisitor methodVisitor, Context context, int codeOffset) {
byte[] classBuffer = this.classFileBuffer;
//常用加載字符串字節(jié)碼命令編號:0x12 -> 18 ldc 表示int、float或String型常量從常量池推送至棧頂
case 18:
//調(diào)用visitLdcInsn獲取常量池中數(shù)據(jù)readConst讀取utf-8字符串
methodVisitor.visitLdcInsn(this.readConst(classBuffer[currentOffset + 1] & 255, charBuffer));
currentOffset += 2;
break;
...
case 178: //0xb2 getstatic
case 179: //0xb3 putstatic
case 180: //0xb4 getfield
case 181: //0xb5 putfield
case 182: //0xb6 invokevirtual
case 183: //0xb7 invokespecial
case 184: //0xb8 invokestatic
case 185: //0xb9 invokeinterface
typeAnnotationOffset = this.cpInfoOffsets[this.readUnsignedShort(currentOffset + 1)];
targetType = this.cpInfoOffsets[this.readUnsignedShort(typeAnnotationOffset + 2)];
annotationDescriptor = this.readClass(typeAnnotationOffset, charBuffer);
descriptor = this.readUTF8(targetType, charBuffer);
signature = this.readUTF8(targetType + 2, charBuffer);
if (startPc < 182) {
//如果字節(jié)碼是對字段操作,調(diào)用methodVisitor.visitFieldInsn
methodVisitor.visitFieldInsn(startPc, annotationDescriptor, descriptor, signature);
} else {
//如果字節(jié)碼是對方法操作,則調(diào)用methodVisitor.visitMethodInsn
boolean isInterface = classBuffer[typeAnnotationOffset - 1] == 11;
methodVisitor.visitMethodInsn(startPc, annotationDescriptor, descriptor, signature, isInterface);
}
if (startPc == 185) {
currentOffset += 5;
} else {
currentOffset += 3;
}
break;
...
//對方法棧楨中的最大操作數(shù)棧和最大局部變量表進行賦值
methodVisitor.visitMaxs(maxStack, maxLocals);
}
- 對方法code中的字節(jié)碼進行解析,調(diào)用methodVisitor對應(yīng)方法,最后調(diào)用 methodVisitor.visitMaxs 設(shè)置棧楨數(shù)據(jù),若ClassWriter中設(shè)置了COMPUTE_FRAMES屬性,則visitMaxs設(shè)置無效;
- 綜上:classReader對.class文件讀取調(diào)用accept(ClassVisitor cv)拆分成各個不同部分,將傳遞給cv對應(yīng)方法,其可以負責將各個不同的部分重新組合成一個完整的.class文件;
ClassWriter組合
- 將各個不同的部分重新組合成一個完整的.class文件,如何實現(xiàn)呢?
public class ClassWriter extends ClassVisitor {
public static final int COMPUTE_MAXS = 1;
public static final int COMPUTE_FRAMES = 2;
private int version; //版本號
private final SymbolTable symbolTable; //常量池信息
private int accessFlags; //標識位
private int thisClass; //當前類索引
private int superClass; //當前類父類索引
private int interfaceCount; //接口數(shù)據(jù)
private int[] interfaces;
private FieldWriter firstField; //字段表
private FieldWriter lastField;
private MethodWriter firstMethod; //方法表
private MethodWriter lastMethod;
private Attribute firstAttribute; //屬性表
//通過構(gòu)造函數(shù)封裝為SymbolTable對象:主要是解析類信息中,主要是常量池信息
public ClassWriter(ClassReader classReader, int flags) {
super(589824);
this.symbolTable = classReader == null ? new SymbolTable(this) : new SymbolTable(this, classReader);
}
public final void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
this.version = version;
this.accessFlags = access;
//根據(jù)類名全限定名獲取在常量池中下標
this.thisClass = this.symbolTable.setMajorVersionAndClassName(version & '\uffff', name);
if (signature != null) {
this.signatureIndex = this.symbolTable.addConstantUtf8(signature);
}
this.superClass = superName == null ? 0 : this.symbolTable.addConstantClass(superName).index;
if (interfaces != null && interfaces.length > 0) {
this.interfaceCount = interfaces.length;
this.interfaces = new int[this.interfaceCount];
for(int i = 0; i < this.interfaceCount; ++i) {
this.interfaces[i] = this.symbolTable.addConstantClass(interfaces[i]).index;
}
}
}
//字段及方法通過鏈表連接,由firstField -> lastField; firstMethod -> lastMethod
public final FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
FieldWriter fieldWriter = new FieldWriter(this.symbolTable, access, name, descriptor, signature, value);
if (this.firstField == null) {
this.firstField = fieldWriter;
} else {
this.lastField.fv = fieldWriter;
}
return this.lastField = fieldWriter;
}
public final MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
...
}
//屬性表也是通過鏈表
public final void visitAttribute(Attribute attribute) {
attribute.nextAttribute = this.firstAttribute;
this.firstAttribute = attribute;
}
}
- ClassWriter通過構(gòu)造函數(shù)將傳入的ClassVisitor信息解析封裝為SymbolTable對象并將用到的classFile中數(shù)據(jù)保存為全局變量,字段field,method,Attribute等數(shù)據(jù)均由鏈表表示;
組裝.class文件
- 組裝過程大致分為以下三步:
- 計算byte[]數(shù)組,即class文件大小size;
- 向byte數(shù)組中按照classFile格式添加對應(yīng)元素;
- 將byte[] 數(shù)據(jù)返回;
計算size
- 按照classFile格式分析各個部分占用數(shù)據(jù)大小
public byte[] toByteArray() {
/**
* 24: u4 magic , 10個必須字段u2(minor_version, major_version, constant_pool_count, access_flags,
* this_class , super_class, interfaces_count, fields_count, methods_count, attributes_count)
* 接口字段集合為 u2 * interfaceCount
* 剩余未計算: cp_info , field_info , method_info , attribute_info
*/
int size = 24 + 2 * this.interfaceCount;
int fieldsCount = 0;
/**
* 鏈表計算字段占用大小 field_info
*/
FieldWriter fieldWriter;
for(fieldWriter = this.firstField; fieldWriter != null; fieldWriter = (FieldWriter)fieldWriter.fv) {
++fieldsCount;
size += fieldWriter.computeFieldInfoSize();
}
/**
* 鏈表計算方法占用大小 method_info
*/
int methodsCount = 0;
MethodWriter methodWriter;
for(methodWriter = this.firstMethod; methodWriter != null; methodWriter = (MethodWriter)methodWriter.mv) {
++methodsCount;
size += methodWriter.computeMethodInfoSize();
}
/**
* 計算屬性表占用大小 attribute_info
*/
int attributesCount = 0;
......
if (firstAttribute != null) {
attributesCount += firstAttribute.getAttributeCount();
size += firstAttribute.computeAttributesSize(symbolTable);
}
/**
* 計算常量池占用大小 cp_info
*/
size += this.symbolTable.getConstantPoolLength();
- 計算數(shù)組大小
- 必要位: 由classFile格式可知總計有24個字節(jié),接口數(shù)據(jù)有 2*interfaceCount
- 其他位: 依次計算剩下的常量池,字段,方法,屬性大小
- 匯總以上數(shù)據(jù)獲取.class文件大小
添加數(shù)據(jù)
- 嚴格按照classFile格式添加對應(yīng)數(shù)據(jù)
public byte[] toByteArray() {
...
//創(chuàng)建ByteVector存儲對象,大小為上方計算的size
ByteVector result = new ByteVector(size);
//添加魔數(shù),version int u4:次版本號+主版本號
result.putInt(0xCAFEBABE).putInt(this.version);
/**
* 添加常量池數(shù)據(jù):常量池大小u2 + 常量數(shù)組內(nèi)容大小
* void putConstantPool(ByteVector output) {
output.putShort(this.constantPoolCount).putByteArray(this.constantPool.data, 0,this.constantPool.length);
}
*/
this.symbolTable.putConstantPool(result);
int mask = (version & 0xFFFF) < Opcodes.V1_5 ? Opcodes.ACC_SYNTHETIC : 0;
//添加類標識位,當前類索引,父類索引
result.putShort(this.accessFlags & ~mask).putShort(this.thisClass).putShort(this.superClass);
//添加接口長度
result.putShort(this.interfaceCount);
//添加接口數(shù)組
for(int i = 0; i < this.interfaceCount; ++i) {
result.putShort(this.interfaces[i]);
}
//添加字段長度u2
result.putShort(fieldsCount);
//循環(huán)添加字段信息
for(fieldWriter = this.firstField; fieldWriter != null; fieldWriter = (FieldWriter)fieldWriter.fv) {
fieldWriter.putFieldInfo(result);
}
//添加方法長度
result.putShort(methodsCount);
boolean hasFrames = false;
boolean hasAsmInstructions = false;
for(methodWriter = this.firstMethod; methodWriter != null; methodWriter = (MethodWriter)methodWriter.mv) {
hasFrames |= methodWriter.hasFrames();
hasAsmInstructions |= methodWriter.hasAsmInstructions();
methodWriter.putMethodInfo(result);
}
//添加屬性長度
result.putShort(attributesCount);
···
//添加屬性表
if (this.firstAttribute != null) {
this.firstAttribute.putAttributes(this.symbolTable, result);
}
- 添加數(shù)據(jù):
- 創(chuàng)建大小為size的字節(jié)集合對象ByteVector
- 按照classFile格式由前往后依次添加元素
返回byte數(shù)據(jù)
- 將獲取到的byte返回后重新寫入文件
public byte[] toByteArray() {
...
// Third step: replace the ASM specific instructions, if any.
if (hasAsmInstructions) { //如果有ASM特定說明,需要替換為JVM字節(jié)碼,否則JVM不認識的
return replaceAsmInstructions(result.data, hasFrames);
} else {
return result.data;
}
}
應(yīng)用
- 通過以上分析,我們對ClassReader,ClassWriter,ClassVisitor運行原理有所了解,那有什么應(yīng)用點呢?這里我們解決三個問題:無狀態(tài)轉(zhuǎn)換:查找方法,替換方法,有狀態(tài)轉(zhuǎn)換:獲取Map<String , T>添加key集合
無狀態(tài)轉(zhuǎn)換
- 轉(zhuǎn)換是局部的,不會依賴于在當前指令之前訪問的指令
查找方法
- 給定一個方法包括(類信息,方法名及方法描述符)查找所有調(diào)用的地方,輸出類和方法集合
- 創(chuàng)建自定義MethodFindRefVisitor繼承自ClassVisitor,重寫visitMethod()判斷code中是否調(diào)用了指定查找信息
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
boolean isAbstractMethod = (access & ACC_ABSTRACT) != 0;
boolean isNativeMethod = (access & ACC_NATIVE) != 0;
if (!isAbstractMethod && !isNativeMethod) {
return new MethodFindRefAdaptor(api, null, owner, name, descriptor);
}
return null;
}
- 創(chuàng)建自定義MethodFindRefAdaptor繼承自MethodVisitor,對方法體code進行解析重寫visitMethodInsn判斷是否調(diào)用查找方法
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
// 首先,處理自己的代碼邏輯,判斷當前方法體中調(diào)用了指定查找的方法,則存儲當前類和方法信息
if (methodOwner.equals(owner) && methodName.equals(name) && methodDesc.equals(descriptor)) {
String info = String.format("%s.%s%s", currentMethodOwner, currentMethodName, currentMethodDesc);
if (!resultList.contains(info)) {
resultList.add(info);
}
}
// 其次,調(diào)用父類的方法實現(xiàn)
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
- 在合適時機對獲取數(shù)據(jù)解析輸出打印或文件中
替換方法
-
若想替換掉某一條instruction,應(yīng)該如何實現(xiàn)呢?當然是首先找到該instruction,然后在同樣的位置替換成另一個instruction就可以啦!
- 需要特別注意: 在替換instruction過程中,operand stack(棧楨數(shù)據(jù))在修改前和修改后是一致的
沒有什么比示例更加方便的說明以上operand stack前后保持一致含義
public class HelloWorld {
public void test(int a, int b){
add(a , b); //調(diào)用非靜態(tài)方法
getDesc("HelloWorld"); //調(diào)用靜態(tài)方法
}
private int add(int a , int b){
return a + b;
}
public static String getDesc(String clazzName){
return "current class " + clazzName;
}
}
//輸出字節(jié)碼如下
public void test(int, int);
descriptor: (II)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=3, args_size=3 //局部變量表中位置: 0:this; 1:a ; 2:b
0: aload_0 //加載this到操作數(shù)棧
1: iload_1 //加載a到操作數(shù)棧
2: iload_2 //加載b到操作數(shù)棧
3: invokespecial #2 // Method add:(II)I 調(diào)用非靜態(tài)方法add
6: pop
7: ldc #3 // String HelloWorld
9: invokestatic #4 // Method getDesc:(Ljava/lang/String;)Ljava/lang/String;
12: pop
13: return
//asm代碼如下
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "test", "(II)V", null, null);
methodVisitor.visitCode();
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitVarInsn(ILOAD, 1);
methodVisitor.visitVarInsn(ILOAD, 2);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "sample/HelloWorld", "add", "(II)I", false); //調(diào)用non-static add方法,上面三個visitVarInsn方法是方法所需參數(shù)
methodVisitor.visitInsn(POP);
methodVisitor.visitLdcInsn("HelloWorld");
methodVisitor.visitMethodInsn(INVOKESTATIC, "sample/HelloWorld", "getDesc", "(Ljava/lang/String;)Ljava/lang/String;", false); //調(diào)用static getDesc需要一個參數(shù)
methodVisitor.visitInsn(POP);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(3, 3);
methodVisitor.visitEnd();
}
- Java虛擬機規(guī)范中定義如下:
- invokespecial: 在操作數(shù)棧中obj之后必須跟隨n個參數(shù)值,他們的數(shù)量,數(shù)據(jù)類型和順序都必須與方法描述符保持一致,若調(diào)用不是本地方法,n個參數(shù)值和obj將從操作數(shù)棧中出棧,方法調(diào)用時,將在java虛擬機棧中創(chuàng)建一個新的棧楨,obj和連續(xù)的n個參數(shù)值將存儲到新棧楨的局部變量表中,obj存儲到局部變量表0,n個參數(shù)依次往后,新棧楨創(chuàng)建后就成為當前棧楨,JVM的PC寄存器指向待調(diào)用方法中首條指令,程序就從這里開始繼續(xù)執(zhí)行;
- invokestatic: 在操作數(shù)棧中必須包含連續(xù)n個參數(shù)值,這些參數(shù)的數(shù)量,數(shù)據(jù)類型和順序都必須遵循實例方法的描述符,若調(diào)用不是本地方法,n個參數(shù)值將從操作數(shù)棧中出棧,方法調(diào)用時,將在java虛擬機棧中創(chuàng)建一個新的棧楨,連續(xù)的n個參數(shù)值將存儲到新棧楨的局部變量表中,新棧楨創(chuàng)建后就成為當前棧楨,JVM的PC寄存器指向待調(diào)用方法中首條指令,程序就從這里開始繼續(xù)執(zhí)行;
- invokespecial調(diào)用add方法時會將已經(jīng)加載到操作數(shù)棧上的b , a, this依次出棧消耗掉,因此當我們替換該instruction時的方法也是需要消耗掉操作數(shù)棧上的 b, a, this數(shù)據(jù),否則后續(xù)執(zhí)行可能會報錯;
- invokestatic調(diào)用之前只執(zhí)行了ldc將常量池中3位置加載到操作數(shù)棧后執(zhí)行g(shù)etDesc方法,只消耗了該String對象;
- 使用工具類AnalyzerAdapter 源碼對上方test方法的每行Instruction執(zhí)行時棧楨中局部變量表和操作數(shù)棧數(shù)據(jù)如下
test:(II)V 局部變量表 | 操作數(shù)棧
// {this, int, int} | {}
0000: aload_0 // {this, int, int} | {this}
0001: iload_1 // {this, int, int} | {this, int}
0002: iload_2 // {this, int, int} | {this, int, int}
0003: invokespecial #2 // {this, int, int} | {int} //將操作數(shù)棧上數(shù)據(jù)消耗掉后存儲返回值
0006: pop // {this, int, int} | {}
0007: ldc #3 // {this, int, int} | {String}
0009: invokestatic #4 // {this, int, int} | {String} //消耗掉String后存儲返回值
0012: pop // {this, int, int} | {}
0013: return // {} | {}
- 通過以上觀察,對方法替換分為靜態(tài)方法和非靜態(tài)方法
//自定義MethodReplaceInvokeAdapter extends MethodVisitor替換visitMethodInsn方法
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
if (oldOwner.equals(owner) && oldMethodName.equals(name) && oldMethodDesc.equals(desc)){
//這里是我們自定義替換的方法
super.visitMethodInsn(newOpcode , newOwner , newMethodName , newMethodDesc , false);
}else{
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
}
/**
* 調(diào)用區(qū)別newMethodDesc方法描述符是否需要消耗掉this
*/
//靜態(tài)方法替換,描述符中不添加this
ClassVisitor cv = new MethodReplaceInvokeVisitor(Opcodes.ASM9, cw,
"sample/HelloWorld", "getDesc", "(Ljava/lang/String;)Ljava/lang/String;",
Opcodes.INVOKESTATIC, "com/asm/method/ReplaceMethodManager", "getDesc", "(Ljava/lang/String;)Ljava/lang/String;");
//非靜態(tài)方法替換
ClassVisitor cv = new MethodReplaceInvokeVisitor(Opcodes.ASM9, cw, "sample/HelloWorld", "add", "(II)I", Opcodes.INVOKESTATIC, "com/asm/method/ReplaceMethodManager", "add", "(Lsample/HelloWorld;II)I");
- 由于替換方法由我們定義,一般使用的是靜態(tài)方法newOpcode設(shè)為Opcodes.INVOKESTATIC,最后一個參數(shù)是boolean isInterface,若為true表示調(diào)用為一個接口里的方法,若為false表示調(diào)用為一個類中方法,由于操作我們擁有自主權(quán),一般寫成在一個類里的static方法
有狀態(tài)轉(zhuǎn)換
- The stateful transformation require memorizing some state about the instructions that have been visited before the current one. This requires storing state inside the method adapter 有狀態(tài)轉(zhuǎn)換需要記住一些在當前指令之前訪問過的指令狀態(tài),這需要在方法適配器中存儲狀態(tài)
- 官方示例:
- 刪除指令: 移除ICONST_0 IADD。例如,int d = c + 0;與int d = c;兩者效果是一樣的,所以+ 0的部分可以刪除掉
- 刪除指令: 移除ALOAD_0 ALOAD_0 GETFIELD PUTFIELD。例如,this.val = this.val;,將字段的值賦值給字段本身,無實質(zhì)意義
- 為何說stateless transformation實現(xiàn)起來比較容易,而stateful transformation會實現(xiàn)起來比較困難呢?
- stateless transformation不依賴于其他Instruction,只需關(guān)注自身,因此實現(xiàn)起來比較簡單
- stateful transformation 依賴其他一條或多條Instruction同時判斷,多個指令是一個組合,不能輕易拆散。
- 實現(xiàn)步驟一般分為三步:
- 將問題轉(zhuǎn)換成Instruciton指令,然后對多個指令組合的特征或遵循的模式進行總結(jié);
- 根據(jù)總結(jié)的特征或模式對指令進行識別,在識別的過程中,每一條Instruction的加入都會引起原有狀態(tài)state的變化,這就是stateful的含義;
- 識別成功之后,要對Class文件進行轉(zhuǎn)換,這就對應(yīng)transformation部分,無非就是對Instruction的內(nèi)容進行增刪改等操作;
- 如何根據(jù)識別記錄狀態(tài)(state)變化呢,這里就要用到state machine(狀態(tài)機)
state machine
有限狀態(tài)機(Finite-state machine,FSM),又稱有限狀態(tài)自動機,簡稱狀態(tài)機,是表示有限個狀態(tài)以及在這些狀態(tài)之間的轉(zhuǎn)移和動作等行為的數(shù)學(xué)模型 , FSM是一種算法思想,簡單而言,有限狀態(tài)機由一組狀態(tài)、一個初始狀態(tài)、輸入和根據(jù)輸入及現(xiàn)有狀態(tài)轉(zhuǎn)換為下一個狀態(tài)的轉(zhuǎn)換函數(shù)組成,其作用主要是描述對象在它的生命周期內(nèi)所經(jīng)歷的狀態(tài)序列,以及如何響應(yīng)來自外界的各種事件。
-
對于ASM來說,一般設(shè)定一個統(tǒng)一原始的state machine,這里命名為MethodPatternAdapter類
- class info:MethodPatternAdapter抽象類,繼承自MethodVisitor
- Fields: 其中定義兩個字段,一個常量SEEN_NOTHING表示“初始狀態(tài)” ,一個state字段用于記錄不斷變化的狀態(tài)
- Methods:MethodPatternAdapter類中定義的visitXxxInsn()方法,都會去調(diào)用一個自定義的visitInsn()方法:該方法是一個抽象方法,作用就是讓所有其他狀態(tài)(state)都回歸到“初始狀態(tài)”;
- 創(chuàng)建MethodPatternAdapter抽象類,visitXxxInsn方法中調(diào)用visitInsn
public abstract class MethodPatternAdapter extends MethodVisitor {
protected final static int SEEN_NOTHING = 0; //初始狀態(tài)
protected int state; //記錄狀態(tài)變化
public MethodPatternAdapter(int api, MethodVisitor methodVisitor) {
super(api, methodVisitor);
}
@Override
public void visitLdcInsn(Object value) {
visitInsn();
super.visitLdcInsn(value);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
visitInsn();
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
......
protected abstract void visitInsn();
- 將問題轉(zhuǎn)換成Instruciton指令,然后對多個指令組合的特征或遵循的模式進行總結(jié):如下Hybrid添加action示例中如何獲取action列表
Map<String, Class<? extends RegisteredActionCtrl>> actions = new HashMap<>();
actions.put(CarPublishBackParser.ACTION, CarPublishBackActionCtrl.class);
actions.put(CarPublishGuideParser.ACTION, CarPublishGuideActionCtrl.class);
Hybrid.add(actions);
//轉(zhuǎn)換為asm代碼如下
methodVisitor.visitCode();
methodVisitor.visitTypeInsn(NEW, "java/util/HashMap");
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/util/HashMap", "<init>", "()V", false);
methodVisitor.visitVarInsn(ASTORE, 0);
methodVisitor.visitVarInsn(ALOAD, 0);
//添加第一個
methodVisitor.visitLdcInsn("publish_car_go_back");
methodVisitor.visitLdcInsn(Type.getType("Lcom/wuba/carLib/manager/CarPublishBackActionCtrl;"));
methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", true);
methodVisitor.visitInsn(POP);
methodVisitor.visitVarInsn(ALOAD, 0);
//添加第二個
methodVisitor.visitLdcInsn("show_publish_leadingpage");
methodVisitor.visitLdcInsn(Type.getType("Lcom/wuba/carLib/manager/CarPublishGuideActionCtrl;"));
methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", true);
methodVisitor.visitInsn(POP);
a. 通過以上觀察,對于actions.put方法調(diào)用實際上是三個instruction:visitLdcInsn(key), visitLdcInsn(value),visitMethodInsn
b. 總共調(diào)用三個方法,我們只需要在添加兩個狀態(tài)即可滿足,狀態(tài)轉(zhuǎn)換如下圖所示
//調(diào)用visitLdcInsn(key)后的狀態(tài)
private static final int SEEN_LDCTYPE = 1
//調(diào)用visitLdcInsn(value)后的狀態(tài)
private static final int SEEN_LDC_METHOD = 2

- 狀態(tài)轉(zhuǎn)移清楚后,寫代碼就很輕松了
public class ReadActionMapMethod extends MethodPatternAdapter {
/**
* 需要校驗的字節(jié)碼順序:Map.put()/HashMap.put()
*/
private static final int SEEN_LDCTYPE = 1
private static final int SEEN_LDC_METHOD = 2
//當前存儲業(yè)務(wù)線
private String businessLine
/**
* 當前第一步緩存數(shù)據(jù)
*/
private String mMapKey
/**
* 當前第二步緩存數(shù)據(jù)
*/
private Type mMapValue
protected ReadActionMapMethod(int api, MethodVisitor methodVisitor, String businessLine) {
super(api, methodVisitor)
this.businessLine = businessLine
}
@Override
void visitLdcInsn(Object value) {
switch (state) {
case SEEN_NOTHING:
if (value instanceof String) {
state = SEEN_LDCTYPE
mMapKey = value
mv.visitLdcInsn(value)
return
}
break
case SEEN_LDCTYPE:
if (value instanceof Type) {
state = SEEN_LDC_METHOD
mMapValue = value
mv.visitLdcInsn(value)
return
}
break
}
super.visitLdcInsn(value)
}
@Override
protected void visitInsn() {
// switch (state) {
//
// case SEEN_LDCTYPE:
// //將攔截的數(shù)據(jù)發(fā)送出去
// mv.visitLdcInsn(mMapKey)
// break
//
// case SEEN_LDC_METHOD:
// //將攔截的數(shù)據(jù)發(fā)送出去
// mv.visitLdcInsn(mMapKey)
// mv.visitLdcInsn(mMapValue)
// break
// }
state = SEEN_NOTHING
}
@Override
void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
switch (state) {
case SEEN_LDC_METHOD:
boolean flag = ((opcode == Opcodes.INVOKEVIRTUAL && owner == "java/util/HashMap" && name == "put")
|| (opcode == Opcodes.INVOKEINTERFACE && owner == "java/util/Map" && name == "put"))
if (flag) { //需要存儲數(shù)據(jù)啦
HybridActionManager.install.addAction(businessLine, mMapKey)
}
break
}
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
}
思考:
- 當前只是檢查三個狀態(tài),如何提升準確性?
- 存儲上方獲取的String -> Type對應(yīng)值(類的符號引用),在此掃描獲取Type class文件,解析其是否實現(xiàn)Action接口RegisteredActionCtrl,若實現(xiàn)該接口在添加,提高準確性;若想了解這部分內(nèi)容,請聯(lián)系我提供demo事例中對RN module字段獲取內(nèi)容,先獲取具體類型再根據(jù)是否添加類注解,重寫getName方法等確定字段值;
@Override
protected List<ModuleSpec> createWubaNativeModules(final ReactApplicationContextWrapper reactApplicationContext) {
List<ModuleSpec> moduleSpecList = new ArrayList<ModuleSpec>();
moduleSpecList.add(new ModuleSpec(new Provider<NativeModule>() {
@Override
public NativeModule get() {
return new WBSingleSelector(reactApplicationContext);
}
},WBSingleSelector.class.getName()));
return moduleSpecList;
//解析字節(jié)碼指令:校驗狀態(tài)共計四步
methodVisitor.visitLdcInsn(Type.getType("Lcom/wuba/wubaaction/rn/selector/WBMultiUnlinkSelector;"));
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "com/facebook/react/bridge/ModuleSpec", "<init>", "(Ljavax/inject/Provider;Ljava/lang/String;)V", false);
methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "add", "(Ljava/lang/Object;)Z", true);
//viewModule注入
@Override
protected List<WubaViewManager> createWubaViewManagers(ReactApplicationContextWrapper reactApplicationContext) {
List<WubaViewManager> list = new ArrayList<WubaViewManager>();
list.add(new WBPublishLoadingView());
list.add(new WBErrorView());
return list;
}
//解析字節(jié)碼指令:校驗狀態(tài)共計四步
methodVisitor.visitTypeInsn(NEW, "com/wuba/wubaaction/rn/selector/view/WBPublishLoadingView");
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "com/wuba/wubaaction/rn/selector/view/WBPublishLoadingView", "<init>", "()V", false);
methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "add", "(Ljava/lang/Object;)Z", true);
-
當引用一些業(yè)務(wù)庫中對異常只簡單捕獲并未輸出堆棧信息導(dǎo)致問題查找困難時,也可以嘗試使用ASM對catch()函數(shù)校驗后增加自定義邏輯:輸出堆棧信息或調(diào)用自定義方法等;catch.png
總結(jié)
- ASM操作字節(jié)碼文件,其前提是熟記classFile文件格式,其各個數(shù)據(jù)項嚴格按順序排列,沒有任何分隔符,對照該文件格式在看ASM中的各個操作才能一目了然;
- 若文章對你有幫助,歡迎添加好友一起交流!
參考資料
- 《深入理解Java虛擬機:JVM高級特性與最佳實踐(第3版)》周志明
- ASM4使用中英文手冊 中文版 英文版
- jvm字節(jié)碼指令集
- Oracle: The Java Virtual Machine Specification, Java SE 8 Edition


