Lesson 45 · Java 高级特性

注解体系:元注解、自定义注解与运行时处理

中级·🔥 极高·#Java·#注解

第 1 站

面试官:你自定义过注解吗?Spring 的 @Autowired 是怎么工作的?

"你用过 @Override、@Autowired 这些注解——但你知道它们底层是怎么运作的吗?你自己写过注解吗?Spring 的依赖注入到底是靠什么机制实现的?" —— 面试官想考察的不是注解的用法,而是你是否理解注解从定义到运行时处理的完整链路。

注解(Annotation)是 Java 5 引入的元数据机制。它本身不改变代码的执行逻辑,但可以被编译器、框架、运行时处理器读取并做出相应的动作。几乎所有现代 Java 框架——Spring、MyBatis、JUnit、Lombok——都构建在注解之上。

  • 内置注解:@Override、@Deprecated、@SuppressWarnings、@FunctionalInterface
  • 元注解:用来"注解注解"的注解——@Retention、@Target、@Documented、@Inherited、@Repeatable
  • 自定义注解:用 @interface 关键字定义,本质是一种特殊的接口
  • 注解处理器:编译期(APT)或运行期(反射)读取注解并执行逻辑

理解注解的关键不在于"会用",而在于理解三层架构:定义层(@interface + 元注解)→ 标注层(用在类/方法/字段上)→ 处理层(反射或 APT 读取并执行逻辑)。没有处理层的注解只是一段摆设。

注解核心公式
注解定义(@interface)+ 元注解约束(@Retention/@Target)+ 运行时处理(反射)= 完整的注解驱动机制

本文从元注解逐一讲起,手写自定义注解,用反射构建注解处理器,最后深入 Spring 注解驱动的底层原理。

第 2 站

元注解:注解的注解

元注解(Meta-Annotation)是 JDK 提供的"用来修饰注解"的注解。它们定义了自定义注解的行为约束——在哪里能用、能活多久、能否继承、能否重复。JDK 5 引入四个,JDK 8 新增一个:

@Retention —— 注解的生命周期

@Retention 决定注解存活到哪个阶段,是最关键的元注解:

策略存活阶段典型用途
SOURCE仅源码,编译后丢弃Lombok @Data、@SuppressWarnings
CLASS编译期保留在 .class 文件中,运行时不可获取(默认值)字节码增强工具
RUNTIME运行时可通过反射获取Spring @Autowired、@Transactional
面试必知:只有 RUNTIME 注解才能被反射读取

这是面试高频考点。如果你自定义的注解需要在运行时通过反射处理(几乎所有框架注解都是如此),必须标注 @Retention(RetentionPolicy.RUNTIME)。SOURCE 和 CLASS 级别的注解在运行时完全不存在,getAnnotation() 将返回 null。

RetentionPolicy.java · JDK 源码
public enum RetentionPolicy {
    SOURCE,    // 编译器处理后丢弃
    CLASS,     // 保留在 class 文件,JVM 不加载
    RUNTIME    // 保留在 class 文件,JVM 加载,反射可读
}

@Target —— 注解能标注在哪里

@Target 限定注解的使用位置,防止误用:

ElementType.java · 常用枚举值
public enum ElementType {
    TYPE,           // 类、接口、枚举
    FIELD,          // 字段(成员变量)
    METHOD,         // 方法
    PARAMETER,      // 方法参数
    CONSTRUCTOR,    // 构造方法
    LOCAL_VARIABLE, // 局部变量
    ANNOTATION_TYPE,// 注解类型
    PACKAGE,        // 包
    TYPE_USE,       // 类型使用(JDK 8:泛型、强转等)
    TYPE_PARAMETER // 类型参数(JDK 8:泛型参数声明)
}

@Documented —— Javadoc 可见

标注了 @Documented 的注解会出现在 Javadoc 文档中。不加则 Javadoc 不会展示该注解。大多数公共 API 注解(如 @Deprecated)都会加上。

@Inherited —— 子类可继承

默认情况下,父类上的注解不会被子类继承。加了 @Inherited 后,子类自动继承父类的类级别注解。注意:只对类有效,方法、字段上的注解不受此影响。

@Repeatable —— 可重复标注(JDK 8)

JDK 8 之前,同一个注解在同一个位置只能使用一次。@Repeatable 允许重复标注,但需要定义一个容器注解

