1. 程式人生 > >框架基礎:深入理解Java註解型別(@Annotation)

框架基礎:深入理解Java註解型別(@Annotation)

註解的概念

註解的官方定義

首先看看官方對註解的描述:

An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.

翻譯:

註解是一種能被新增到java程式碼中的元資料,類、方法、變數、引數和包都可以用註解來修飾。註解對於它所修飾的程式碼並沒有直接的影響。

通過官方描述得出以下結論:

註解是一種元資料形式。即註解是屬於java的一種資料型別,和類、介面、陣列、列舉類似。
註解用來修飾,類、方法、變數、引數、包。
註解不會對所修飾的程式碼產生直接的影響。

註解的使用範圍

繼續看看官方對它的使用範圍的描述:

Annotations have a number of uses, among them:Information for the complier - Annotations can be used by the compiler to detect errors or suppress warnings.Compiler-time and deployment-time processing - Software tools can process annotation information to generate code, XML files, and so forth.Runtime processing - Some annotations are available to be examined at runtime.

翻譯:

註解又許多用法,其中有:為編譯器提供資訊 - 註解能被編譯器檢測到錯誤或抑制警告。編譯時和部署時的處理 - 軟體工具能處理註解資訊從而生成程式碼,XML檔案等等。執行時的處理 - 有些註解在執行時能被檢測到。

##2 如何自定義註解
基於上一節,已對註解有了一個基本的認識:註解其實就是一種標記,可以在程式程式碼中的關鍵節點(類、方法、變數、引數、包)上打上這些標記,然後程式在編譯時或執行時可以檢測到這些標記從而執行一些特殊操作。因此可以得出自定義註解使用的基本流程:

第一步,定義註解——相當於定義標記;
第二步,配置註解——把標記打在需要用到的程式程式碼中;

第三步,解析註解——在編譯期或執行時檢測到標記,並進行特殊操作。

基本語法

註解型別的宣告部分:

註解在Java中,與類、介面、列舉類似,因此其宣告語法基本一致,只是所使用的關鍵字有所不同@interface。在底層實現上,所有定義的註解都會自動繼承java.lang.annotation.Annotation介面。

public @interface CherryAnnotation {
}

註解型別的實現部分:

根據我們在自定義類的經驗,在類的實現部分無非就是書寫構造、屬性或方法。但是,在自定義註解中,其實現部分只能定義一個東西:註解型別元素(annotation type element)。咱們來看看其語法:

public @interface CherryAnnotation {
    public String name();
    int age() default 18;
    int[] array();
}

定義註解型別元素時需要注意如下幾點:

  1. 訪問修飾符必須為public,不寫預設為public;

  2. 該元素的型別只能是基本資料型別、String、Class、列舉型別、註解型別(體現了註解的巢狀效果)以及上述型別的一位陣列;

  3. 該元素的名稱一般定義為名詞,如果註解中只有一個元素,請把名字起為value(後面使用會帶來便利操作);

  4. ()不是定義方法引數的地方,也不能在括號中定義任何引數,僅僅只是一個特殊的語法;

  5. default代表預設值,值必須和第2點定義的型別一致;

  6. 如果沒有預設值,代表後續使用註解時必須給該型別元素賦值。

常用的元註解

一個最最基本的註解定義就只包括了上面的兩部分內容:1、註解的名字;2、註解包含的型別元素。但是,我們在使用JDK自帶註解的時候發現,有些註解只能寫在方法上面(比如@Override);有些卻可以寫在類的上面(比如@Deprecated)。當然除此以外還有很多細節性的定義,那麼這些定義該如何做呢?接下來就該元註解出場了!
元註解:專門修飾註解的註解。它們都是為了更好的設計自定義註解的細節而專門設計的。我們為大家一個個來做介紹。

@Target

@Target註解,是專門用來限定某個自定義註解能夠被應用在哪些Java元素上面的。它使用一個列舉型別定義如下:

public enum ElementType {
    /** 類,介面(包括註解型別)或列舉的宣告 */
    TYPE,

    /** 屬性的宣告 */
    FIELD,

    /** 方法的宣告 */
    METHOD,

    /** 方法形式引數宣告 */
    PARAMETER,

    /** 構造方法的宣告 */
    CONSTRUCTOR,

    /** 區域性變數宣告 */
    LOCAL_VARIABLE,

