註解是 Java 元編程的核心基礎,Spring、Hibernate、MyBatis、JUnit 等主流框架均大量依賴註解驅動。本文系統講解自定義註解全生命周期:定義語法、元註解配置、保留策略選擇、反射運行時處理、編譯時註解處理器 (Annotation Processor) 實現,並給出框架級設計模式。
@Target(ElementType.FIELD) // 作用目標:字段
@Retention(RetentionPolicy.RUNTIME) // 保留策略:運行時可見
@Documented // 納入 Javadoc
@Inherited // 允許子類繼承 (僅類註解)
@Repeatable(Validators.class) // Java 8+ 可重複註解
public @interface Validator {
// 註解屬性:類型 + 名稱 + 默認值
String message() default "驗證失敗";
int priority() default 0;
Class>[] groups() default {}; // 數組屬性
ValidatorMode mode() default ValidatorMode.STRICT; // 枚舉屬性
}
// 配套枚舉
public enum ValidatorMode { STRICT, LENIENT }
// 容器註解 (Java 8+ @Repeatable 需要)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Validators {
Validator[] value();
}
| 元註解 | 作用 | 可選值 | 關鍵決策點 |
|---|---|---|---|
@Target | 註解可用位置 | TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE, TYPE_PARAMETER, TYPE_USE (Java 8+) | 精確限制使用範圍,防止濫用 |
@Retention | 生命周期 | SOURCE (源碼), CLASS (字節碼, 默認), RUNTIME (運行時) | SOURCE: 編譯時處理 (Lombok/MapStruct) CLASS: 字節碼分析工具 RUNTIME: 反射驅動框架 (Spring/JPA) |
@Documented | 是否進入 Javadoc | 無屬性 | 公共 API 註解建議加上 |
@Inherited | 類註解是否可繼承 | 無屬性 | 僅對 @Target(TYPE) 生效,接口實現不繼承 |
@Repeatable | 同一位置可多次使用 | 容器註解類 | Java 8+,替代舊式容器註解模式 |
@Native | 常量可能被 JNI 使用 | 無屬性 | 極少用,JDK 內部 |
需要在運行時通過反射讀取?
├─ 是 → @Retention(RUNTIME) (Spring @Autowired, JPA @Entity)
└─ 否
├─ 需要在編譯時處理 (生成代碼、檢查、修改 AST)?
│ ├─ 是 → @Retention(SOURCE) (Lombok @Getter, MapStruct @Mapper)
│ └─ 否
│ ├─ 需要在字節碼分析工具中可見?
│ │ ├─ 是 → @Retention(CLASS) (默認)
│ │ └─ 否 → @Retention(SOURCE) (最輕量)
// 1. 註解定義
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiEndpoint {
String path();
HttpMethod method() default HttpMethod.GET;
boolean authRequired() default true;
}
// 2. 掃描與處理器
public class ApiScanner {
public static Map scanEndpoints(Class> controllerClass) {
Map endpoints = new HashMap<>();
for (Method method : controllerClass.getDeclaredMethods()) {
ApiEndpoint annotation = method.getAnnotation(ApiEndpoint.class);
if (annotation != null) {
String key = annotation.method() + ":" + annotation.path();
if (endpoints.containsKey(key)) {
throw new IllegalStateException("重複端點: " + key);
}
endpoints.put(key, method);
// 處理其他邏輯:權限檢查、參數驗證、文檔生成...
}
}
return endpoints;
}
// 3. 運行時調用示例
public static Object invokeEndpoint(Object controller, String path, HttpMethod method, Object... args)
throws Exception {
Map endpoints = scanEndpoints(controller.getClass());
String key = method + ":" + path;
Method m = endpoints.get(key);
if (m == null) throw new NoSuchMethodException("端點不存在: " + key);
m.setAccessible(true); // 支援 private 方法
return m.invoke(controller, args);
}
}
// 使用
@Controller
class UserController {
@ApiEndpoint(path = "/users", method = HttpMethod.GET)
public List listUsers() { ... }
@ApiEndpoint(path = "/users/{id}", method = HttpMethod.GET, authRequired = false)
public User getUser(@Param("id") Long id) { ... }
}
// 1. 註解定義 (SOURCE 保留)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface GenerateBuilder {
String prefix() default "";
}
// 2. 處理器實現
@SupportedAnnotationTypes("com.example.GenerateBuilder")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class BuilderProcessor extends AbstractProcessor {
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
if (element.getKind() != ElementKind.CLASS) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@GenerateBuilder 僅能用於類", element);
continue;
}
TypeElement classElement = (TypeElement) element;
generateBuilderClass(classElement);
}
}
return true; // 聲稱處理了這些註解
}
private void generateBuilderClass(TypeElement classElement) {
// 使用 JavaPoet / Filer API 生成代碼
// 詳見: https://github.com/square/javapoet
String packageName = processingEnv.getElementUtils().getPackageOf(classElement).getQualifiedName().toString();
String className = classElement.getSimpleName().toString();
String builderName = className + "Builder";
// 生成邏輯...
// JavaFile.builder(packageName, typeSpec).build().writeTo(filer);
}
}
// 3. 註冊處理器: META-INF/services/javax.annotation.processing.Processor
// 內容: com.example.BuilderProcessor
// 4. 使用者項目啟用: maven-compiler-plugin 或 IDE 設置
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Transactional // 組合現有註解
@Cacheable(cacheNames = "users") // 組合緩存註解
@Retryable(maxAttempts = 3) // 組合重試註解
public @interface UserServiceOperation {
String value() default "";
}
public @interface CacheConfig {
@AliasFor("cacheNames") String value() default "";
@AliasFor("value") String[] cacheNames() default {};
// 使用者可寫 @CacheConfig("users") 或 @CacheConfig(cacheNames="users")
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ConditionalOnProperty(prefix = "feature", name = "enabled", havingValue = "true")
public @interface EnableFeature {
String name() default "";
}
@Transactional、@Nullable),屬性名用名詞 (value、message、groups)@Documented,屬性加 Javadoc@Target 精確限制位置,避免濫用Messager 輸出友好編譯錯誤,包含位置信息| 坑點 | 現象 | 解決 |
|---|---|---|
| 數組屬性單值寫法 | @Anno("a") vs @Anno({"a"}) | 單值可省略大括號,多值必須加 |
| 枚舉屬性默認值 | 無法直接寫枚舉常量 | 默認值用字符串,處理器中轉換 |
| @Inherited 失效 | 接口實現/字段不繼承 | 僅類繼承生效,接口需手工掃描 |
| SOURCE 註解運行時不可見 | 反射獲取為 null | 確認保留策略匹配使用場景 |
| APT 增量編譯失效 | 修改註解類不觸發重新處理 | 配置 maven-compiler-plugin forceJavacCompilerUse |