Repeatable 示例
// ① 定义容器注解
@interface Roles {
    Role[] value();
}

// ② 定义可重复注解,指定容器
@Repeatable(Roles.class)
@interface Role {
    String value();
}

// ③ 使用:同一个类上标注多次
@Role("admin")
@Role("user")
public class UserService {}
元注解速记

@Retention 管生命周期(RUNTIME 才能反射读),@Target 管使用位置(TYPE/METHOD/FIELD),@Documented 管文档可见,@Inherited 管子类继承(仅类级别),@Repeatable 管重复标注(需容器注解)。

第 3 站

自定义注解:手写一个 @Log

自定义注解用 @interface 关键字定义,语法类似接口,但本质是一个继承自 java.lang.annotation.Annotation 的特殊接口。来看一个实际的日志注解:

Log.java · 自定义日志注解
import java.lang.annotation.*;

@Target({ElementType.METHOD})            // 只能标注在方法上
@Retention(RetentionPolicy.RUNTIME)       // 运行时可通过反射获取
@Documented                                // 出现在 Javadoc 中
public @interface Log {
    // 注解属性:描述信息,无默认值(使用时必须指定)
    String value();

    // 注解属性:日志级别,有默认值 INFO
    String level() default "INFO";

    // 注解属性:是否记录参数,默认 true
    boolean logArgs() default true;

    // 注解属性:模块名
    String module() default "";
}

注解属性的类型有限制,只允许以下类型:

允许的类型示例
基本数据类型int priority() default 1;
StringString value();
ClassClass<?> type() default Void.class;
EnumLogLevel level() default LogLevel.INFO;
AnnotationAuthor author();
以上类型的数组String[] tags() default {};

使用自定义注解:

OrderService.java · 使用 @Log
public class OrderService {

    @Log(value = "创建订单", level = "WARN", module = "订单")
    public void createOrder(String orderId) {
        // 业务逻辑...
    }

    @Log("查询订单")   // 只有 value 属性时可以省略属性名
    public void getOrder(String orderId) {
        // 业务逻辑...
    }
}
value 属性的特殊规则

如果注解只有一个属性且名为 value,使用时可以省略 value=,直接写 @Log("描述")。一旦有其他属性需要指定,就必须显式写 value="描述"。这是 Java 语法糖,面试中偶尔会考。

自定义注解的三层结构 定义层 @interface + 元注解 属性 + 默认值 标注层 @Log("创建订单") 标注在方法/类/字段上 处理层 反射 / APT 读取 执行业务逻辑 没有处理层的注解 = 无用的摆设 注解本身不改变代码行为,必须由处理器赋予意义
图 1自定义注解的三层结构:定义 → 标注 → 处理
第 4 站

运行时处理:用反射读取注解

定义注解只是第一步——真正赋予注解意义的是运行时处理器。通过反射 API,我们可以在运行时检查类、方法、字段上的注解并执行对应逻辑。

核心反射 API

反射注解 API · AnnotatedElement 接口
// 判断是否标注了指定注解
boolean hasLog = method.isAnnotationPresent(Log.class);

// 获取指定注解实例(不存在返回 null)
Log log = method.getAnnotation(Log.class);

// 获取所有注解(包括继承来的)
Annotation[] all = method.getAnnotations();

// 获取直接声明的注解(不包括继承的)
Annotation[] declared = method.getDeclaredAnnotations();

// 获取重复注解(JDK 8+)
Log[] logs = method.getAnnotationsByType(Log.class);

实战:构建一个简单的日志注解处理器

LogProcessor.java · 注解处理器
public class LogProcessor {

    public static void process(Class<?> clazz) {
        // 遍历类中所有声明的方法
        for (Method method : clazz.getDeclaredMethods()) {
            // 检查方法是否标注了 @Log
            if (!method.isAnnotationPresent(Log.class)) {
                continue;
            }
            // 获取注解实例并读取属性
            Log log = method.getAnnotation(Log.class);
            String desc = log.value();
            String level = log.level();
            boolean logArgs = log.logArgs();
            String module = log.module();

            // 执行日志记录逻辑
            System.out.printf("[%s] [%s] %s.%s - logArgs=%b%n",
                level, module,
                clazz.getSimpleName(),
                method.getName(),
                logArgs);
        }
    }

    public static void main(String[] args) {
        LogProcessor.process(OrderService.class);
    }
}
为什么注解属性看起来像方法调用?

