在項目中經常會用到自定義注解,下面講解一下自定義注解的理解及其應用。
一、元注解
元注解的作用就是負責注解其他注解。Java5.0定義了4個標准的meta-annotation類型,它們被用來提供對其它 annotation類型作說明。
Java5.0定義的元注解:
- @Target
- @Retention
- @Documented
- @Inherited
@Target
作用:描述該注解修飾的范圍,可被用於 packages、types(類、接口、枚舉、Annotation類型)、類型成員(方法、構造方法、成員變量、枚舉值)、方法參數和本地變量(如循環變量、catch參數)。
取值(ElementType):
CONSTRUCTOR:用於描述構造器;
FIELD:用於描述域;
LOCAL_VARIABLE:用於描述局部變量;
METHOD:用於描述方法;
PACKAGE:用於描述包;
PARAMETER:用於描述參數;
TYPE:用於描述類、接口(包括注解類型) 或enum聲明;
@Retention
作用:描述該注解的生命周期,表示在什么編譯級別上保存該注解的信息。Annotation被保留的時間有長短:某些Annotation僅出現在源代碼中,而被編譯器丟棄;而另一些卻被編譯在class文件中;編譯在class文件中的Annotation可能會被虛擬機忽略,而另一些在class被裝載時將被讀取(請注意並不影響class的執行,因為Annotation與class在使用上是被分離的)
取值(RetentionPoicy):
SOURCE:在源文件中有效(即源文件保留)
CLASS:在class文件中有效(即class保留)
RUNTIME:在運行時有效(即運行時保留)
@Documented
@Documented Annotation的作用是在生成javadoc文檔的時候將該Annotation也寫入到文檔中
@Inherited
作用:@Inherited 元注解是一個標記注解,@Inherited闡述了某個被標注的類型是被繼承的。如果一個使用了@Inherited修飾的annotation類型被用於一個class,則這個annotation將被用於該class的子類。
二、自定義注解
使用@interface自定義注解,自動繼承了java.lang.annotation.Annotation接口,由編譯程序自動完成其他細節。在定義注解時,不能繼承其他的注解或接口。@interface用來聲明一個注解,其中的每一個方法實際上是聲明了一個配置參數。方法的名稱就是參數的名稱,返回值類型就是參數的類型(返回值類型只能是基本類型、Class、String、enum)。可以通過default來聲明參數的默認值。
定義注解格式:public @interface 注解名 {定義體}
注解參數的可支持數據類型:
- 所有基本數據類型(int,float,boolean,byte,double,char,long,short)
- String類型
- Class類型
- enum類型
- Annotation類型
- 以上所有類型的數組
參數定義要點:
- 只能用public或默認(default)這兩個訪問權修飾;
- 參數成員只能用基本類型byte,short,char,int,long,float,double,boolean八種基本數據類型和 String,Enum,Class,annotations等數據類型,以及這一些類型的數組;
- 如果只有一個參數成員,建議參數名稱設為value();
- 注解元素必須有確定的值,要么在定義注解的默認值中指定,要么在使用注解時指定,非基本類型的注解元素的值不可為null。因此, 使用空字符串或負數作為默認值是一種常用的做法。
簡單的自定義注解實例:
/** *自定義注解MyAnnotation */ @Target(ElementType.TYPE) //目標對象是類型 @Retention(RetentionPolicy.RUNTIME) //保存至運行時 @Documented //生成javadoc文檔時,該注解內容一起生成文檔 @Inherited //該注解被子類繼承 public @interface MyAnnotation { public String value() default ""; //當只有一個元素時,建議元素名定義為value(),這樣使用時賦值可以省略"value=" String name() default "devin"; //String int age() default 18; //int boolean isStudent() default true; //boolean String[] alias(); //數組 enum Color {GREEN, BLUE, RED,} //枚舉類型 Color favoriteColor() default Color.GREEN; //枚舉值 } @MyAnnotation( value = "info", name = "myname", age = 99, isStudent = false, alias = {"name1", "name2"}, favoriteColor = MyAnnotation.Color.RED ) public class MyClass { //使用MyAnnotation注解,該類生成的javadoc文檔包含注解信息如下: /* @MyAnnotation(value = "info", name = "myname", age = 99, isStudent = false, alias = {"name1","name2"}, favoriteColor = Color.RED) public class MyClass extends Object */ } public class MySubClass extends MyClass{ //子類MySubClass繼承了父類MyClass的注解 }
解析注解信息
Java使用Annotation接口來代表程序元素前面的注解,該接口是所有Annotation類型的父接口。相應地,Java在java.lang.reflect 包下新增了AnnotatedElement接口,該接口代表程序中可以接受注解的程序元素。
實際上,java.lang.reflect 包所有提供的反射API擴充了讀取運行時Annotation信息的能力。當一個Annotation類型被定義為運行時的Annotation后,該注解才能是運行時可見,當class文件被裝載時被保存在class文件中的Annotation才會被虛擬機讀取。
AnnotatedElement接口是所有程序元素(Field、Method、Package、Class和Constructor)的父接口,所以程序通過反射獲取了某個類的AnnotatedElement對象之后,程序就可以調用該對象的如下七個方法來訪問Annotation信息:
- <T extends Annotation> T getAnnotation(Class<T> annotationClass) :返回該程序元素上存在的、指定類型的注解,如果該類型注解不存在,則返回null;
- Annotation[] getDeclaredAnnotation(Class<T>):返回該程序元素上存在的、指定類型的注解,如果該類型注解不存在,則返回null;與此接口中的其他方法不同,該方法將忽略繼承的注解;
- Annotation[] getAnnotations():返回該程序元素上存在的所有注解;
- Annotation[] getDeclaredAnnotations():返回直接存在於此元素上的所有注釋。與此接口中的其他方法不同,該方法將忽略繼承的注解;
- Annotation[] getAnnotationsByType(Class<T>):返回直接存在於此元素上指定注解類型的所有注解;
- Annotation[] getDeclaredAnnotationsByType(Class<T>):返回直接存在於此元素上指定注解類型的所有注解。與此接口中的其他方法不同,該方法將忽略繼承的注解;
- boolean isAnnotationPresent(Class<?extends Annotation> annotationClass):判斷該程序元素上是否包含指定類型的注解,存在則返回true,否則返回false;
/***********注解聲明***************/ /** * 水果名稱注解 */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface FruitName { String value() default " "; } /** * 水果顏色注解 */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface FruitColor { /** * 顏色枚舉 */ public enum Color{BLUE, RED, GREEN}; /** * 顏色屬性 * @return */ Color fruitColor() default Color.GREEN; } /** * 水果供應商注解 */ @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 " "; } /***********注解使用***************/ public class Apple { @FruitName("Apple") private String appleName; @FruitColor(fruitColor = FruitColor.Color.RED) private String appleColor; @FruitProvider(id = 1, name = "陝西紅富士集團", address = "陝西紅富士大廈") private String appleProvider; public String getAppleProvider() { return appleProvider; } public void setAppleProvider(String appleProvider) { this.appleProvider = 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 void displayName(){ System.out.println(getAppleName()); } } /***********注解信息獲取***************/ public class AnnotationParser { public static void main(String[] args) { Field[] fields = Apple.class.getDeclaredFields(); for (Field field : fields) { //System.out.println(field.getName().toString()); if (field.isAnnotationPresent(FruitName.class)){ FruitName fruitName = field.getAnnotation(FruitName.class); System.out.println("水果的名稱:" + fruitName.value()); }else if (field.isAnnotationPresent(FruitColor.class)){ FruitColor fruitColor = field.getAnnotation(FruitColor.class); System.out.println("水果的顏色:"+fruitColor.fruitColor()); }else if (field.isAnnotationPresent(FruitProvider.class)){ FruitProvider fruitProvider = field.getAnnotation(FruitProvider.class); System.out.println("水果供應商編號:" + fruitProvider.id() + " 名稱:" + fruitProvider.name() + " 地址:" + fruitProvider.address()); } } } } /***********輸出結果***************/ 水果的名稱:Apple 水果的顏色:RED 水果供應商編號:1 名稱:陝西紅富士集團 地址:陝西紅富士大廈
JDK8注解新特性
JDK 8 主要有兩點改進:類型注解和重復注解
- 類型注解:類型注解在@Target中增加了兩個ElementType參數:
1、ElementType.TYPE_PARAMETER 表示該注解能寫在類型變量的聲明語句中;
2、ElementType.TYPE_USE 表示該注解能寫在使用類型的任何語句中(例如聲明語句、泛型和強制轉換語句中的類型);
從而擴展了注解使用的范圍,可以使用在創建類實例、類型映射、implements語句、throw exception聲明中的類型前面。例如:
1、創建類實例: new @Interned MyObject();
2、類型映射:myString = (@NonNull String) str;
3、implements 語句中 :class UnmodifiableList<T> implements @Readonly List<@Readonly T> { ... }
4、throw exception聲明:void monitorTemperature() throws @Critical TemperatureException { ... }
簡單示例:
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) public @interface Encrypted { } public class MyTypeAnnotation { @Encrypted String data; List<@Encrypted String> strings; }
類型注解的作用:
首先,局域變量聲明中的類型注解也可以保留在類文件中,完整泛型被保留,並且在運行期可以訪問,從而有助於我們獲取更多的代碼信息;其次,類型注解可以支持在的程序中做強類型檢查。配合第三方工具check framework,可以在編譯的時候檢測出runtime error,以提高代碼質量;最后,代碼中包含的注解清楚表明了編寫者的意圖,使代碼更具有表達意義,有助於閱讀者理解程序,畢竟代碼才是“最根本”的文檔、“最基本”的注釋。
重復注解
重復注釋就是運行在同一元素上多次使用同一注解,使用@Repeatable注解。之前也有重復使用注解的解決方案,但可讀性不是很好,例如:
public @interface Authority { String role(); } public @interface Authorities { Authority[] value(); } public class RepeatAnnotationUseOldVersion { @Authorities({@Authority(role="Admin"),@Authority(role="Manager")}) public void doSomeThing(){ } }
而現在的實現如下:
@Repeatable(Authorities.class) public @interface Authority { String role(); } public @interface Authorities { Authority[] value(); } public class RepeatAnnotationUseNewVersion { @Authority(role="Admin") @Authority(role="Manager") public void doSomeThing(){ } }
不同的地方是,創建重復注解Authority時,加上@Repeatable,指向存儲注解Authorities,在使用時候,直接可以重復使用Authority注解。從上面例子看出,java 8里面做法更適合常規的思維,可讀性強一點。
參見:http://www.jianshu.com/p/4068da3c8d3d
三、自定義注解的應用
(1)利用自定義注解打印接口調用時長日志
#創建ServiceLog類用來自定義注解 @Retention(RetentionPolicy.Runtime) @Target(ElementType.METHOD) public @interface ServiceLog { } #定義ServiceLogAspect類用來定義日志打印信息 @Component @Aspect public class ServiceLogAspect { public ThreadLocal<Long> local = new ThreadLocal<Long>(); @Pointcut("@annotation(com.test.XXX.ServiceLong)") public void pointCut() { } @Before("pointCut()") public void before(JoinPoint point) { String methodName = point.getTarget().getClass().getName()+"."+point.getSignature().getName(); local.set(System.currentTimeMillis()); } @After("pointCut()") public void after(JoinPoint point) { long start = local.get(); String methodName = point.getTarget().getClass().getName()+"."+point.getSignature().getName(); System.out.println(System.currentTimeMillis()-start)); } @AfterThrowing(pointcut="pointCut()",throwing="error") public void throwing(JoinPoint point,Throwable error) { System.out.println("error"); } }
完成上述定義,如果需要記錄方法調用時長時,可以直接使用@ServiceLog注解。