Java 注解全过程原理
注解不是魔法,也不是写上去就自动生效。注解本质上是“写在代码上的元数据”。真正产生行为的是编译器、注解处理器、反射、框架扫描器、AOP 或运行时拦截器。
一句话先建立直觉:
注解负责声明“这里有什么含义”,处理器负责读取这个含义并执行规则。没有处理器,注解只是 class 文件或源码里的标记。
学习目标
学完这一页,你要能说清楚:
- 注解是什么,为什么 Spring、JUnit、MyBatis、Jackson、Lombok 都大量使用注解。
@Retention、@Target、@Documented、@Inherited分别控制什么。SOURCE、CLASS、RUNTIME三种保留策略有什么区别。- 注解从源码到编译期、class 文件、运行期反射的完整流程。
- 为什么写了注解不一定生效,谁负责让注解生效。
- 运行时注解和编译期注解处理器有什么区别。
- Spring 为什么能扫描
@Component、@Autowired、@RequestMapping。 - 如何写可运行的参数校验、权限校验、路由映射 Demo。
- 注解和反射、动态代理、AOP 的关系。
- 生产环境里注解失效、扫描不到、代理不生效怎么排查。
注解到底是什么
注解是 Java 提供的一种元数据语法。它可以写在类、字段、方法、参数、构造方法、包、类型使用位置等地方,用来表达额外含义。
例如:
@Deprecated
public void oldMethod() {
}这段代码不是说 oldMethod 会自动不能用,而是告诉编译器和 IDE:“这个方法已经过时了”。编译器和 IDE 读取到这个注解后,才会给出警告。
再比如:
@Service
public class OrderService {
}@Service 本身不会创建对象。Spring 扫描 classpath,读取到这个注解后,才会把 OrderService 注册成 Bean。
注解为什么重要
没有注解时,很多框架配置要写在 XML 或手工注册代码里:
<bean id="orderService" class="com.example.OrderService"/>有注解后,业务代码可以直接声明意图:
@Service
public class OrderService {
}注解带来的价值:
| 价值 | 说明 |
|---|---|
| 声明式开发 | 业务代码声明规则,框架执行规则 |
| 减少重复配置 | 不必到处写 XML 或注册表 |
| 贴近代码 | 类、字段、方法的元信息和代码放在一起 |
| 方便扫描 | 框架可以统一扫描 classpath 并建立模型 |
| 方便扩展 | 自定义注解可以扩展框架行为 |
但注解也有代价:如果不知道谁处理它,就很容易出现“我明明写了注解,为什么没生效”的问题。
总体流程图
注解完整链路可以拆成四步:
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,注解是否产生行为取决于有没有对应处理逻辑。
自定义注解的基本写法
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) | 只能标在字段上 |
使用:
public class CreateUserRequest {
@NotBlank(message = "用户名不能为空")
private String username;
}注意:上面只是在字段上放了一个标记。要让它真正校验,还需要写校验器或交给 Bean Validation 这类框架处理。
元注解是什么
元注解就是“修饰注解的注解”。它控制自定义注解自己的行为。
| 元注解 | 作用 |
|---|---|
@Retention | 注解保留到哪个阶段 |
@Target | 注解可以写在哪里 |
@Documented | 是否出现在 Javadoc 文档里 |
@Inherited | 类上的注解是否可以被子类继承 |
@Repeatable | 是否允许同一个位置重复使用同一注解 |
@Retention 保留策略
@Retention 是注解学习里最重要的元注解之一。它决定注解能活到什么时候。
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}三种策略:
| 策略 | 保留阶段 | 能否反射读取 | 典型场景 |
|---|---|---|---|
SOURCE | 只在源码中存在,编译后丢弃 | 不能 | Lombok、@Override |
CLASS | 写入 class 文件,但运行期不一定给反射读取 | 通常不能 | 字节码工具、编译后处理 |
RUNTIME | 写入 class 文件,并运行期可见 | 可以 | Spring、JUnit、MyBatis、Jackson |
为什么 Spring 注解必须是 RUNTIME?
因为 Spring 是应用启动时运行的框架,它需要通过反射读取 class 上的注解。如果注解编译后就没了,Spring 根本读不到。
SOURCE 注解
SOURCE 注解只在源码阶段存在。
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface NeedReview {
String value() default "";
}编译后,这个注解不会进入 class 文件。
适合场景:
- 给编译器看的注解,比如
@Override。 - 给源码工具看的注解。
- 编译期代码生成,比如 Lombok。
@Override 为什么不需要运行期保留?因为它只需要编译器检查“这个方法是否真的重写了父类方法”,运行时不需要再知道。
CLASS 注解
CLASS 是默认保留策略。如果不写 @Retention,默认就是 CLASS。
public @interface ClassLevelAnnotation {
}它会进入 class 文件,但运行期反射通常读不到。
它适合字节码工具或编译后处理工具读取,比如某些静态分析、字节码增强、构建插件。
初学者常见坑:自定义注解忘记写 @Retention(RetentionPolicy.RUNTIME),然后运行时怎么反射都读不到。
RUNTIME 注解
运行期框架常用 RUNTIME:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
}运行时读取:
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 决定注解能写在哪里。
@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 写错,编译器会直接阻止你把注解放到不允许的位置。
注解属性规则
注解属性看起来像方法:
public @interface ApiLimit {
int value();
String message() default "请求太频繁";
String[] roles() default {};
}使用:
@ApiLimit(value = 100, message = "每分钟最多 100 次")
public void query() {
}如果属性名叫 value,并且只设置这个属性,可以简写:
@ApiLimit(100)
public void query() {
}注解属性类型有限制,常见允许:
- 基本类型。
String。Class。- 枚举。
- 注解。
- 以上类型的一维数组。
不能使用普通对象作为注解属性:
// Object value(); // 不允许原因是注解属性要能写入 class 文件常量结构,必须是有限、可序列化到字节码元数据里的类型。
@Inherited 不是所有位置都继承
@Inherited 只对“类上的注解”生效,而且只对类继承生效,不对接口实现、方法、字段生效。
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();
}@Role("admin")
public class BaseController {
}
public class UserController extends BaseController {
}Role role = UserController.class.getAnnotation(Role.class);
System.out.println(role.value()); // admin但如果注解在方法上,即使加了 @Inherited,子类重写方法也不会自动继承方法注解。Spring 自己会做更复杂的合并注解查找,所以不能把 Java 原生 @Inherited 和 Spring 的注解合并机制混为一谈。
@Repeatable 重复注解
JDK 8 引入重复注解。比如一个任务可以配置多个调度规则:
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();
}容器注解:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Schedules {
Schedule[] value();
}使用:
@Schedule(cron = "0 0 1 * * ?")
@Schedule(cron = "0 0 2 * * ?")
public void syncData() {
}读取:
Schedule[] schedules = method.getAnnotationsByType(Schedule.class);这是 JDK 8 常见注解能力,和 Lambda、Stream 一样,在现代 Java 后端里经常出现。
注解读取 API
以类注解为例:
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。
注解为什么不会自动生效
下面代码不会自动校验:
public class CreateOrderRequest {
@NotBlank(message = "订单号不能为空")
private String orderNo;
}因为 JVM 不会看到 @NotBlank 就自动拦截构造方法、setter 或接口调用。
必须有处理器:
flowchart TD
A["业务代码写 @NotBlank"] --> B["注解进入 class 文件"]
B --> C["校验器或框架反射读取字段注解"]
C --> D["读取字段值"]
D --> E["判断是否为空"]
E --> F["不通过则抛异常或返回错误"]所以学习注解必须同时问两个问题:
- 注解写在哪里?
- 谁在什么时候读取它?
Demo 1:参数校验注解
定义注解:
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 class CreateUserRequest {
@NotBlank(message = "用户名不能为空")
private String username;
public CreateUserRequest(String username) {
this.username = username;
}
}校验器:
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);
}
}
}
}运行:
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:权限注解和动态代理
定义权限注解:
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();
}业务接口:
public interface OrderService {
@RequireRole("admin")
void refund(String orderNo);
}实现类:
public class OrderServiceImpl implements OrderService {
@Override
public void refund(String orderNo) {
System.out.println("refund: " + orderNo);
}
}代理处理器:
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);
}
}
}运行:
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 路由
定义路由注解:
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 {
}@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequestMapping {
String value();
}控制器:
@Controller
public class UserController {
@RequestMapping("/users")
public String list() {
return "user list";
}
}路由注册器:
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;
}
}
}运行:
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、JUnit | Lombok、MapStruct |
如果一个注解是 SOURCE,通常就是给编译期工具使用,不要期待运行时反射读到。
Spring 注解为什么能生效
以 @Service 和 @Autowired 为例,Spring 大致流程:
flowchart TD
A["启动 Spring 容器"] --> B["扫描指定包下的 class"]
B --> C["读取类上的 @Component/@Service"]
C --> D["生成 BeanDefinition"]
D --> E["创建 Bean 实例"]
E --> F["读取构造器、字段、方法上的注入注解"]
F --> G["从容器查找依赖"]
G --> H["注入依赖并完成初始化"]@Transactional 又是另一类,它通常依赖代理:
flowchart TD
A["方法上有 @Transactional"] --> B["Spring 创建代理对象"]
B --> C["外部调用代理方法"]
C --> D["代理先开启事务"]
D --> E["调用真实业务方法"]
E --> F["成功提交,异常回滚"]所以 Spring 注解生效的背后,常见机制包括:
- classpath 扫描。
- 反射读取注解。
- BeanDefinition 注册。
- 依赖注入。
- Bean 后置处理器。
- 动态代理或 CGLIB 代理。
- 拦截器链。
如果只背“Spring 通过注解实现 IOC/AOP”,还是太浅。要说清楚:注解只是标记,容器和代理机制才是执行者。
商业项目怎么用
参数校验
public class CreateOrderRequest {
@NotBlank(message = "订单号不能为空")
private String orderNo;
@NotNull(message = "用户ID不能为空")
private Long userId;
}商业价值:接口入口统一校验,避免脏数据进入业务层。
权限控制
@RequireRole("finance")
public void exportPaymentReport() {
}商业价值:把权限规则声明在接口或服务方法上,由 AOP 或拦截器统一处理,避免每个方法手写重复判断。
审计日志
@OperationLog("退款")
public void refund(String orderNo) {
}商业价值:统一记录操作人、操作时间、业务单号、执行结果,常用于后台管理、金融、医疗、资产平台。
ORM 映射
@TableName("sys_user")
public class UserEntity {
@ColumnName("user_name")
private String userName;
}商业价值:Java 字段和数据库表字段之间建立映射关系,框架根据注解生成 SQL 或映射结果。
常见坑
忘记写 RUNTIME
@Target(ElementType.FIELD)
public @interface NotBlank {
}默认是 CLASS,运行时反射读不到。需要:
@Retention(RetentionPolicy.RUNTIME)只写注解,没有处理器
@OperationLog("新增订单")
public void createOrder() {
}如果没有 AOP、拦截器或手写代码读取它,不会自动记录日志。
@Target 写错
定义时只允许字段:
@Target(ElementType.FIELD)却想标在方法上,编译器会报错。要根据真实使用位置设置。
注解加在接口方法上,代理读取实现类方法
JDK 动态代理调用时拿到的 Method 通常来自接口;CGLIB 代理可能面对实现类方法。注解到底加在接口还是实现类,会影响读取逻辑。
生产框架一般会做合并查找:接口、实现类、父类、桥接方法都可能要查。自己写简单 AOP 时要特别注意。
Spring 事务注解自调用失效
public void outer() {
inner();
}
@Transactional
public void inner() {
}outer 内部直接调用 inner,没有经过代理对象,所以事务注解可能不生效。这不是注解没写对,而是代理调用链没有经过。
注解放在 private 方法上却期待 AOP 生效
Spring AOP 基于代理拦截外部方法调用。private 方法不能被代理正常覆盖拦截,注解通常不会按预期生效。
线上排查流程
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、配置类加载"]排查建议:
- 反射读不到注解,先查
@Retention(RetentionPolicy.RUNTIME)。 - 编译就报注解位置错误,查
@Target。 - Spring 注解不生效,先确认对象是不是 Spring Bean。
- AOP 类注解、方法注解不生效,查是否经过代理对象。
- 事务、缓存、权限注解不生效,查自调用、private/final 方法、代理类型。
- 自定义注解没效果,查是否真的写了处理器或切面。
- 多模块项目扫描不到,查
@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 创建代理对象,外部调用经过代理,代理再根据注解开启事务、提交或回滚。
