Skip to content

Java 注解全过程原理

注解不是魔法,也不是写上去就自动生效。注解本质上是“写在代码上的元数据”。真正产生行为的是编译器、注解处理器、反射、框架扫描器、AOP 或运行时拦截器。

一句话先建立直觉:

注解负责声明“这里有什么含义”,处理器负责读取这个含义并执行规则。没有处理器,注解只是 class 文件或源码里的标记。

学习目标

学完这一页,你要能说清楚:

  1. 注解是什么,为什么 Spring、JUnit、MyBatis、Jackson、Lombok 都大量使用注解。
  2. @Retention@Target@Documented@Inherited 分别控制什么。
  3. SOURCECLASSRUNTIME 三种保留策略有什么区别。
  4. 注解从源码到编译期、class 文件、运行期反射的完整流程。
  5. 为什么写了注解不一定生效,谁负责让注解生效。
  6. 运行时注解和编译期注解处理器有什么区别。
  7. Spring 为什么能扫描 @Component@Autowired@RequestMapping
  8. 如何写可运行的参数校验、权限校验、路由映射 Demo。
  9. 注解和反射、动态代理、AOP 的关系。
  10. 生产环境里注解失效、扫描不到、代理不生效怎么排查。

注解到底是什么

注解是 Java 提供的一种元数据语法。它可以写在类、字段、方法、参数、构造方法、包、类型使用位置等地方,用来表达额外含义。

例如:

java
@Deprecated
public void oldMethod() {
}

这段代码不是说 oldMethod 会自动不能用,而是告诉编译器和 IDE:“这个方法已经过时了”。编译器和 IDE 读取到这个注解后,才会给出警告。

再比如:

java
@Service
public class OrderService {
}

@Service 本身不会创建对象。Spring 扫描 classpath,读取到这个注解后,才会把 OrderService 注册成 Bean。

注解为什么重要

没有注解时,很多框架配置要写在 XML 或手工注册代码里:

xml
<bean id="orderService" class="com.example.OrderService"/>

有注解后,业务代码可以直接声明意图:

java
@Service
public class OrderService {
}

注解带来的价值:

价值说明
声明式开发业务代码声明规则,框架执行规则
减少重复配置不必到处写 XML 或注册表
贴近代码类、字段、方法的元信息和代码放在一起
方便扫描框架可以统一扫描 classpath 并建立模型
方便扩展自定义注解可以扩展框架行为

但注解也有代价:如果不知道谁处理它,就很容易出现“我明明写了注解,为什么没生效”的问题。

总体流程图

注解完整链路可以拆成四步:

mermaid
flowchart TD
    A["源码中写注解"] --> B["javac 编译"]
    B --> C["根据 Retention 决定保留位置"]
    C --> D["SOURCE 只给源码/编译器使用"]
    C --> E["CLASS 写入 class 文件"]
    C --> F["RUNTIME 运行期可反射读取"]
    F --> G["框架扫描 Class、Method、Field"]
    G --> H["根据注解创建 Bean、映射路由、校验参数、织入 AOP"]

结论很关键:注解的生命周期取决于 @Retention,注解的使用位置取决于 @Target,注解是否产生行为取决于有没有对应处理逻辑。

自定义注解的基本写法

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.FIELD)
public @interface NotBlank {
    String message() default "不能为空";
}

语法说明:

代码含义
public @interface NotBlank定义一个注解类型
String message()定义注解属性
default "不能为空"属性默认值
@Retention(RUNTIME)运行期还能通过反射读取
@Target(FIELD)只能标在字段上

使用:

java
public class CreateUserRequest {
    @NotBlank(message = "用户名不能为空")
    private String username;
}

注意:上面只是在字段上放了一个标记。要让它真正校验,还需要写校验器或交给 Bean Validation 这类框架处理。

元注解是什么

元注解就是“修饰注解的注解”。它控制自定义注解自己的行为。

元注解作用
@Retention注解保留到哪个阶段
@Target注解可以写在哪里
@Documented是否出现在 Javadoc 文档里
@Inherited类上的注解是否可以被子类继承
@Repeatable是否允许同一个位置重复使用同一注解

@Retention 保留策略

@Retention 是注解学习里最重要的元注解之一。它决定注解能活到什么时候。

java
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}

三种策略:

策略保留阶段能否反射读取典型场景
SOURCE只在源码中存在,编译后丢弃不能Lombok、@Override
CLASS写入 class 文件,但运行期不一定给反射读取通常不能字节码工具、编译后处理
RUNTIME写入 class 文件,并运行期可见可以Spring、JUnit、MyBatis、Jackson

