ASM也是字節碼編輯庫,如果我們的目的僅僅是為目標類添加某些功能,也可以考慮動態代理,但是動態代理是面向接口的,因為proxy.newinstance實際上是對某個接口定義一個invocaionHandler,那么這樣限制就比較大,並且對代理的每一次函數調用都將被invocationHandler處理,加上handlder中反射的應用,因此動態代理整體來說和直接改變目標class的內部結構來說性能並沒有太多優勢
ASM采用樹這種結構來描述字節碼,通過push模型(訪問者模式)遍歷樹的過程中對字節碼進行修改,ASM提供classReader可以從字節數組或者class文件中去獲得字節碼,如何訪問字節碼中所表示的類結構:
通過調用該類的accept方法傳入一個classVisitor的實例來進行class字節碼的訪問,另一個參數就是解析選項,定義在解析class過程中是否跳過某些區域的解析
接着在accept方法中調用傳入的classsVisitor接口的各個方法,把字節碼中不同的區域想成樹上不同的位置,每個位置都有對應的visitor,我們只需要提供不同的visitor就能訪問字節碼不同偏移位置的所實際代表的成員變量、方法、修飾符等,如下圖所示第一步將解析class源碼
比如可以用ClassVistor、AnnotationVistor、FieldVistor、MethodVistor(都是抽象類)對不同區域做處理,classReader解析到不同位置時,將自動調用這些vistor,這里接用綠盟的一張字節碼解析流程:
其中通過classAdaptor類實現Classvistor中定義的函數,因為處理class是有順序的,因此在聲明classAdaptor時傳入下一個訪問區域的visitor,這里把這種機制成為責任鏈,所以定義責任鏈的時候從后往前聲明
比如:
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassAdaptor delLoginClassAdaptor = new DelLoginClassAdapter(classWriter); ClassAdaptor accessClassAdaptor = new AccessClassAdaptor(delLoginClassAdaptor); ClassReader classReader = new ClassReader(strFileName); classReader.accept(accessClassAdapter, ClassReader.SKIP_DEBUG);
classWrite作為責任鏈的最后一部分,其中每一步的ClassAdaptor都是鏈接起來的(通過轉發ClassVisitor方法的調用),asm提供的toByteArray就能轉為字節數組存入class文件實現hotspot或者直接返回(結合插樁agentmain),如下圖的時序圖,從左到右,就可以看到第一步是ClassReader->ClassReader.accept(av)->av(av是個classAdaptor,接着依次讀取class字節碼文件所構成的那顆樹的不同區域,我們復寫了哪個訪問方法就調用我們的方法去訪問某個區域)->cw(責任鏈最后一步份),整個這樣設計的話我們就不用去管class字節碼文件到底是某個偏移具體是什么含義,只需要根據需要用ClassAdaptor中來重寫ClassVistor的某個方法即可,如果選擇不轉發ClassVisitor的某些方法(也就是想直接刪除類中的某些塊,則可以重寫置空某些方法),比如不轉發visitSource,那么最后ClassWriter就收不到visitSouce所代表的部分:
@Override public void visitSource(String s, String s1) { //直接置空不進行任何操作 }
刪除field或者method:
@Override public MethodVisitor visitMethod(int var1, String var2, String var3, String var4, String[] var5) { return null; }
或者直接想刪除某個類的繼承關系,那么直接重寫visit方法,讓其表示父類的那個參數為null,然后再委托給下一個訪問者
people.java
package asm; public class people { public void eat(){ System.out.println("i like eat"); } public static void main(String[] args) { people w = new people(); w.eat(); } }
drink.java
package asm; public class drink { public static void appale(){ System.out.println("add appale"); } }
如果目前需要在eat方法中添加調用drink.appale的話,則需要修改peope.class對應的字節碼,並到eat對應的方法區修改其方法塊代碼,所以根據上面的學習,要繼承ClassAdaptor重寫其visitMethod方法
其返回的是一個MethodVisitor,默認情況下這里調用this.cv的visitMethod,那么這里的this.cv就是傳入的信任鏈的下一個節點,即傳入的ClassWriter(ClassWriter也是繼承自ClassVistor)
asmtest.java
package asm; import org.objectweb.asm.ClassAdapter; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class asmTest { public static void main(String[] args) throws IOException { FileInputStream fi = new FileInputStream(System.getProperty("user.dir")+"/target/classes/asm/people.class"); byte[] fib = new byte[fi.available()]; fi.read(fib); ClassReader clar = new ClassReader(fib); ClassWriter claw = new ClassWriter(ClassWriter.COMPUTE_MAXS); //ClassWriter.COMPUTE_MAXS表示自動計算局部變量表和操作數棧 ClassAdapter clap = new asmadd(claw); clar.accept(clap,ClassReader.SKIP_DEBUG); byte[] fo = claw.toByteArray(); File file; file = new File("people1.class"); FileOutputStream fof = new FileOutputStream(file); fof.write(fo); fof.close(); } }
那么原始的this.cv.visitMethod實際是來自ClassVisitor,先看該方法的作用即訪問一個類的方法區,該方法返回一個CodeVistor實例或null(返回null,即當前訪問者並不想進一步訪問該方法區的具體代碼塊),這里返回的必須是之前未返回的visitor,因此這樣規定說明實際上責任鏈邏輯上是一定順序的。access:方法的訪問標識符(public protected private etc.),以及當前方法區的名字(有了這個就能精確的匹配並修改某個方法了),desc即方法描述符即返回值類型包括其參數類型都能進行精准匹配,還有exceptions異常和attrs
所以目前就要定義我們自己的ClassAdaptor去匹配eat方法,然后進一步訪問其代碼塊,此時訪問代碼塊使用MethodAdaptor是實現了MethodVistor接口,使用該Adaptor就能重寫visitCode在eat方法中添加代碼塊
由其構造方法看所以此時傳入的也應該是MethodVistor,即已經匹配到的方法,把其當作樹中的某一個父節點,那么此時進入其子節點,通過MethodAdaptor的visitcode就可以訪問方法體,所以在這里面加入調用drink.apple的代碼即可
asmadd.java
package asm; import org.objectweb.asm.ClassAdapter; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; public class asmadd extends ClassAdapter { public asmadd(ClassVisitor cv) { super(cv); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = this.cv.visitMethod(access, name, desc, signature, exceptions); MethodVisitor mvwrapper = mv; if(name.equals("eat")){ mvwrapper = new methodWrapper(mv); } return mvwrapper; } }
methodWrapper.java
package asm; import org.objectweb.asm.MethodAdapter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; public class methodWrapper extends MethodAdapter { public methodWrapper(MethodVisitor mv) { super(mv); } @Override public void visitCode(){ visitMethodInsn(Opcodes.INVOKESTATIC,"drink","eat","()V"); } }
植入代碼塊用的api為visitMethodInsn,即調用某個類的某個方法,比如這里調用的類名為drink,方法名為eat,描述符為void,opcode為invokestatic
因為此時desc是個字符串,因此在Type類中就定義了如何去掃描給定的字符串來判斷目標方法的參數類型以及返回值類型
跟一下整個過程:
首先加載classReader,先從appclassloader到extclassloader中找,再從extclassloader到bootstrapclassloader中找,boot沒找到,ext沒找到,則到app中findclass,利用URLClasspath中存儲的jar包來進行查找,這里就包括maven中引入的class文件,找到之后就defineclass來生成一個class類型的實例了,最后再將其放入保護域中,后面幾個new依然進行相同操作,
直到調用ClassReader的accept方法,里面具體的asm處理過程比如如何掃描指定的()V來確定type貌似跟進不去(不過要是處理過程是對我們可見的,那就可以自己改寫處理過程構造了)
所以按照asm操作指南中說明的來定義,我們要調用的目標eat方法的desc描述包括方法的參數類型和返回類型,參數無,返回為void,所以這里就對應的為()V,其他的情況根據指南直接對應構造即可
ASM指南記錄:
接口ClassVisitor中每個方法都能訪問下圖中class字節碼結構的某一個部分
ClassVisitor中方法的訪問順序:
visit:主要包括class頭部的一些信息
public void visit(int version, //類的版本信息 int access, String name, //類名 String signature, String superName, String[] interfaces)
visitField:訪問某個字段時調用
public FieldVisitor visitField(int access, String name, String desc, String signature, //泛型 Object value)
返回一個Visitor去具體實現對字段的訪問操作(或者為null,跳過)
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) //該方法拋出的異常,也是全限定名
通過MethodVisitor來對方法的字節碼進行訪問
@Override public void visitSource(String s, String s1) { }
總結
核心還是理解好asm處理字節碼的過程以及信任鏈如何構造,然后再根據需求去找asm具體操作的api,對着asm指南編寫代碼。
參考
https://www.ibm.com/developerworks/cn/java/j-lo-asm30/index.html
http://blog.nsfocus.net/rasp-tech/
http://www.blogjava.net/DLevin/archive/2014/06/25/414292.html
https://www.cnblogs.com/liuling/archive/2013/05/25/asm.html
http://www.blogjava.net/DLevin/archive/2014/06/25/414292.html asm處理過程說得很細
https://javadoc.io/doc/org.ow2.asm/asm/5.2/org/objectweb/asm/ClassVisitor.html