Skip to content

注解定义与运行时读取 ​

学习目标 ​

  • 能定义带成员的注解,并选择正确的保留策略和目标位置。
  • 能从类、方法、字段和类型使用位置读取运行时注解。
  • 能分清 @Inherited、@Repeatable、默认值和 SOURCE/CLASS/RUNTIME 的边界。

核心知识点 ​

专业术语 ​

  • 元注解(meta-annotation):用于描述另一个注解的注解,如 @Retention、@Target。
  • RetentionPolicy:控制注解保留到 SOURCE、CLASS 还是 RUNTIME。
  • Target:限制注解能标注的 Java 元素,如 TYPE、METHOD、FIELD、TYPE_USE。
  • AnnotatedElement:Class、Method、Field 等可被查询注解的统一接口。

白话解释与边界 ​

注解只是元数据,不会自己执行逻辑;处理它的是编译器、注解处理器或运行时框架。需要反射读取就必须选择 RUNTIME,只给编译器看的检查可以选 SOURCE。注解成员只能是基本类型、String、Class、枚举、注解或数组,默认值不能使用 null。

常用用法 ​

@Retention(RUNTIME):让注解留到运行时 ​

RUNTIME 是反射读取的前提;如果没有声明 @Retention,默认策略是 CLASS,运行时通常读不到。

java
// 说明:@Retention(RUNTIME):让注解留到运行时。
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@interface Endpoint { String value(); }

@Endpoint("/health")
class HealthEndpoint { }

public class RuntimeRetentionDemo {
    public static void main(String[] args) {
        System.out.println(HealthEndpoint.class.isAnnotationPresent(Endpoint.class));
// 输出:true
    }
}

@Target:限制注解使用位置 ​

只开放实际需要的位置能减少误用;如果注解要标注泛型、数组元素或返回类型,要额外考虑 TYPE_USE。

java
// 说明:@Target:限制注解使用位置。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
// 作用:@Target({ElementType.TYPE, ElementType.METHOD});限制注解使用位置。
@interface DocumentedRoute { String value(); }

@DocumentedRoute("/users")
class UserApi {
    @DocumentedRoute("/list")
    void list() { }
}

public class TargetDemo {
    public static void main(String[] args) throws Exception {
        System.out.println(UserApi.class.getDeclaredMethod("list")
                .isAnnotationPresent(DocumentedRoute.class));
// 输出:true
    }
}

注解成员与默认值 ​

有合理默认值的成员可以减少调用噪声;需要强制调用方说明的成员不要随意给默认值,否则配置缺失可能静默采用错误行为。

java
// 说明:注解成员与默认值。
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@interface Retry {
    int maxAttempts() default 3;
    String reason() default "temporary";
}

@Retry(maxAttempts = 5)
class PaymentClient { }

public class AnnotationMemberDemo {
    public static void main(String[] args) {
        Retry retry = PaymentClient.class.getAnnotation(Retry.class);
// 初始状态:retry = PaymentClient.class.getAnnotation(Retry.class)。
// 作用:Retry retry = PaymentClient.class.getAnnotation(Retry.class);;读取 PaymentClient 的 @Retry(maxAttempts = 5) 实例,maxAttempts 输出 5。
        System.out.println(retry.maxAttempts() + " / " + retry.reason());
// 输出:5 / temporary
    }
}

查询单个注解:先判断或直接读取 ​

isAnnotationPresent 只问是否存在,getAnnotation 返回实例或 null;调用方要把缺失注解视为正常分支或明确失败,避免盲目解引用。

java
// 说明:查询单个注解:先判断或直接读取。
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@interface Owner { String value(); }

@Owner("team-java")
class Account { }

public class AnnotationLookupDemo {
    public static void main(String[] args) {
        System.out.println(Account.class.isAnnotationPresent(Owner.class));
// 输出:true
        System.out.println(Account.class.getAnnotation(Owner.class).value());
// 输出:team-java
    }
}