    /** 註解型別宣告 */
    ANNOTATION_TYPE,

    /** 包的宣告 */
    PACKAGE
}

 

//@CherryAnnotation被限定只能使用在類、介面或方法上面
@Target(value = {ElementType.TYPE,ElementType.METHOD})
public @interface CherryAnnotation {
    String name();
    int age() default 18;
    int[] array();
}

@Retention

@Retention註解,翻譯為持久力、保持力。即用來修飾自定義註解的生命力。
註解的生命週期有三個階段:1、Java原始檔階段;2、編譯到class檔案階段;3、執行期階段。同樣使用了RetentionPolicy列舉型別定義了三個階段:

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     * (註解將被編譯器忽略掉)
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     * (註解將被編譯器記錄在class檔案中,但在執行時不會被虛擬機器保留,這是一個預設的行為)
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     * (註解將被編譯器記錄在class檔案中,而且在執行時會被虛擬機器保留,因此它們能通過反射被讀取到)
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

我們再詳解一下:

  1. 如果一個註解被定義為RetentionPolicy.SOURCE,則它將被限定在Java原始檔中,那麼這個註解即不會參與編譯也不會在執行期起任何作用,這個註解就和一個註釋是一樣的效果,只能被閱讀Java檔案的人看到;

  2. 如果一個註解被定義為RetentionPolicy.CLASS,則它將被編譯到Class檔案中,那麼編譯器可以在編譯時根據註解做一些處理動作,但是執行時JVM(Java虛擬機器)會忽略它,我們在執行期也不能讀取到;

  3. 如果一個註解被定義為RetentionPolicy.RUNTIME,那麼這個註解可以在執行期的載入階段被載入到Class物件中。那麼在程式執行階段,我們可以通過反射得到這個註解,並通過判斷是否有這個註解或這個註解中屬性的值,從而執行不同的程式程式碼段。我們實際開發中的自定義註解幾乎都是使用的RetentionPolicy.RUNTIME;

自定義註解

在具體的Java類上使用註解

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@Documented
public @interface CherryAnnotation {
    String name();
    int age() default 18;
    int[] score();
}

 

public class Student {
    @CherryAnnotation(name = "cherry-peng",age = 23,score = {99,66,77})
    public void study(int times){
        for(int i = 0; i < times; i++){
            System.out.println("Good Good Study, Day Day Up!");
        }
    }
}

簡單分析下:

  1. CherryAnnotation的@Target定義為ElementType.METHOD,那麼它書寫的位置應該在方法定義的上方,即:public void study(int times)之上;

  2. 由於我們在CherryAnnotation中定義的有註解型別元素,而且有些元素是沒有預設值的,這要求我們在使用的時候必須在標記名後面打上(),並且在()內以“元素名=元素值“的形式挨個填上所有沒有預設值的註解型別元素(有預設值的也可以填上重新賦值),中間用“,”號分割;

註解與反射機制

為了執行時能準確獲取到註解的相關資訊,Java在java.lang.reflect 反射包下新增了AnnotatedElement介面,它主要用於表示目前正在 VM 中執行的程式中已使用註解的元素,通過該介面提供的方法可以利用反射技術地讀取註解的資訊,如反射包的Constructor類、Field類、Method類、Package類和Class類都實現了AnnotatedElement介面,它簡要含義如下:

Class:類的Class物件定義   
Constructor:代表類的構造器定義   
Field:代表類的成員變數定義 
Method:代表類的方法定義   
Package:代表類的包定義

下面是AnnotatedElement中相關的API方法,以上5個類都實現以下的方法

 返回值  方法名稱  說明
 <A extends Annotation>  getAnnotation(Class<A> annotationClass)  該元素如果存在指定型別的註解,則返回這些註解,否則返回 null。
 Annotation[]  getAnnotations()  返回此元素上存在的所有註解,包括從父類繼承的
 boolean  isAnnotationPresent(Class<? extends Annotation> annotationClass)  如果指定型別的註解存在於此元素上,則返回 true,否則返回 false。
 Annotation[]  getDeclaredAnnotations()  返回直接存在於此元素上的所有註解,注意,不包括父類的註解,呼叫者可以隨意修改返回的陣列;這不會對其他呼叫者返回的陣列產生任何影響,沒有則返回長度為0的陣列

 

簡單案例演示如下:

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DocumentA {
}

 

package com.zejian.annotationdemo;

