你的開發利器Spring自定義註解

不一樣的科技宅發表於2020-11-12

前言

  自定義註解在開發中是一把利器,經常會被使用到。在上一篇文章中有提到了自定義校驗註解的用法。 然而最近接到這樣一個需求,主要是針對某些介面的返回資料需要進行一個加密操作。於是很自然的就想到了自定義註解+AOP去實現這樣一個功能。但是對於自定義註解,只是停留在表面的使用,沒有做到知其然,而知其所以然。所以這篇文章就是來了解自定義註解這把開發利器的。

什麼是自定義註解?

官方定義

  An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.

Google翻譯一下

  註解是後設資料的一種形式,可以新增到Java原始碼中。 類,方法,變數,引數和包都可以被註釋。 註解對其註釋的程式碼的操作沒有直接影響。

看完這個定義是不是有點摸不到頭腦,不要慌實踐出真知。

建立一個自定義註解

  我們先回顧一下需求的場景,是要針對xx介面的返回資料需要做一個加密操作。之前說到使用自定義註解+AOP來實現這個功能。所以我們先定義一個註解叫Encryption,被Encryption註解修飾後介面,返回的資料要被加密。

public @interface Encryption {
}

  你會發現建立自定義註解,就和建立普通的介面一樣簡單。只是所使用的關鍵字有所不同。在底層實現上,所有定義的註解都會自動繼承java.lang.annotation.Annotation介面。

編寫相應的介面

@Encryption
@GetMapping("/encrypt")
public ResultVo encrypt(){
    return ResultVoUtil.success("不一樣的科技宅");
}

@GetMapping("/normal")
public ResultVo normal(){
    return ResultVoUtil.success("不一樣的科技宅");
}

編寫切面

@Around("@annotation(com.hxh.unified.param.check.annotation.Encryption)")
public ResultVo encryptPoint(ProceedingJoinPoint joinPoint) throws Throwable {
  ResultVo resultVo = (ResultVo) joinPoint.proceed();

  // 獲取註解
  MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
  Method method = methodSignature.getMethod();
  Encryption annotation = method.getAnnotation(Encryption.class);

  // 如果被標識了,則進行加密
  if(annotation != null){
    // 進行加密
    String encrypt = EncryptUtil.encryptByAes(JSON.toJSONString(resultVo.getData()));
    resultVo.setData(encrypt);
  }

  return resultVo;
}

測試結果

  這個時候,你會發現返回的資料並沒有被加密。 那麼這個是為啥呢?俗話說遇到問題不要慌,先掏出手機發個朋友圈(稍微有點跑題了)。出現這個原因是,缺少了@Retention@Encryption的修飾,讓我們把它加上。

@Retention(RetentionPolicy.RUNTIME)
public @interface Encryption {

}

繼續測試

  這個時候返回的資料就被加密了,說明自定義註解生效了。

測試普通介面

  沒有用@Encryption的介面,返回的資料沒有被加密。到此需求就已經實現了,接下來就該瞭解原理了。

@Retention

@Retention作用是什麼

  Retention的翻譯過來就是"保留"的意思。也就意味著它的作用是,用來定義註解的生命週期的,並且在使用時需要指定RetentionPolicyRetentionPolicy有三種策略,分別是:

  • SOURCE - 註解只保留在原始檔,當Java檔案編譯成class檔案的時候,註解被遺棄。
  • CLASS - 註解被保留到class檔案,但jvm載入class檔案時候被遺棄,這是預設的生命週期。
  • RUNTIME - 註解不僅被儲存到class檔案中,jvm載入class檔案之後,仍然存在。

選擇合適的生命週期

  首先要明確生命週期 RUNTIME > CLASS > SOURCE 。一般如果需要在執行時去動態獲取註解資訊,只能使用RUNTIME。如果要在編譯時進行一些預處理操作,比如生成一些輔助程式碼就用CLASS。如果只是做一些檢查性的操作,比如 @Override和@SuppressWarnings,則可選用 SOURCE。

我們實際開發中的自定義註解幾乎都是使用的RUNTIME

  最開始@Encryption沒有使用@Retention對其生命週期進行定義。所以導致AOP在獲取的時候一直為空,如果為空就不會對資料進行加密。

  是不是感覺這個註解太簡陋。那再給他加點東西,加上個@Target

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

}

@Target

  @Target註解是限定自定義註解可以使用在哪些地方。這就和引數校驗一樣,約定好規則,防止亂用而導致問題的出現。針對上述的需求可以限定它只能用方法上。根據不同的場景,還可以使用在更多的地方。比如說屬性、包、構造器上等等。

  • TYPE - 類,介面(包括註解型別)或列舉
  • FIELD - 欄位(包括列舉常量)
  • METHOD - 方法
  • PARAMETER - 引數
  • CONSTRUCTOR - 建構函式
  • LOCAL_VARIABLE - 區域性變數
  • ANNOTATION_TYPE -註解型別
  • PACKAGE - 包
  • TYPE_PARAMETER - 型別引數
  • TYPE_USE - 使用型別

  上面兩個是比較常用的元註解,Java一共提供了4個元註解。你可能會問元註解是什麼?元註解的作用就是負責註解其他註解。

