Java註解學習與實戰

2022-09-13 21:01:48

背景

為什麼要再次梳理一下java註解,顯而易見,因為重要啊。也是為研究各大類開源框架做鋪墊,只有弄清楚Java註解相關原理,才能看懂大部分框架底層的設計。

緣起

註解也叫做後設資料,是JDK1.5版本開始引入的一個特性,用來對程式碼進行標記說明,可以對包、類、介面、欄位、方法引數、區域性變數等進行註解修飾。其本身不包含任何業務邏輯。
一般註解大類分為三種:

  • JDK自帶的相關注解
  • 自定義的註解
  • 第三方的(例如相關的框架中的註解)

註解三步走:定義、設定、解析

  • 定義:定義標記
  • 設定:把標記打到需要用到的程式碼中
  • 解析:在編譯器或執行時檢測到標記,並進行特殊操作

元註解

什麼是元註解?元註解的作用就是負責註解其他註解。元註解有以下五種:

  • @Retention:指定其所修飾的註解的保留策略
  • @Document:該註解是一個標記註解,用於指示一個註解將被檔案化
  • @Target:用來限制註解的使用範圍
  • @Inherited:該註解使父類別的註解能被其子類繼承
  • @Repeatable:該註解是Java8新增的註解,用於開發重複註解

@Retention註解

用於指定被修飾的註解可以保留多長時間,即指定JVM策略在哪個時間點上刪除當前註解。
目前存在以下三種策略

策略值 功能描述
Retention.SOURCE 註解只在原始檔中保留,在編譯期間刪除
Retention.CLASS 註解只在編譯期間存在於.class檔案中,執行時JVM不可獲取註解資訊,該策略值也是預設值
Retention.RUNTIME 執行時JVM可以獲取註解資訊(反射),是最長註解持續期

@Document註解

@Document註解用於指定被修飾的註解可以被javadoc工具提取成檔案。定義註解類時使用@Document註解進行修飾,則所有使用該註解修飾的程式元素的API檔案中將會包含該註解說明。

@Target註解

@Target註解用來限制註解的使用範圍,即指定被修飾的註解能用於哪些程式單元。標記註解方式如下:@Target({應用型別1, 應用型別2,...})【@Target(ElementType.FIELD)】
列舉值的介紹如下:

列舉值 功能描述
ElementType.Type 可以修飾類、介面、註解或列舉型別
ElementType.FIELD 可以修飾屬性(成員變數),包括列舉常數
ElementType.METHOD 可以修飾方法
ElementType.PAPAMETER 可以修飾引數
ElementType.CONSTRUCTOR 可以修飾構造方法
ElementType.LOCAL_VARIABLE 可以修飾區域性變數
ElementType.ANNOTATION_TYPE 可以修飾註解類
ElementType.PACKAGE 可以修飾包
ElementType.TYPE_PARAMETER JDK8之後的新特性,表示該註解能寫在型別變數的宣告語句中(如,泛型宣告)
ElementType.TYPE_USE JDK8之後的新特性,表示該註解能寫在使用型別的任何語句中(例如:宣告語句、泛型和強制轉換語句中的型別)

@Inherited註解

@Inherited註解指定註解具有繼承性,如果某個註解使用@Inherited進行修飾,則該類使用該註解時,其子類將自動被修飾。
按照以上三步走的流程,咱們這裡來舉例子寫程式碼說明一下:
(1)定義註解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface InheritedExtend {
    String comment();
    int order() default 1;
}

(2)設定:標記打到類上

@InheritedExtend(comment ="註解繼承",order = 2)
public class Base {
}

(3)解析:獲取註解並解析做測試

public class InheritedDemo extends Base{
    public static void main(String[] args) {
        //從本類中獲取父類別註解資訊
        InheritedExtend extend = InheritedDemo.class.getAnnotation(InheritedExtend.class);
        //輸出InheritedExtend註解成員資訊
        System.out.println(extend.comment()+":"+extend.order());
        //列印出InheritedDemo是否類是否具有@InheritedExtend修飾
                          System.out.println(InheritedDemo.class.isAnnotationPresent(InheritedExtend.class));
    }
}

結果輸出:
註解繼承:2 true
以上結果就很好地說明了該註解的繼承性質。

@Repeatable註解

