如果你只写CRUD,那这种技术栈你永远碰不到
一、前言
寫這篇文章的時候我在想可能大部分程序員包括你我,常常都在忙于業務開發或奔波在日常維護與修復BUG的路上,當不能從中吸取技術營養與改變現狀后,就像一臺恒定運行的機器,逃不出限定宇宙速度的一個圈里。可能你也會有自己的難處,平時加班太晚沒有時間學習、周末家里瑣事太多沒有精力投入,放假計劃太滿沒有空閑安排??傊?#xff0c;學習就會被擱置。而當一年年的過去后,當自己的年齡與能力不成匹配后又會后悔沒有給多投入一些時間學習成長。
尤其是一線編碼的技術人,除了我們所能看到的在技術框架里(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
ASM 是一個 Java 字節碼操控框架。它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態改變類行為。Java class 被存儲在嚴格格式定義的 .class 文件里,這些類文件擁有足夠的元數據來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節碼(指令)。ASM 從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據用戶要求生成新類。
為了更方便的學習ASM,我將《ASM4使用手冊》以及一些技術點整理成在線文檔,可以隨時方便查閱(http://asm.itstack.org);
另外關于本文中出現的代碼例子,可以通過在公眾號(bugstack蟲洞棧)內回復,源碼下載獲取。
二、環境配置
三、工程信息
- 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");} }那你有嘗試反解析下他的類查看下匯編指令嗎,javap -c HelloWorld
public class org.itstack.demo.test.HelloWorld {public org.itstack.demo.test.HelloWorld();Code:0: aload_01: invokespecial #1 ? ? ? ? ? ? ? ? ?// Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: getstatic ? ? #2 ? ? ? ? ? ? ? ? ?// Field java/lang/System.out:Ljava/io/PrintStream;3: ldc ? ? ? ? ? #3 ? ? ? ? ? ? ? ? ?// String Hello World5: invokevirtual #4 ? ? ? ? ? ? ? ? ?// Method java/io/PrintStream.println:(Ljava/lang/String;)V8: return }| 指令 | 描述 |
| getstatic | 獲取靜態字段的值 |
| ldc | 常量池中的常量值入棧 |
| invokevirtual | 運行時方法綁定調用方法 |
| return?? ? | void函數返回 |
如果你還感興趣其他指令,可以參考這個字節碼指令表: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 constantmethodVisitor.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(); }以上的代碼,“小朋友,你是否有很多問好???^1024”,其實以上的代碼都是來自于 ASM 框架的代碼,這里面所有的操作與我們使用使用 javap -c XXX 所反解析出的字節碼是一樣的,只不過是反過來使用指令來編寫代碼。
類型描述符
| 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)
這樣輸出一個 HelloWorld 是不還是蠻有意思的,雖然你可能覺得這編碼起來實在太難了吧,也非常難理解。首先如果你看過我的專欄,用《Java寫一個Jvm虛擬機》,那么你可能會感受到這里面的知識點還是不那么陌生的。另外這里的編寫,ASM還提供了插件,可以方便的讓你開發字節碼。接下來就介紹一下使用方式。
五、有插件的幫助字節碼開發也不是很難
對于新人來說如果用字節碼增強開發一些東西確實挺難,尤其是一些復雜的代碼塊使用字節碼指令操作還是很有難度的。那么,其實也是有簡單辦法就是使用 ASM 插件。這個插件可以很輕松的讓你看到一段代碼的指令碼以及如何用ASM去開發。
安裝插件(ASM Bytecode Outline)
測試使用
是不是看到有插件的幫助下,心里有所激動了,至少寫這樣的東西有了抓手。這樣你就可以很方便的去操作一些增強字節碼的功能了。
六、用字節碼寫出一個兩數之和計算
好!有了上面的插件,也有了一些基礎知識的了解。那么我們開發一個計算兩數之和的方法,之后運行計算結果。
這是我們的目標
public class SumOfTwoNumbers {public int sum(int i, int m) {return i + m;}}使用字節碼編程方式實現
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, "<init>", "()V", null, null);methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);methodVisitor.visitInsn(Opcodes.RETURN);methodVisitor.visitMaxs(1, 1);methodVisitor.visitEnd();}{// 定義對象頭;版本號、修飾符、全類名、簽名、父類、實現的接口classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "org/itstack/demo/asm/AsmSumOfTwoNumbers", null, "java/lang/Object", null);// 添加方法;修飾符、方法名、描述符、簽名、異常MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "sum", "(II)I", null, null);methodVisitor.visitVarInsn(Opcodes.ILOAD, 1);methodVisitor.visitVarInsn(Opcodes.ILOAD, 2);methodVisitor.visitInsn(Opcodes.IADD);// 返回methodVisitor.visitInsn(Opcodes.IRETURN);// 設置操作數棧的深度和局部變量的大小methodVisitor.visitMaxs(2, 3);methodVisitor.visitEnd();}// 類完成classWriter.visitEnd();// 生成字節數組return classWriter.toByteArray(); }上面有兩個括號 {},第一個是用于生成一個空的構造函數
public AsmSumOfTwoNumbers() { }接下來的指令就比較簡單了,首先使用 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;} }執行代碼塊
public static void main(String[] args) throws Exception {// 生成二進制字節碼byte[] bytes = generate();// 輸出字節碼outputClazz(bytes);// 加載AsmSumOfTwoNumbersGenerateSumOfTwoNumbers 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); }這段執行操作和我們在使用 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;}}像這個方法插入監控
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, "<init>", "()V", null, null);methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);methodVisitor.visitInsn(Opcodes.RETURN);methodVisitor.visitMaxs(1, 1);methodVisitor.visitEnd();}ClassVisitor cv = new ProfilingClassAdapter(cw, MyMethod.class.getSimpleName());cr.accept(cv, ClassReader.EXPAND_FRAMES);byte[] bytes = cw.toByteArray();outputClazz(bytes);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);}static class ProfilingClassAdapter extends ClassVisitor {public ProfilingClassAdapter(final ClassVisitor cv, String innerClassName) {super(ASM5, cv);}public MethodVisitor visitMethod(int access,String name,String desc,String signature,String[] exceptions) {System.out.println("access:" + access);System.out.println("name:" + name);System.out.println("desc:" + desc);if (!"queryUserInfo".equals(name)) return null;MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);return new ProfilingMethodVisitor(mv, access, name, desc);}}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;}@Overrideprotected void onMethodEnter() {mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);mv.visitVarInsn(LSTORE, 2);mv.visitVarInsn(ALOAD, 1);}@Overrideprotected void onMethodExit(int opcode) {if ((IRETURN <= opcode && opcode <= RETURN) || opcode == ATHROW) {mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitTypeInsn(NEW, "java/lang/StringBuilder");mv.visitInsn(DUP);mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);mv.visitLdcInsn("方法執行耗時(納秒)->" + methodName+":");mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);mv.visitVarInsn(LLOAD, 2);mv.visitInsn(LSUB);mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);}}}}整體的代碼塊有點大,我們可以分為塊來看,如下;
測試結果:
直接運行TestMonitor.java;
access:1 name:<init> desc:()V access:1 name:queryUserInfo desc:(Ljava/lang/String;)Ljava/lang/String; ASM類輸出路徑:/E:/itstack/git/github.com/itstack-demo-asm/itstack-demo-asm-03/target/classes/AsmTestMonitor.class xxxx xxxx xxxx xxxx 方法執行耗時(納秒)->queryUserInfo:132300 測試結果:10001八、字節碼控制打印方法的入參
那么除了可以監控方法的執行耗時,還可以將方法的入參信息進行打印出來。這樣就可以在一些異常情況下,看到日志信息。
其他代碼與上面相同,這里只列一下修改的地方
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;}@Overrideprotected 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);}@Overrideprotected void onMethodExit(int opcode) {} }從這里可以看到,在方法進入時候使用指令碼 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);結果;
access:1 name:<init> desc:()V access:1 name:queryUserInfo desc:(Ljava/lang/String;)Ljava/lang/String; ASM類輸出路徑:/E:/itstack/git/github.com/itstack-demo-asm/itstack-demo-asm-04/target/classes/AsmTestMonitor.class 10001...10001 就是我們的方法入參
九、用字節碼增強調用外部方法
好!那么執行到這,我們可以想到如果只是將一些信息打印到控制臺還是沒有辦法做業務的,我們需要在這個時候將各種屬性信息調用外部的類,進行發送到服務端。比如使用;mq、日志等。
定義日志信息輸出類
public class MonitorLog {public static void info(String name, int... parameters) {System.out.println("方法:" + name);System.out.println("參數:" + "[" + parameters[0] + "," + parameters[1] + "]");}}這個類主要模擬字節碼增強后,方法調用輸出一些信息
增強字節碼
static class ProfilingMethodVisitor extends AdviceAdapter {private String name;...@Overrideprotected 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);} }這里的有一部分字節碼操作,其實在增強后最終的效果如下;
public int sum(int i, int m) {Monitor.info("sum", i, m);return i + m; }測試結果:
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通過測試內容可以看到,我們已將方法名稱與參數信息打印完整。好!到這我們已經基本入門了 ASM 字節碼編程的大門
十、總結
高級編程技術的內容還不止于此,不要只為了一時的功能實現,而放棄深挖深究的機會。也許就是你不斷的增強拓展個人的知識技能,才讓你越來越與眾不同。
ASM 這種字節碼編程的應用是非常廣的,但可能確實平時看不到的,因為他都是與其他框架結合一起作為支撐服務使用。像這樣的技術還有很多,比如 javaassit、netty等等。
對于真的要學習一樣技術時,不要只看爽文,但爽文也確實給了你敲門磚。當你要徹底的掌握某個知識的時候,最重要的是成體系的學習!壓榨自己的時間,做有意義的事,是3-7年開發人員最正確的事!
————————————————
版權聲明:本文為CSDN博主「小傅哥」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/generalfu/article/details/105110600
總結
以上是生活随笔為你收集整理的如果你只写CRUD,那这种技术栈你永远碰不到的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 什么是Low Code ? 居然能威胁到
- 下一篇: 看看阿里的考核尺度, 阿里人工资高是有原