1. 程式人生 > 實用技巧 >Java註解實現

Java註解實現

1、前言 Java EE下用Spring boot框架後,開始面向註解程式設計,可以說Spring boot就是建立在註解之上。那麼什麼是註解呢?
  • Java 註解(Annotation)又稱 Java 標註,是JDK5.0引入的一種註釋機制。 註解是元資料的一種形式,提供有關於程式但不屬於程式本身的資料。註解對它們註解的程式碼的操作沒有直接影響。
  • Annotation(註解)就是Java提供了一種元程式中的元素關聯任何資訊和著任何元資料(metadata)的途徑和方法。Annotion(註解)是一個介面,程式可以通過反射來獲取指定程式元素的Annotion物件,然後通過Annotion物件來獲取註解裡面的元資料。
註解可以實現以下功能:
  1. 生成文件。這是最常見的,也是java 最早提供的註解。常用的有@param @return 等
  2. 跟蹤程式碼依賴性,實現替代配置檔案功能。比如Dagger 2依賴注入,未來java開發,將大量註解配置,具有很大用處;
  3. 在編譯時進行格式檢查。如@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;
/**
* [email protected].
* 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);
    }
}