@Documented

  @Documented的作用是對自定義註解進行標註,如果使用@Documented標註了,在生成javadoc的時候就會把@Documented註解給顯示出來。沒什麼實際作用,瞭解一下就好了。

@Inherited

  被@Inherited修飾的註解,被用在父類上時其子類也擁有該註解。 簡單的說就是,當在父類使用了被@Inherited修飾的註解@InheritedTest時,繼承它的子類也擁有@InheritedTest註解。

這個可以單獨講下

註解元素型別

  參照我們在定義介面的經驗,在介面中能定義方法和常量。但是在自定義註解中,只能定義一個東西:註解型別元素Annotation type element

其實可以簡單的理解為只能定義方法,但是和介面中的方法有區別。

定義註解型別元素時需要注意如下幾點:

  • 訪問修飾符必須為public,不寫預設為public。
  • 元素的型別只能是基本資料型別、String、Class、列舉型別、註解型別。
  • type()括號中不能定義方法引數,僅僅只是一個特殊的語法。但是可以通過default關鍵字設定"預設值"。
  • 如果沒有預設值,則使用註解時必須給該型別元素賦值。

繼續改造

  需求這個東西經常都在變動。原本需要加密的介面只使用AES進行加密,後面又告知有些介面要使用DES加密。針對這樣的情況,我們可以在註解內,新增一下配置項,來選擇使用何種方式加密。

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

    /**
     * 加密型別
     */
    String value() default "AES";
  
}

調整介面

@Encryption
@GetMapping("/encrypt")
public ResultVo encrypt(){
    return ResultVoUtil.success("不一樣的科技宅");
}

@Encryption(value = "DES")
@GetMapping("/encryptDes")
public ResultVo encryptDes(){
    return ResultVoUtil.success("不一樣的科技宅");
}

@GetMapping("/normal")
public ResultVo normal(){
    return ResultVoUtil.success("不一樣的科技宅");
}

調整AOP

@Around("@annotation(com.hxh.unified.param.check.annotation.Encryption)")
public ResultVo encryptPoint(ProceedingJoinPoint joinPoint) throws Throwable {
  ResultVo resultVo = (ResultVo) joinPoint.proceed();

  // 獲取註解
  MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
  Method method = methodSignature.getMethod();
  Encryption annotation = method.getAnnotation(Encryption.class);

  // 如果被標識了,則進行加密
  if(annotation != null){
    // 進行加密
    String encrypt = null;
    switch (annotation.value()){
      case "AES":
        encrypt = EncryptUtil.encryptByAes(JSON.toJSONString(resultVo.getData()));
        break;
      case "DES":
        encrypt = EncryptUtil.encryptByDes(JSON.toJSONString(resultVo.getData()));
        break;
      default:
        break;
    }
    resultVo.setData(encrypt);
  }

  return resultVo;
}

  至此就改造完了。可以發現註解元素型別,在使用的時候,操作元素型別像在操作屬性。解析的時候,操作元素型別像在操作方法。

小技巧

  • 當註解沒有註解型別元素,使用時候可直接寫為@Encryption@Encryption()等效。
  • 當註解只有一個註解型別元素,並且命名是value。在使用時@Encryption("DES")@Encryption(value = "DES")等效。

注意的點

  • 需要根據實際情況指定註解的生命週期@Retention
  • 使用@Target來限制註解的使用範圍,防止註解被亂用。
  • 如果註解是配置在方法上的,那麼我們要從Method物件上獲取。如果是配置在屬性上,就需要從該屬性對應的Field物件上去獲取。總之用在哪裡,就去哪裡獲取。

總結

  註解可以理解為就是一個標識。可以在程式程式碼中的關鍵節點上打上這些標識,它不會改變原有程式碼的執行邏輯。然後程式在編譯時或執行時可以檢測到這些標記,在做出相應的操作。結合上面的小場景,可以得出自定義註解使用的基本流程:

  1. 定義註解 --> 根據業務進行建立。
  2. 使用註解 --> 在相應的程式碼中進行使用。
  3. 解析註解 --> 在編譯期或執行時檢測到標記,並進行特殊操作。

上期回顧

結尾

  如果覺得對你有幫助,可以多多評論,多多點贊哦,也可以到我的主頁看看,說不定有你喜歡的文章,也可以隨手點個關注哦,謝謝。

  我是不一樣的科技宅,每天進步一點點,體驗不一樣的生活。我們下期見!

相關文章