getDeclaredAnnotation:只看当前声明 ​

普通注解默认不沿继承链查找;getDeclaredAnnotation 更明确地只查看当前元素,适合框架避免把父类元数据误当作子类声明。

java
// 说明:getDeclaredAnnotation:只看当前声明。
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@interface Marker { }

@Marker
class ParentType { }
class ChildType extends ParentType { }

public class DeclaredAnnotationDemo {
    public static void main(String[] args) {
        System.out.println(ChildType.class.getDeclaredAnnotation(Marker.class));
// 输出:null
        System.out.println(ChildType.class.getAnnotation(Marker.class) != null);
// 输出:false
    }
}

查询注解集合:选择继承或当前声明范围 ​

数组方法返回当前可见的全部注解;如果需要判断继承,先明确注解是否有 @Inherited,不要从数组长度推断业务语义。

java
// 说明:查询注解集合:选择继承或当前声明范围。
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@interface Tag { String value(); }

@Tag("parent")
class TaggedParent { }
class TaggedChild extends TaggedParent { }

public class AnnotationArrayDemo {
    public static void main(String[] args) {
        System.out.println(TaggedChild.class.getAnnotations().length);
// 输出:0
        System.out.println(TaggedChild.class.getDeclaredAnnotations().length);
// 输出:0(第2次输出)
    }
}

@Inherited:类级注解继承 ​

@Inherited 只影响类继承和类级注解查询,不影响方法、字段、接口实现或注解本身复制;框架仍需明确覆盖规则。

java
// 说明:@Inherited:类级注解继承。
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Inherited
// 作用:@Inherited;类级注解继承。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Role { String value(); }

@Role("admin")
class AdminBase { }
class AdminChild extends AdminBase { }

public class InheritedDemo {
    public static void main(String[] args) {
        System.out.println(AdminChild.class.getAnnotation(Role.class).value());
// 输出:admin
    }
}

@Repeatable:重复标注 ​

通过 getAnnotationsByType 读取重复注解最直观;容器注解 Routes 仍是底层表示,业务代码不应同时维护两套读取路径而产生重复。

java
// 说明:@Repeatable:重复标注。
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(Routes.class)
// 作用:@Repeatable(Routes.class);重复标注。
@interface Route { String value(); }

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Routes { Route[] value(); }

@Route("/v1")
@Route("/v2")
class VersionedApi { }

public class RepeatableDemo {
    public static void main(String[] args) {
        System.out.println(VersionedApi.class.getAnnotationsByType(Route.class).length);
// 输出:2
    }
}

TYPE_USE:类型使用位置注解 ​

TYPE_USE 让注解落在类型表达式上,适合 nullness、类型校验等工具;它和 ElementType.METHOD 的方法声明注解不是同一个位置。

java
// 说明:TYPE_USE:类型使用位置注解。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
@interface NonNull { }

public class TypeUseDemo {
    static @NonNull String message() { return "java"; }

    public static void main(String[] args) throws Exception {
        var type = TypeUseDemo.class.getDeclaredMethod("message").getAnnotatedReturnType();
// 初始状态:type = TypeUseDemo.class.getDeclaredMethod("message").getAnnotatedReturnType()。
// 作用:var type = TypeUseDemo.class.getDeclaredMethod("message").getAnnotatedReturnType();;类型使用位置注解。
        System.out.println(type.isAnnotationPresent(NonNull.class));
// 输出:true
    }
}

不常用但需要知道 ​

RetentionPolicy.SOURCE:与 CLASS 保留策略对照 ​

SOURCE 不进入 class 文件,CLASS 进入 class 文件但通常不在反射运行时可见;需要运行时读取必须使用 RUNTIME。

java
// 说明:CompileOnly 在编译后被丢弃,ClassFileOnly 保留于 class 文件但不供反射读取,所以 getAnnotations() 返回空数组。
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.SOURCE)
@interface CompileOnly { }