import java.lang.annotation.Annotation;
import java.util.Arrays;

@DocumentA
class A{

}

//繼承了A類
@DocumentB
public class DocumentDemo extends A{

    public static void main(String... args){

        Class<?> clazz = DocumentDemo.class;
        //根據指定註解型別獲取該註解
        DocumentA documentA=clazz.getAnnotation(DocumentA.class);
        System.out.println("A:"+documentA);

        //獲取該元素上的所有註解,包含從父類繼承
        Annotation[] an= clazz.getAnnotations();
        System.out.println("an:"+ Arrays.toString(an));
        //獲取該元素上的所有註解,但不包含繼承!
        Annotation[] an2=clazz.getDeclaredAnnotations();
        System.out.println("an2:"+ Arrays.toString(an2));

        //判斷註解DocumentA是否在該元素上
        boolean b=clazz.isAnnotationPresent(DocumentA.class);
        System.out.println("b:"+b);

    }
}

執行結果:

A:@com.zejian.annotationdemo.DocumentA()
an:[@com.zejian.annotationdemo.DocumentA(), @com.zejian.annotationdemo.DocumentB()]
an2:@com.zejian.annotationdemo.DocumentB()
b:true

通過反射獲取上面我們自定義註解

public class TestAnnotation {
    public static void main(String[] args){
        try {
            //獲取Student的Class物件
            Class stuClass = Class.forName("pojos.Student");

            //說明一下,這裡形參不能寫成Integer.class,應寫為int.class
            Method stuMethod = stuClass.getMethod("study",int.class);

            if(stuMethod.isAnnotationPresent(CherryAnnotation.class)){
                System.out.println("Student類上配置了CherryAnnotation註解!");
                //獲取該元素上指定型別的註解
                CherryAnnotation cherryAnnotation = stuMethod.getAnnotation(CherryAnnotation.class);
                System.out.println("name: " + cherryAnnotation.name() + ", age: " + cherryAnnotation.age()
                    + ", score: " + cherryAnnotation.score()[0]);
            }else{
                System.out.println("Student類上沒有配置CherryAnnotation註解!");
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }
}

執行時註解處理器

瞭解完註解與反射的相關API後,現在通過一個例項(該例子是博主改編自《Tinking in Java》)來演示利用執行時註解來組裝資料庫SQL的構建語句的過程

/**
 * Created by ChenHao on 2019/6/14.
 * 表註解
 */
@Target(ElementType.TYPE)//只能應用於類上
@Retention(RetentionPolicy.RUNTIME)//儲存到執行時
public @interface DBTable {
    String name() default "";
}


/**
 * Created by ChenHao on 2019/6/14.
 * 註解Integer型別的欄位
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
    //該欄位對應資料庫表列名
    String name() default "";
    //巢狀註解
    Constraints constraint() default @Constraints;
}


/**
 * Created by ChenHao on 2019/6/14.
 * 註解String型別的欄位
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {

    //對應資料庫表的列名
    String name() default "";

    //列型別分配的長度,如varchar(30)的30
    int value() default 0;

    Constraints constraint() default @Constraints;
}


/**
 * Created by ChenHao on 2019/6/14.
 * 約束註解
 */

@Target(ElementType.FIELD)//只能應用在欄位上
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
    //判斷是否作為主鍵約束
    boolean primaryKey() default false;
    //判斷是否允許為null
    boolean allowNull() default false;
    //判斷是否唯一
    boolean unique() default false;
}

/**
 * Created by ChenHao on 2019/6/14.
 * 資料庫表Member對應例項類bean
 */
@DBTable(name = "MEMBER")
public class Member {
    //主鍵ID
    @SQLString(name = "ID",value = 50, constraint = @Constraints(primaryKey = true))
    private String id;

    @SQLString(name = "NAME" , value = 30)
    private String name;

    @SQLInteger(name = "AGE")
    private int age;

    @SQLString(name = "DESCRIPTION" ,value = 150 , constraint = @Constraints(allowNull = true))
    private String description;//個人描述

