Java 核心知識體系:註解機制詳解
1 Java 註解基礎
註解是 JDK1.5 版本開始引入的一個特性,用於對程序代碼的說明,可以對包、類、接口、字段、方法參數、局部變量等進行註解。
它主要的作用有以下四方面:
-
生成 javadoc 文檔,通過在代碼裏面標識元數據生成 javadoc 文檔。
-
編譯期的檢查,通過標識的元數據讓編譯器在編譯期間對代碼進行驗證。
-
編譯時動態處理,編譯時通過代碼中標識的元數據動態處理,比如動態生成代碼。
-
運行時動態處理,運行時通過代碼中標識的元數據動態處理,比如使用反射技術注入實例。
註解的常見分類有三種:
-
Java 自帶的標準註解,包括 @Override、@Deprecated 和 @SuppressWarnings,分別代表 方法重寫、某個類或方法過時、以及忽略警告,用這些註解標明後編譯器就會進行檢查。
-
元註解,元註解是用於定義註解的註解,包括 @Retention、@Target、@Inherited、@Documented 等 6 種
-
@Retention:指定其所修飾的註解的保留策略
-
@Document:該註解是一個標記註解,用於指示一個註解將被文檔化
-
@Target:用來限制註解的使用範圍
-
@Inherited:該註解使父類的註解能被其子類繼承
-
@Repeatable:該註解是 Java8 新增的註解,用於開發重複註解
-
類型註解(Type Annotation):該註解是 Java8 新增的註解,可以用在任何用到類型的地方
-
自定義註解,可以根據自己的需求定義註解,並可用元註解對自定義註解進行註解。
接下來我們通過這三種分類來逐一理解註解。
1.1 Java 內置註解
我們先從 Java 內置註解開始說起,先看下下面的代碼:
class Parent { public void rewriteMethod() {
}
}class Child extends Parent { /**
* 重載父類的 rewriteMethod() 方法
*/
@Override
public void rewriteMethod() {
} /**
* 被棄用的過時方法
*/
@Deprecated
public void oldMethod() {
} /**
* 忽略告警
*
* @return
*/
@SuppressWarnings("keep run") public List infoList() { List list = new ArrayList(); return list;
}
}
Java 1.5 開始自帶的標準註解,包括 @Override、@Deprecated 和 @SuppressWarnings:
-
@Override
:表示當前類中的方法定義將覆蓋父類中的方法 -
@Deprecated
:表示該代碼段被棄用,但是可以使用,只是編譯器會發出警告而已 -
@SuppressWarnings
:表示關閉編譯器的警告信息
我們再具體看下這幾個內置註解,同時通過這幾個內置註解中的元註解的定義來引出元註解。
1.1.1 內置註解 - @Override
我們先來看一下這個註解類型的定義:
@Target(ElementType.METHOD)@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
從它的定義我們可以看到,這個註解可以被用來修飾方法,並且它只在編譯時有效,在編譯後的 class 文件中便不再存在。這個註解的作用我們大家都不陌生,那就是告訴編譯器被修飾的方法是重寫的父類的中的相同簽名的方法,編譯器會對此做出檢查,
若發現父類中不存在這個方法或是存在的方法簽名不同,則會報錯。
1.1.2 內置註解 - @Deprecated
這個註解的定義如下:
@Documented@Retention(RetentionPolicy.RUNTIME)@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
從它的定義我們可以知道,它會被文檔化,能夠保留到運行時,能夠修飾構造方法、屬性、局部變量、方法、包、參數、類型。這個註解的作用是告訴編譯器被修飾的程序元素已被 “廢棄”,不再建議用戶使用。
1.1.3 內置註解 - @SuppressWarnings
這個註解我們也比較常用到,先來看下它的定義:
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings { String[] value();
}
它能夠修飾的程序元素包括類型、屬性、方法、參數、構造器、局部變量,只能存活在源碼時,取值爲 String[]。它的作用是告訴編譯器忽略指定的警告信息,它可以取的值如下所示:
1.2 元註解
上述內置註解的定義中使用了一些元註解(註解類型進行註解的註解類),在 JDK 1.5 中提供了 4 個標準的元註解:@Target,@Retention,@Documented,@Inherited, 在 JDK 1.8 中提供了兩個新的元註解 @Repeatable 和 @Native。
1.2.1 元註解 - @Target
Target 註解的作用是:描述註解的使用範圍(即:被修飾的註解可以用在什麼地方) 。
Target 註解用來說明那些被它所註解的註解類可修飾的對象範圍:
-
packages、types(類、接口、枚舉、註解類)
-
類成員(方法、構造方法、成員變量、枚舉值)
-
方法參數和本地變量(如循環變量、catch 參數)
在定義註解類時使用了 @Target 能夠更加清晰的知道它能夠被用來修飾哪些對象,它的取值範圍定義在 ElementType 枚舉中。枚舉信息如下:
public enum ElementType {
TYPE, // 類、接口、枚舉類
FIELD, // 成員變量(包括:枚舉常量)
METHOD, // 成員方法
PARAMETER, // 方法參數
CONSTRUCTOR, // 構造方法
LOCAL_VARIABLE, // 局部變量
ANNOTATION_TYPE, // 註解類
PACKAGE, // 可用於修飾:包
TYPE_PARAMETER, // 類型參數,JDK 1.8 新增
TYPE_USE // 使用類型的任何地方,JDK 1.8 新增 }
1.2.2 元註解 - @Retention & @RetentionTarget
Reteniton 註解的作用是:描述註解保留的時間範圍(即:被描述的註解在它所修飾的類中可以被保留到何時) 。
Reteniton 註解用來限定那些被它所註解的註解類在註解到其他類上以後,可被保留到何時,一共有三種策略,定義在 RetentionPolicy 枚舉中。枚舉如下:
public enum RetentionPolicy {
SOURCE, // 源文件保留
CLASS, // 編譯期保留,默認爲該值,CLASS
RUNTIME // 運行期保留,可通過反射去獲取註解信息}
我們測試下這三種策略,在定義註解類的時候什麼區別:
@Retention(RetentionPolicy.SOURCE)
public @interface SourcePolicy { // 源文件保留策略}@Retention(RetentionPolicy.CLASS)
public @interface ClassPolicy { // 編譯器保留策略}@Retention(RetentionPolicy.RUNTIME)
public @interface RuntimePolicy { // 運行期保留策略}
上面已經定義好了三個註解類,我們再用這三個註解類再去註解方法,如下:
public class RetentionTest {
@SourcePolicy
public void sourcePolicy() {
}
@ClassPolicy
public void classPolicy() {
}
@RuntimePolicy
public void runtimePolicy() {
}
}
通過執行 javap -verbose RetentionTest 命令獲取到的 RetentionTest 的 class 字節碼內容如下。
{ public retention.RetentionTest();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public void sourcePolicy();
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 7: 0
public void classPolicy();
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 11: 0
RuntimeInvisibleAnnotations:
0: #11()
public void runtimePolicy();
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 15: 0
RuntimeVisibleAnnotations:
0: #14()}
從 RetentionTest 的字節碼內容我們可以得出以下兩點結論:
-
編譯器並沒有記錄下 sourcePolicy() 方法的註解信息
-
編譯器使用 RuntimeInvisibleAnnotations 去記錄 classPolicy() 方法的註解信息
-
編譯器使用 RuntimeVisibleAnnotations 去記錄 runtimePolicy() 方法的註解信息
1.2.3 元註解 - @Documented
Documented 註解的作用如下:使用 javadoc 工具爲類生成幫助文檔,並確認是否保留註解信息。
以下代碼在使用 Javadoc 工具可以生成 @DocAnnotation 註解信息。
import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Target;
@Documented@Target({ElementType.TYPE,ElementType.METHOD})public @interface DocAnnotation {
public String value() default "default";
}
@DocAnnotation("some method doc")public void testMethod() { // 測試方法的文檔註解}
1.2.4 元註解 - @Inherited
Inherited 註解的作用:被它修飾的 Annotation 將具有繼承特性。父類使用了被 @Inherited 修飾的 Annotation,則子類將自動具備該註解。
我們來測試下這個註解:
- 定義 @Inherited 註解:
@Inherited@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE,ElementType.METHOD})
public @interface InheritedAnnotation { String [] values(); int number();
}
- 使用這個註解
@InheritedAnnotation(values = {"brand"}, number = 100)
public class UserInfo {
}class Customer extends UserInfo { @Test
public void testMethod(){ Class clazz = Student.class; Annotation[] annotations = clazz.getAnnotations(); for (Annotation annotation : annotations) { System.out.println(annotation.toString());
}
}
}
- 輸出
xxx.InheritedAnnotation(values=[brand], number=100)
雖然 Customer 類沒有顯示地被註解 @InheritedAnnotation,但是它的父類 UserInfo 被註解,而且 @InheritedAnnotation 被 @Inherited 註解,因此 Customer 類自動繼承註解
1.2.4 元註解 - @Repeatable (Java8)
Repeatable 是可重複使用的意思,允許在同一聲明的類型 (類,屬性,或方法) 中,可以多次使用同一個註解
JDK8 之前要想實現註解重複使用,需要組合模式,編寫和可讀性都不是很好
public @interface Pet { String myPet();
}public @interface Pets { Pet[] value();
}public class RepeatAnnotationOV { @Pets({@Pet(myPet="dog"),@Pet(myPet="cat")})
public void workMethod(){
}
}
由另一個註解來存儲重複註解,在使用時候,用存儲註解 Authorities 來擴展重複註解。
Java 8 中的做法:
@Repeatable(Pets.class)
public @interface Pet { String myPet();
}public @interface Pets { Pet[] value();
}public class RepeatAnnotationNV { @Pet(role="dog") @Pet(role="cat")
public void workMethod(){ }
}
不同的地方是,創建重複註解 Authority 時,加上 @Repeatable, 指向存儲註解 Authorities,在使用時候,直接可以重複使用 Authority 註解。從上面例子看出,java 8 裏面做法更適合常規的思維,可讀性強一點
1.2.5 元註解 - @Native (Java8)
使用 @Native 註解修飾成員變量,則表示這個變量可以被本地代碼引用,常常被代碼生成工具使用。對於 @Native 註解不常使用,瞭解即可
1.3 註解與反射接口
定義註解後,如何獲取註解中的內容呢?反射包 java.lang.reflect 下的 AnnotatedElement 接口提供這些方法。這裏注意:只有註解被定義爲 RUNTIME 後,該註解才能是運行時可見,當 class 文件被裝載時被保存在 class 文件中的 Annotation 纔會被虛擬機讀取。
AnnotatedElement 接口是所有程序元素(Class、Method 和 Constructor)的父接口,所以程序通過反射獲取了某個類的 AnnotatedElement 對象之後,程序就可以調用該對象的方法來訪問 Annotation 信息。我們看下具體的相關接口
-
boolean isAnnotationPresent(Class<?extends Annotation> annotationClass)
判斷該程序元素上是否包含指定類型的註解,存在則返回 true,否則返回 false。注意:此方法會忽略註解對應的註解容器。 -
T getAnnotation(Class annotationClass)
返回該程序元素上存在的、指定類型的註解,如果該類型註解不存在,則返回 null。 -
Annotation[] getAnnotations()
返回該程序元素上存在的所有註解,若沒有註解,返回長度爲 0 的數組。 -
T[] getAnnotationsByType(Class annotationClass)
返回該程序元素上存在的、指定類型的註解數組。沒有註解對應類型的註解時,返回長度爲 0 的數組。該方法的調用者可以隨意修改返回的數組,而不會對其他調用者返回的數組產生任何影響。getAnnotationsByType 方法與 getAnnotation 的區別在於,getAnnotationsByType 會檢測註解對應的重複註解容器。若程序元素爲類,當前類上找不到註解,且該註解爲可繼承的,則會去父類上檢測對應的註解。 -
T getDeclaredAnnotation(Class annotationClass)
返回直接存在於此元素上的所有註解。與此接口中的其他方法不同,該方法將忽略繼承的註釋。如果沒有註釋直接存在於此元素上,則返回 null -
T[] getDeclaredAnnotationsByType(Class annotationClass)
返回直接存在於此元素上的所有註解。與此接口中的其他方法不同,該方法將忽略繼承的註釋 -
Annotation[] getDeclaredAnnotations()
返回直接存在於此元素上的所有註解及註解對應的重複註解容器。與此接口中的其他方法不同,該方法將忽略繼承的註解。如果沒有註釋直接存在於此元素上,則返回長度爲零的一個數組。該方法的調用者可以隨意修改返回的數組,而不會對其他調用者返回的數組產生任何影響。
1.4 自定義註解
當我們理解了內置註解, 元註解和獲取註解的反射接口後,我們便可以開始自定義註解了。這個例子我把上述的知識點全部融入進來, 代碼很簡單:
- 定義自己的註解
package com.helenlyn.common.annotation;import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/**
* <p>Description: 水果供應者註解 </p>
* <p>Copyright: Copyright (c) 2021 </p>
* <p>Company: helenlyn Co., Ltd. </p>
*
* @author brand
* @date 2021/5/16 16:35
* <p>Update Time: </p>
* <p>Updater: </p>
* <p>Update Comments: </p>
*/@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface FruitProvider { /**
* 供應商編號
* @return
*/
public int id() default -1; /**
* 供應商名稱
* @return
*/
public String name() default ""; /**
* 供應商地址
* @return
*/
public String address() default "";
}
- 使用註解
package com.helenlyn.common.dto;import com.helenlyn.common.annotation.FruitColor;import com.helenlyn.common.annotation.FruitName;import com.helenlyn.common.annotation.FruitProvider;/**
* <p>Description: </p>
* <p>Copyright: Copyright (c) 2021 </p>
* <p>Company: helenlyn Co., Ltd. </p>
*
* @author brand
* @date 2021/5/16 16:28
* <p>Update Time: </p>
* <p>Updater: </p>
* <p>Update Comments: </p>
*/public class AppleDto { @FruitName("Apple")
private String appleName; @FruitColor(fruitColor=FruitColor.Color.RED)
private String appleColor; @FruitProvider(id=1,)
private String appleProvider;
}
- 用反射接口獲取註解信息
在 FruitInfoUtil 中進行測試:
/**
* <p>Description: FruitInfoUtil註解實現 </p>
* <p>Copyright: Copyright (c) 2021 </p>
* <p>Company: helenlyn Co., Ltd. </p>
*
* @author brand
* @date 2021/5/16 16:37
* <p>Update Time: </p>
* <p>Updater: </p>
* <p>Update Comments: </p>
*/
public class FruitInfoUtil {
public static String getFruitInfo(Class<?> clazz) {
String strFruitName = " 水果名稱:";
String strFruitColor = " 水果顏色:";
String strFruitProvicer = "供應商信息:";
Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(FruitName.class)) {
FruitName fruitName = (FruitName) field.getAnnotation(FruitName.class);
strFruitName += fruitName.value();
System.out.println(strFruitName);
} else if (field.isAnnotationPresent(FruitColor.class)) {
FruitColor fruitColor = (FruitColor) field.getAnnotation(FruitColor.class);
strFruitColor += fruitColor.fruitColor().toString();
System.out.println(strFruitColor);
} else if (field.isAnnotationPresent(FruitProvider.class)) {
FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class);
strFruitProvicer = " 供應商編號:" + fruitProvider.id() + " 供應商名稱:" + fruitProvider.name() + " 供應商地址:" + fruitProvider.address();
System.out.println(strFruitProvicer);
}
} return String.format("%s;%s;%s;", strFruitName, strFruitColor, strFruitProvicer);
}
}
- 測試的輸出
2022-07-09 11:33:41.688 INFO 5895 --- [TaskExecutor-35] o.s.a.r.c.CachingConnectionFactory : Attempting to connect to: cl-debug-rabbitmq-erp-service-7w0cpa.docker.sdp:9146Hibernate: update UserBasicInfo set personName=? where personCode=?
水果名稱:Apple
水果顏色:RED
供應商編號:1 供應商名稱:helenlyn 貿易公司 供應商地址:福州xx路xxx大樓
2 理解註解的原理
2.1 Java 8 提供了哪些新的註解
-
@Repeatable
-
ElementType.TYPE_USE
-
ElementType.TYPE_PARAMETER
ElementType.TYPE_USE(此類型包括類型聲明和類型參數聲明,是爲了方便設計者進行類型檢查) 包含了 ElementType.TYPE(類、接口(包括註解類型)和枚舉的聲明) 和 ElementType.TYPE_PARAMETER(類型參數聲明), 不妨再看個例子
// 自定義ElementType.TYPE_PARAMETER註解@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE_PARAMETER)public @interface MyNotEmpty {
}// 自定義ElementType.TYPE_USE註解@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE_USE)public @interface MyNotNull {
}// 測試類public class TypeParameterAndTypeUseAnnotation<@MyNotEmpty T>{ //使用TYPE_PARAMETER類型,會編譯不通過// public @MyNotEmpty T test(@MyNotEmpty T a){// new ArrayList<@MyNotEmpty String>();// return a;// }
//使用TYPE_USE類型,編譯通過
public @MyNotNull T test2(@MyNotNull T a){ new ArrayList<@MyNotNull String>(); return a;
}
}
2.2 註解支持繼承嗎?
註解是不支持繼承的
不能使用關鍵字 extends 來繼承某個 @interface,但註解在編譯後,編譯器會自動繼承 java.lang.annotation.Annotation 接口. 雖然反編譯後發現註解繼承了 Annotation 接口,請記住,即使 Java 的接口可以實現多繼承,但定義註解時依然無法使用 extends 關鍵字繼承 @interface。區別於註解的繼承,被註解的子類繼承父類註解可以用 @Inherited:如果某個類使用了被 @Inherited 修飾的 Annotation,則其子類將自動具有該註解。
3 註解的應用場景
自定義註解和 AOP - 通過切面實現解耦
筆者曾經在 《基於 AOP 的動態數據源切換》 這篇文章中有個典型的例子,就是使用 AOP 切面來對多數據源進行使用場景的切換,下面展示下如何通過註解實現解耦的。
- 自定義 Annotation,映射的目標範圍爲 類型和方法。
/**
* @author brand
* @Description: 數據源切換註解
* @Copyright: Copyright (c) 2021
* @Company: Helenlyn, Inc. All Rights Reserved.
* @date 2021/12/15 7:36 下午
*/@Target({ ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface DataSource {
String name() default "";
}
- 編寫 AOP 實現, 切面代碼,以實現對註解的 PointCut, 切點攔截
/**
* @author brand
* @Description:
* @Copyright: Copyright (c) 2021
* @Company: Helenlyn, Inc. All Rights Reserved.
* @date 2021/12/15 7:49 下午
*/@Aspect@Componentpublic class DataSourceAspect implements Ordered { /**
* 定義一個切入點,匹配到上面的註解DataSource
*/
@Pointcut("@annotation(com.helenlyn.dataassist.annotation.DataSource)")
public void dataSourcePointCut() {
} /**
* Around 環繞方式做切面注入
* @param point
* @return
* @throws Throwable
*/
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); DataSource ds = method.getAnnotation(DataSource.class); String routeKey = ds.name(); // 從頭部中取出註解的name(basic 或 cloudoffice 或 attend),用這個name進行數據源查找。
String dataSourceRouteKey = DynamicDataSourceRouteHolder.getDataSourceRouteKey(); if (StringUtils.isNotEmpty(dataSourceRouteKey)) { // StringBuilder currentRouteKey = new StringBuilder(dataSourceRouteKey);
routeKey = ds.name();
}
DynamicDataSourceRouteHolder.setDataSourceRouteKey(routeKey); try { return point.proceed();
} finally { // 最後做清理,這個步驟很重要,因爲我們的配置中有一個默認的數據源,執行完要回到默認的數據源。
DynamicDataSource.clearDataSource();
DynamicDataSourceRouteHolder.clearDataSourceRouteKey();
}
} @Override
public int getOrder() { return 1;
}
}
- 測試,在 Control 中寫三個測試方法
/**
* 無註解默認情況:數據源指向basic
* @return
*/
@RequestMapping(value = "/default/{user_code}", method = RequestMethod.GET)
public UserInfoDto getUserInfo(@PathVariable("user_code") String userCode) { return userInfoService.getUserInfo(userCode);
} /**
* 數據源指向attend
* @return
*/
@DataSource(name= Constant.DATA_SOURCE_ATTEND_NAME)
@RequestMapping(value = "/attend/{user_code}", method = RequestMethod.GET) public UserInfoDto getUserInfoAttend(@PathVariable("user_code") String userCode) { return userInfoService.getUserInfo(userCode);
} /**
* 數據源指向cloud
* @return
*/
@DataSource(name= Constant.DATA_SOURCE_CLOUD_NAME)
@RequestMapping(value = "/cloud/{user_code}", method = RequestMethod.GET) public UserInfoDto getUserInfoCloud(@PathVariable("user_code") String userCode) { return userInfoService.getUserInfo(userCode);
}
- 執行效果
除此之外,我們可以看到很多日誌管理、權限管理,也都是也是通過類似的註解機制來實現的,通過註解 + AOP 來最終實現模塊之間的解耦,以及業務與系統層面的解耦 。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/a03X7hjwVrlUsPQn9o-Ecg