Java 5 引入註解機制,將元數據與代碼統一,成為框架開發、編譯時檢查、代碼生成的基石。本文基於《Thinking in Java》第 20 章,系統梳理註解語法、元註解、屬性約束、處理器編寫、apt 工具、Visitor 模式應用及單元測試整合。
.class 文件,運行期可通過反射讀取(RetentionPolicy.RUNTIME)。
@Target(ElementType.METHOD | ElementType.FIELD) // 作用目標
@Retention(RetentionPolicy.RUNTIME) // 生命週期:SOURCE/CLASS/RUNTIME
@Documented // 納入 Javadoc
@Inherited // 允許子類繼承(僅對類級註解有效)
public @interface MyAnnotation {
String value() default ""; // 默認屬性名為 value,單屬性可省略名稱
int count() default 1;
String[] tags() default {}; // 數組屬性
Class> clazz() default Object.class; // Class 類型屬性
MyEnum enumVal() default MyEnum.V1; // 枚舉類型
// 不允許基本類型包裝器作為屬性類型(自動裝箱後可用)
}
| 元註解 | 作用 | 可選值 |
|---|---|---|
@Target | 限定註解可應用位置 | ElementType 枚舉:TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE, TYPE_PARAMETER, TYPE_USE |
@Retention | 限定註解保留階段 | SOURCE(源碼丟棄)、CLASS(編譯期保留,默認)、RUNTIME(運行期反射可見) |
@Documented | 標記納入 Javadoc | 無 |
@Inherited | 子類自動繼承父類類級註解 | 無(僅對 @Target(TYPE) 生效) |
@Repeatable | Java 8+ 允許同一位置重複使用 | 需定義容器註解 |
null 默認值(基本類型不可為 null),但可顯式指定 default "" 或 default null(針對 Class/枚舉/註解/數組)。value 時,使用註解可省略屬性名:@MyAnnotation("hello")。
public class RuntimeProcessor {
public static void process(Class> clazz) {
if (clazz.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation ann = clazz.getAnnotation(MyAnnotation.class);
System.out.println("value=" + ann.value() + ", count=" + ann.count());
}
// 方法級處理
for (Method m : clazz.getDeclaredMethods()) {
if (m.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation ann = m.getAnnotation(MyAnnotation.class);
// 業務邏輯:生成代碼、驗證、註冊路由等
}
}
}
}
javax.annotation.processing (JSR 269)實現 AbstractProcessor,在編譯階段掃描註解,生成源碼/資源文件/編譯錯誤/警告。優勢:零運行期開銷,可生成 Builder、DTO、Router 等樣板代碼。
@SupportedAnnotationTypes("com.example.MyAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyProcessor extends AbstractProcessor {
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element elem : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) {
// 使用 Filer 生成文件
Filer filer = processingEnv.getFiler();
// 例如生成 XXX_Generated.java
}
return true; // 聲稱已處理,不再傳遞給其他處理器
}
}
註冊方式:META-INF/services/javax.annotation.processing.Processor 文件中寫入處理器全限定名。
apt (Annotation Processing Tool)Visitor 模式遍歷 AST,解耦操作與節點類型。當註解類型增多,處理邏輯分散在多個 if-else 中難以維護。引入 Visitor:
AnnotationVisitor 接口,每種註解一個 visitXxx 方法。accept(visitor)。將測試代碼內嵌業務類,通過註解標記測試方法,運行期掃描執行:
public @interface Test { }
public class Calculator {
public int add(int a, int b) { return a + b; }
@Test
private void testAdd() { // 可測試 private 方法
assert add(1, 2) == 3;
}
}
// 測試運行器掃描 @Test 並執行
public class TestRunner {
public static void run(Class> clazz) throws Exception {
for (Method m : clazz.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
m.setAccessible(true); // 突破 private
m.invoke(clazz.newInstance());
}
}
}
}
優勢:測試與被測代碼同文件,重構同步;可直接測試私有方法。缺點:生產代碼體積膨脹,現代實踐多採用獨立測試源碼集(JUnit/TestNG)。
@interface 不能 extends 其他註解。@Inherited 實現類級繼承語義。