@Repeatable註解是Java8新增的註解,用於開發重複註解。在Java8之前,同一個程式元素前只能使用一個相同型別的註解,如果需要在同一個元素前使用多個相同型別的註解必須通過註解容器來實現。從Java8開始,允許使用多個相同的型別註解來修飾同一個元素,前提是該型別的註解是可重複的,即在定義註解時要用 @Repeatable元註解進行修飾。

(1)定義註解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(AnnolContents.class)
public @interface RepeatableAnnol {
    String name() default "老貓";
    int age();
}

//註解為容器,
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface AnnolContents {
    //定義value成員變數,該成員變數可以接受多個@RepeatableAnnol註解
    RepeatableAnnol[] value();
}

(2)註解使用以及解析

@RepeatableAnnol(name = "張三",age = 12)
@RepeatableAnnol(age = 23)
public class RepeatableAnnolDemo {
    public static void main(String[] args) {
        RepeatableAnnol[] repeatableAnnols = RepeatableAnnolDemo.class.getDeclaredAnnotationsByType(RepeatableAnnol.class);

        for(RepeatableAnnol repeatableAnnol : repeatableAnnols){
            System.out.println(repeatableAnnol.name() + "----->" + repeatableAnnol.age());
        }

        AnnolContents annolContents = RepeatableAnnolDemo.class.getDeclaredAnnotation(AnnolContents.class);
        System.out.println(annolContents);
    }
}

結果輸出:

張三----->12
老貓----->23
@com.ktdaddy.annotation.repeatable.AnnolContents(value={@com.ktdaddy.annotation.repeatable.RepeatableAnnol(name="張三", age=12), @com.ktdaddy.annotation.repeatable.RepeatableAnnol(name="老貓", age=23)})

自定義註解實戰應用

利用註解+springAOP實現系統紀錄檔記錄,主要用於記錄相關的紀錄檔到資料庫,當然,老貓這裡的demo只會到紀錄檔列印層面,至於資料庫落庫儲存有興趣的小夥伴可以進行擴充套件。

以下是maven依賴:

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        
        <dependency>
              <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
        <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.79</version>
        </dependency>
    </dependencies>

註解的定義如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLog {
    String desc() default "";
}

這個地方只是定義了一個欄位,當然大家也可以進行拓展。

接下來,咱們以這個註解作為切點編寫相關的切面程式。具體程式碼如下:

@Aspect
@Component
@Order(0)
public class OperateLogAdvice {

    @Pointcut("@annotation(com.ktdaddy.annotation.OperateLog)")
    public void recordLog(){
    }

    @Around("recordLog()")
    public Object recordLogOne(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("進來了");
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        OperateLog operateLog = methodSignature.getMethod().getAnnotation(OperateLog.class);
        String spELString = operateLog.desc();
        //建立解析器
        SpelExpressionParser parser = new SpelExpressionParser();
        //獲取表示式
        Expression expression = parser.parseExpression(spELString);
        //設定解析上下文(有哪些預留位置,以及每種預留位置的值)
        EvaluationContext context = new StandardEvaluationContext();
        //獲取引數值
        Object[] args = joinPoint.getArgs();
        //獲取執行時引數的名稱
        DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
        String[] parameterNames = discoverer.getParameterNames(method);
        for (int i = 0; i < parameterNames.length; i++) {
            context.setVariable(parameterNames[i],args[i]);
        }
        //解析,獲取替換後的結果
        String result = expression.getValue(context).toString();
        System.out.println(result);
        return joinPoint.proceed();
    }
}

關於切面這塊不多做贅述,非本篇文章的重點。
接下來就可以在我們的程式碼層面使用相關的註解了,具體如下:

@Service
public class UserServiceImpl implements UserService {
    @OperateLog(desc = "#user.desc")
    public void saveUser(User user){
        System.out.println("測試註解...");
    }
}

關於controller層面就省略了,都是比較簡單的。
通過上述切面以及註解解析,我們可以獲取每次傳參的引數內容,並且將相關的紀錄檔進行記錄下來,當然這裡面涉及到了SpEL表示式注入,相關的知識點,小夥伴們可以自行學習。
最終啟動服務,並且請求之後具體的紀錄檔如下。

進來了
這是測試
測試註解...
{"age":12,"desc":"這是測試","id":1,"name":"張三","operator":"操作人"}

至此關於Java註解的回顧學習已經結束,之後咱們再去看一些底層程式碼的時候或許會輕鬆很多。