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

做者:小傅哥
博客:https://bugstack.cn - 彙總原創系列專題文章html

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

1、前言

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

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

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

2、開發環境

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

3、案例目標

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

測試方法學習

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

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

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

4、技術實現

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 獲取的,這樣按照名稱能夠只獲取最須要的註解名稱。

輸出結果:spa

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 執行的操做流程。

輸出結果:rest

指令碼: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 類;code

@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() {
    }
}

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

5、總結

  • 本章節咱們看到字節碼編程不僅能夠像之前使用強大的api去直接編寫代碼,還能夠向方法中添加指令,控制方法。這樣就能夠很是方便的處理一些特殊場景。例如 TryCatch 中的開始位置。
  • 關於 javassist 字節碼編程自己經常使用的方法基本已經覆蓋完成,後續會集合 JavaAgent 作一些案例彙總,將知識點與實際場景進行串聯。
  • 學習終究仍是要成體系的系統化深刻學習,隻言片語有的內容不能很好的造成一個技術棧的閉環,也不利於在項目中實戰。
相關文章
相關標籤/搜索