首先看看官方對註解的描述:html
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代碼中的元數據,類、方法、變量、參數和包均可以用註解來修飾。註解對於它所修飾的代碼並無直接的影響。數據庫
經過官方描述得出如下結論:數組
註解是一種元數據形式。即註解是屬於java的一種數據類型,和類、接口、數組、枚舉相似。
註解用來修飾,類、方法、變量、參數、包。
註解不會對所修飾的代碼產生直接的影響。app
繼續看看官方對它的使用範圍的描述:框架
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.ide
翻譯:工具
註解又許多用法,其中有:爲編譯器提供信息 - 註解能被編譯器檢測到錯誤或抑制警告。編譯時和部署時的處理 - 軟件工具能處理註解信息從而生成代碼,XML文件等等。運行時的處理 - 有些註解在運行時能被檢測到。ui
##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(); }
定義註解類型元素時須要注意以下幾點:
訪問修飾符必須爲public,不寫默認爲public;
該元素的類型只能是基本數據類型、String、Class、枚舉類型、註解類型(體現了註解的嵌套效果)以及上述類型的一位數組;
該元素的名稱通常定義爲名詞,若是註解中只有一個元素,請把名字起爲value(後面使用會帶來便利操做);
()不是定義方法參數的地方,也不能在括號中定義任何參數,僅僅只是一個特殊的語法;
default表明默認值,值必須和第2點定義的類型一致;
若是沒有默認值,表明後續使用註解時必須給該類型元素賦值。
一個最最基本的註解定義就只包括了上面的兩部份內容:一、註解的名字;二、註解包含的類型元素。可是,咱們在使用JDK自帶註解的時候發現,有些註解只能寫在方法上面(好比@Override);有些卻能夠寫在類的上面(好比@Deprecated)。固然除此之外還有不少細節性的定義,那麼這些定義該如何作呢?接下來就該元註解出場了!
元註解:專門修飾註解的註解。它們都是爲了更好的設計自定義註解的細節而專門設計的。咱們爲你們一個個來作介紹。
@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註解,翻譯爲持久力、保持力。即用來修飾自定義註解的生命力。
註解的生命週期有三個階段:一、Java源文件階段;二、編譯到class文件階段;三、運行期階段。一樣使用了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 }
咱們再詳解一下:
若是一個註解被定義爲RetentionPolicy.SOURCE,則它將被限定在Java源文件中,那麼這個註解即不會參與編譯也不會在運行期起任何做用,這個註解就和一個註釋是同樣的效果,只能被閱讀Java文件的人看到;
若是一個註解被定義爲RetentionPolicy.CLASS,則它將被編譯到Class文件中,那麼編譯器能夠在編譯時根據註解作一些處理動做,可是運行時JVM(Java虛擬機)會忽略它,咱們在運行期也不能讀取到;
若是一個註解被定義爲RetentionPolicy.RUNTIME,那麼這個註解能夠在運行期的加載階段被加載到Class對象中。那麼在程序運行階段,咱們能夠經過反射獲得這個註解,並經過判斷是否有這個註解或這個註解中屬性的值,從而執行不一樣的程序代碼段。咱們實際開發中的自定義註解幾乎都是使用的RetentionPolicy.RUNTIME;
@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!"); } } }
簡單分析下:
CherryAnnotation的@Target定義爲ElementType.METHOD,那麼它書寫的位置應該在方法定義的上方,即:public void study(int times)之上;
因爲咱們在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語句的簡單的處理器模型。