@Retention(RetentionPolicy.CLASS)
@interface ClassFileOnly { }

@CompileOnly
@ClassFileOnly
class CompileMetadata { }

public class RetentionBoundaryDemo {
    public static void main(String[] args) {
        System.out.println(CompileMetadata.class.getAnnotations().length);
// 输出:0
    }
}

@Documented:生成 API 文档 ​

@Documented 影响 Javadoc 展示,不会改变运行时读取或继承;公共库注解应按文档契约单独决定是否加上它。

java
// 作用:通过 @Documented 生成 API 文档。
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Documented
// 作用:@Documented;生成 API 文档。
@Retention(RetentionPolicy.RUNTIME)
@interface PublicApi { }

@PublicApi
class PublicService { }

public class DocumentedDemo {
    public static void main(String[] args) {
        System.out.println(PublicService.class.isAnnotationPresent(PublicApi.class));
// 输出:true
    }
}

识别注解类型:从实例取得声明接口 ​

统一使用 Annotation 接口可以构建通用扫描器;访问具体成员时仍需转换为目标注解类型,避免反射式字符串访问。

java
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@interface Version { int value(); }

@Version(2)
class Versioned { }

public class AnnotationTypeDemo {
    public static void main(String[] args) {
        var annotation = Versioned.class.getAnnotation(Version.class);
// 初始状态:annotation = Versioned.class.getAnnotation(Version.class)。
// 作用:var annotation = Versioned.class.getAnnotation(Version.class);;从实例取得声明接口。
        System.out.println(annotation.annotationType().getSimpleName());
// 输出:Version
    }
}

getAnnotationsByType:统一处理单个或重复注解 ​

该方法会展开容器注解,适合扫描器直接消费;如果需要区分“未标注”和“显式容器为空”,仍应读取声明注解做更细的策略。

java
// 作用:通过 getAnnotationsByType 统一处理单个或重复注解。
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(Checks.class)
@interface Check { String value(); }

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Checks { Check[] value(); }

public class AnnotationByTypeDemo {
    @Check("auth")
    @Check("audit")
    static void save() { }

    public static void main(String[] args) throws Exception {
        System.out.println(AnnotationByTypeDemo.class.getDeclaredMethod("save")
                .getAnnotationsByType(Check.class).length);
// 输出:2
// 输入:目标方法为 AnnotationByTypeDemo.save,查询类型为 Check.class。
// 作用:getAnnotationsByType(Check.class) 展开 save 上的两个 Check 注解并返回数组。
    }
}

注解属性不能为 null ​

注解成员声明不能用 null 默认值;需要表达“未配置”时使用空字符串、特殊枚举值或拆分成显式 boolean 成员,并在解析层做校验。

java
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@interface Label { String value(); }

public class AnnotationNullBoundaryDemo {
    public static void main(String[] args) {
        Label label = null;
// 初始状态:label = null。
// 作用:Label label = null;;label 保持 null,label == null 返回 true。
        System.out.println(label == null);
// 输出:true
    }
}

简单案例 ​

Field.get:反射读取字段值 ​

读取实例字段要传接收对象,并处理可访问性与受检异常边界。

java
// 作用:通过 Field.get 反射读取字段值。
import java.lang.reflect.Field;
record User(String name) {}
Field field = User.class.getDeclaredField("name");
// 初始状态:field = User.class.getDeclaredField("name")。
// 作用:Field field = User.class.getDeclaredField("name");;反射读取字段值。
field.setAccessible(true);
// 作用:field.setAccessible(true);;反射读取字段值。
System.out.println(field.get(new User("Ann")));
// 输出:Ann

Field.set:反射写入可变字段 ​

写入值必须与字段类型兼容;框架代码还应限制允许修改的字段集合。

