字節碼編程,Javassist篇五《使用Bytecode指令碼生成含有自定義註解的類和方法》

在這裏插入圖片描述

作者:小傅哥
博客:https://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收穫!

一、前言

到本章爲止已經寫了四篇關於字節碼編程的內容,涉及了大部分的API方法。整體來說對 Javassist 已經有一個基本的使用認知。那麼在 Javassist 中不僅提供了高級 API 用於創建和修改類、方法,還提供了低級 API 控制字節碼指令的方式進行操作類、方法。

有了這樣的 javassist API 在一些特殊場景下就可以使用字節碼指令控制方法。

接下來我們通過字節碼指令模擬一段含有自定義註解的方法修改和生成。在修改的過程中會將原有方法計算息費的返回值替換成 0,最後我們使用這樣的技術去生成一段計算息費的方法。通過這樣的練習學會字節碼操作。

二、開發環境

  1. JDK 1.8.0
  2. javassist 3.12.1.GA
  3. 本章涉及源碼在:itstack-demo-bytecode-1-05,可以關注公衆號bugstack蟲洞棧,回覆源碼下載獲取。你會獲得一個下載鏈接列表,打開后里面的第17個「因爲我有好多開源代碼」,記得給個Star

三、案例目標

  1. 使用指令碼修改原有方法返回值
  2. 使用指令碼生成一樣的方法

測試方法

@RpcGatewayClazz(clazzDesc = "用戶信息查詢服務", alias = "api", timeOut = 500)
public class ApiTest {

    @RpcGatewayMethod(methodDesc = "查詢息費", methodName = "interestFee")
    public double queryInterestFee(String uId){
        return BigDecimal.TEN.doubleValue();  // 模擬息費計算返回
    }

}
  • 這裏使用的註解是測試中自定義的,模擬一個相當於網關接口的暴漏。

四、技術實現

1. 讀取類自定義註解

ClassPool pool = ClassPool.getDefault();
// 類、註解
CtClass ctClass = pool.get(ApiTest.class.getName());
// 通過集合獲取自定義註解
Object[] clazzAnnotations = ctClass.getAnnotations();
RpcGatewayClazz rpcGatewayClazz = (RpcGatewayClazz) clazzAnnotations[0];
System.out.println("RpcGatewayClazz.clazzDesc:" + rpcGatewayClazz.clazzDesc());
System.out.println("RpcGatewayClazz.alias:" + rpcGatewayClazz.alias());
System.out.println("RpcGatewayClazz.timeOut:" + rpcGatewayClazz.timeOut());
  • ctClass.getAnnotations(),可以獲取所有的註解,進行操作

輸出結果:

RpcGatewayClazz.clazzDesc:用戶信息查詢服務
RpcGatewayClazz.alias:api
RpcGatewayClazz.timeOut:500

2. 讀取方法的自定義註解

CtMethod ctMethod = ctClass.getDeclaredMethod("queryInterestFee");
RpcGatewayMethod rpcGatewayMethod = (RpcGatewayMethod) ctMethod.getAnnotation(RpcGatewayMethod.class);
System.out.println("RpcGatewayMethod.methodName:" + rpcGatewayMethod.methodName());
System.out.println("RpcGatewayMethod.methodDesc:" + rpcGatewayMethod.methodDesc());
  • 在讀取方法自定義註解時,通過的是註解的 class 獲取的,這樣按照名稱可以只獲取最需要的註解名稱。

輸出結果:

RpcGatewayMethod.methodName:interestFee
RpcGatewayMethod.methodDesc:查詢息費

3. 讀取方法指令碼

MethodInfo methodInfo = ctMethod.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
CodeIterator iterator = codeAttribute.iterator();
while (iterator.hasNext()) {
    int idx = iterator.next();
    int code = iterator.byteAt(idx);
    System.out.println("指令碼:" + idx + " > " + Mnemonic.OPCODE[code]);
}
  • 這裏的指令碼就是一個方法編譯後在 JVM 執行的操作流程。

輸出結果:

指令碼:0 > getstatic
指令碼:3 > invokevirtual
指令碼:6 > dreturn

4. 通過指令修改方法

ConstPool cp = methodInfo.getConstPool();
Bytecode bytecode = new Bytecode(cp);
bytecode.addDconst(0);
bytecode.addReturn(CtClass.doubleType);
methodInfo.setCodeAttribute(bytecode.toCodeAttribute());
  • addDconst,將 double 型0推送至棧頂
  • addReturn,返回 double 類型的結果

此時的方法的返回值已經被修改,下面的是新的 class 類;

@RpcGatewayClazz(
    clazzDesc = "用戶信息查詢服務",
    alias = "api",
    timeOut = 500L
)
public class ApiTest {
    public ApiTest() {
    }

    @RpcGatewayMethod(
        methodDesc = "查詢息費",
        methodName = "interestFee"
    )
    public double queryInterestFee(String var1) {
        return 0.0D;
    }
}
  • 可以看到查詢息費的返回結果已經是 0.0D。如果你的程序被這樣操作,那麼還是很危險的。所以有時候會進行一些混淆編譯,降低破解風險。

