注解的概念
注解的官方定義
首先看看官方對注解的描述:
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(); }
定義注解類型元素時需要注意如下幾點:
-
訪問修飾符必須為public,不寫默認為public;
-
該元素的類型只能是基本數據類型、String、Class、枚舉類型、注解類型(體現了注解的嵌套效果)以及上述類型的一位數組;
-
該元素的名稱一般定義為名詞,如果注解中只有一個元素,請把名字起為value(后面使用會帶來便利操作);
-
()不是定義方法參數的地方,也不能在括號中定義任何參數,僅僅只是一個特殊的語法;
-
default代表默認值,值必須和第2點定義的類型一致;
-
如果沒有默認值,代表后續使用注解時必須給該類型元素賦值。
常用的元注解
一個最最基本的注解定義就只包括了上面的兩部分內容: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 }
我們再詳解一下:
-
如果一個注解被定義為RetentionPolicy.SOURCE,則它將被限定在Java源文件中,那么這個注解即不會參與編譯也不會在運行期起任何作用,這個注解就和一個注釋是一樣的效果,只能被閱讀Java文件的人看到;
-
如果一個注解被定義為RetentionPolicy.CLASS,則它將被編譯到Class文件中,那么編譯器可以在編譯時根據注解做一些處理動作,但是運行時JVM(Java虛擬機)會忽略它,我們在運行期也不能讀取到;
-
如果一個注解被定義為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!"); } } }
簡單分析下:
-
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語句的簡單的處理器模型。