   //省略set get.....
}

上述定義4個註解,分別是@DBTable(用於類上)、@Constraints(用於欄位上)、 @SQLString(用於欄位上)、@SQLString(用於欄位上)並在Member類中使用這些註解,這些註解的作用的是用於幫助註解處理器生成建立資料庫表MEMBER的構建語句,在這裡有點需要注意的是,我們使用了巢狀註解@Constraints,該註解主要用於判斷欄位是否為null或者欄位是否唯一。必須清楚認識到上述提供的註解生命週期必須為@Retention(RetentionPolicy.RUNTIME),即執行時,這樣才可以使用反射機制獲取其資訊。有了上述註解和使用,剩餘的就是編寫上述的註解處理器了,前面我們聊了很多註解,其處理器要麼是Java自身已提供、要麼是框架已提供的,我們自己都沒有涉及到註解處理器的編寫,但上述定義處理SQL的註解,其處理器必須由我們自己編寫了,如下

package com.chenHao.annotationdemo;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by chenhao on 2019/6/14.
 * 執行時註解處理器,構造表建立語句
 */
public class TableCreator {

  public static String createTableSql(String className) throws ClassNotFoundException {
    Class<?> cl = Class.forName(className);
    DBTable dbTable = cl.getAnnotation(DBTable.class);
    //如果沒有表註解,直接返回
    if(dbTable == null) {
      System.out.println(
              "No DBTable annotations in class " + className);
      return null;
    }
    String tableName = dbTable.name();
    // If the name is empty, use the Class name:
    if(tableName.length() < 1)
      tableName = cl.getName().toUpperCase();
    List<String> columnDefs = new ArrayList<String>();
    //通過Class類API獲取到所有成員欄位
    for(Field field : cl.getDeclaredFields()) {
      String columnName = null;
      //獲取欄位上的註解
      Annotation[] anns = field.getDeclaredAnnotations();
      if(anns.length < 1)
        continue; // Not a db table column

      //判斷註解型別
      if(anns[0] instanceof SQLInteger) {
        SQLInteger sInt = (SQLInteger) anns[0];
        //獲取欄位對應列名稱,如果沒有就是使用欄位名稱替代
        if(sInt.name().length() < 1)
          columnName = field.getName().toUpperCase();
        else
          columnName = sInt.name();
        //構建語句
        columnDefs.add(columnName + " INT" +
                getConstraints(sInt.constraint()));
      }
      //判斷String型別
      if(anns[0] instanceof SQLString) {
        SQLString sString = (SQLString) anns[0];
        // Use field name if name not specified.
        if(sString.name().length() < 1)
          columnName = field.getName().toUpperCase();
        else
          columnName = sString.name();
        columnDefs.add(columnName + " VARCHAR(" +
                sString.value() + ")" +
                getConstraints(sString.constraint()));
      }


    }
    //資料庫表構建語句
    StringBuilder createCommand = new StringBuilder(
            "CREATE TABLE " + tableName + "(");
    for(String columnDef : columnDefs)
      createCommand.append("\n    " + columnDef + ",");

    // Remove trailing comma
    String tableCreate = createCommand.substring(
            0, createCommand.length() - 1) + ");";
    return tableCreate;
  }


    /**
     * 判斷該欄位是否有其他約束
     * @param con
     * @return
     */
  private static String getConstraints(Constraints con) {
    String constraints = "";
    if(!con.allowNull())
      constraints += " NOT NULL";
    if(con.primaryKey())
      constraints += " PRIMARY KEY";
    if(con.unique())
      constraints += " UNIQUE";
    return constraints;
  }

  public static void main(String[] args) throws Exception {
    String[] arg={"com.zejian.annotationdemo.Member"};
    for(String className : arg) {
      System.out.println("Table Creation SQL for " +
              className + " is :\n" + createTableSql(className));
    }
  }
}

推薦部落格

  程式設計師寫程式碼之外,如何再賺一份工資?

輸出結果:

Table Creation SQL for com.zejian.annotationdemo.Member is :
CREATE TABLE MEMBER(
ID VARCHAR(50) NOT NULL PRIMARY KEY,
NAME VARCHAR(30) NOT NULL,
AGE INT NOT NULL,
DESCRIPTION VARCHAR(150)
);

如果對反射比較熟悉的同學,上述程式碼就相對簡單了,我們通過傳遞Member的全路徑後通過Class.forName()方法獲取到Member的class物件,然後利用Class物件中的方法獲取所有成員欄位Field,最後利用field.getDeclaredAnnotations()遍歷每個Field上的註解再通過註解的型別判斷來構建建表的SQL語句。這便是利用註解結合反射來構建SQL語句的簡單的處理器模型。

&n