5. 使用指令碼生成方法

5.1 創建基礎方法信息

ClassPool pool = ClassPool.getDefault();
// 創建類信息
CtClass ctClass = pool.makeClass("org.itstack.demo.javassist.HelloWorld");
// 添加方法
CtMethod mainMethod = new CtMethod(CtClass.doubleType, "queryInterestFee", new CtClass[]{pool.get(String.class.getName())}, ctClass);
mainMethod.setModifiers(Modifier.PUBLIC);
MethodInfo methodInfo = mainMethod.getMethodInfo();
ConstPool cp = methodInfo.getConstPool();
  • 創建類和方法的信息在我們幾個章節中也經常使用,主要是創建方法的時候需要傳遞;返回類型、方法名稱、入參類型,以及最終標記方法的可訪問量。

5.2 創建類使用註解

// 類添加註解
AnnotationsAttribute clazzAnnotationsAttribute = new AnnotationsAttribute(cp, AnnotationsAttribute.visibleTag);
Annotation clazzAnnotation = new Annotation("org/itstack/demo/javassist/RpcGatewayClazz", cp);
clazzAnnotation.addMemberValue("clazzDesc", new StringMemberValue("用戶信息查詢服務", cp));
clazzAnnotation.addMemberValue("alias", new StringMemberValue("api", cp));
clazzAnnotation.addMemberValue("timeOut", new LongMemberValue(500L, cp));
clazzAnnotationsAttribute.setAnnotation(clazzAnnotation);
ctClass.getClassFile().addAttribute(clazzAnnotationsAttribute);
  • AnnotationsAttribute,創建自定義註解標籤
  • Annotation,創建實際需要的自定義註解,這裏需要傳遞自定義註解的類路徑
  • addMemberValue,用於添加自定義註解中的值。需要注意不同類型的值 XxxMemberValue 前綴不一樣;StringMemberValueLongMemberValue
  • setAnnotation,最終設置自定義註解。如果不設置,是不能生效的。

5.3 創建方法註解

// 方法添加註解
AnnotationsAttribute methodAnnotationsAttribute = new AnnotationsAttribute(cp, AnnotationsAttribute.visibleTag);
Annotation methodAnnotation = new Annotation("org/itstack/demo/javassist/RpcGatewayMethod", cp);
methodAnnotation.addMemberValue("methodName", new StringMemberValue("查詢息費", cp));
methodAnnotation.addMemberValue("methodDesc", new StringMemberValue("interestFee", cp));
methodAnnotationsAttribute.setAnnotation(methodAnnotation);
methodInfo.addAttribute(methodAnnotationsAttribute);
  • 設置類的註解與設置方法的註解,前面的內容都是一樣的。唯獨需要注意的是方法的註解,需要設置到方法的;addAttribute 上。

5.4 字節碼編寫方法快

// 指令控制
Bytecode bytecode = new Bytecode(cp);
bytecode.addGetstatic("java/math/BigDecimal", "TEN", "Ljava/math/BigDecimal;");
bytecode.addInvokevirtual("java/math/BigDecimal", "doubleValue", "()D");
bytecode.addReturn(CtClass.doubleType);
methodInfo.setCodeAttribute(bytecode.toCodeAttribute());
  • Javassist 中的指令碼通過,Bytecode 的方式進行添加。基本所有的指令你都可以在這裏使用,它有非常強大的 API
  • addGetstatic,獲取指定類的靜態域, 並將其壓入棧頂
  • addInvokevirtual,調用實例方法
  • addReturn,從當前方法返回double
  • 最終講字節碼添加到方法中,也就是會變成方法體。

5.5 添加方法信息並輸出

// 添加方法
ctClass.addMethod(mainMethod);
 
// 輸出類信息到文件夾下
ctClass.writeFile();
  • 這部分內容就比較簡單了,也是我們做 Javassist 字節碼開發常用的內容。添加方法和輸出字節碼編程後的類信息。

5.6 最終創建的類方法

@RpcGatewayClazz(
    clazzDesc = "用戶信息查詢服務",
    alias = "api",
    timeOut = 500L
)
public class HelloWorld {
    @RpcGatewayMethod(
        methodName = "查詢息費",
        methodDesc = "interestFee"
    )
    public double queryInterestFee(String var1) {
        return BigDecimal.TEN.doubleValue();
    }

    public HelloWorld() {
    }
}

字節碼生成含有註解的類和方法

五、總結

  • 本章節我們看到字節碼編程不只可以像以前使用強大的api去直接編寫代碼,還可以向方法中添加指令,控制方法。這樣就可以非常方便的處理一些特殊場景。例如 TryCatch 中的開始位置。
  • 關於 javassist 字節碼編程本身常用的方法基本已經覆蓋完成,後續會集合 JavaAgent 做一些案例彙總,將知識點與實際場景進行串聯。
  • 學習終究還是要成體系的系統化深入學習,隻言片語有的內容不能很好的形成一個技術棧的閉環,也不利於在項目中實戰。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章