1、前言
Java EE下用Spring boot框架后,開始面向注解編程,可以說Spring boot就是建立在注解之上。那么什么是注解呢?
-
Java 注解(Annotation)又稱 Java 標注,是JDK5.0引入的一種注釋機制。 注解是元數據的一種形式,提供有關於程序但不屬於程序本身的數據。注解對它們注解的代碼的操作沒有直接影響。
-
Annotation(注解)就是Java提供了一種元程序中的元素關聯任何信息和着任何元數據(metadata)的途徑和方法。Annotion(注解)是一個接口,程序可以通過反射來獲取指定程序元素的Annotion對象,然后通過Annotion對象來獲取注解里面的元數據。
注解可以實現以下功能:
-
生成文檔。這是最常見的,也是java 最早提供的注解。常用的有@param @return 等
-
跟蹤代碼依賴性,實現替代配置文件功能。比如Dagger 2依賴注入,未來java開發,將大量注解配置,具有很大用處;
-
在編譯時進行格式檢查。如@override 放在方法前,如果你這個方法並不是覆蓋了超類方法,則編譯時就能檢查出。
Annotation的成員在Annotation類型中以無參數的方法的形式被聲明。其方法名和返回值定義了該成員的名字和類型。在此有一個特定的默認語法:允許聲明任何Annotation成員的默認值:一個Annotation可以將name=value對作為沒有定義默認值的Annotation成員的值,當然也可以使用name=value對來覆蓋其它成員默認值。這一點有些近似類的繼承特性,父類的構造函數可以作為子類的默認構造函數,但是也可以被子類覆蓋。
Annotation能被用來為某個程序元素(類、方法、成員變量等)關聯任何的信息。需要注意的是,這里存在着一個基本的規則:Annotation不能影響程序代碼的執行,無論增加、刪除 Annotation,代碼都始終如一的執行。
2、注解的本質
所有的注解類型都繼承自這個普通的接口(Annotation),所以注解的本質就是一個繼承了 Annotation 接口的接口。
package java.lang.annotation; public interface Annotation { boolean equals(Object obj); int hashCode(); String toString(); Class<? extends Annotation> annotationType(); }
接口的具體實現類是Java運行時動態生成的代理類,通過反射獲取注解的時候,返回的是Java運行時動態生成的代理對象,通過代理對象調用自定義注解(接口)的方法,會最終調用AnnotationInvocationHandler 的invoke方法。該方法會從memberValues這個Map中索引出對應的值,而memberValues的來源是Java 常量池。
一個注解准確意義上來說,只不過是一種特殊的注釋而已,如果沒有解析它的代碼,它可能連注釋都不如。使用注解的過程中,很重要的一部分就是創建使用注解處理器。
那注解用來注釋什么呢?類,成員函數,成員屬性,接口!注釋一旦完成,再結合Java的反射特性,運行的時候就可以動態的獲取到注解的標注信息,然后執行其他的邏輯,例如面向切面編程等等。
解析一個類或者方法的注解往往有兩種形式,一種是編譯期直接的掃描,一種是運行期反射。
編譯期的掃描指的是編譯器在對 java代碼編譯字節碼的過程中會檢測到某個類或者方法被一些注解修飾,這時它就會對於這些注解進行某些處理。典型的就是注解 @Override,一旦編譯器檢測到某個方法被修飾了 @Override 注解,編譯器就會檢查當前方法的方法簽名是否真正重寫了父類的某個方法,也就是比較父類中是否具有一個同樣的方法簽名。這一種情況只適用於那些編譯器已經熟知的注解類,比如 JDK 內置的幾個注解,而對於自定義的注解,編譯器是不知道這個注解的作用的,當然也不知道該如何處理,往往只是會根據該注解的作用范圍來選擇是否編譯進字節碼文件,僅此而已。
區分Annotation和Annotation類型:
Annotation:Annotation使用了在java5.0所帶來的新語法,它的行為十分類似public、final這樣的修飾符。每個Annotation具有一個名字和成員個數>=0。每個Annotation的成員具有被稱為name=value對的名字和值(就像javabean一樣),name=value裝載了Annotation的信息。
Annotation類型:Annotation類型定義了Annotation的名字、類型、成員默認值。一個Annotation類型可以說是一個特殊的java接口,它的成員變量是受限制的,而聲明Annotation類型時需要使用新語法。當我們通過java反射api訪問Annotation時,返回值將是一個實現了該annotation類型接口的對象,通過訪問這個對象我們能方便的訪問到其Annotation成員。
3、注解處理器類庫(java.lang.reflect.AnnotatedElement)
如果沒有用來讀取注解的方法和工作,那么注解也就不會比注釋更有用處了。使用注解的過程中,很重要的一部分就是創建於使用注解處理器。Java SE5擴展了反射機制的API,以幫助程序員快速的構造自定義注解處理器。Java使用Annotation接口來代表程序元素前面的注解,該接口是所有Annotation類型的父接口。除此之外,Java在java.lang.reflect 包下新增了AnnotatedElement接口,該接口代表程序中可以接受注解的程序元素,該接口主要有如下幾個實現類:
-
Class:類定義
-
Constructor:構造器定義
-
Field:累的成員變量定義
-
Method:類的方法定義
-
Package:類的包定義
java.lang.reflect包下主要包含一些實現反射功能的工具類,實際上,java.lang.reflect 包所有提供的反射API擴充了讀取運行時Annotation信息的能力。當一個Annotation類型被定義為運行時的Annotation后,該注解才能是運行時可見,當class文件被裝載時被保存在class文件中的Annotation才會被虛擬機讀取。AnnotatedElement接口是所有程序元素(Class、Method、Constructor、Field和Package)的父接口,所以程序通過反射獲取了某個類的AnnotatedElement對象之后,程序就可以調用該對象的如下四個個方法來訪問Annotation信息:
方法1:<T extends Annotation> T getAnnotation(Class<T> annotationClass): 返回改程序元素上存在的、指定類型的注解,如果該類型注解不存在,則返回null。
方法2:Annotation[] getAnnotations():返回該程序元素上存在的所有注解。
方法3:boolean is AnnotationPresent(Class<?extends Annotation> annotationClass):判斷該程序元素上是否包含指定類型的注解,存在則返回true,否則返回false.
方法4:Annotation[] getDeclaredAnnotations():返回直接存在於此元素上的所有注釋。與此接口中的其他方法不同,該方法將忽略繼承的注釋。(如果沒有注釋直接存在於此元素上,則返回長度為零的一個數組。)該方法的調用者可以隨意修改返回的數組;這不會對其他調用者返回的數組產生任何影響。
4、元注解
在定義注解時,注解類也能夠使用其他的注解聲明。對注解類型進行注解的注解類,我們稱之為 meta-annotation(元注解)。一般的,我們在定義自定義注解時,需要指定的元注解有兩個 :@Target和@Retention。(另外@Documented 與 @Inherited 元注解,前者用於被javadoc工具提取成文檔,后者表示允許子類繼承父類中定義的注解。)
4.1、什么是元注解
java.lang.annotation 提供了四種元注解,專門注解其他的注解(在自定義注解的時候,需要使用到元注解):
-
@Target – 注解用於什么地方
-
@Retention – 什么時候使用該注解
-
@Documented – 注解是否將包含在JavaDoc中
-
@Inherited – 是否允許子類繼承該注解
@Retention – 負責定義該注解的生命周期
-
RetentionPolicy.SOURCE : 在編譯階段丟棄。這些注解在編譯結束之后就不再有任何意義,所以它們不會寫入字節碼。@Override, @SuppressWarnings都屬於這類注解。
-
RetentionPolicy.CLASS : 在類加載的時候丟棄。在字節碼文件的處理中有用。注解默認使用這種方式。
-
RetentionPolicy.RUNTIME : 始終不會丟棄,運行期也保留該注解,因此可以使用反射機制讀取該注解的信息。我們自定義的注解通常使用這種方式。
@Target :表示該注解用於什么地方。默認值為任何元素,表示該注解用於什么地方。可用的ElementType 參數包括
● ElementType.CONSTRUCTOR: 用於描述構造器
● ElementType.FIELD: 成員變量、對象、屬性(包括enum實例)
● ElementType.LOCAL_VARIABLE: 用於描述局部變量
● ElementType.METHOD: 用於描述方法
● ElementType.PACKAGE: 用於描述包
● ElementType.PARAMETER: 用於描述參數
● ElementType.TYPE: 用於描述類、接口(包括注解類型) 或enum聲明
一個例子:
//@Target(ElementType.TYPE) 只能在類上標記該注解 @Target({ElementType.TYPE,ElementType.FIELD}) // 允許在類與類屬性上標記該注解 @Retention(RetentionPolicy.SOURCE) //注解保留在源碼中 public @interface Lance { }
@Documented – 一個簡單的Annotations 標記注解,表示是否將注解信息添加在java 文檔中,沒有成員。
@Inherited – 定義該注釋和子類的關系
-
@Inherited 元注解是一個標記注解,@Inherited 闡述了某個被標注的類型是被繼承的。如果一個使用了@Inherited 修飾的annotation 類型被用於一個class,則這個annotation 將被用於該class 的子類。
實現例子:
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface Column { public String name() default "fieldName"; public String setFuncName() default "setField"; public String getFuncName() default "getField"; public boolean defaultDBValue() default false; }
5、注解語法
注解的定義:注解通過@interface 關鍵字進行定義。注解的定義格式是 :public @interface 注解名 {定義體}
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MyAnnotation { String value() default ""; }
注解的定義的形式跟接口很類似,不過前面多了一個 @ 符號。上面的代碼就創建了一個名字為MyAnnotaion 的注解。
寫注解的時候需要注意:
1、注解的成員變量只能使用基本類型、 String、 enum枚舉、Class類型、注解類型以及這些類型的數組,比如int可以,但Integer這種包裝類型就不行
2、成員函數只能用public 或默認這兩個訪問修飾符
3、注解上面的注解@Target、 @Retention,稱為 “元注解”,元注解就是專門用於給注解添加注解的注解,元注解就是天生就有的注解,直接用於注解的定義上,注意元注解不可少
4、注解里面的方法可以有默認值 default, 最好把參數名稱設為"value",后加小括號
5、注解元素必須有確定的值,要么在定義注解的默認值中指定,要么在使用注解時指定,非基本類型的注解元素的值不能為null。因此, 使用空字符串或0作為默認值是一種常用的做法。這個約束使得處理器很難表現一個元素的存在或缺失的狀態,因為每個注解的聲明中,所有元素都存在,並且都具有相應的值,為了繞開這個約束,我們只能定義一些特殊的值,例如空字符串或者負數,一次表示某個元素不存在,在定義注解時,這已經成為一個習慣用法,像下面這樣:
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interfaceMyAnnotation{ public int id() default -1; public String name() default ""; public String address() default ""; }
6、注解類型元素
在上文元注解中,允許在使用注解時傳遞參數。我們也能讓自定義注解的主體包含annotation type element (注解類型元素) 聲明,它們看起來很像方法,可以定義可選的默認值。
@Target({ElementType.TYPE,ElementType.FIELD}) @Retention(RetentionPolicy.SOURCE) public @interface Test { String value(); //無默認值 int age() default 1; //有默認值 } //注意:在使用注解時,如果定義的注解中的類型元素無默認值,則必須進行傳值。 @Test("test") //如果只存在value元素需要傳值的情況,則可以省略:元素名= @Test(value="test",age = 2) int i;
7、注解應用實例
沒有注解加持的時候,如果我們想對下面的類進行校驗:
class Teacher { private Long id; private String name; private String mobilephone; }
沒有注解的話,檢驗邏輯應該是:
@PostMapping("/add") public String addTeacher(@RequestBody Teacher teacher) { if(teacher == null) return "傳入的對象為null,請傳值"; if(teacher.getId()==null || "".equals(teacher.getName())) return "傳入的ID為空,請傳值"; if(teacher.getName()==null || "".equals(teacher.getName())) return "傳入的姓名為空,請傳值"; if( teacher.getMobile()==null||"".equals(teacher.getMobile())) return"傳入的手機號為空,請傳值"; return "SUCCESS"; }
如果成員變量比較多的話,就很繁瑣,這個時候就可以使用注解來解決數據的校驗工作, 比如說校驗mobilephone這個字段。
class Teacher { private Long id; private String name; @NotNull(message = "傳入電話號碼為null,請傳值") @NotEmpty(message = "傳入電話號碼為,請空傳值") @Length(min =11, max =11, message ="傳入的手機號長度有誤,必須為11位") private String mobilephone; }
按照下述步驟來寫一個簡單的注解:
7.1、定義一個注解
@Target({ElementType.TYPE,ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface Length { int max(); int min(); String errorMsg(); }
7.2、使用上述定義的注解
class Teacher { private Long id; private String name; @Length(min =11, max =11, errorMsg ="傳入的手機號長度有誤,必須為11位") private String mobilephone; }
7.3、獲取注解並驗證注解(注解處理器,最重要!!)
Annotation是被動的元數據,永遠不會有主動行為,但凡Annotation起作用的場合都是有一個執行機制或者調用者通過反射獲得了這個元數據然后根據它采取行動。
利用反射特性在運行時獲取注解的信息(注意注解的本質還是一個接口)
//注解處理器 //最重要的最關鍵的:注解處理器 class TeacherProcessor{ //傳入對象(表明注解在對象上) public static String validate(Object object) throws Exception{ //首先通過反射獲取到object對象的字段信息 //對上面的字段就可以獲取到Teacher類中的Id,name和mobilephone三個字段 Field[] fields = object.getClass().getDeclaredFields(); for(Field field : fields) { //if 判斷:檢查該字段上有沒有注解Length if(field.isAnnotationPresent(Length.class)){ Length length = field.getAnnotation(Length.class); //設置一下權限,保證能夠通過反射得到私有成員變量mobilephone field.setAccessible(true); //通過反射獲取字段實際值的長度 int value = ( (String) (field.get(object) )).length(); //將字段值的長度和注解上面傳入的值進行對比 if(value < length.min() || value > length.max()){ return length.errorMsg(); } } } return null; } //直接傳入類對象(表明注解在類上) public static String validate(Class<?> clazz) throws NoSuchMethodException{ //首先通過反射獲取到object對象的字段信息 //對上面的字段就可以獲取到Teacher類中的Id,name和mobilephone三個字段 Field[] fields = clazz.getDeclaredFields(); for(Field field : fields) { //if 判斷:檢查該字段上有沒有注解Length if(field.isAnnotationPresent(Length.class)){ Length length = field.getAnnotation(Length.class); //設置一下權限,保證能夠通過反射得到私有成員變量mobilephone field.setAccessible(true); // field.get //通過反射獲取字段實際值的長度 // int value = ( (String) field.get(cla) ).length(); //將字段值的長度和注解上面傳入的值進行對比 if(11 < length.min() || 11 > length.max()){ return length.errorMsg(); } } } return null; } }
7.4、驗證結果
//驗證結果 public class TestTecherAnnotation { public static void main(String[] args) throws Exception { Teacher teacher = new Teacher(); teacher.setMobilephone("123456789101"); TeacherProcessor.validate(teacher); } }
7.5 使用注解的例子(綜合)
import java.lang.annotation.*; import java.lang.reflect.Field; /** * Copyright@www.jrliu.com. * Author:jrliu * Date:2019/10/29 * Description: */ //定義一個注解 @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented @interface Length { int max() ; int min() ; String errorMsg() default ""; } //來針對具體的類使用注解 class Teacher { private Long id; private String name; @Length(min =11, max =11, errorMsg ="傳入的手機號長度有誤,必須為11位") private String mobilephone; public String getMobilephone() { return mobilephone; } public void setMobilephone(String mobilephone) { this.mobilephone = mobilephone; } } //注解處理器 //最重要的最關鍵的:注解處理器 class TeacherProcessor{ //傳入對象 public static String validate(Object object) throws Exception{ //首先通過反射獲取到object對象的字段信息 //對上面的字段就可以獲取到Teacher類中的Id,name和mobilephone三個字段 Field[] fields = object.getClass().getDeclaredFields(); for(Field field : fields) { //if 判斷:檢查該字段上有沒有注解Length if(field.isAnnotationPresent(Length.class)){ Length length = field.getAnnotation(Length.class); //設置一下權限,保證能夠通過反射得到私有成員變量mobilephone field.setAccessible(true); //通過反射獲取傳入對象的字段實際值的長度 int value = ( (String) (field.get(object) )).length(); //將字段值的長度和注解上面傳入的值進行對比 if(value < length.min() || value > length.max()){ return length.errorMsg(); } } } return null; } } //驗證結果 public class TestTecherAnnotation { public static void main(String[] args) throws Exception { Teacher teacher = new Teacher(); teacher.setMobilephone("123456789101"); String message = TeacherProcessor.validate(teacher); System.out.println("注解信息:\n" + message); } }
8、Java中常見的注解
@Override標記類型注解,它被用作標注方法。它說明了被標注的方法重載了父類的方法,起到了斷言的作用。如果我們使用了這種注解在一個沒有覆蓋父類方法的方法時,java 編譯器將以一個編譯錯誤來警示。 這個annotaton常常在我們試圖覆蓋父類方法而確又寫錯了方法名時發揮威力。使用方法極其簡單:在使用此annotation時只要在被修飾的方法前面加上@Override即可。
@Deprecated當一個類型或者類型成員使用@Deprecated修飾的話,編譯器將不鼓勵使用這個被標注的程序元素。所以使用這種修飾具有一定的“延續性”:如果我們在代碼中通過繼承或者覆蓋的方式使用了這個過時的類型或者成員,雖然繼承或者覆蓋后的類型或者成員並不是被聲明為@Deprecated,但編譯器仍然要報警。
@SuppressWarnings 不是一個標記類型注解。它有一個類型為String[] 的成員,這個成員的值為被禁止的警告名,同時編譯器忽略掉無法識別的警告名。
@SuppressWarnings 被用於有選擇的關閉編譯器對類、方法、成員變量、變量初始化的警告。 在java5.0,sun提供的javac編譯器為我們提供了-Xlint選項來使編譯器對合法的程序代碼提出警告,此種警告從某種程度上代表了程序錯誤。例如當我們使用一個generic collection類而又沒有提供它的類型時,編譯器將提示出"unchecked warning"的警告。通常當這種情況發生時,我們就需要查找引起警告的代碼。如果它真的表示錯誤,我們就需要糾正它。例如如果警告信息表明我們代碼中的switch語句沒有覆蓋所有可能的case,那么我們就應增加一個默認的case來避免這種警告。
有時我們無法避免這種警告,例如,我們使用必須和非generic的舊代碼交互的generic collection類時,我們不能避免這個unchecked warning。此時@SuppressWarning就要派上用場了,在調用的方法前增加@SuppressWarnings修飾,告訴編譯器停止對此方法的警告。
SuppressWarning不是一個標記注解。它有一個類型為String[]的成員,這個成員的值為被禁止的警告名。對於javac編譯器來講,被-Xlint選項有效的警告 名也同樣對@SuppressWarings有效,同時編譯器忽略掉無法識別的警告名。
SuppressWarnings注解的常見參數值的簡單說明:
1.deprecation:使用了不贊成使用的類或方法時的警告;
2.unchecked:執行了未檢查的轉換時的警告,例如當使用集合時沒有用泛型 (Generics) 來指定集合保存的類型;
3.fallthrough:當 Switch 程序塊直接通往下一種情況而沒有 Break 時的警告;
4.path:在類路徑、源文件路徑等中有不存在的路徑時的警告;
5.serial:當在可序列化的類上缺少 serialVersionUID 定義時的警告;
6.finally:任何 finally 子句不能正常完成時的警告;
7.all:關於以上所有情況的警告。
9、完整的一個注解范例
import java.lang.annotation.*; import java.lang.reflect.Field; /*** Author:jrliu * Date:2020/2/29 * Description:定義注解並且測試注解使用 */ //水果名稱注釋 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented @interface FruitName{ public String value() default ""; } //水果顏色注釋 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented @interface FruitColor{ //顏色枚舉 public enum Color {RED, GREEN, YELLOW}; //顏色屬性 Color getFruitColor() default Color.GREEN; } @Retention(RetentionPolicy.RUNTIME) //水果供應商注解 @interface FruitProvider { //供應商編號 public int id() default -1; //供應商名稱 public String name() default ""; //供應商地址 public String address() default ""; } //來使用注解 class Apple{ @FruitName("Apple") private String appleName; @FruitColor(getFruitColor = FruitColor.Color.RED) private String appleColor; @FruitProvider(id = 1, name = "紅富士集團", address = "紅富士大廈") private String appleProvider; public String getAppleName() { return appleName; } public void setAppleName(String appleName) { this.appleName = appleName; } public String getAppleColor() { return appleColor; } public void setAppleColor(String appleColor) { this.appleColor = appleColor; } public String getAppleProvider() { return appleProvider; } public void setAppleProvider(String appleProvider) { this.appleProvider = appleProvider; } } //最重要的最關鍵的:注解處理器 class FruitProcessor{ public static void getFruitInfo(Class<?> clazz){ String strFruitName = " 水果名稱:"; String strFruitColor = " 水果顏色:"; String strFruitProvicer = "水果供應商信息:"; Field[] fields = clazz.getDeclaredFields(); for(Field field: fields) { field.setAccessible(true); if(field.isAnnotationPresent(FruitName.class)) { FruitName fruitName = (FruitName) field.getAnnotation(FruitName.class); strFruitName = strFruitName + fruitName.value(); System.out.println(strFruitName); } else if(field.isAnnotationPresent(FruitColor.class)) { FruitColor fruitColor = (FruitColor)field.getAnnotation(FruitColor.class); strFruitColor = strFruitColor + fruitColor.getFruitColor().toString(); System.out.println(strFruitColor); } else if(field.isAnnotationPresent(FruitProvider.class)){ FruitProvider fruitProvider = (FruitProvider)field.getAnnotation(FruitProvider.class); strFruitProvicer = strFruitProvicer + " 供應商編號" + fruitProvider.id() +"供應商名稱"+ fruitProvider.name() + "供應商地址" + fruitProvider.address() ; System.out.println(strFruitProvicer); } } } } //驗證測試結果。 public class TestFruitAnnotation { public static void main(String[] args) { FruitProcessor.getFruitInfo(Apple.class); } }