注解编译后生成一个接口,每个属性对应接口中的一个方法。当你调用 log.value() 时,JDK 在底层通过动态代理(AnnotationParser)返回注解中声明的值。这就是为什么注解属性的语法看起来和方法定义一模一样——它们本质上就是接口方法。

运行时注解处理流程 获取 Class Class.forName() 遍历成员 getMethods() 检查注解 isAnnotationPresent() 读取属性 getAnnotation() 前提条件:@Retention(RUNTIME) 只有 RUNTIME 级别的注解才能在运行时被反射读取 SOURCE 和 CLASS 级别的注解在运行时完全不存在 getAnnotation() 返回 null → 处理器必须做 null 判断
图 2运行时注解处理的标准四步流程
反射处理注解要点

① 必须先确保注解是 @Retention(RUNTIME);② 用 isAnnotationPresent() 做判断,getAnnotation() 拿实例;③ 注解属性通过方法调用读取(本质是动态代理);④ 遍历类成员时注意 getDeclaredMethods() 不包含继承方法,getMethods() 只包含 public 方法。

第 5 站

Spring 注解驱动:@Autowired 与 @Transactional 的底层原理

Spring 框架是注解驱动的最佳范例。面试常问"Spring 的 @Autowired 是怎么工作的",本质是考察你是否理解注解 + 反射 + BeanPostProcessor的协作机制。

@Autowired 的底层实现

@Autowired 的注入并非 Spring 容器"魔法般"自动完成,而是靠一个具体的后处理器:AutowiredAnnotationBeanPostProcessor

AutowiredAnnotationBeanPostProcessor · 简化逻辑
public class AutowiredAnnotationBeanPostProcessor
    implements BeanPostProcessor {

    // Spring 在每个 Bean 初始化前调用此方法
    public Object postProcessBeforeInitialization(
            Object bean, String beanName) {

        Class<?> clazz = bean.getClass();

        // ① 扫描所有字段,查找 @Autowired
        for (Field field : clazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(Autowired.class)) {
                // ② 从容器中查找依赖
                Object dependency = findBean(field.getType());
                // ③ 反射注入
                field.setAccessible(true);
                field.set(bean, dependency);
            }
        }
        return bean;
    }
}

@Transactional 的底层实现

@Transactional 的实现机制与 @Autowired 不同——它基于 AOP 代理。Spring 在 Bean 创建后检查是否标注了 @Transactional,如果有,则为该 Bean 创建代理对象,在方法调用前后织入事务管理逻辑:

事务代理 · 简化流程
// Spring 内部简化伪代码
if (beanClass.isAnnotationPresent(Transactional.class)
    || hasTransactionalMethod(beanClass)) {
    // 为这个 Bean 创建 AOP 代理
    proxy = ProxyFactory.createProxy(bean);
    // 代理拦截方法调用
    proxy.addAdvice(new TransactionInterceptor());
}

// 代理拦截器内部
public Object invoke(MethodInvocation invocation) {
    TransactionAttribute attr =
        getTransactionAttribute(invocation.getMethod());
    try {
        TransactionStatus status = tm.getTransaction(attr);
        Object result = invocation.proceed(); // 执行目标方法
        tm.commit(status);
        return result;
    } catch (RuntimeException e) {
        tm.rollback(status);  // 回滚
        throw e;
    }
}
Spring 注解驱动:两种处理模式 Spring IoC 容器启动 扫描 @Component / @Bean 注册的类 BeanPostProcessor 模式 @Autowired / @Value / @Resource Bean 初始化阶段反射扫描并注入 AOP 代理模式 @Transactional / @Cacheable 检查注解 → 创建代理 → 方法拦截 核心链路:注解定义(@Retention RUNTIME)→ 容器启动时反射扫描 → 执行对应处理逻辑 没有反射,注解驱动就是空中楼阁
图 3Spring 注解驱动的两种处理模式:BeanPostProcessor 与 AOP 代理
为什么同类内部方法调用 @Transactional 不生效?

因为 @Transactional 基于代理实现。同类内部调用(this.methodB())走的是原始对象而非代理对象,事务拦截器不会被触发。解决方案:注入自身(@Autowired self)、使用 AopContext.currentProxy()、或拆分成不同 Bean。

第 6 站

注解驱动 vs XML 配置:优劣对比

