tddebug怎么读取asm文件_如何利用 ASM 实现既有方法的增强?
寫在前面
在面向對象編程領域,方法增強有好幾種做法:
繼承:符合開閉原則,簡單地通過繼承類的擴展,實現方法增強,但需通過調用子類方法才能增強;
裝飾:同樣符合開閉原則,繼承的優化方案,相比于繼承更加靈活。裝飾者(Decorator)本身就是某一塊功能增強的組件,可以通過一層一層的裝飾實現漸進式功能增強,既能無限增強也能直接一層到位,這些都是靈活的,開發者可以根據現有的裝飾者組合成自己想要最終對象。典型實現就是 http://java.io 中的 stream;
代理:代理分靜態代理和動態代理。靜態代理比較麻煩了,需要開發者顯式創建代理類,把真實類實例賦給代理類,此時代理類持有了對真實類實例的引用,做到對真實類的增強,而一旦真實類方法簽名發生變化,靜態代理類一樣需要跟著變化,這便是靜態代理的痛點。
而我們今天的重點就是在動態代理(Dynamic Proxy)。
動態代理
動態代理好處不用說,無代碼侵入且更加靈活,其實現方式很多,JDK 的默認實現便是通過實現java.lang.reflect.InvocationHandler#invoke接口方法,進而實現對具體類的方法增強。而功能強大的 CGlib 便是通過 ASM 工具修改字節碼的方式實現的。
本文便基于 ASM 操作,完成一次入門級的演示。
“偷梁換柱”
圖中我們可以看到,正常的類加載過程中,byte code 被加載進內存,走完類驗證以及初始化流程以后,理論上就可以直接執行了。
然而這次,我們需要在 JVM 進程完全啟動之前,監聽類加載事件,替換字節碼。此所謂 “偷梁換柱”。
具體怎么做?
借助 java.lang.instrument.ClassFileTransformer。
javaagent 的 premain 方法是在 main方法執行前執行的,那么我們只需要在 premain 方法里通過 instrumentation 指定transformer 實現對類加載事件的監聽,代碼奉上:
io.libriraries.asm.agent.Agent
public class Agent {public static void premain(String agentArgs, Instrumentation inst) {inst.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {// 只對 io/libriraries/asm/agent/Person 類做方法增強if ("io/libriraries/asm/agent/Person".equals(className)) {System.out.println("io/libriraries/asm/agent/Person transforming...");ClassReader reader = new ClassReader(classfileBuffer);// 要指定 COMPUTE_MAXS 新生成字節碼需要自動計算操作數棧的最大值,否則會報錯ClassWriter writer = new ClassWriter(reader, COMPUTE_MAXS);ClassVisitor cv = new EnhancerAdapter(writer);reader.accept(cv, 0);System.out.println("io/libriraries/asm/agent/Person transformed");// debug 輸出文件到磁盤,方便核查try (FileOutputStream fos = new FileOutputStream("F:Person.class")) {fos.write(writer.toByteArray());} catch (IOException e) {e.printStackTrace();}return writer.toByteArray();}return classfileBuffer;});} }
lambda 表達式部分,實際上就是 ClassFileTransformer 的匿名類,實現其 transform 方法便可以做到對類加載事件的監聽。 在這里我們做了過濾,只對 io/libriraries/asm/agent/Person 這個類做方法增強。
transform 方法的返回值便是 ASM 修改以后的類的二進制流,這部分的二進制流會替代之前原始的二進制流,進入到類加載的流程中,驗證并初始化。
io.libriraries.asm.agent.EnhancerAdapter
/*** 增強適配器*/ class EnhancerAdapter extends ClassVisitor {private final TraceClassVisitor tracer;public EnhancerAdapter(ClassVisitor cv) {super(ASM6, cv);PrintWriter pw = new PrintWriter(System.out);tracer = new TraceClassVisitor(cv, pw);}@Overridepublic MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {final MethodVisitor mv = tracer.visitMethod(access, name, desc, signature, exceptions);if (isIgnore(mv, access, name)) {return mv;}return new EnhancerMethodAdapter(mv, access, name, desc);}@Overridepublic void visitEnd() {System.out.println(tracer.p.getText());super.visitEnd();}/*** 忽略構造方法、類加載初始化方法,final方法和 abstract 方法** @param mv * @param access* @param methodName* @return*/private boolean isIgnore(MethodVisitor mv, int access, String methodName) {return null == mv|| isAbstract(access)|| isFinalMethod(access)|| "<clinit>".equals(methodName)|| "<init>".equals(methodName);}private boolean isAbstract(int access) {return (ACC_ABSTRACT & access) == ACC_ABSTRACT;}private boolean isFinalMethod(int methodAccess) {return (ACC_FINAL & methodAccess) == ACC_FINAL;} }io.libriraries.asm.agent.EnhancerMethodAdapter
/*** 方法級適配器*/ class EnhancerMethodAdapter extends AdviceAdapter {private final String name;protected EnhancerMethodAdapter(MethodVisitor mv, int access, String name, String desc) {super(ASM6, mv, access, name, desc);this.name = name;}/*** 方法前置*/@Overrideprotected void onMethodEnter() {// 前置邏輯 => System.out.println("method : " + name + " invoke start...");mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitLdcInsn("method : " + name + " invoke start...");mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);}/*** 方法后置** @param opcode*/@Overrideprotected void onMethodExit(int opcode) {// 后置邏輯 => System.out.println("method : " + name + " invoke end...");mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitLdcInsn("method : " + name + " invoke end...");mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);super.onMethodExit(opcode);} }ASM 語法很有趣,是基于Visit Design Pattern的。所以你能看到ClassVisitor,MethodVisitor,FieldVisitor甚至AnnotationVisitor這些接口的存在。作為開發者,你只需要根據需求實現它們的方法即可。
實現它們有什么用呢?
答:實現基于事件的回調。
在通過 ClassReader 讀取類二進制流的過程中,ClassReader會根據一定的順序讀取類結構元素,當讀取(visit)到某個元素的時候,會觸發你實現的回調邏輯(也就是上述好幾個visitor的實現方法,由你自己實現),典型例子參考上面的 EnhancerAdapter 。所以通常說 ASM 也是事件驅動的 Event-Based API。
ClassReader 的 visit 順序 是這樣的:
visit [ visitSource ] [ visitOuterClass ] ( visitAnnotation | visitAttribute )* (visitInnerClass | visitField | visitMethod)* visitEnd當然 ClassReader 不能修改、刪除或新增類元素。這里我們要借助 ClassWriter 實現。visit 這個詞也很有意思,在 ASM 的設計上是模棱兩可的,對于 ClassReader 來說,可以理解為訪問、讀取;對于 ClassWriter來說, 就得理解成寫入了。當然這里的寫入并非寫入到class文件,也不是寫入到類加載讀取的 Class 二進制流里,這里僅僅是寫入到 ClassWriter 維護的內存副本。這個副本到最后可以通過 toByteArray() 方法拿到修改后的類字節碼二進制流。
無論 ClassWriter 還是 ClassReader ,他們都是 ClassVisitor的實現類。因此不難理解 visit 本身的多義性。
TraceClassVisitor 在這里是可有可無,不影響邏輯,只是為了方便觀察修改后的字節碼是怎樣的。可以理解 tracer 是 ClassWriter 的代理。
onMethodEnter 和 onMethodExit 分別對應方法的進入和退出,這就和我們之前的動態代理對應上了。ASM 方法增強本質就是在這兩個回調方法里注入邏輯(當然是以字節碼的形式注入啦!)
嗯,這里就涉及到 JVM 字節碼指令問題,回頭我會在一篇文章里整理字節碼的速查表(Cheat Sheet),以備隨時翻查。除此之外還有異常處理的邏輯,不在此篇闡述。
再回頭看上面代碼,是不是容易理解很多呢?
上面的代碼已經完成了必要的邏輯,而我們在使用 premain 時千萬不要忘記在 MANIFEST中填寫這個所屬類的全限定名。
具體我借助了 Maven 插件,指定 io.libriraries.asm.agent.Agent 為Premain Class。
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>2.3.1</version><configuration><archive><manifest><addClasspath>true</addClasspath></manifest><manifestEntries><Premain-Class>io.libriraries.asm.agent.Agent</Premain-Class></manifestEntries></archive></configuration> </plugin>以上,我們的 Agent 端就大功告成了!我們將其打成 jar 包。
這時候我們需要寫個簡單的調用來驗證下這里的方法增強是否成功。
public class Main {public static void main(String[] args) {// p => ASM EnhancerPerson p = new Person();p.doSth();} }執行這個 main 方法的時候要帶上一個 JVM 參數
-javaagent:target/asm-enhance-agent-1.0-SNAPSHOT.jarasm-enhance-agent-1.0-SNAPSHOT.jar 就是剛才打出來的 jar 包。
執行結果
io/libriraries/asm/agent/Person transforming... io/libriraries/asm/agent/Person transformed method : doSth invoke start... this guy is doing sth method : doSth invoke end...done.
小結
ASM 是個很龐大的工具,除了本篇涉及到的仍然有許多 API 需要探索,這里僅僅是九牛一毛。希望本篇有助于初學者打破 ASM 入門的壁壘。
本篇代碼完整奉上:
https://gist.github.com/leonlibraries/7c56db347939866f9513b961088e64d6
參考資料:
《ASM 使用指南》
http://web.cs.ucla.edu/~msb/cs239-tutorial/
https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html
總結
以上是生活随笔為你收集整理的tddebug怎么读取asm文件_如何利用 ASM 实现既有方法的增强?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python源代码最多的学习网站_史上最
- 下一篇: 软件使用说明书模板_想要快速定制表单模板