Lesson 45 · Java 高级特性
注解体系:元注解、自定义注解与运行时处理
面试官:你自定义过注解吗?Spring 的 @Autowired 是怎么工作的?
注解(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 注解驱动的底层原理。
元注解:注解的注解
元注解(Meta-Annotation)是 JDK 提供的"用来修饰注解"的注解。它们定义了自定义注解的行为约束——在哪里能用、能活多久、能否继承、能否重复。JDK 5 引入四个,JDK 8 新增一个:
@Retention —— 注解的生命周期
@Retention 决定注解存活到哪个阶段,是最关键的元注解:
| 策略 | 存活阶段 | 典型用途 |
|---|---|---|
| SOURCE | 仅源码,编译后丢弃 | Lombok @Data、@SuppressWarnings |
| CLASS | 编译期保留在 .class 文件中,运行时不可获取(默认值) | 字节码增强工具 |
| RUNTIME | 运行时可通过反射获取 | Spring @Autowired、@Transactional |
这是面试高频考点。如果你自定义的注解需要在运行时通过反射处理(几乎所有框架注解都是如此),必须标注 @Retention(RetentionPolicy.RUNTIME)。SOURCE 和 CLASS 级别的注解在运行时完全不存在,getAnnotation() 将返回 null。
public enum RetentionPolicy {
SOURCE, // 编译器处理后丢弃
CLASS, // 保留在 class 文件,JVM 不加载
RUNTIME // 保留在 class 文件,JVM 加载,反射可读
}
@Target —— 注解能标注在哪里
@Target 限定注解的使用位置,防止误用:
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 允许重复标注,但需要定义一个容器注解:
// ① 定义容器注解
@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 管重复标注(需容器注解)。
自定义注解:手写一个 @Log
自定义注解用 @interface 关键字定义,语法类似接口,但本质是一个继承自 java.lang.annotation.Annotation 的特殊接口。来看一个实际的日志注解:
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; |
| String | String value(); |
| Class | Class<?> type() default Void.class; |
| Enum | LogLevel level() default LogLevel.INFO; |
| Annotation | Author author(); |
| 以上类型的数组 | String[] tags() default {}; |
使用自定义注解:
public class OrderService {
@Log(value = "创建订单", level = "WARN", module = "订单")
public void createOrder(String orderId) {
// 业务逻辑...
}
@Log("查询订单") // 只有 value 属性时可以省略属性名
public void getOrder(String orderId) {
// 业务逻辑...
}
}
如果注解只有一个属性且名为 value,使用时可以省略 value=,直接写 @Log("描述")。一旦有其他属性需要指定,就必须显式写 value="描述"。这是 Java 语法糖,面试中偶尔会考。
运行时处理:用反射读取注解
定义注解只是第一步——真正赋予注解意义的是运行时处理器。通过反射 API,我们可以在运行时检查类、方法、字段上的注解并执行对应逻辑。
核心反射 API
// 判断是否标注了指定注解
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);
实战:构建一个简单的日志注解处理器
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)返回注解中声明的值。这就是为什么注解属性的语法看起来和方法定义一模一样——它们本质上就是接口方法。
① 必须先确保注解是 @Retention(RUNTIME);② 用 isAnnotationPresent() 做判断,getAnnotation() 拿实例;③ 注解属性通过方法调用读取(本质是动态代理);④ 遍历类成员时注意 getDeclaredMethods() 不包含继承方法,getMethods() 只包含 public 方法。
Spring 注解驱动:@Autowired 与 @Transactional 的底层原理
Spring 框架是注解驱动的最佳范例。面试常问"Spring 的 @Autowired 是怎么工作的",本质是考察你是否理解注解 + 反射 + BeanPostProcessor的协作机制。
@Autowired 的底层实现
@Autowired 的注入并非 Spring 容器"魔法般"自动完成,而是靠一个具体的后处理器: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;
}
}
因为 @Transactional 基于代理实现。同类内部调用(this.methodB())走的是原始对象而非代理对象,事务拦截器不会被触发。解决方案:注入自身(@Autowired self)、使用 AopContext.currentProxy()、或拆分成不同 Bean。
注解驱动 vs XML 配置:优劣对比
在 Spring 早期(2.x),所有配置都写在 XML 文件中。随着注解机制的成熟(Spring 3+ 尤其是 Spring Boot),注解驱动逐渐成为主流。面试中经常要求对比两者:
| 维度 | XML 配置 | 注解驱动 |
|---|---|---|
| 配置位置 | 独立于代码,集中管理 | 分散在各类上,与代码耦合 |
| 可读性 | 结构清晰,适合大型项目 | 直观简洁,适合中小型项目 |
| 修改成本 | 改 XML 无需重新编译 | 改注解需重新编译 |
| 类型安全 | 字符串配置,易出错 | 编译期检查,类型安全 |
| 灵活性 | 运行时可替换配置 | 编译后固定,不可动态切换 |
| Spring Boot | 不推荐,需要额外配置 | 原生支持,自动配置 |
| 第三方集成 | 适合无源码的第三方 Bean | 需要能修改源码 |
<!-- 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 外部化配置
总结:注解设计原则与面试回答模板
注解体系的设计遵循三个核心原则:
- 声明式编程:注解让开发者"声明意图"而非"编写实现"。@Transactional 声明"这个方法需要事务",而非手动写 begin/commit/rollback
- 关注点分离:业务代码保持纯净,横切逻辑(日志、事务、缓存)通过注解 + 处理器统一管理
- 框架扩展点:注解是框架提供给开发者的"插件接口",框架通过反射扫描注解来执行开发者定义的逻辑
常见的注解设计模式:
| 模式 | 代表注解 | 处理方式 |
|---|---|---|
| 标记注解 | @Override、@FunctionalInterface | 编译器检查(SOURCE) |
| 元数据注解 | @Deprecated、@SuppressWarnings | 编译器警告 / IDE 提示 |
| 依赖注入 | @Autowired、@Resource、@Inject | BeanPostProcessor 反射注入 |
| AOP 切面 | @Transactional、@Cacheable | 代理 + 方法拦截器 |
| 代码生成 | @Data、@Builder(Lombok) | 编译期 APT 注解处理器 |
| 路由映射 | @RequestMapping、@GET | 启动时扫描注册路由表 |
面试回答模板
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。"