面試官:什么是 Java 注解?
本文轉(zhuǎn)載自微信公眾號(hào)「JavaFish」,作者nasus 。轉(zhuǎn)載本文請(qǐng)聯(lián)系JavaFish公眾號(hào)。
哈嘍,我是狗哥。隨著開發(fā)經(jīng)驗(yàn)的累積,我越發(fā)覺得基礎(chǔ)真的非常重要。比如:大部分框架 (如 Spring) 都使用了注解簡(jiǎn)化代碼并提高編碼的效率,掌握注解是一名 JAVA 程序員必備的技能。
但我發(fā)現(xiàn)很多工作 2、3 年的同學(xué)居然還沒寫過自定義注解,問起注解的原理也是一臉懵。我是很震驚的,你們咋理解代碼的?基于此,今天我們就來(lái)一起學(xué)習(xí)下注解。
國(guó)際慣例,先上腦圖:
01 什么是注解?
Java 注解(Annotation),相信大家沒用過也見過。個(gè)人理解,注解就是代碼中的特殊標(biāo)記,這些標(biāo)記可以在編譯、類加載、運(yùn)行時(shí)被讀取,從而做相對(duì)應(yīng)的處理。
注解跟注釋很像,區(qū)別是注釋是給人看的(想想自己遇到那些半句注釋沒有的業(yè)務(wù)代碼,還是不是很難受?);而注解是給程序看的,它可以被編譯器讀取。
1.1 注解的作用
注解大多時(shí)候與反射或者 AOP 切面結(jié)合使用,它的作用有很多,比如標(biāo)記和檢查,最重要的一點(diǎn)就是簡(jiǎn)化代碼,降低耦合性,提高執(zhí)行效率。比如我司就是通過自定義注解 + AOP 切面結(jié)合,解決了寫接口重復(fù)提交的問題。
簡(jiǎn)單描述下我司防止重復(fù)提交注解的邏輯:請(qǐng)求寫接口提交參數(shù) —— 參數(shù)拼接字符串生成 MD5 編碼 —— 以 MD5 編碼加用戶信息拼接成 key,set Redis 分布式鎖,能獲取到就順利提交(分布式鎖默認(rèn) 3 秒過期),不能獲取就是重復(fù)提交了,報(bào)錯(cuò)。
如果每加一個(gè)寫接口,就要寫一次以上邏輯的話,那程序員會(huì)瘋的。所以,有大佬就使用注解 + AOP 切面的方式解決了這個(gè)問題。只要在寫接口 Controller 方法上加這個(gè)注解即可解決,也方便維護(hù)。
1.2 注解的語(yǔ)法
以我司防止重復(fù)提交的自定義注解,介紹下注解的語(yǔ)法。它的定義如下:
- // 聲明 NoRepeatSubmit 注解
- @Target(ElementType.METHOD) // 元注解
- @Retention(RetentionPolicy.RUNTIME) // 元注解
- public @interface NoRepeatSubmit {
- /**
- * 鎖定時(shí)間,默認(rèn)單位(秒)
- */
- long lockTime() default 3L;
- }
Java 注解使用 @interface 修飾,我司的 NoRepeatSubmit 注解也不例外。此外,還使用兩個(gè)元注解。其中 @Target 注解傳入 ElementType.METHOD 參數(shù)來(lái)標(biāo)明 @NoRepeatSubmit 只能用于方法上,@Retention(RetentionPolicy.RUNTIME) 則用來(lái)表示該注解生存期是運(yùn)行時(shí),從代碼上看注解的定義很像接口的定義,在編譯后也會(huì)生成 NoRepeatSubmit.class 文件。
1.3 注解的元素
定義在注解內(nèi)部的變量,稱之為元素。注解可以有元素,也可以沒有元素。像 @Override 就是無(wú)元素的注解,@SuppressWarnings 就屬于有元素的注解。
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.SOURCE)
- public @interface Override {
- }
- @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
- @Retention(RetentionPolicy.SOURCE)
- public @interface SuppressWarnings {
- String[] value();
- }
帶元素的自定義注解:
- @Target({ElementType.METHOD})
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface NoRepeatSubmit {
- /**
- * 鎖定時(shí)間,默認(rèn)單位(秒)
- */
- long lockTime() default 2L;
- }
1.3.1 注解元素的格式
- // 基本格式
- 數(shù)據(jù)類型 元素名稱();
- // 帶默認(rèn)值
- 數(shù)據(jù)類型 元素名稱() default 默認(rèn)值;
1.3.2 注解元素的數(shù)據(jù)類型
注解元素支持如下數(shù)據(jù)類型:
- 所有基本類型(int,float,boolean,byte,double,char,long,short)
- String
- Class
- enum
- Annotation
- 上述類型的數(shù)組
聲明注解元素時(shí)可以使用基本類型但不允許使用任何包裝類型,同時(shí)注解也可以作為元素的類型,也就是嵌套注解。
1.3.3 編譯器對(duì)元素默認(rèn)值的限制
遵循規(guī)則:
元素要么具有默認(rèn)值,要么在使用注解時(shí)提供元素的值。
對(duì)于非基本類型的元素,無(wú)論是在源代碼中聲明,還是在注解接口中定義默認(rèn)值,都不能以 null 作為值。
1.4 注解的使用
注解是以 @注釋名 的格式在代碼中使用,比如:以下常見的用法。
- public class TestController {
- // NoRepeatSubmit 注解修飾 save 方法,防止重復(fù)提交
- @NoRepeatSubmit
- public static void save(Object o){
- // 保存邏輯
- }
- // 一個(gè)方法上可以有多個(gè)不同的注解
- @Deprecated
- @SuppressWarnings("uncheck")
- public static void getDate(){
- }
- }
在 save 方法上使用 @NoRepeatSubmit (我司自定義注解),加上之后,編譯期會(huì)自動(dòng)識(shí)別該注解并執(zhí)行注解處理器的方法,防止重復(fù)提交;
而對(duì)于 @Deprecated 和 @SuppressWarnings (“uncheck”),則是 Java 的內(nèi)置注解,前者意味著該方法是過時(shí)的,后者則是忽略指定的異常檢查。
02 Java 注解的分類
上面介紹注解的語(yǔ)法和使用,我們遇到了 @Target、@Retention 等沒見過的注解,你可能有點(diǎn)懵。但沒關(guān)系,聽我說(shuō)道說(shuō)道。Java 中有 @Override、@Deprecated 和 @SuppressWarnings 等內(nèi)置注解;也有 @Target、@Retention、@Documented、@Inherited 等修飾注解的注解,稱之為元注解。
2.1 內(nèi)置注解
Java 定義了一套自己的注解,其中作用在代碼上的是:
@Override - 檢查該方法是否是重寫方法。如果發(fā)現(xiàn)其父類,或者是引用的接口中并沒有該方法時(shí),會(huì)報(bào)編譯錯(cuò)誤。
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.SOURCE)
- public @interface Override {
- }
- @Deprecated - 標(biāo)記過時(shí)方法。如果使用該方法,會(huì)報(bào)編譯警告。
- @Documented
- @Retention(RetentionPolicy.RUNTIME)
- @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
- public @interface Deprecated {
- }
- @SuppressWarnings - 用于有選擇的關(guān)閉編譯器對(duì)類、方法、成員變量、變量初始化的警告。
- @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
- @Retention(RetentionPolicy.SOURCE)
- public @interface SuppressWarnings {
- String[] value();
- }
JDK7 之后又加了 3 個(gè),這幾個(gè)的用法,我也用得很少。就不過多介紹了,感興趣的小伙伴自行百度分別是:
- @SafeVarargs - Java 7 開始支持,忽略任何使用參數(shù)為泛型變量的方法或構(gòu)造函數(shù)調(diào)用產(chǎn)生的警告。
- @Documented
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
- public @interface SafeVarargs {}
- @FunctionalInterface - Java 8 開始支持,標(biāo)識(shí)一個(gè)匿名函數(shù)或函數(shù)式接口。
- @Documented
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.TYPE)
- public @interface FunctionalInterface {}
- @Repeatable - Java 8 開始支持,標(biāo)識(shí)某注解可以在同一個(gè)聲明上使用多次。
- @Documented
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.ANNOTATION_TYPE)
- public @interface Repeatable {
- Class<? extends Annotation> value();
- }
2.2 元注解
元注解就是修飾注解的注解,分別有:
2.2.1 @Target
用來(lái)指定注解的作用域(如方法、類或字段),其中 ElementType 是枚舉類型,其定義如下,也代表可能的取值范圍
- public enum ElementType {
- /**標(biāo)明該注解可以作用于類、接口(包括注解類型)或enum聲明*/
- TYPE,
- /** 標(biāo)明該注解可以作用于字段(域)聲明,包括enum實(shí)例 */
- FIELD,
- /** 標(biāo)明該注解可以作用于方法聲明 */
- METHOD,
- /** 標(biāo)明該注解可以作用于參數(shù)聲明 */
- PARAMETER,
- /** 標(biāo)明注解可以作用于構(gòu)造函數(shù)聲明 */
- CONSTRUCTOR,
- /** 標(biāo)明注解可以作用于局部變量聲明 */
- LOCAL_VARIABLE,
- /** 標(biāo)明注解可以作用于注解聲明(應(yīng)用于另一個(gè)注解上)*/
- ANNOTATION_TYPE,
- /** 標(biāo)明注解可以作用于包聲明 */
- PACKAGE,
- /**
- * 標(biāo)明注解可以作用于類型參數(shù)聲明(1.8新加入)
- * @since 1.8
- */
- TYPE_PARAMETER,
- /**
- * 類型使用聲明(1.8新加入)
- * @since 1.8
- */
- TYPE_USE
- }
PS:如果 @Target 無(wú)指定作用域,則默認(rèn)可以作用于任何元素上。等同于:
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
2.2.2 @Retention
用來(lái)指定注解的生命周期,它有三個(gè)值,對(duì)應(yīng) RetentionPolicy 中的三個(gè)枚舉值,分別是:源碼級(jí)別(source),類文件級(jí)別(class)或者運(yùn)行時(shí)級(jí)別(runtime)
- SOURCE:只在源碼中可用
- CLASS:注解在 class 文件中可用,但會(huì)被 VM 丟棄(該類型的注解信息會(huì)保留在源碼里和 class 文件里,在執(zhí)行的時(shí)候,不會(huì)加載到虛擬機(jī)中),PS:當(dāng)注解未定義 Retention 值時(shí),默認(rèn)值是 CLASS,如 Java 內(nèi)置注解,@Override、@Deprecated、@SuppressWarnning 等
- RUNTIME:在源碼,class,運(yùn)行時(shí)均可用,因此可以通過反射機(jī)制讀取注解的信息(源碼、class 文件和執(zhí)行的時(shí)候都有注解的信息),如 SpringMvc 中的 @Controller、@Autowired、@RequestMapping 等。此外,我們自定義的注解也大多在這個(gè)級(jí)別。
2.2.2.1 理解 @Retention
這里引申一下話題,要想理解 @Retention 就要理解下從 java 文件到 class 文件再到 class 被 jvm 加載的過程了。下圖描述了從 .java 文件到編譯為 class 文件的過程:
其中有一個(gè)注解抽象語(yǔ)法樹的環(huán)節(jié),這個(gè)環(huán)節(jié)其實(shí)就是去解析注解然后做相應(yīng)的處理。
所以重點(diǎn)來(lái)了,如果你要在編譯期根據(jù)注解做一些處理,你就需要繼承 Java 的抽象注解處理器 AbstractProcessor,并重寫其中的 process () 方法。
一般來(lái)說(shuō)只要是注解的 @Target 范圍是 SOURCE 或 CLASS,我們就要繼承它;因?yàn)檫@兩個(gè)生命周期級(jí)別的注解等加載到 JVM 后,就會(huì)被抹除了。
比如,lombok 就用 AnnotationProcessor 繼承了 AbstractProcessor,以實(shí)現(xiàn)編譯期的處理。這也是為什么我們使用 @Data 就能實(shí)現(xiàn) get、set 方法的原因。
2.2.3 @Documented
執(zhí)行 javadoc 的時(shí)候,標(biāo)記這些注解是否包含在生成的用戶文檔中。
2.2.4 @Inherited
標(biāo)記這個(gè)注解具有繼承性,比如 A 類被注解 @Table 標(biāo)記,而 @Table 注解被 @Inherited 聲明(具備繼承性);繼承于 A 的子類,也繼承 @Table 注解。
- //聲明 Table 注解,有繼承性
- @Inherited
- @Target(ElementType.TYPE)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface Table {
- }
03 自定義注解
好啦,說(shuō)了這么多理論。大家也聽累了,我也聊累了。那怎么自定義一個(gè)注解并讓它起作用呢?下面我將帶著你們看看我司的防止重復(fù)提交的注解是怎么實(shí)現(xiàn)的?當(dāng)然,由于設(shè)計(jì)內(nèi)部的東西,我只會(huì)寫寫偽代碼。思路在前面介紹過了,為方便閱讀我拿下來(lái),大家理解就行。
需求是:同一用戶,三秒內(nèi)重復(fù)提交一樣的參數(shù),就會(huì)報(bào)異常阻止重復(fù)提交,否則正常提交處理寫請(qǐng)求。
3.1 定義注解
首先,定義注解必須是 @interface 修飾;其次,有四個(gè)考慮的點(diǎn):
- 注解的生命周期 @Retention,一般都是 RUNTIME 運(yùn)行時(shí)。
- 注解的作用域 @Target,作用于寫請(qǐng)求,也就是 controller 方法上。
- 是否需要元素,用分布式鎖實(shí)現(xiàn),必須要有鎖的過期時(shí)間。給定默認(rèn)值,也支持自定義。
- 是否生成 javadoc @Documented,這個(gè)注解無(wú)腦加就對(duì)了。
基于此,我司的防止重復(fù)提交的自定義注解就出來(lái)了:
- @Documented
- @Target({ElementType.METHOD})
- @Retention(RetentionPolicy.RUNTIME)
- public @interface BanReSubmitLock {
- /**
- * 鎖定時(shí)間,默認(rèn)單位(秒)默認(rèn)時(shí)間(3秒)
- */
- long lockTime() default 3L;
- }
3.2 AOP 切面處理
- @Aspect
- @Component
- public class BanRepeatSubmitAop {
- @Autowired
- private final RedisUtils redisUtils;
- @Pointcut("@annotation(com.nasus.framework.web.annotation.BanReSubmitLock)")
- private void banReSubmitLockAop() {
- }
- @Around("banReSubmitLockAop()")
- public Object aroundApi(ProceedingJoinPoint point) throws Throwable {
- // 獲取 AOP 切面方法簽名
- MethodSignature signature = (MethodSignature) point.getSignature();
- // 方法
- Method method = signature.getMethod();
- // 獲取目標(biāo)方法上的 BanRepeatSubmitLock 注解
- BanReSubmitLock banReSubmitLock = method.getAnnotation(BanReSubmitLock.class);
- // 根據(jù)用戶信息以及提交參數(shù),創(chuàng)建 Redis 分布式鎖的 key
- String lockKey = createReSumbitLockKey(point, method);
- // 根據(jù) key 獲取分布式鎖對(duì)象
- Lock lock = redisUtils.getReSumbitLock(lockKey);
- // 上鎖
- boolean result = lock.tryLock();
- // 上鎖失敗,拋異常
- if (!result) {
- throw new Exception("請(qǐng)不要重復(fù)請(qǐng)求");
- }
- // 其他處理
- ...
- }
- /**
- * 生成 key
- */
- private String createReSumbitLockKey(ProceedingJoinPoint point, Method method) {
- // 拼接用戶信息 & 請(qǐng)求參數(shù)
- ...
- // MD5 處理
- ...
- // 返回
- }
- }
可以看到這里利用了 AOP 切面的方式獲取被 @NoReSubmitLock 修飾的方法,并借此拿到切點(diǎn)(被注解修飾方法)的參數(shù)、用戶信息等等,通過 MD5 處理,最終嘗試上鎖。
3.3 使用
- public class TestController {
- // NoReSubmitLock 注解修飾 save 方法,防止重復(fù)提交
- @NoReSubmitLock
- public boolean save(Object o){
- // 保存邏輯
- }
- }
使用也非常簡(jiǎn)單,只需要一個(gè)注解就可以完成大部分的邏輯;如果不用注解,每個(gè)寫接口的方法都要寫一遍防止重復(fù)提交的邏輯的話,代碼非常繁瑣,難以維護(hù)。通過這個(gè)例子相信你也看到了,注解的作用。
04 總結(jié)
本文介紹了注解的作用主要是標(biāo)記、檢查以及解耦;介紹了注解的語(yǔ)法;介紹了注解的元素以及傳值方式;介紹了 Java 的內(nèi)置注解和元注解,最后通過我司的一個(gè)實(shí)際例子,介紹了注解是如何起作用的?
注解是代碼的特殊標(biāo)記,可以在程序編譯、類加載、運(yùn)行時(shí)被讀取并做相關(guān)處理。其對(duì)應(yīng) RetentionPolicy 中的三個(gè)枚舉,其中 SOURCE、CLASS 需要繼承 AbstractProcessor (注解抽象處理器),并實(shí)現(xiàn) process () 方法來(lái)處理我們自定義的注解。而 RUNTIME 級(jí)別是我們常用的級(jí)別,結(jié)合 Java 的反射機(jī)制,可以在很多場(chǎng)景優(yōu)化代碼。
05 參考鏈接
bilibili.com/video/BV1p4411P7V3
mp.weixin.qq.com/s/BPKvLbdCyuWijkD-si75Dw
blog.csdn.net/javazejian/article/details/71860633