为什么 Spring 注解必须是 RUNTIME

因为 Spring 是应用启动时运行的框架,它需要通过反射读取 class 上的注解。如果注解编译后就没了,Spring 根本读不到。

SOURCE 注解

SOURCE 注解只在源码阶段存在。

java
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface NeedReview {
    String value() default "";
}

编译后,这个注解不会进入 class 文件。

适合场景:

  1. 给编译器看的注解,比如 @Override
  2. 给源码工具看的注解。
  3. 编译期代码生成,比如 Lombok。

@Override 为什么不需要运行期保留?因为它只需要编译器检查“这个方法是否真的重写了父类方法”,运行时不需要再知道。

CLASS 注解

CLASS 是默认保留策略。如果不写 @Retention,默认就是 CLASS

java
public @interface ClassLevelAnnotation {
}

它会进入 class 文件,但运行期反射通常读不到。

它适合字节码工具或编译后处理工具读取,比如某些静态分析、字节码增强、构建插件。

初学者常见坑:自定义注解忘记写 @Retention(RetentionPolicy.RUNTIME),然后运行时怎么反射都读不到。

RUNTIME 注解

运行期框架常用 RUNTIME

java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
}

运行时读取:

java
boolean present = OrderService.class.isAnnotationPresent(Component.class);
Component component = OrderService.class.getAnnotation(Component.class);

RUNTIME 适合:

场景示例
IOC 扫描@Component@Service
Web 路由@RequestMapping
参数校验@NotBlank@NotNull
ORM 映射@Table@Column
单元测试@Test
AOP 切点自定义 @LogRecord@RequiresRole

@Target 使用位置

@Target 决定注解能写在哪里。

java
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Audit {
}

常见 ElementType

可以标在哪里示例
TYPE类、接口、枚举@Service
FIELD字段@Autowired@Column
METHOD方法@RequestMapping@Test
PARAMETER方法参数@RequestParam
CONSTRUCTOR构造方法构造器注入
PACKAGE包级注解
TYPE_PARAMETER类型参数泛型类型参数注解
TYPE_USE类型使用位置@NonNull String

如果 @Target 写错,编译器会直接阻止你把注解放到不允许的位置。

注解属性规则

注解属性看起来像方法:

java
public @interface ApiLimit {
    int value();

    String message() default "请求太频繁";

    String[] roles() default {};
}

使用:

java
@ApiLimit(value = 100, message = "每分钟最多 100 次")
public void query() {
}

如果属性名叫 value,并且只设置这个属性,可以简写:

java
@ApiLimit(100)
public void query() {
}

注解属性类型有限制,常见允许:

  1. 基本类型。
  2. String
  3. Class
  4. 枚举。
  5. 注解。
  6. 以上类型的一维数组。

不能使用普通对象作为注解属性:

java
// Object value(); // 不允许

原因是注解属性要能写入 class 文件常量结构,必须是有限、可序列化到字节码元数据里的类型。

@Inherited 不是所有位置都继承

@Inherited 只对“类上的注解”生效,而且只对类继承生效,不对接口实现、方法、字段生效。

java
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Role {
    String value();
}
java
@Role("admin")
public class BaseController {
}

public class UserController extends BaseController {
}
java
Role role = UserController.class.getAnnotation(Role.class);
System.out.println(role.value()); // admin

但如果注解在方法上,即使加了 @Inherited,子类重写方法也不会自动继承方法注解。Spring 自己会做更复杂的合并注解查找,所以不能把 Java 原生 @Inherited 和 Spring 的注解合并机制混为一谈。

@Repeatable 重复注解

JDK 8 引入重复注解。比如一个任务可以配置多个调度规则:

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