java
// 作用:通过 Field.set 反射写入可变字段。
import java.lang.reflect.Field;
class User { private String name = "old"; }
// 初始状态:name = "old"; }。
var user = new User();
// 作用:var user = new User();;反射写入可变字段。
// 初始状态:user = new User()。
Field field = User.class.getDeclaredField("name");
// 作用:Field field = User.class.getDeclaredField("name");;反射写入可变字段。
// 初始状态:field = User.class.getDeclaredField("name")。
field.setAccessible(true);
// 作用:field.setAccessible(true);;反射写入可变字段。
field.set(user, "new");
// 初始状态:field.set(user, "new")。
// 作用:field.set(user, "new");;反射写入可变字段。
System.out.println(field.get(user));
// 输出:new

AnnotationUtils.findAnnotation:查找类层次中的注解 ​

Spring 的工具方法会沿类层次查找注解,适合代理或继承场景下读取声明元数据。

java
// 作用:通过 AnnotationUtils.findAnnotation 查找类层次中的注解。
import org.springframework.core.annotation.AnnotationUtils;
@Deprecated class Parent {}
class Child extends Parent {}
var annotation = AnnotationUtils.findAnnotation(Child.class, Deprecated.class);
// 初始状态:annotation = AnnotationUtils.findAnnotation(Child.class, Deprecated.class)。
// 作用:var annotation = AnnotationUtils.findAnnotation(Child.class, Deprecated.class);;查找类层次中的注解。
System.out.println(annotation != null);
// 输出:true
System.out.println(annotation.annotationType().getSimpleName());
// 输出:Deprecated
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Command { String value(); }

public class AnnotationDemo {
    @Command("status")
    static String status() { return "ok"; }

    public static void main(String[] args) throws Exception {
        var method = AnnotationDemo.class.getDeclaredMethod("status");
// 初始状态:method = AnnotationDemo.class.getDeclaredMethod("status")。
// 作用:var method = AnnotationDemo.class.getDeclaredMethod("status");;查找类层次中的注解。
        Command command = method.getAnnotation(Command.class);
// 作用:Command command = method.getAnnotation(Command.class);;查找类层次中的注解。
// 初始状态:command = method.getAnnotation(Command.class)。
        System.out.println(command.value() + " -> " + method.invoke(null));
// 输出:status -> ok
    }
}

这是运行时命令注册的最小模型:注解提供元数据,反射执行目标;生产扫描器还要限制包范围、校验参数签名和处理重复命令。

易混点 ​

  • SOURCE、CLASS、RUNTIME 分别面向源代码、class 文件和运行时;反射读取只能依赖 RUNTIME。
  • @Inherited 只对类级注解生效,不会让方法和字段注解自动沿继承链出现。
  • @Repeatable 的业务读取优先用 getAnnotationsByType,不必手动解包容器。
  • ElementType.METHOD 和 TYPE_USE 标注的位置不同,不能只看注解名字推断范围。

课后小问 ​

  1. 为什么运行时扫描不到一个刚定义的注解? 答案:最常见原因是注解使用了默认 CLASS 或 SOURCE 保留策略,而不是 RUNTIME。 解析:先检查 Retention,再检查扫描的元素类型、模块 opens 和读取 API 是否匹配。

  2. @Inherited 能让子类自动继承父类的方法注解吗? 答案:不能,它只对类级注解的继承查询生效。 解析:方法覆盖时要由框架自己决定是否合并父类方法元数据,不能依赖 @Inherited。

本节小结 ​

  • 注解由声明、元注解、使用和处理器组成,不会自行执行。
  • Retention 决定生命周期,Target 决定落点,Inherited/Repeatable 决定读取语义。
  • 运行时注解用 AnnotatedElement API 读取,缺失和重复要有明确策略。
  • 注解解析与反射调用结合时,要限制扫描范围和访问权限。

快速回顾 ​

  • 需要反射读取就使用 @Retention(RUNTIME)。
  • @Target 先限制允许的位置,TYPE_USE 是类型表达式位置。
  • 重复注解优先用 getAnnotationsByType 读取。
  • @Inherited 不是“所有成员都继承”的开关。