一、概念
Annotation(注解)就是Java提供了一種源程式中的元素關聯任何資訊和任何中繼資料(metadata)的途徑和方法。
定義:注解(Annotation),也叫中繼資料,是一種代碼級别的說明。它是JDK1.5及以後版本引入的一個特性,與類、接口、枚舉是在同一個層次。它可以聲明在包、類、字段、方法、局部變量、方法參數等的前面,用來對這些元素進行說明,注釋,建立文檔,跟蹤代碼中的依賴性,甚至執行基本編譯時檢查等。從某些方面看,Annotation就像修飾符一樣被使用,并應用于包、類型、構造方法、方法、成員變量、參數、本地變量的聲明中。這些資訊被存儲在Annotation的“name=value”結構對中。
Annotation(注解)是一個接口,程式可以通過反射來擷取指定程式元素的Annotation對象,然後通過Annotation對象來擷取注解裡面的中繼資料。
1、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成員。
Annotation的成員在Annotation類型中以無參數的方法的形式被聲明。其方法名和傳回值定義了該成員的名字和類型。在此有一個特定的預設文法:允許聲明任何Annotation成員的預設值:一個Annotation可以将name=value對作為沒有定義預設值的Annotation成員的值,當然也可以使用name=value對來覆寫其它成員預設值。這一點有些近似類的繼承特性,父類的構造函數可以作為子類的預設構造函數,但是也可以被子類覆寫。
2、什麼是metadata(中繼資料)
中繼資料從metadata一詞譯來,就是“關于資料的資料”的意思。
中繼資料的功能作用有很多,比如:你可能用過javadoc的注釋自動生成文檔。這就是中繼資料功能的一種。總的來說,中繼資料可以用來建立文檔,跟蹤代碼的依賴性,執行編譯時格式檢查,代替已有的配置檔案。如果要對于中繼資料的作用進行分類,目前還沒有明确的定義,不過我們可以根據它所起的作用,大緻可分為三類:
1)、編寫文檔:通過代碼裡辨別的中繼資料生成文檔;
2)、代碼分析:通過代碼裡辨別的中繼資料對代碼進行分析;
3)、編譯檢查:通過代碼裡辨別的中繼資料讓編譯器能實作基本的編譯檢查。
在Java中中繼資料以标簽的形式存在于Java代碼中,中繼資料标簽的存在并不影響程式代碼的編譯和執行,它隻是被用來生成其它的檔案或正在運作時知道被運作代碼的描述資訊。
綜上所述:
第一,中繼資料以标簽的形式存在于Java代碼中。
第二,中繼資料描述的資訊是類型安全的,即中繼資料内部的字段都是有明确類型的。
第三,中繼資料需要編譯器之外的工具額外的處理用來生成其它的程式部件。
第四,中繼資料可以隻存在于Java源代碼級别,也可以存在于編譯之後的Class檔案内部。
二、Java中常見的注解
1、注解的分類
1)、按照運作機制分
源碼注解:注解隻在源碼中存在,編譯成class檔案之後就不存在了;
編譯時注解:注解在源碼和class檔案中都存在(@Override、@Deprecated、@Suppvisewarnings);
運作時注解:在運作階段還在起作用,甚至會影響程式的運作邏輯。
2)、按照來源劃分
來自JDK的注解
來自第三方的注解(使用最多)
自定義注解
3)、另外還有一種較特殊的注解
元注解:注解的注解。
2、JDK自帶注解
注解的文法比較簡單,除了@符号的使用外,他基本與Java固有的文法一緻,JavaSE中内置三個标準注解,定義在java.lang中:
@Override:用于修飾此方法覆寫了父類的方法;
@Deprecated:用于修飾已經過時的方法;
@SuppressWarnnings:用于通知java編譯器禁止特定的編譯警告。
2.1、@Override
@Override,限定重寫父類方法:@Override 是一個标記注解類型,它被用作标注方法。它說明了被标注的方法重載了父類的方法,起到了斷言的作用。 如果我們使用了這種Annotation在一個沒有覆寫父類方法的方法時,java編譯器将以一個編譯錯誤來警示。這個Annotation常常在我們試圖覆寫父類方法而确又寫錯了方法名時發揮威力(是以建議在重載父類方法時加上該注解)。使用方法極其簡單:在使用此Annotation時隻要在被修飾的方法前面加上@Override即可。下面的代碼是一個使用@Override修飾一個企圖重載父類的displayName()方法,而又存在拼寫錯誤的執行個體:
/**
* @Description: @Override注解的使用
* @author: zxt
* @time: 2019年1月24日 下午6:53:12
*/
public class Fruit {
public void displayName() {
System.out.println("水果的名稱是:***!");
}
}
class Orange extends Fruit {
@Override
public void displayName() {
System.out.println("水果的名稱是:桔子!");
}
}
/**
* @Description: 當沒有重載父類方法(或者寫錯方法名)時使用了@Override将報編譯時錯誤
*
*/
class Apple extends Fruit {
// @Override
public void displayname() {
System.out.println("水果的名稱是:蘋果!");
}
@Override
public void displayName() {
System.out.println("水果的名稱是:蘋果!");
}
}
2.2、@Deprecated
标記已過的方法,同樣@Deprecated也是一個标記注解。當一個類型或者類型成員使用@Deprecated修飾的話,編譯器将不鼓勵使用這個被标注的程式元素。 而且這種修飾具有一定的“延續性”:如果我們在代碼中通過繼承或者覆寫的方式使用了這個過時的類型或者成員,雖然繼承或者覆寫後的類型或者成員并不是被聲明為 @Deprecated,但編譯器仍然要報警。
值得注意的是,@Deprecated這個Annotation類型和javadoc中的 @deprecated這個tag是有差別的:前者是java編譯器識别的,而後者是被javadoc工具所識别用來生成文檔(包含程式成員為什麼已經過時、它應當如何被禁止或者替代的描述)。
在java5.0,java編譯器仍然像其從前版本那樣尋找@deprecated這個javadoc的tag,并使用它們産生警告資訊。但是這種狀況将在後續版本中改變,我們應在現在就開始使用@Deprecated來修飾過時的方法而不是 @deprecated這個javadoc tag。
/**
* @Description: @Deprecated注解的使用
* @author: zxt
* @time: 2019年1月24日 下午7:54:40
*/
class AppleService {
public void displayName() {
System.out.println("水果的名稱是:蘋果!");
}
/**
* @Description:@Deprecated标記方法已經過時,不推薦使用
*/
@Deprecated
public void showTaste() {
System.out.println("水果的蘋果的口感是:脆甜");
}
public void showTaste(int typeId) {
if (typeId == 1) {
System.out.println("水果的蘋果的口感是:酸澀");
} else if (typeId == 2) {
System.out.println("水果的蘋果的口感是:脆甜");
} else {
System.out.println("水果的蘋果的口感是:超甜");
}
}
}
public class FruitRun {
public static void main(String[] args) {
Apple apple = new Apple();
apple.displayName();
AppleService appleService = new AppleService();
appleService.showTaste();
appleService.showTaste(1);
appleService.showTaste(2);
}
}
2.3、@Suppvisewarnings
抑制編譯器警告,@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有效,同時編譯器忽略掉無法識别的警告名。
Annotation文法允許在Annotation名後跟括号,括号中是使用逗号分割的name=value對用于為Annotation的成員指派。執行個體如下:
/**
* @Description: @SuppressWarnings注解的使用
* @author: zxt
* @time: 2019年1月24日 下午8:48:52
*/
public class FruitService {
/**
* @Description:可以放在方法的前面,也可以放在有編譯警告的語句前面;可以使用value={}的形式,也可以省略value
*/
public static void main(String[] args) {
@SuppressWarnings("unused")
List<String> strList = new ArrayList<String>();
}
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public static List<Fruit> getFruitList() {
List<Fruit> fruitList = new ArrayList();
return fruitList;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public static List<Fruit> getFruit() {
List<Fruit> fruitList = new ArrayList();
return fruitList;
}
}
在這個例子中SuppressWarnings annotation類型隻定義了一個單一的成員,是以隻有一個簡單的value={…}作為name=value對。又由于成員值是一個數組,故使用大括号來聲明數組值。注意:我們可以在下面的情況中縮寫Annotation:當Annotation隻有單一成員,并成員命名為"value="。這時可以省去"value="。比如将上面方法getFruit()的SuppressWarnings Annotation就是縮寫的。
@SuppressWarnings注解的常見參數值的簡單說明:
1、deprecation:使用了不贊成使用的類或方法時的警告;
2、unchecked:執行了未檢查的轉換時的警告,例如當使用集合時沒有用泛型 (Generics) 來指定集合儲存的類型;
3、unused:抑制沒被使用過的代碼的警告;
4、rawtypes:使用generics時忽略沒有指定相應的類型;
5、fallthrough:當 Switch 程式塊直接通往下一種情況而沒有 Break 時的警告;
6、path:在類路徑、源檔案路徑等中有不存在的路徑時的警告;
7、serial:當在可序列化的類上缺少 serialVersionUID 定義時的警告;
8、finally:任何 finally 子句不能正常完成時的警告;
9、all:關于以上所有情況的警告。
3、常見的第三方注解
第三方的注解主要在使用一些架構的時候會用到,例如:
三、元注解
元注解的作用就是負責注解其他注解。Java5.0定義了4個标準的meta-annotation類型,它們被用來提供對其它 annotation類型作說明。Java5.0定義的元注解:
1、@Target;
2、@Retention;
3、@Documented;
4、@Inherited。
這些類型和它們所支援的類在java.lang.annotation包中可以找到。下面我們看一下每個元注解的作用和相應分參數的使用說明。
1、@Target
@Target說明了Annotation所修飾的對象範圍:Annotation可被用于 packages、types(類、接口、枚舉、Annotation類型)、類型成員(方法、構造方法、成員變量、枚舉值)、方法參數和本地變量(如循環變量、catch參數)。在Annotation類型的聲明中使用了target可更加明晰其修飾的目标。
作用:用于描述注解的使用範圍(即:被描述的注解可以用在什麼地方)。
取值(ElementType)有:
1、CONSTRUCTOR:用于描述構造器;
2、FIELD:用于描述域;
3、LOCAL_VARIABLE:用于描述局部變量;
4、METHOD:用于描述方法;
5、PACKAGE:用于描述包;
6、PARAMETER:用于描述參數;
7、TYPE:用于描述類、接口(包括注解類型) 或enum聲明。
使用執行個體:
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
public @interface Table {
/**
* @Description:資料表名稱注解,預設值為類名稱
* @return
*/
public String tableName() default "className";
}
@Target(ElementType.FIELD)
public @interface NoDBColumn {
}
注解Table 可以用于注解類、接口(包括注解類型) 或enum聲明,而注解NoDBColumn僅可用于注解類的成員變量。
2、@Retention
@Retention定義了該Annotation被保留的時間長短:某些Annotation僅出現在源代碼中,而被編譯器丢棄;而另一些卻被編譯在class檔案中;編譯在class檔案中的Annotation可能會被虛拟機忽略,而另一些在class被裝載時将被讀取(請注意并不影響class的執行,因為Annotation與class在使用上是被分離的)。使用這個meta-Annotation可以對 Annotation的“生命周期”限制。
作用:表示需要在什麼級别儲存該注釋資訊,用于描述注解的生命周期(即:被描述的注解在什麼範圍内有效)。
取值(RetentionPoicy)有:
1、SOURCE:在源檔案中有效(即源檔案保留);
2、CLASS:在class檔案中有效(即class保留);
3、RUNTIME:在運作時有效(即運作時保留)。
Retention meta-annotation類型有唯一的value作為成員,它的取值來自java.lang.annotation.RetentionPolicy的枚舉類型值。具體執行個體如下:
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
public String name() default "fieldName";
public String setFuncName() default "setField";
public String getFuncName() default "getField";
public boolean defaultDBValue() default false;
}
Column注解的的RetentionPolicy的屬性值是RUTIME,這樣注解處理器可以通過反射,擷取到該注解的屬性值,進而去做一些運作時的邏輯處理。
3、@Inherited
@Inherited 元注解是一個标記注解,@Inherited闡述了某個被标注的類型是被繼承的。如果一個使用了@Inherited修飾的annotation類型被用于一個class,則這個annotation将被用于該class的子類。
注意:@Inherited annotation類型是被标注過的class的子類所繼承。類并不從它所實作的接口繼承annotation,方法并不從它所重載的方法繼承annotation。
當@Inherited annotation類型标注的annotation的Retention是RetentionPolicy.RUNTIME,則反射API增強了這種繼承性。如果我們使用java.lang.reflect去查詢一個@Inherited annotation類型的annotation時,反射代碼檢查将展開工作:檢查class和其父類,直到發現指定的annotation類型被發現,或者到達類繼承結構的頂層。
4、@Documented
@Documented用于描述其它類型的annotation應該被作為被标注的程式成員的公共API,是以可以被例如javadoc此類的工具文檔化。Documented是一個标記注解,沒有成員。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Column {
public String name() default "fieldName";
public String setFuncName() default "setField";
public String getFuncName() default "getField";
public boolean defaultDBValue() default false;
}
四、自定義注解
使用@interface自定義注解時,自動繼承了java.lang.annotation.Annotation接口,由編譯程式自動完成其他細節。在定義注解時,不能繼承其他的注解或接口。@interface用來聲明一個注解,其中的每一個方法實際上是聲明了一個配置參數。方法的名稱就是參數的名稱,傳回值類型就是參數的類型(傳回值類型隻能是基本類型、Class、String、enum)。可以通過default來聲明參數的預設值。
定義注解格式:
public @interface 注解名 {定義體}
注解參數的可支援資料類型:
1、所有基本資料類型(int,float,boolean,byte,double,char,long,short);
2、String類型;
3、Class類型;
4、enum類型;
5、Annotation類型;
6、以上所有類型的數組;
Annotation類型裡面的參數該怎麼設定:
第一,隻能用public或預設(default)這兩個通路權修飾。例如,String value();這裡把方法設為defaul預設類型;
第二,參數成員隻能用基本類型byte,short,char,int,long,float,double,boolean八種基本資料類型和 String,Enum,Class,Annotations等資料類型,以及這一些類型的數組。例如,String value();這裡的參數成員就為String;
第三,如果隻有一個參數成員,最好把參數名稱設為"value",後加小括号。
1、注解元素的預設值
注解元素必須有确定的值,要麼在定義注解的預設值中指定,要麼在使用注解時指定,非基本類型的注解元素的值不可為null。是以,使用空字元串或0作為預設值是一種常用的做法。這個限制使得處理器很難表現一個元素的存在或缺失的狀态,因為每個注解的聲明中,所有元素都存在,并且都具有相應的值,為了繞開這個限制,我們隻能定義一些特殊的值,例如空字元串或者負數,以此表示某個元素不存在,在定義注解時,這已經成為一個習慣用法。
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 水果供應者注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
/**
* 供應商編号
* @return
*/
public int id() default -1;
/**
* 供應商名稱
* @return
*/
public String name() default "";
/**
* 供應商位址
* @return
*/
public String address() default "";
}
2、注解處理器
定義了注解,并在需要的時候給相關類,類屬性加上注解資訊,如果沒有響應的注解資訊處理流程,注解可以說是沒有實用價值。如何讓注解真正的發揮作用,主要就在于注解處理方法。
注解處理器類庫(java.lang.reflect.AnnotatedElement):
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)的父接口,是以程式通過反射擷取了某個類的AnnotatedElement對象之後,程式就可以調用該對象的如下四個個方法來通路Annotation資訊:
方法1: T getAnnotation(Class annotationClass): 傳回該程式元素上存在的、指定類型的注解,如果該類型注解不存在,則傳回null。
方法2:Annotation[] getAnnotations():傳回該程式元素上存在的所有注解。
方法3:boolean isAnnotationPresent(Class<? extends Annotation> annotationClass):判斷該程式元素上是否包含指定類型的注解,存在則傳回true,否則傳回false。
方法4:Annotation[] getDeclaredAnnotations():傳回直接存在于此元素上的所有注解。與此接口中的其他方法不同,該方法将忽略繼承的注解。(如果沒有注釋直接存在于此元素上,則傳回長度為零的一個數組。)該方法的調用者可以随意修改傳回的數組;這不會對其他調用者傳回的數組産生任何影響。
3、項目實戰
需求:有一張使用者表,字段有使用者ID、使用者名、昵稱、年齡、性别、所在城市、郵箱、手機号。現在需要完成的是,友善得對每個字段或者字段的組合條件進行索引,并列印出SQL。
package com.zxt.test;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// Table注解的作用域為類或者接口
@Target(ElementType.TYPE)
// 在運作時解析
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
// 隻有一個值标明表名即可
String value();
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// Column注解的作用域為屬性
@Target(ElementType.FIELD)
// 在運作時解析
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
// 标注屬性名
String value();
}
package com.zxt.test;
/**
* @Description: 首先要做的就是将這個表與資料庫表進行一個映射
* @author: zxt
* @time: 2019年2月2日 下午11:14:28
*/
@Table("user")
public class Filter {
@Column("id")
private int id;
@Column("user_name")
private String userName;
@Column("nick_name")
private String nickName;
@Column("age")
private int age;
@Column("city")
private String city;
@Column("email")
private String email;
@Column("mobile")
private String mobile;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
}
package com.zxt.test;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Test {
public static void main(String[] args) {
Filter f1 = new Filter();
// 查詢使用者id為10的使用者
f1.setId(10);
Filter f2 = new Filter();
// 查詢使用者名為lucy 且 年齡為12 的使用者
f2.setUserName("lucy");
f2.setAge(12);
Filter f3 = new Filter();
// 查詢郵箱為其中之一的使用者
f3.setEmail("[email protected],[email protected],[email protected]");
String sql1 = query(f1);
String sql2 = query(f2);
String sql3 = query(f3);
System.out.println(sql1);
System.out.println(sql2);
System.out.println(sql3);
}
/**
* @Description:query方法的實作,通過反射機制來讀取注解
* @param f
* @return
*/
@SuppressWarnings("unchecked")
private static String query(Filter f) {
StringBuilder sb = new StringBuilder();
// 解析注解
// 1.擷取class
@SuppressWarnings("rawtypes")
Class clazz = f.getClass();
// 2.擷取到table的名稱
boolean tableExist = clazz.isAnnotationPresent(Table.class);
if (!tableExist) {
return null;
}
Table t = (Table) clazz.getAnnotation(Table.class);
String tableName = t.value();
sb.append("select * from ").append(tableName).append(" where 1 = 1 ");
// 3.周遊所有的字段,如果有值就加到查詢條件中
Field[] fieldArray = clazz.getDeclaredFields();
for (Field field : fieldArray) {
// 4.處理每個字段相對應的sql
boolean fieldExist = field.isAnnotationPresent(Column.class);
if (!fieldExist) {
continue;
}
// 4.1.擷取字段名
String fieldName = field.getName();
// 4.2.拿到字段的值(通過字段名相應的get方法)
String getMethodName = "get" + fieldName.substring(0, 1).toUpperCase() +
fieldName.substring(1);
Object fieldValue = null;
try {
Method getMethod = clazz.getMethod(getMethodName);
// 反射調用
fieldValue = getMethod.invoke(f);
} catch (Exception e) {
e.printStackTrace();
}
// 4.3.拼接sql
if(fieldValue == null ||
(fieldValue instanceof Integer && (Integer) fieldValue == 0)) {
continue;
}
sb.append("and ").append(fieldName);
if(fieldValue instanceof String) {
if(((String) fieldValue).contains(",")) {
String[] fieldValues = ((String) fieldValue).split(",");
sb.append(" in (");
for(String value : fieldValues) {
sb.append("'").append(value).append("',");
}
sb.deleteCharAt(sb.length() - 1);
sb.append(")");
} else {
sb.append(" = '").append(fieldValue).append("'");
}
} else if(fieldValue instanceof Integer) {
sb.append(" = ").append(fieldValue);
}
}
return sb.toString();
}
}