在 Spring 早期(2.x),所有配置都写在 XML 文件中。随着注解机制的成熟(Spring 3+ 尤其是 Spring Boot),注解驱动逐渐成为主流。面试中经常要求对比两者:

维度XML 配置注解驱动
配置位置独立于代码,集中管理分散在各类上,与代码耦合
可读性结构清晰,适合大型项目直观简洁,适合中小型项目
修改成本改 XML 无需重新编译改注解需重新编译
类型安全字符串配置,易出错编译期检查,类型安全
灵活性运行时可替换配置编译后固定,不可动态切换
Spring Boot不推荐,需要额外配置原生支持,自动配置
第三方集成适合无源码的第三方 Bean需要能修改源码
XML 配置 vs 注解配置
<!-- XML 方式:独立配置文件 -->
<bean id="userService" class="com.example.UserService">
    <property name="userDao" ref="userDao"/>
</bean>

// 注解方式:嵌入代码
@Service
public class UserService {
    @Autowired
    private UserDao userDao;
}

现代 Spring Boot 的哲学是"约定优于配置"(Convention over Configuration),注解驱动完美契合这一理念:

  • @SpringBootApplication = @Configuration + @ComponentScan + @EnableAutoConfiguration
  • 自动配置:Spring Boot 通过扫描 classpath 下的 jar 包和注解,自动装配 Bean
  • 条件注解:@ConditionalOnClass、@ConditionalOnMissingBean 实现按需装配
选型原则
项目内部代码 → 注解驱动(简洁、类型安全)
第三方库 / 无法修改源码的类 → @Bean 方法(Java Config)或 XML
需要运行时动态切换 → XML 或 properties/yml 外部化配置
第 7 站

总结:注解设计原则与面试回答模板

注解体系的设计遵循三个核心原则:

  • 声明式编程:注解让开发者"声明意图"而非"编写实现"。@Transactional 声明"这个方法需要事务",而非手动写 begin/commit/rollback
  • 关注点分离:业务代码保持纯净,横切逻辑(日志、事务、缓存)通过注解 + 处理器统一管理
  • 框架扩展点:注解是框架提供给开发者的"插件接口",框架通过反射扫描注解来执行开发者定义的逻辑

常见的注解设计模式:

模式代表注解处理方式
标记注解@Override、@FunctionalInterface编译器检查(SOURCE)
元数据注解@Deprecated、@SuppressWarnings编译器警告 / IDE 提示
依赖注入@Autowired、@Resource、@InjectBeanPostProcessor 反射注入
AOP 切面@Transactional、@Cacheable代理 + 方法拦截器
代码生成@Data、@Builder(Lombok)编译期 APT 注解处理器
路由映射@RequestMapping、@GET启动时扫描注册路由表
注解体系全景:从定义到处理 元注解 @Retention @Target @Repeatable ... 自定义注解 @interface + 属性 default 默认值 标注使用 @Log("创建订单") 类/方法/字段/参数 处理器 反射 / APT / 代理 执行业务逻辑 设计三原则 声明式编程 · 关注点分离 · 框架扩展点 注解本身无行为,处理器赋予意义
图 4注解体系全景:元注解 → 自定义注解 → 标注 → 处理

面试回答模板

Q:请介绍 Java 注解的机制以及你自定义注解的经验。

"Java 注解是元数据机制,用 @interface 定义,通过元注解约束行为。@Retention(RUNTIME) 保证运行时可反射获取,@Target 限定使用位置。自定义注解需要三个层次:定义(@interface + 属性)、标注(用在代码上)、处理(反射扫描并执行逻辑)。

Spring 的 @Autowired 通过 AutowiredAnnotationBeanPostProcessor 实现——在 Bean 初始化阶段反射扫描字段,查找 @Autowired 注解,从容器中查找依赖并注入。@Transactional 则基于 AOP 代理——检测到注解后为 Bean 创建代理,在方法调用前后织入事务的 begin/commit/rollback 逻辑。

注解相比 XML 的优势是类型安全和与代码紧密结合,劣势是修改需重新编译。现代 Spring Boot 以注解驱动为主,Java Config 和 yml 配置为辅。"

Q:@Transactional 同类内部调用为什么不生效?

"因为 @Transactional 基于 AOP 代理实现。同类内部调用走的是 this 引用而非代理对象,事务拦截器不会被触发。解决方案是注入自身、使用 AopContext.currentProxy()、或拆分成不同 Bean。"