Appearance
第31课:注解
🎯 学习目标
- 理解注解的本质(附加元数据的标记,本身无逻辑)
- 掌握内置注解(@Override/@Deprecated/@SuppressWarnings)
- 掌握自定义注解 + 四个元注解(@Target/@Retention/@Documented/@Inherited)
- 掌握运行时反射读取注解(RetentionPolicy.RUNTIME)
- 知道编译时注解处理器的作用
📖 一、概念讲解:注解是什么
注解(Annotation):附加在类/方法/字段上的元数据标记。注解本身不含逻辑,需要"处理器"(反射或编译期)读取它并执行相应行为。
java
@Override // 标记:这是重写方法,编译器检查
@Deprecated // 标记:已过时
@Test(timeout=100) // 自定义:带参数的标记
public void run() { ... }注解 vs 注释
- 注释(// /**/):给人看,编译后丢弃。
- 注解(@):给"工具/框架/编译器"看,能在编译期或运行时被读取。
注解的两种处理时机
- 编译时(SOURCE/CLASS):编译期处理,如 @Override(检查)、Lombok @Data(生成代码)。
- 运行时(RUNTIME):反射读取,如 Spring @Autowired、JUnit @Test。
📖 二、内置注解
java
@Override // 标记重写父类/接口方法,签名错则编译报错(防拼写错误)
@Deprecated // 标记过时,使用时编译器警告
@SuppressWarnings("unchecked") // 抑制指定警告(unchecked/deprecation/rawtypes...)
@FunctionalInterface // 标记函数式接口(只有一个抽象方法),便于 Lambda@Override 是"编译时检查"注解的典型——它本身不改变行为,只让编译器验证方法签名是否真的重写。
📖 三、自定义注解 + 元注解
java
import java.lang.annotation.*;
@Target(ElementType.METHOD) // 能标注在哪些位置
@Retention(RetentionPolicy.RUNTIME) // 保留到何时
@Documented // 出现在 Javadoc
@Inherited // 子类继承(仅对类有效)
public @interface MyTest {
String value() default ""; // 注解元素(无参方法语法)
int timeout() default 0;
String[] tags() default {};
}四个元注解
| 元注解 | 作用 |
|---|---|
| @Target | 能标注位置:TYPE(类)/METHOD/FIELD/CONSTRUCTOR/PARAMETER/ANNOTATION_TYPE/PACKAGE 等 |
| @Retention | 保留策略:SOURCE(编译丢弃)/CLASS(默认,class 文件有但运行时不可见)/RUNTIME(运行时反射可见) |
| @Documented | 出现在 Javadoc |
| @Inherited | 子类继承该注解(仅 @Target=TYPE 有效) |
注解元素语法
- 用"无参方法"声明元素:
String value() default ""; - 支持类型:基本类型、String、Class、枚举、注解、及它们的数组。
- 有 default 的元素可不填;无 default 必须填。
value()特殊:唯一元素且名为 value 时,可省略value=,写@MyTest("x")。
📖 四、运行时反射读取注解
java
@Retention(RetentionPolicy.RUNTIME) // 必须 RUNTIME 才能反射读
public @interface Route { String path(); }
class Controller {
@Route(path = "/users")
public void listUsers() { }
}
// 反射读取
Method m = Controller.class.getMethod("listUsers");
if (m.isAnnotationPresent(Route.class)) {
Route r = m.getAnnotation(Route.class);
System.out.println(r.path()); // /users
}关键:只有 @Retention(RUNTIME) 的注解才能在运行时反射读取(isAnnotationPresent/getAnnotation)。SOURCE/CLASS 的注解运行时不可见。
这是 Spring MVC @RequestMapping、JUnit @Test 的机制——框架启动时反射扫描注解,按注解内容路由/执行。
📖 五、编译时注解处理器
编译时注解处理器(APT)能在编译期扫描注解并生成代码/校验:
- Lombok:
@Data@Getter编译期生成 getter/setter。 - Dagger/Hilt:
@Inject生成依赖注入代码。 - MapStruct:
@Mapper生成对象转换代码。 - ButterKnife(Android):
@BindView生成 View 绑定。
优势:编译期生成代码,运行时无反射开销。比运行时反射更快。
⚠️ 六、常见陷阱
陷阱1:忘加 @Retention(RUNTIME)
默认 CLASS,运行时反射读不到。要运行时读取必须显式 RUNTIME。
陷阱2:@Inherited 不对接口生效
@Inherited 只对类继承有效,实现接口不会继承接口上的注解。
陷阱3:注解元素不能为 null
注解元素有默认值或必填,但不能设 null(编译错误)。用空数组/空字符串模拟"无"。
陷阱4:以为注解能改变行为
注解只是标记,不改变被注解代码行为。必须配处理器(反射/APT)才有作用。
陷阱5:@Target 限制位置
@Target(METHOD) 的注解不能放类上(编译错误)。按需设置 Target。
🆚 七、Java vs C / 其他
| 特性 | C | Java |
|---|---|---|
| 元数据 | 无(靠注释/约定) | 注解(标准化、可程序读取) |
| 编译期代码生成 | 宏(文本替换,易错) | 注解处理器(类型安全) |
对 C 程序员:注解类似"强类型的、可被工具读取的注释"。C 靠 #pragma/宏/约定做元数据,脆弱。Java 注解让框架能"声明式"编程(标注即配置,框架扫描注解执行),这是 Spring/JUnit 等的基础。
💡 八、最佳实践
- 运行时读取必须 @Retention(RUNTIME)。
- 按需设 @Target,限制注解位置。
- value 元素利用简写:单一元素名 value 时可省略。
- 编译期优先 APT:比运行时反射快(Lombok/MapStruct 模式)。
- 注解配合反射实现声明式框架:标记 + 扫描执行。
📝 练习预告
完成 练习/Ex31_Annotation.java 中的 6 道题:
- 内置注解观察(@Override 检查)
- 自定义注解 + 元注解
- 运行时反射读注解
- 模拟 @Route 路由扫描
- 注解元素默认值
- 综合:简易 @Test 注解运行器
完成后对比 答案/Sol31.java,查看逐行讲解与多解法。
📖 九、注解的三种使用方式
注解本身只是元数据,必须被工具或框架读取后才有意义。
1. 编译期检查
java
@Override
public String toString() {
return "User";
}@Override 由编译器检查。如果方法签名写错,编译失败。
2. 运行时反射读取
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Route {
String value();
}框架启动时扫描:
java
for (Method method : controllerClass.getDeclaredMethods()) {
Route route = method.getAnnotation(Route.class);
if (route != null) {
routeRegistry.put(route.value(), method);
}
}3. 编译期生成代码
APT 注解处理器可以在编译期生成代码,例如:
text
Lombok 生成 getter/setter。
MapStruct 生成对象转换类。
Dagger 生成依赖注入代码。编译期处理通常比运行时反射更高效,但实现更复杂。
📖 十、元注解详解
常用元注解:
java
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Inherited
public @interface MyAnnotation {
}说明:
| 元注解 | 作用 |
|---|---|
@Retention | 注解保留到源码、class 还是运行时 |
@Target | 限制注解能放在哪里 |
@Documented | 是否进入 Javadoc |
@Inherited | 类注解是否可被子类继承 |
@Repeatable | 是否允许重复标注 |
最容易错的是 @Retention。如果运行时要反射读取,必须是 RUNTIME。
🧪 十一、实战案例:简易测试框架
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTest {
}运行器:
java
public class TestRunner {
public static void run(Class<?> testClass) throws Exception {
Object instance = testClass.getDeclaredConstructor().newInstance();
for (Method method : testClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(MyTest.class)) {
method.setAccessible(true);
method.invoke(instance);
}
}
}
}这就是 JUnit 的核心思想:注解标记 + 反射扫描 + 方法调用。
📌 十二、注解设计建议
设计自定义注解时,先问:
text
这个注解由谁读取?
是在编译期读取还是运行时读取?
能标在哪里?
是否需要默认值?
是否允许重复?
是否需要组合注解?示例:
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Audit {
String action();
boolean recordArgs() default false;
}注解元素支持的类型有限:
text
基本类型
String
Class
enum
注解
以上类型的数组不能使用普通对象、集合或 null。
🔍 十三、自测问题
text
注解为什么本身不会改变程序行为?
SOURCE、CLASS、RUNTIME 三种 Retention 有什么区别?
@Target 不写会发生什么?
注解元素为什么不能是 null?
运行时注解读取为什么依赖反射?
Lombok 和 JUnit 使用注解的方式有什么不同?
什么情况下应该使用编译期注解处理器?
组合注解在 Spring 中有什么价值?🧭 十四、注解落地清单
设计一个注解前,先回答这些问题:
text
这个注解解决的是配置问题、校验问题还是代码生成问题?
读取它的是编译器、构建工具、框架启动器还是运行时逻辑?
Retention 应该是 SOURCE、CLASS 还是 RUNTIME?
Target 应该限制到 TYPE、METHOD、FIELD 还是 PARAMETER?
是否需要默认值?
是否允许重复标注?
是否需要被子类继承?
是否需要进入 Javadoc?
注解名称是否表达业务语义,而不是实现细节?如果注解只给人看,不被任何工具读取,那普通注释往往更合适。
🧩 十五、组合注解与语义建模
Spring 中大量使用组合注解:
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
String value() default "";
}@RestController 本质上组合了 @Controller 和 @ResponseBody。
组合注解的价值:
text
减少重复配置。
把技术细节包装成业务语义。
让代码更容易被扫描和统一处理。
提供团队级约定。你也可以为自己的项目设计组合注解:
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Audit(action = "PAYMENT")
@RequireLogin
public @interface PaymentOperation {
}框架或拦截器只要识别 @PaymentOperation,就能统一处理登录校验和审计逻辑。
设计组合注解时要避免过度抽象。一个注解应该表达稳定语义,不应该把临时需求塞成全局约定。
🛠 十六、注解排查清单
当注解“不生效”时,按下面顺序排查:
text
注解是否写在正确的位置?
@Target 是否允许这个位置?
@Retention 是否为 RUNTIME?
扫描范围是否包含当前类?
目标类是否被框架管理?
方法是否因为 private/final/static 导致代理无法增强?
注解是否写在接口上但框架只扫描实现类?
是否存在组合注解但框架没有递归查找元注解?
是否在测试环境和生产环境使用了不同依赖版本?Spring 中尤其要注意代理边界:
text
同类内部方法调用不会经过代理。
final 类或 final 方法不能被 CGLIB 正常增强。
JDK 动态代理只能代理接口方法。
注解放在实现类还是接口上,取决于具体框架读取策略。注解排查的关键是:注解只是元数据,真正决定行为的是“谁读取它、什么时候读取、读取后做什么”。
🧪 十七、实战案例:权限注解
定义一个方法级权限注解:
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequireRole {
String[] value();
}业务方法:
java
public class OrderService {
@RequireRole({"ADMIN", "OPERATOR"})
public void cancelOrder(long orderId) {
System.out.println("cancel order: " + orderId);
}
}运行时检查:
java
public static void checkPermission(Method method, Set<String> userRoles) {
RequireRole requireRole = method.getAnnotation(RequireRole.class);
if (requireRole == null) {
return;
}
for (String role : requireRole.value()) {
if (userRoles.contains(role)) {
return;
}
}
throw new SecurityException("permission denied: " + method.getName());
}这类注解通常不会直接手写反射调用,而是放到拦截器、过滤器或 AOP 切面里统一处理。
✅ 十八、掌握标准
学完本课后,应能做到:
text
能解释注解为什么只是元数据。
能正确选择 Retention 和 Target。
能定义带默认值、数组、Class、枚举元素的注解。
能用反射读取类、字段、方法上的注解。
能解释 Lombok 与 JUnit 处理注解的区别。
能排查运行时读取不到注解的问题。
能识别组合注解的价值与风险。
能说明注解、反射、动态代理在 Spring 中如何协作。如果看到 @Something 就以为它自动改变程序行为,说明还需要回到“注解处理器”这个核心概念。
🎓 下一步
- 第32课:Lambda表达式 — 函数式接口、方法引用、闭包