- 使用註解
- 註解是放在Java原始碼的類、方法、欄位、引數前的一種特殊“註釋”。
1 // this is a component: 2 @Resource("hello") 3 public class Hello { 4 @Inject 5 int n; 6 7 @PostConstruct 8 public void hello(@Param String name) { 9 System.out.println(name); 10 } 11 12 @Override 13 public String toString() { 14 return "Hello"; 15 } 16 }
註釋會被編譯器直接忽略,註解則可以被編譯器打包進入class檔案,因此,註解是一種用作標註的“後設資料”。
-
從JVM的角度看,註解本身對程式碼邏輯沒有任何影響,如何使用註解完全由工具決定。
-
Java的註解可以分為三類:
-
第一類是由編譯器使用的註解,這類註解不會被編譯進入
.class
檔案,它們在編譯後就被編譯器扔掉了。@Override
:讓編譯器檢查該方法是否正確地實現了覆寫;@SuppressWarnings
:告訴編譯器忽略此處程式碼產生的警告。
- 第二類是由工具處理
.class
檔案使用的註解,比如有些工具會在載入class的時候,對class做動態修改,實現一些特殊的功能。這類註解會被編譯進入.class
檔案,但載入結束後並不會存在於記憶體中。這類註解只被一些底層庫使用,一般我們不必自己處理。 - 第三類是在程式執行期能夠讀取的註解,它們在載入後一直存在於JVM中,這也是最常用的註解。例如,一個配置了
@PostConstruct
的方法會在呼叫構造方法後自動被呼叫(這是Java程式碼讀取該註解實現的功能,JVM並不會識別該註解)。
-
-
定義一個註解時,還可以定義配置引數。配置引數可以包括:
- 所有基本型別;
- String;
- 列舉型別;
- 基本型別、String、Class以及列舉的陣列。
- 因為配置引數必須是常量,所以,上述限制保證了註解在定義時就已經確定了每個引數的值。
- 註解的配置引數可以有預設值,缺少某個配置引數時將使用預設值。此外,大部分註解會有一個名為
value
的配置引數,對此引數賦值,可以只寫常量,相當於省略了value引數。如果只寫註解,相當於全部使用預設值。public class Hello { @Check(min=0, max=100, value=55) public int n; @Check(value=99) public int p; @Check(99) // @Check(value=99) public int x; @Check public int y; }
- 註解是放在Java原始碼的類、方法、欄位、引數前的一種特殊“註釋”。
- 定義註解
- Java語言使用
@interface
語法來定義註解(Annotation
),它的格式為:public @interface Report { int type() default 0; String level() default "info"; String value() default ""; }
註解的引數類似無引數方法,可以用
default
設定一個預設值(強烈推薦)。最常用的引數應當命名為value
。 -
有一些註解可以修飾其他註解,這些註解就稱為元註解(meta annotation)。Java標準庫已經定義了一些元註解,我們只需要使用元註解,通常不需要自己去編寫元註解。
-
最常用的元註解是
@Target
。使用@Target
可以定義Annotation
能夠被應用於原始碼的哪些位置。- 類或介面:
ElementType.TYPE
; - 欄位:
ElementType.FIELD
; - 方法:
ElementType.METHOD
; - 構造方法:
ElementType.CONSTRUCTOR
; - 方法引數:
ElementType.PARAMETER
。
- 類或介面:
- 例如,定義註解
@Report
可用在方法上,我們必須新增一個@Target(ElementType.METHOD)
。定義註解@Report
可用在方法或欄位上,可以把@Target
註解引數變為陣列{ ElementType.METHOD, ElementType.FIELD }
@Target(ElementType.METHOD) public @interface Report { int type() default 0; String level() default "info"; String value() default ""; }
@Target({ ElementType.METHOD, ElementType.FIELD }) public @interface Report { ... }
實際上
@Target
定義的value
是ElementType[]
陣列,只有一個元素時,可以省略陣列的寫法。 - 另一個重要的元註解
@Retention
定義了Annotation
的生命週期。- 僅編譯期:
RetentionPolicy.SOURCE
; - 僅class檔案:
RetentionPolicy.CLASS
; - 執行期:
RetentionPolicy.RUNTIME
。
- 僅編譯期:
- Java語言使用
如果@Retention
不存在,則該Annotation
預設為CLASS
。因為通常我們自定義的Annotation
都是RUNTIME
,所以,務必要加上@Retention(RetentionPolicy.RUNTIME)
這個元註解。
-
@Repeatable
這個元註解可以定義Annotation
是否可重複。@Repeatable(Reports.class) @Target(ElementType.TYPE) public @interface Report { int type() default 0; String level() default "info"; String value() default ""; } @Target(ElementType.TYPE) public @interface Reports { Report[] value(); }
經過@Repeatable修飾後,在某個型別宣告處,就可以新增多個@Report註解: @Report(type=1, level="debug") @Report(type=2, level="warning") public class Hello { }
- 使用
@Inherited
定義子類是否可繼承父類定義的Annotation
。@Inherited
僅針對@Target(ElementType.TYPE)
型別的annotation
有效,並且僅針對class
的繼承,對interface
的繼承無效。@Inherited @Target(ElementType.TYPE) public @interface Report { int type() default 0; String level() default "info"; String value() default ""; } 在使用的時候,如果一個類用到了@Report: @Report(type=1) public class Person { } 則它的子類預設也定義了該註解: public class Student extends Person { }
-
如何定義Annotation
1 //第一步,用@interface定義註解: 2 3 public @interface Report { 4 } 5 6 //第二步,新增引數、預設值: 7 8 public @interface Report { 9 int type() default 0; 10 String level() default "info"; 11 String value() default ""; 12 } 13 //把最常用的引數定義為value(),推薦所有引數都儘量設定預設值。 14 15 //第三步,用元註解配置註解: 16 17 @Target(ElementType.TYPE) 18 @Retention(RetentionPolicy.RUNTIME) 19 public @interface Report { 20 int type() default 0; 21 String level() default "info"; 22 String value() default ""; 23 }
其中,必須設定
@Target
和@Retention
,@Retention
一般設定為RUNTIME
,因為我們自定義的註解通常要求在執行期讀取。一般情況下,不必寫@Inherited
和@Repeatable
。
-
處理註解
SOURCE
型別的註解主要由編譯器使用,因此我們一般只使用,不編寫。CLASS
型別的註解主要由底層工具庫使用,涉及到class的載入,一般我們很少用到。只有RUNTIME
型別的註解不但要使用,還經常需要編寫。-
Java提供的使用反射API讀取
Annotation
的方法包括:判斷某個註解是否存在於
Class
、Field
、Method
或Constructor
:Class.isAnnotationPresent(Class)
Field.isAnnotationPresent(Class)
Method.isAnnotationPresent(Class)
Constructor.isAnnotationPresent(Class)
// 判斷@Report是否存在於Person類: Person.class.isAnnotationPresent(Report.class);
使用反射API讀取Annotation:
-
-
Class.getAnnotation(Class)
Field.getAnnotation(Class)
Method.getAnnotation(Class)
Constructor.getAnnotation(Class)
// 獲取Person定義的@Report註解: Report report = Person.class.getAnnotation(Report.class); int type = report.type(); String level = report.level();
- 使用反射API讀取
Annotation
有兩種方法。方法一是先判斷Annotation
是否存在,如果存在,就直接讀取。Class cls = Person.class; if (cls.isAnnotationPresent(Report.class)) { Report report = cls.getAnnotation(Report.class); ... }
-
第二種方法是直接讀取
Annotation
,如果Annotation
不存在,將返回null。
Class cls = Person.class; Report report = cls.getAnnotation(Report.class); if (report != null) { ... }
-
讀取方法、欄位和構造方法的
Annotation
和Class類似。但要讀取方法引數的Annotation
就比較麻煩一點,因為方法引數本身可以看成一個陣列,而每個引數又可以定義多個註解,所以,一次獲取方法引數的所有註解就必須用一個二維陣列來表示。例如,對於以下方法定義的註解。public void hello(@NotNull @Range(max=5) String name, @NotNull String prefix) { }
要讀取方法引數的註解,我們先用反射獲取
Method
例項,然後讀取方法引數的所有註解。// 獲取Method例項: Method m = ... // 獲取所有引數的Annotation: Annotation[][] annos = m.getParameterAnnotations(); // 第一個引數(索引為0)的所有Annotation: Annotation[] annosOfName = annos[0]; for (Annotation anno : annosOfName) { if (anno instanceof Range) { // @Range註解 Range r = (Range) anno; } if (anno instanceof NotNull) { // @NotNull註解 NotNull n = (NotNull) anno; } }
-
使用註解
-
註解如何使用,完全由程式自己決定。例如,JUnit是一個測試框架,它會自動執行所有標記為
@Test
的方法。 -
來看一個
@Range
註解,我們希望用它來定義一個String
欄位的規則:欄位長度滿足@Range
的引數定義。@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Range { int min() default 0; int max() default 255; }
在某個JavaBean中,我們可以使用該註解:
public class Person { @Range(min=1, max=20) public String name; @Range(max=10) public String city; }
但是,定義了註解,本身對程式邏輯沒有任何影響。我們必須自己編寫程式碼來使用註解。這裡,我們編寫一個
Person
例項的檢查方法,它可以檢查Person
例項的String
欄位長度是否滿足@Range
的定義:void check(Person person) throws IllegalArgumentException, ReflectiveOperationException { // 遍歷所有Field: for (Field field : person.getClass().getFields()) { // 獲取Field定義的@Range: Range range = field.getAnnotation(Range.class); // 如果@Range存在: if (range != null) { // 獲取Field的值: Object value = field.get(person); // 如果值是String: if (value instanceof String) { String s = (String) value; // 判斷值是否滿足@Range的min/max: if (s.length() < range.min() || s.length() > range.max()) { throw new IllegalArgumentException("Invalid field: " + field.getName()); } } } } }
這樣一來,我們通過
@Range
註解,配合check()
方法,就可以完成Person
例項的檢查。注意檢查邏輯完全是我們自己編寫的,JVM不會自動給註解新增任何額外的邏輯。
-
-