@Repeatable(Schedules.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface Schedule {
    String cron();
}

容器注解:

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

@Retention(RetentionPolicy.RUNTIME)
public @interface Schedules {
    Schedule[] value();
}

使用:

java
@Schedule(cron = "0 0 1 * * ?")
@Schedule(cron = "0 0 2 * * ?")
public void syncData() {
}

读取:

java
Schedule[] schedules = method.getAnnotationsByType(Schedule.class);

这是 JDK 8 常见注解能力,和 Lambda、Stream 一样,在现代 Java 后端里经常出现。

注解读取 API

以类注解为例:

java
Class<?> clazz = OrderService.class;

boolean present = clazz.isAnnotationPresent(Component.class);
Component component = clazz.getAnnotation(Component.class);
Annotation[] annotations = clazz.getAnnotations();
Annotation[] declared = clazz.getDeclaredAnnotations();

区别:

API含义
isAnnotationPresent判断某个注解是否存在
getAnnotation获取某个注解,可能考虑 @Inherited
getDeclaredAnnotation只获取当前元素直接声明的注解
getAnnotations获取 public 语义下可见的注解,类上可能包含继承注解
getDeclaredAnnotations获取当前元素直接声明的所有注解
getAnnotationsByType获取重复注解

字段、方法、参数上也有类似 API。

注解为什么不会自动生效

下面代码不会自动校验:

java
public class CreateOrderRequest {
    @NotBlank(message = "订单号不能为空")
    private String orderNo;
}

因为 JVM 不会看到 @NotBlank 就自动拦截构造方法、setter 或接口调用。

必须有处理器:

mermaid
flowchart TD
    A["业务代码写 @NotBlank"] --> B["注解进入 class 文件"]
    B --> C["校验器或框架反射读取字段注解"]
    C --> D["读取字段值"]
    D --> E["判断是否为空"]
    E --> F["不通过则抛异常或返回错误"]

所以学习注解必须同时问两个问题:

  1. 注解写在哪里?
  2. 谁在什么时候读取它?

Demo 1:参数校验注解

定义注解:

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.FIELD)
public @interface NotBlank {
    String message() default "不能为空";
}

请求对象:

java
public class CreateUserRequest {
    @NotBlank(message = "用户名不能为空")
    private String username;

    public CreateUserRequest(String username) {
        this.username = username;
    }
}

校验器:

java
import java.lang.reflect.Field;

public class Validator {
    public static void validate(Object target) {
        Class<?> clazz = target.getClass();
        for (Field field : clazz.getDeclaredFields()) {
            NotBlank notBlank = field.getAnnotation(NotBlank.class);
            if (notBlank == null) {
                continue;
            }

            try {
                field.setAccessible(true);
                Object value = field.get(target);
                if (value == null || value.toString().trim().isEmpty()) {
                    throw new IllegalArgumentException(notBlank.message());
                }
            } catch (IllegalAccessException e) {
                throw new IllegalStateException("读取字段失败: " + field.getName(), e);
            }
        }
    }
}

运行:

java
public class ValidateDemo {
    public static void main(String[] args) {
        CreateUserRequest request = new CreateUserRequest(" ");
        Validator.validate(request);
    }
}

这个 Demo 对应商业项目里的接口参数校验。真实项目一般使用 Bean Validation,比如 @NotBlank@NotNull@Size,Spring MVC 在参数绑定后触发校验。

Demo 2:权限注解和动态代理

定义权限注解:

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)
public @interface RequireRole {
    String value();
}

业务接口:

java
public interface OrderService {
    @RequireRole("admin")
    void refund(String orderNo);
}

实现类:

java
public class OrderServiceImpl implements OrderService {
    @Override
    public void refund(String orderNo) {
        System.out.println("refund: " + orderNo);
    }
}

代理处理器:

java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class SecurityProxy {
    @SuppressWarnings("unchecked")
    public static <T> T create(T target, String currentRole) {
        Class<?>[] interfaces = target.getClass().getInterfaces();
        return (T) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                interfaces,
                new SecurityHandler(target, currentRole)
        );
    }

    private static class SecurityHandler implements InvocationHandler {
        private final Object target;
        private final String currentRole;

        SecurityHandler(Object target, String currentRole) {
            this.target = target;
            this.currentRole = currentRole;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            RequireRole requireRole = method.getAnnotation(RequireRole.class);
            if (requireRole != null && !requireRole.value().equals(currentRole)) {
                throw new SecurityException("没有权限,需要角色: " + requireRole.value());
            }
            return method.invoke(target, args);
        }
    }
}

运行:

java
public class SecurityDemo {
    public static void main(String[] args) {
        OrderService service = SecurityProxy.create(new OrderServiceImpl(), "user");
        service.refund("PO1001");
    }
}

这个 Demo 说明:注解只声明“退款需要 admin”,真正拦截调用的是代理对象。Spring Security、Spring AOP、事务注解都有类似思想:注解声明规则,代理或拦截器执行规则。

Demo 3:简化版 Controller 路由

定义路由注解:

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.TYPE)
public @interface Controller {
}
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequestMapping {
    String value();
}

控制器:

java
@Controller
public class UserController {
    @RequestMapping("/users")
    public String list() {
        return "user list";
    }
}

路由注册器:

