字節碼編程,Byte-buddy篇三《使用委託實現抽象類方法並注入自定義註解信息》


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

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

一、前言

截至到本章節關於字節碼框架 Byte-buddy 的大部分常用 API 的使用已經通過案例介紹比較全面了,接下來介紹關於如何去實現一個抽象類以及創建出相應註解(包括類的註解和方法的註解)的知識點。而註解的這部分內容在一些監控或者攔截處理的場景下還是比較常用的,所以在這章節我們會通過一個例子來創建出含有自定義註解的類和方法。

如果你已經閱讀了之前的系列文章,這部分學習的內容並不會有太多的陌生,主要是關於委託(MethodDelegation)方法的使用以及補充自定義註解。

那麼,接下來我們就使用委託和註解方式來創建這樣的案例進行學習。

二、開發環境

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

三、案例目標

在這裏我們定義了一個抽象並且含有泛型的接口類,如下;

public abstract class Repository<T> {

    public abstract T queryData(int id);

}

那麼接下來的案例會使用到委託的方式進行實現抽象類方法並加入自定義註解,也就相當於我們使用代碼進行編程實現的效果。

@RpcGatewayClazz( clazzDesc = "查詢數據信息", alias = "dataApi", timeOut = 350L )
public class UserRepository extends Repository<String> {      

    @RpcGatewayMethod( methodName = "queryData", methodDesc = "查詢數據" )
    public String queryData(int var1) {
        // ...
    }

}
  • 這裏就是最終效果,我們模擬是一種網關接口的實現和定義註解暴漏接口信息(如果你是在互聯網中做開發,類似這樣的需求還是蠻多的,接口統一走網關服務)。

四、技術實現

在技術實現的過程中會逐步的去實現我們需要的功能,將需要的用到知識點信息拆開講解,以達到最終的案例目標。

1. 創建自定義註解

模擬網關類註解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RpcGatewayClazz {

    String clazzDesc() default "";
    String alias() default "";
    long timeOut() default 350;

}

模擬網關方法註解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RpcGatewayMethod {

    String methodName() default "";
    String methodDesc() default "";
    
}
  • 這部分你可以創建任何類型的註解,主要是用於模擬類和方法上分別添加註解並獲取最終屬性值的效果。

2. 創建委託函數

public class UserRepositoryInterceptor {

    public static String intercept(@Origin Method method, @AllArguments Object[] arguments) {
        return "小傅哥博客,查詢文章數據:https://bugstack.cn/?id=" + arguments[0];
    }

}
  • 最終我們的字節碼操作會通過委託的方式來實現抽象類的功能。
  • 在委託函數中的用到註解已經在上一章節中完整的介紹了,可以回顧參考。
  • @Origin 可以綁定到以下類型的參數:Method 被調用的原始方法 Constructor 被調用的原始構造器 Class 當前動態創建的類 MethodHandle MethodType String 動態類的toString()的返回值 int 動態方法的修飾符.
  • @AllArguments 綁定所有參數的數組。

3. 創建方法主體信息

// 生成含有註解的泛型實現字類
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
        .subclass(TypeDescription.Generic.Builder.parameterizedType(Repository.class, String.class).build()) // 創建複雜類型的泛型註解
        .name(Repository.class.getPackage().getName().concat(".").concat("UserRepository"))                  // 添加類信息包括地址
        .method(ElementMatchers.named("queryData"))                                                          // 匹配處理的方法
        .intercept(MethodDelegation.to(UserRepositoryInterceptor.class))                                     // 交給委託函數
        .annotateMethod(AnnotationDescription.Builder.ofType(RpcGatewayMethod.class).define("methodName", "queryData").define("methodDesc", "查詢數據").build())
        .annotateType(AnnotationDescription.Builder.ofType(RpcGatewayClazz.class).define("alias", "dataApi").define("clazzDesc", "查詢數據信息").define("timeOut", 350L).build())
        .make();
  • 這部分基本是Byte-buddy的模板方法,通過核心API;subclassnamemethodinterceptannotateMethodannotateType 的使用構建方法。
  • 首先是定義複雜類型的自定義註解,設定爲本方法的父類,這部分內容也就是抽象類。Repository<T>,通過 TypeDescription.Generic.Builder.parameterizedType(Repository.class, String.class).build() 來構建。
  • 設定類名稱在我們之前就已經使用過,這裏多加類的路徑信息。concat 函數是字符串的連接符,替換 + 號。
  • method,設定匹配處理方法名稱。
  • MethodDelegation.to(UserRepositoryInterceptor.class),最終的核心是關於委託函數的使用。這裏的使用也就可以調用到我們上面定義的委託函數,等最終我們通過字節碼生成的 class 類進行查看。
  • annotateMethodannotateType,定義類和方法的註解,通過 define 設定值(可以多次使用)。

