註解 是 Java 5 引入的「元數據標記」機制,不直接改變代碼邏輯,但為編譯器、工具、運行時框架提供豐富語義信息。本文系統梳理註解定義語法、四大元註解、保留策略、目標類型、反射 API 獲取、APT (Annotation Processing Tool) 編譯時處理流程,並以 JUnit/TestNG 風格單元測試框架為例展示註解驅動開發全鏈路。
package com.example.anno;
import java.lang.annotation.*;
@Target(ElementType.METHOD) // 作用目標:方法
@Retention(RetentionPolicy.RUNTIME) // 保留策略:運行時可見
@Documented // 納入 Javadoc
@Inherited // 子類繼承 (僅針對類註解)
public @interface TestCase {
int id() default -1; // 元素:類型 + 名稱 + default
String description() default "";
String[] tags() default {}; // 數組元素
Priority priority() default Priority.NORMAL; // 枚舉元素
enum Priority { LOW, NORMAL, HIGH, CRITICAL }
}
使用:@TestCase(id = 1001, description = "登錄功能", tags = {"smoke", "auth"}, priority = Priority.CRITICAL)
| 元註解 | 作用 | 可選值 / 含義 | 關鍵決策點 |
|---|---|---|---|
@Target | 註解可標註的程序元素 |
TYPE(類/接口/枚舉/註解)、FIELD、METHOD、PARAMETER、CONSTRUCTOR、LOCAL_VARIABLE、ANNOTATION_TYPE、PACKAGE、TYPE_PARAMETER(JDK8)、TYPE_USE(JDK8)
| 精準限制使用範圍,防止濫用;TYPE_USE 允許泛型類型參數上標註 (如 List<@NonNull String>) |
@Retention | 註解生命週期 |
SOURCE:僅源碼期,編譯器丟棄 (如 @Override、@SuppressWarnings)CLASS:編譯入 .class,JVM 不載入 (默認)RUNTIME:JVM 載入,反射可見 (框架首選)
| 框架註解必須 RUNTIME;純編譯期檢查用 SOURCE,減小 class 體積 |
@Documented | 標記是否納入 Javadoc | 無屬性,標註即生效 | 公共 API 註解建議加上,生成文檔時自動顯示 |
@Inherited | 類註解是否可被子類繼承 | 僅對 @Target(TYPE) 生效;不適用於接口實現、方法覆寫 | 慎用!容易誤以為方法註解可繼承,實際只對類繼承鏈生效 |
value(),單元素可簡寫 @Anno("val")。// 類註解
TestCase tc = MyClass.class.getAnnotation(TestCase.class);
// 方法註解
for (Method m : MyClass.class.getDeclaredMethods()) {
TestCase tc = m.getAnnotation(TestCase.class);
if (tc != null) {
System.out.println(m.getName() + " -> " + tc.id() + ", " + tc.description());
}
}
// 獲取所有註解 (含繼承)
Annotation[] annos = MyClass.class.getAnnotations(); // 僅 @Inherited + 直接標註
Annotation[] declared = MyClass.class.getDeclaredAnnotations(); // 僅直接標註
// 參數註解
Annotation[][] paramAnnos = method.getParameterAnnotations();
實戰模式:掃描包路徑 → 篩選帶註解類/方法 → 實例化 → 動態調用 → 收集結果 → 生成報告 (JUnit 核心流程)。
@SupportedAnnotationTypes("com.example.anno.TestCase")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class TestCaseProcessor extends AbstractProcessor {
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element elem : roundEnv.getElementsAnnotatedWith(TestCase.class)) {
// elem 可能是 TYPE/METHOD/FIELD...
TestCase anno = elem.getAnnotation(TestCase.class);
// 使用 Filer 生成代碼/文件
// processingEnv.getFiler().createSourceFile(...)
}
return true; // 聲稱已處理,不再傳遞給其他 processor
}
}
META-INF/services/javax.annotation.processing.Processor 文件內寫入處理器全限定名,或 Gradle annotationProcessor 'com.example:processor:1.0'。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test { int order() default 0; }
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BeforeEach {}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AfterEach {}
public class MiniTestRunner {
public static void run(Class> testClass) throws Exception {
Object instance = testClass.getDeclaredConstructor().newInstance();
// 收集並按 order 排序
List tests = Arrays.stream(testClass.getDeclaredMethods())
.filter(m -> m.isAnnotationPresent(Test.class))
.sorted(Comparator.comparingInt(m -> m.getAnnotation(Test.class).order()))
.toList();
Method before = Arrays.stream(testClass.getDeclaredMethods())
.filter(m -> m.isAnnotationPresent(BeforeEach.class))
.findFirst().orElse(null);
Method after = Arrays.stream(testClass.getDeclaredMethods())
.filter(m -> m.isAnnotationPresent(AfterEach.class))
.findFirst().orElse(null);
for (Method test : tests) {
if (before != null) before.invoke(instance);
try {
test.invoke(instance);
System.out.println("[PASS] " + test.getName());
} catch (InvocationTargetException e) {
System.err.println("[FAIL] " + test.getName() + " - " + e.getCause());
} finally {
if (after != null) after.invoke(instance);
}
}
}
}
public class UserServiceTest {
private UserService service;
@BeforeEach void init() { service = new UserServiceImpl(); }
@AfterEach void cleanup() { service = null; }
@Test(order = 1) void testLoginSuccess() {
assertTrue(service.login("admin", "123456"));
}
@Test(order = 2) void testLoginFail() {
assertFalse(service.login("admin", "wrong"));
}
}
// 主程序
MiniTestRunner.run(UserServiceTest.class);
| 場景 | 建議 |
|---|---|
| 框架開發 | 核心註解 RUNTIME + APT 生成輔助代碼 (如註冊表、元數據索引),雙軌並行 |
| 庫作者 | 提供 @Target(TYPE_USE) 類型註解 (如 @NonNull),配合 Checker Framework/SpotBugs 靜態分析 |
避坑 @Inherited | 方法/字段註解永不繼承,子類覆寫需重新標註;類註解繼承僅限 extends,不含 implements |
| 性能敏感 | 緩存 AnnotatedElement 反射結果 (Guava Cache / ConcurrentHashMap),避免重複解析 |
| 模塊化 | 註解定義獨立模塊 (anno-api),處理器單獨模塊 (anno-processor),消費者僅依賴 api |
Java 註解是「聲明式編程」在 Java 生態的核心載體。掌握 元註解語義、保留策略選擇、反射 vs APT 權衡、處理器編寫規範,即可從「註解使用者」進化為「註解框架設計者」。現代框架 (Spring 6、Spring Boot 3、Quarkus、Micronaut) 均採用 APT 優先、反射輔助 的混合模式,實現極致啟動速度與開發體驗的平衡。