java
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class Router {
    private final Map<String, HandlerMethod> routes = new HashMap<String, HandlerMethod>();

    public void register(Object controller) {
        Class<?> clazz = controller.getClass();
        if (!clazz.isAnnotationPresent(Controller.class)) {
            return;
        }

        for (Method method : clazz.getDeclaredMethods()) {
            RequestMapping mapping = method.getAnnotation(RequestMapping.class);
            if (mapping == null) {
                continue;
            }
            routes.put(mapping.value(), new HandlerMethod(controller, method));
        }
    }

    public Object handle(String path) {
        HandlerMethod handlerMethod = routes.get(path);
        if (handlerMethod == null) {
            throw new IllegalArgumentException("404: " + path);
        }
        try {
            return handlerMethod.method.invoke(handlerMethod.controller);
        } catch (Exception e) {
            throw new IllegalStateException("调用接口失败: " + path, e);
        }
    }

    private static class HandlerMethod {
        private final Object controller;
        private final Method method;

        HandlerMethod(Object controller, Method method) {
            this.controller = controller;
            this.method = method;
        }
    }
}

运行:

java
public class RouterDemo {
    public static void main(String[] args) {
        Router router = new Router();
        router.register(new UserController());
        System.out.println(router.handle("/users"));
    }
}

Spring MVC 的真实流程更复杂,包括参数解析、返回值处理、异常处理、拦截器、消息转换器,但“扫描注解 -> 建立 path 到 method 的映射 -> 请求到来后反射或方法句柄调用”是理解入口。

编译期注解处理器

运行时注解依赖反射;编译期注解处理器在 javac 编译阶段工作。

典型例子:

工具工作方式
Lombok编译期修改 AST 或生成代码
MapStruct编译期生成 Mapper 实现类
Dagger编译期生成依赖注入代码

运行期处理和编译期处理对比:

对比项运行期注解编译期注解处理器
发生时间应用启动或运行时javac 编译时
读取方式反射读取 RUNTIME 注解注解处理 API 读取源码结构
性能运行时有一定扫描/反射成本运行时成本低
灵活性可以根据运行环境动态处理编译后结果固定
典型框架Spring、JUnitLombok、MapStruct

如果一个注解是 SOURCE,通常就是给编译期工具使用,不要期待运行时反射读到。

Spring 注解为什么能生效

@Service@Autowired 为例,Spring 大致流程:

mermaid
flowchart TD
    A["启动 Spring 容器"] --> B["扫描指定包下的 class"]
    B --> C["读取类上的 @Component/@Service"]
    C --> D["生成 BeanDefinition"]
    D --> E["创建 Bean 实例"]
    E --> F["读取构造器、字段、方法上的注入注解"]
    F --> G["从容器查找依赖"]
    G --> H["注入依赖并完成初始化"]

@Transactional 又是另一类,它通常依赖代理:

mermaid
flowchart TD
    A["方法上有 @Transactional"] --> B["Spring 创建代理对象"]
    B --> C["外部调用代理方法"]
    C --> D["代理先开启事务"]
    D --> E["调用真实业务方法"]
    E --> F["成功提交,异常回滚"]

所以 Spring 注解生效的背后,常见机制包括:

  1. classpath 扫描。
  2. 反射读取注解。
  3. BeanDefinition 注册。
  4. 依赖注入。
  5. Bean 后置处理器。
  6. 动态代理或 CGLIB 代理。
  7. 拦截器链。

如果只背“Spring 通过注解实现 IOC/AOP”,还是太浅。要说清楚:注解只是标记,容器和代理机制才是执行者。

商业项目怎么用

参数校验

java
public class CreateOrderRequest {
    @NotBlank(message = "订单号不能为空")
    private String orderNo;

    @NotNull(message = "用户ID不能为空")
    private Long userId;
}

商业价值:接口入口统一校验,避免脏数据进入业务层。

权限控制

java
@RequireRole("finance")
public void exportPaymentReport() {
}

商业价值:把权限规则声明在接口或服务方法上,由 AOP 或拦截器统一处理,避免每个方法手写重复判断。

审计日志

java
@OperationLog("退款")
public void refund(String orderNo) {
}

商业价值:统一记录操作人、操作时间、业务单号、执行结果,常用于后台管理、金融、医疗、资产平台。

ORM 映射

java
@TableName("sys_user")
public class UserEntity {
    @ColumnName("user_name")
    private String userName;
}

商业价值:Java 字段和数据库表字段之间建立映射关系,框架根据注解生成 SQL 或映射结果。

常见坑

忘记写 RUNTIME