4. 將創建的類寫入目錄

// 輸出類信息到目標文件夾下
dynamicType.saveIn(new File(ApiTest.class.getResource("/").getPath()));
  • 這部分內容是 Byte-buddy 提供的 API 方法;saveIn,把字節碼信息寫成 class 到執行的文件夾下。這樣就可以非常方便的驗證通過字節碼框架創建的方法內容。

字節碼方法內容

package org.itstack.demo.bytebuddy;

@RpcGatewayClazz(
    clazzDesc = "查詢數據信息",
    alias = "dataApi",
    timeOut = 350L
)
public class UserRepository extends Repository<String> {
    @RpcGatewayMethod(
        methodName = "queryData",
        methodDesc = "查詢數據"
    )
    public String queryData(int var1) {
        return FindOneInterceptor.intercept(cachedValue$aGmAjHXh$iha1qv0, new Object[]{var1});
    }

    public UserRepository() {
    }

    static {
        cachedValue$aGmAjHXh$iha1qv0 = Repository.class.getMethod("queryData", Integer.TYPE);
    }
}
  • 從上可以看出來我們的自定義類已經實現了抽象類,同時也添加了類和方法的註解信息。
  • 而在實現的類中有一步是使用委託函數進行處理方法的內容。

5. 輸出自定義註解信息

// 從目標文件夾下加載類信息
Class<Repository<String>> repositoryClass = (Class<Repository<String>>) Class.forName("org.itstack.demo.bytebuddy.UserRepository");

// 獲取類註解
RpcGatewayClazz rpcGatewayClazz = repositoryClass.getAnnotation(RpcGatewayClazz.class);
System.out.println("RpcGatewayClazz.clazzDesc:" + rpcGatewayClazz.clazzDesc());
System.out.println("RpcGatewayClazz.alias:" + rpcGatewayClazz.alias());
System.out.println("RpcGatewayClazz.timeOut:" + rpcGatewayClazz.timeOut()); 

// 獲取方法註解
RpcGatewayMethod rpcGatewayMethod = repositoryClass.getMethod("queryData", int.class).getAnnotation(RpcGatewayMethod.class);
System.out.println("RpcGatewayMethod.methodName:" + rpcGatewayMethod.methodName());
System.out.println("RpcGatewayMethod.methodDesc:" + rpcGatewayMethod.methodDesc());
  • 在這裏我們使用的是 Class.forName,進行加載類信息。也可以像以前的章節一樣使用;unloadedType.load(XXX.class.getClassLoader()) 的方式進行直接處理字節碼。
  • 最後是讀取自定義註解的信息內容,包括類和方法。

6. 測試驗證運行

// 實例化對象
Repository<String> repository = repositoryClass.newInstance();
// 測試輸出
System.out.println(repository.queryData(10001));
  • 通過 Class.forName 的方式就可以直接調用方法,如果加載字節碼的方式就需要通過反射進行處理(以往章節有案例可以對照學習)。

測試結果

RpcGatewayClazz.clazzDesc:查詢數據信息
RpcGatewayClazz.alias:dataApi
RpcGatewayClazz.timeOut:350
RpcGatewayMethod.methodName:queryData
RpcGatewayMethod.methodDesc:查詢數據
小傅哥博客,查詢文章數據:https://bugstack.cn/?id=10001

Process finished with exit code 0
  • 不出意外你會看到以上的結果信息,通過我們使用字節碼創建的方法已經可以按照我們的需求進行內容輸出。

五、總結

  • 在本章節的學習中需要注意幾個知識點的使用,包括;委託方法使用複雜類型的泛型創建類和方法自定義註解的添加以及寫入字節碼信息到文件中
  • 截至到目前基本我們已經對常用的字節碼框架自我學習和分享的基本完成了,另外一些其他的API的使用可以參考官方文檔;https://bytebuddy.net
  • 每一段知識都是隻有進行系統化的學習纔能有完整的收穫,隻言片語帶來的碎片化體驗總是不能對一個技術進行全方面的瞭解。在技術的這條路上,多加油!

六、彩蛋

CodeGuide | 程序員編碼指南 Go!

本代碼庫是作者小傅哥多年從事一線互聯網 Java 開發的學習歷程技術彙總,旨在爲大家提供一個清晰詳細的學習教程,側重點更傾向編寫Java核心內容。如果本倉庫能爲您提供幫助,請給予支持(關注、點贊、分享)!

CodeGuide | 程序員編碼指南

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章