ASM字節碼編程 | 如果你只寫CRUD,那這種技術棧你永遠碰不到!!!(ASM字節碼)
小傅哥 | https://bugstack.cn

沉淀、分享、成長,專注于原創專題案例,以最易學習編程的方式分享知識,讓自己和他人都能有所收獲。目前已完成的專題有;Netty4.x實戰專題案例、用Java實現JVM、基于JavaAgent的全鏈路監控、手寫RPC框架、架構設計專題案例、源碼分析、算法學習等。
一、前言
寫這篇文章的時候我在想可能大部分程序員包括你我,常常都在忙于業務開發或奔波在日常維護與修復BUG的路上,當不能從中吸取技術營養與改變現狀后,就像一臺恒定運行的機器,逃不出限定宇宙速度的一個圈里。可能你也會有自己的難處,平時加班太晚沒有時間學習、周末家里瑣事太多沒有精力投入,放假計劃太滿沒有空閑安排。總之,學習就會被擱置。而當一年年的過去后,當自己的年齡與能力不成匹配后又會后悔沒有給多投入一些時間學習成長。
尤其是一線編碼的技術人,除了我們所能看到的在技術框架里(SSM)開發的業務代碼,你是否有遇到過學習瓶頸,而這種瓶頸又是你自己不知道自己不會什么,就像下面這些技術列表里,你有了解多少;
1. javaagent 2. asm 3. jvmti 4. javaassit 5. netty 6. 算法,搜索引擎 7. cglib 8. 混沌工程 9. 中間件開發 10. 高級測試;壓力測試、鏈路測試、流量回放、流量染色 11. 故障系列;突襲、重現、演練 12. 分布式的數據一致性 13. 文件操作;es、hive 14. 注冊中心;zookeeper、Eureka 15. 互聯網工程開發技術棧;spring、mybaits、網關、rpc(thrift, grpc, dubbo)、mq、緩存redis、分庫分表、定時任務、分布式事物、限流、熔斷、降級 16. 數據庫binlog解析 17. 架構設計;DDD領域驅動設計、微服務、服務治理 18. 容器;k8s, docker 19. 分布式存儲;ceph 20. 服務istio 21. 壓測 jmter 22. Jenkins-部署java代碼項目 + ansible 23. 全鏈路監控,分布式追蹤 24. 語音識別、語音合成 26. lvs nginx haproxy iptables 27. hadoop mapreduce hive sqoop hbase flink kylin druid
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
那么!在本公眾號(bugstack蟲洞棧)中,會專門介紹一些高級技術的應用,可能在平時開發中看不到,但是卻一直出現在你的框架中,以某個支撐服務而存在。好,現在開始就搞一下其中的一個技術點 ASM,看看它的真面目。那么學習之前先看下他有什么用途;
類的代理,如cglib
混沌工程
反向工程
結合 javaagent 做到非入侵式監控,方法耗時、日志、機器性能等等
破解
ASM 是一個 Java 字節碼操控框架。它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態改變類行為。Java class 被存儲在嚴格格式定義的 .class 文件里,這些類文件擁有足夠的元數據來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節碼(指令)。ASM 從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據用戶要求生成新類。
為了更方便的學習ASM,我將《ASM4使用手冊》以及一些技術點整理成在線文檔,可以隨時方便查閱(http://asm.itstack.org);
另外關于本文中出現的代碼例子,可以通過在公眾號(bugstack蟲洞棧)內回復,源碼下載獲取。
二、環境配置
jdk 1.8
idea 2019.3.1
asm-commons 6.2.1
三、工程信息
itstack-demo-asm-01:字節碼編程,HelloWorld
itstack-demo-asm-02:字節碼編程,兩數之和
itstack-demo-asm-03:字節碼增強,輸出入參
itstack-demo-asm-04:字節碼增強,調用外部方法
四、HelloWorld還可以這樣寫
你所熟悉的HelloWorld是不這樣;
public class HelloWorld { public static void main(String[] var0) { System.out.println("Hello World"); } }
1
2
3
4
5
那你有嘗試反解析下他的類查看下匯編指令嗎,javap -c HelloWorld
public class org.itstack.demo.test.HelloWorld { public org.itstack.demo.test.HelloWorld(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."
1
2
3
4
5
6
7
8
9
10
11
12
13
14
如果你還感興趣其他指令,可以參考這個字節碼指令表:Go!
好! 以上呢,是我很熟悉的一段代碼了,那么現在我們把這段代碼用ASM方式寫出來;
import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; private static byte[] generate() { ClassWriter classWriter = new ClassWriter(0); // 定義對象頭;版本號、修飾符、全類名、簽名、父類、實現的接口 classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "org/itstack/demo/asm/AsmHelloWorld", null, "java/lang/Object", null); // 添加方法;修飾符、方法名、描述符、簽名、異常 MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); // 執行指令;獲取靜態屬性 methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); // 加載常量 load constant methodVisitor.visitLdcInsn("Hello World"); // 調用方法 methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); // 返回 methodVisitor.visitInsn(Opcodes.RETURN); // 設置操作數棧的深度和局部變量的大小 methodVisitor.visitMaxs(2, 1); // 方法結束 methodVisitor.visitEnd(); // 類完成 classWriter.visitEnd(); // 生成字節數組 return classWriter.toByteArray(); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
以上的代碼,“小朋友,你是否有很多問好???^1024”,其實以上的代碼都是來自于 ASM 框架的代碼,這里面所有的操作與我們使用使用 javap -c XXX 所反解析出的字節碼是一樣的,只不過是反過來使用指令來編寫代碼。
定義一個類的生成 ClassWriter
設定版本、修飾符、全類名、簽名、父類、實現的接口,其實也就是那句;public class HelloWorld
接下來開始創建方法,方法同樣需要設定;修飾符、方法名、描述符等。這里面有幾個固定標識;
類型描述符
| Java 類型 | 類型描述符 |
|:—|:—|
| boolean | Z |
| char | C |
| byte | B |
| short | S |
| int | I |
| float | F |
| long | J |
| double | D |
| Object | Ljava/lang/Object; |
| int[] | [I |
| Object[][] | [[Ljava/lang/Object; |
方法描述符
| 源文件中的方法聲明 | 方法描述符 |
|:—|:—|
| void m(int i, float f) | (IF)V |
| int m(Object o) | (Ljava/lang/Object;)I |
| int[] m(int i, String s) | (ILjava/lang/String;)[I |
| Object m(int[] i) | ([I)Ljava/lang/Object; |
([Ljava/lang/String;)V== void main(String[] args)
執行指令;獲取靜態屬性。主要是獲得 System.out
加載常量 load constant,輸出我們的HelloWorld methodVisitor.visitLdcInsn("Hello World");
最后是調用輸出方法并設置空返回,同時在結尾要設置操作數棧的深度和局部變量的大小
這樣輸出一個 HelloWorld 是不還是蠻有意思的,雖然你可能覺得這編碼起來實在太難了吧,也非常難理解。首先如果你看過我的專欄,用《Java寫一個Jvm虛擬機》,那么你可能會感受到這里面的知識點還是不那么陌生的。另外這里的編寫,ASM還提供了插件,可以方便的讓你開發字節碼。接下來就介紹一下使用方式。
五、有插件的幫助字節碼開發也不是很難
對于新人來說如果用字節碼增強開發一些東西確實挺難,尤其是一些復雜的代碼塊使用字節碼指令操作還是很有難度的。那么,其實也是有簡單辦法就是使用 ASM 插件。這個插件可以很輕松的讓你看到一段代碼的指令碼以及如何用ASM去開發。
安裝插件(ASM Bytecode Outline)
測試使用
是不是看到有插件的幫助下,心里有所激動了,至少寫這樣的東西有了抓手。這樣你就可以很方便的去操作一些增強字節碼的功能了。
六、用字節碼寫出一個兩數之和計算
好!有了上面的插件,也有了一些基礎知識的了解。那么我們開發一個計算兩數之和的方法,之后運行計算結果。
這是我們的目標
public class SumOfTwoNumbers { public int sum(int i, int m) { return i + m; } }
1
2
3
4
5
6
7
使用字節碼編程方式實現
import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; private static byte[] generate() { ClassWriter classWriter = new ClassWriter(0); { MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
上面有兩個括號 {},第一個是用于生成一個空的構造函數
public AsmSumOfTwoNumbers() { }
1
2
接下來的指令就比較簡單了,首先使用 ILOAD進行數值的兩次壓棧也就是弄到操作數棧里去操作,接下來開始執行 IADD,將兩數相加。
最后返回結果 IRETURN,注意是返回的 I 類型。到此這段方法快就實現完成了。反編譯后如下;
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.itstack.demo.asm; public class AsmSumOfTwoNumbers { public AsmSumOfTwoNumbers() { } public int doSum(int var1, int var2) { return var1 + var2; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
執行代碼塊
public static void main(String[] args) throws Exception { // 生成二進制字節碼 byte[] bytes = generate(); // 輸出字節碼 outputClazz(bytes); // 加載AsmSumOfTwoNumbers GenerateSumOfTwoNumbers generateSumOfTwoNumbers = new GenerateSumOfTwoNumbers(); Class> clazz = generateSumOfTwoNumbers.defineClass("org.itstack.demo.asm.AsmSumOfTwoNumbers", bytes, 0, bytes.length); // 反射獲取 main 方法 Method method = clazz.getMethod("sum", int.class, int.class); Object obj = method.invoke(clazz.newInstance(), 6, 2); System.out.println(obj); }
1
2
3
4
5
6
7
8
9
10
11
12
13
這段執行操作和我們在使用 java 的反射操作一樣,也是比較容易的。此時我們是調用了新的字節碼類,同時還將字節碼輸出方便我們查看生成的 class類。
七、在原有方法上字節碼增強監控耗時
到這我們基本了解到通過字節碼編程,可以動態的生成一個類。但是在實際使用的過程中,我們可能有的時候是需要修改一個原有的方法,在開始和結尾添加一些代碼,來監控這個方法的耗時。這也是非侵入式監控的最基本模型。
定義一個方法
public class MyMethod { public String queryUserInfo(String uid) { System.out.println("xxxx"); System.out.println("xxxx"); System.out.println("xxxx"); System.out.println("xxxx"); return uid; } }
1
2
3
4
5
6
7
8
9
10
11
像這個方法插入監控
public class TestMonitor extends ClassLoader { public static void main(String[] args) throws IOException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException { ClassReader cr = new ClassReader(MyMethod.class.getName()); ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS); { MethodVisitor methodVisitor = cw.visitMethod(Opcodes.ACC_PUBLIC, "
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
整體的代碼塊有點大,我們可以分為塊來看,如下;
ClassReader cr = new ClassReader(MyMethod.class.getName());讀取原有類,也是字節碼增強的開始
ClassVisitor cv = new ProfilingClassAdapter(cw, MyMethod.class.getSimpleName());開始增強字節碼
onMethodEnter,onMethodExit,在方法進入和方法退出時添加耗時執行的代碼。
測試結果:
直接運行TestMonitor.java;
access:1 name:
1
2
3
4
5
6
7
8
9
10
11
12
13
八、字節碼控制打印方法的入參
那么除了可以監控方法的執行耗時,還可以將方法的入參信息進行打印出來。這樣就可以在一些異常情況下,看到日志信息。
其他代碼與上面相同,這里只列一下修改的地方
static class ProfilingMethodVisitor extends AdviceAdapter { private String methodName = ""; protected ProfilingMethodVisitor(MethodVisitor methodVisitor, int access, String name, String descriptor) { super(ASM5, methodVisitor, access, name, descriptor); this.methodName = name; } @Override protected void onMethodEnter() { mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitVarInsn(ALOAD, 1); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); } @Override protected void onMethodExit(int opcode) { } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
從這里可以看到,在方法進入時候使用指令碼 GETSTATIC,獲取輸出對象類
接下來使用 ALOAD,從局部變量1中裝載引用類型值入棧
最后輸出入參信息
測試結果:
直接運行TestMonitor.java;
Class> clazz = new TestMonitor().defineClass("org.itstack.demo.asm.MyMethod", bytes, 0, bytes.length); Method queryUserInfo = clazz.getMethod("queryUserInfo", String.class); Object obj = queryUserInfo.invoke(clazz.newInstance(), "10001"); System.out.println("測試結果:" + obj);
1
2
3
4
結果;
access:1 name:
1
2
3
4
5
6
7
8
9
10
10001 就是我們的方法入參
九、用字節碼增強調用外部方法
好!那么執行到這,我們可以想到如果只是將一些信息打印到控制臺還是沒有辦法做業務的,我們需要在這個時候將各種屬性信息調用外部的類,進行發送到服務端。比如使用;mq、日志等。
定義日志信息輸出類
public class MonitorLog { public static void info(String name, int... parameters) { System.out.println("方法:" + name); System.out.println("參數:" + "[" + parameters[0] + "," + parameters[1] + "]"); } }
1
2
3
4
5
6
7
8
這個類主要模擬字節碼增強后,方法調用輸出一些信息
增強字節碼
static class ProfilingMethodVisitor extends AdviceAdapter { private String name; ... @Override protected void onMethodEnter() { // 輸出方法和參數 mv.visitLdcInsn(name); mv.visitInsn(ICONST_2); mv.visitIntInsn(NEWARRAY, T_INT); mv.visitInsn(DUP); mv.visitInsn(ICONST_0); mv.visitVarInsn(ILOAD, 1); mv.visitInsn(IASTORE); mv.visitInsn(DUP); mv.visitInsn(ICONST_1); mv.visitVarInsn(ILOAD, 2); mv.visitInsn(IASTORE); mv.visitMethodInsn(INVOKESTATIC, "org/itstack/demo/asm/MonitorLog", "info", "(Ljava/lang/String;[I)V", false); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
這里的有一部分字節碼操作,其實在增強后最終的效果如下;
public int sum(int i, int m) { Monitor.info("sum", i, m); return i + m; }
1
2
3
4
測試結果:
access:1 name:sum desc:(II)I signature:null ASM類輸出路徑:/E:/itstack/git/github.com/itstack-demo-asm/itstack-demo-asm-05/target/classes/AsmTestMonitor.class 方法:sum 參數:[6,2] 結果:8
1
2
3
4
5
6
7
8
十、總結
高級編程技術的內容還不止于此,不要只為了一時的功能實現,而放棄深挖深究的機會。也許就是你不斷的增強拓展個人的知識技能,才讓你越來越與眾不同。
ASM 這種字節碼編程的應用是非常廣的,但可能確實平時看不到的,因為他都是與其他框架結合一起作為支撐服務使用。像這樣的技術還有很多,比如 javaassit、netty等等。
對于真的要學習一樣技術時,不要只看爽文,但爽文也確實給了你敲門磚。當你要徹底的掌握某個知識的時候,最重要的是成體系的學習!壓榨自己的時間,做有意義的事,是3-7年開發人員最正確的事!
Java 容器
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。