java
@Target(ElementType.FIELD)
public @interface NotBlank {
}

默认是 CLASS,运行时反射读不到。需要:

java
@Retention(RetentionPolicy.RUNTIME)

只写注解,没有处理器

java
@OperationLog("新增订单")
public void createOrder() {
}

如果没有 AOP、拦截器或手写代码读取它,不会自动记录日志。

@Target 写错

定义时只允许字段:

java
@Target(ElementType.FIELD)

却想标在方法上,编译器会报错。要根据真实使用位置设置。

注解加在接口方法上,代理读取实现类方法

JDK 动态代理调用时拿到的 Method 通常来自接口;CGLIB 代理可能面对实现类方法。注解到底加在接口还是实现类,会影响读取逻辑。

生产框架一般会做合并查找:接口、实现类、父类、桥接方法都可能要查。自己写简单 AOP 时要特别注意。

Spring 事务注解自调用失效

java
public void outer() {
    inner();
}

@Transactional
public void inner() {
}

outer 内部直接调用 inner,没有经过代理对象,所以事务注解可能不生效。这不是注解没写对,而是代理调用链没有经过。

注解放在 private 方法上却期待 AOP 生效

Spring AOP 基于代理拦截外部方法调用。private 方法不能被代理正常覆盖拦截,注解通常不会按预期生效。

线上排查流程

mermaid
flowchart TD
    A["注解没有生效"] --> B["确认注解是否被编译保留"]
    B --> C["检查 Retention 是否是 RUNTIME"]
    A --> D["确认注解位置是否正确"]
    D --> E["检查 Target 和实际标注位置"]
    A --> F["确认是否有处理器"]
    F --> G["Spring Bean、AOP、拦截器、校验器是否启用"]
    A --> H["确认调用是否经过代理"]
    H --> I["排查自调用、private、final、非 Spring Bean"]
    A --> J["确认扫描范围"]
    J --> K["检查包路径、ComponentScan、配置类加载"]

排查建议:

  1. 反射读不到注解,先查 @Retention(RetentionPolicy.RUNTIME)
  2. 编译就报注解位置错误,查 @Target
  3. Spring 注解不生效,先确认对象是不是 Spring Bean。
  4. AOP 类注解、方法注解不生效,查是否经过代理对象。
  5. 事务、缓存、权限注解不生效,查自调用、private/final 方法、代理类型。
  6. 自定义注解没效果,查是否真的写了处理器或切面。
  7. 多模块项目扫描不到,查 @ComponentScan 包路径和依赖是否被打进来。

面试标准回答

注解是什么

注解是 Java 的元数据机制,可以给类、字段、方法、参数等代码元素添加额外信息。注解本身不直接执行业务逻辑,真正产生行为的是编译器、注解处理器、反射、框架扫描、AOP 或拦截器。

@Retention 有哪些取值

SOURCE 只保留在源码阶段,编译后丢弃,适合 @Override、Lombok 这类编译期工具;CLASS 会进入 class 文件,但运行时通常不能反射读取;RUNTIME 会进入 class 文件并且运行时可反射读取,Spring、JUnit、MyBatis 等运行时框架常用。

为什么写了注解不生效

因为注解只是元数据。要生效必须有人读取它并执行逻辑,比如 Spring 扫描 @Service 注册 Bean,参数校验器读取 @NotBlank 做校验,AOP 读取权限注解做拦截。如果没有处理器,注解只是标记。

@Target 是什么

@Target 用来限制注解可以标在哪里,比如类、字段、方法、参数、构造方法、类型使用位置等。写错后编译器会阻止你把注解放在不允许的位置。

@Inherited 为什么有时没用

Java 原生 @Inherited 只对类上的注解生效,只支持类继承,不支持接口、方法、字段。方法注解、字段注解不会因为 @Inherited 自动继承。Spring 的合并注解查找是框架额外能力,不等同于 Java 原生继承规则。

运行期注解和编译期注解处理器区别

运行期注解通常使用 RUNTIME,应用启动或运行时通过反射读取,典型是 Spring、JUnit;编译期注解处理器在 javac 阶段工作,可以生成代码或做检查,典型是 Lombok、MapStruct。运行期更灵活,编译期运行成本更低。

Spring 注解为什么能生效

Spring 启动时扫描指定包下的 class,读取 @Component@Service 等注解生成 BeanDefinition,再创建 Bean 并处理依赖注入。像 @Transactional 这类注解通常还需要 Spring 创建代理对象,外部调用经过代理,代理再根据注解开启事务、提交或回滚。

关联知识点