主题
界面
主题
注解(Annotation)是"给代码贴标签":声明式地表达意图,而不是用代码去描述。
《反射基础》 学了反射(运行时操作类信息),本节学注解(在代码里留下可被读取的标记)。两者结合,就是框架的核心机制——@Autowired 只是一个标记,真正干活的是读取它的那个反射代码。
完成本节后,你应该能够:
@Override 与 @Deprecated 的作用;@Retention 与 @Target;看两种表达"这个方法已过时"的方式:
// 方式一:写在注释里(编译器不知道,IDE 不提示)
// TODO: 这个方法已废弃,请用 newMethod()
// 方式二:用注解(编译器知道,IDE 会划横线,调用方会收到警告)
@Deprecated
public void oldMethod() { }注解的价值是把"意图"变成机器可读的信息。它能被编译器检查(@Override)、被 IDE 识别(@Deprecated)、被框架读取(Spring 的 @Service)。
再看框架为什么依赖它:
@Service // ← 告诉 Spring:这是一个服务组件
public class OrderService {
@Autowired // ← 告诉 Spring:这里需要注入依赖
private OrderRepository repository;
}没有注解,Spring 就只能靠 XML 配置来描述"哪个类是组件、哪个字段要注入"。注解把配置搬进了代码里,且保持了类型安全。
@NotBlank
private String name;这一行本身什么也不做。它只是给 name 字段贴了一个标记。要让它产生效果,必须有代码去读它:
NotBlank notBlank = field.getAnnotation(NotBlank.class);
if (notBlank != null) { /* 执行校验 */ }这个认知很重要:遇到"注解不生效",问题通常不在注解本身,而在"有没有读取它的代码"。
| 注解 | 作用 | 什么时候用 |
|---|---|---|
@Override | 声明"这是重写父类方法" | 永远都该写(《继承与重写》 讲过它的价值) |
@Deprecated | 标记已过时 | 保留老方法但不想让人用时 |
@SuppressWarnings("unchecked") | 压制编译器警告 | 确认警告可忽略时 |
@FunctionalInterface | 声明"这是函数式接口" | 自定义函数式接口时(《Lambda 表达式》) |
@SafeVarargs | 声明可变参数安全 | 泛型可变参数方法 |
自定义注解时,必须告诉编译器三件事:
@Retention(RetentionPolicy.RUNTIME) // 保留到什么时候
@Target(ElementType.FIELD) // 能贴在哪里
@Documented // 是否进入 Javadoc
public @interface NotBlank {
String message() default "不能为空";
int minLength() default 1;
}@Retention 的三种取值(最关键的元注解):
| 取值 | 保留到 | 能否被反射读取 | 典型用途 |
|---|---|---|---|
SOURCE | 源码 | 不能 | 只给编译器看(@Override) |
CLASS | 编译后的 class 文件 | 不能 | 默认值,字节码工具用 |
RUNTIME | 运行时 | 能 | 框架读取的注解都用它 |
记住:只要你想在运行时用反射读到注解,就必须写 @Retention(RetentionPolicy.RUNTIME)。忘了这一行,是"注解不生效"最常见的原因。
@Target 的常用取值:TYPE(类/接口/枚举)、FIELD(字段)、METHOD(方法)、PARAMETER(参数)、CONSTRUCTOR(构造方法)、ANNOTATION_TYPE(注解)。
public @interface NotBlank {
String message() default "不能为空"; // 有默认值
int minLength() default 1;
}规则:
String message()),但实际是属性;String、Class、枚举、注解、以及它们的数组——不能是自定义对象;default 提供默认值,有默认值的属性可以不写;value 的属性可以省略属性名:@Table("users") 等价于 @Table(name = "users")。// 类上的注解
Table table = ProductForm.class.getAnnotation(Table.class);
// 字段上的
NotBlank notBlank = field.getAnnotation(NotBlank.class);
// 方法上的
Deprecated deprecated = method.getAnnotation(Deprecated.class);
// 判断是否存在(不需要属性时更简洁)
if (field.isAnnotationPresent(NotBlank.class)) { }查询方法的区别:getAnnotation 会沿继承链查找父类/接口上的注解(对于 @Inherited 的注解),而 getDeclaredAnnotation 只看当前元素。日常用 getAnnotation 即可。
| 时机 | 怎么做 | 例子 |
|---|---|---|
| 编译期 | 注解处理器(APT) | Lombok 的 @Data 生成 getter/setter、MapStruct 生成映射代码 |
| 运行期 | 反射读取 | Spring 的依赖注入、本节的校验器 |
Lombok 为什么"改了代码却看不到":它在编译期生成方法并写进字节码,源码里没有——S3 相关章节 会提到它的取舍。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface NotBlank {
String message() default "不能为空";
}
class Form {
@NotBlank(message = "用户名不能为空")
private String username;
}第 7 节的代码实现了一个完整校验器:遍历字段 → 读取注解 → 按注解规则校验 → 收集错误。这是参数校验框架的最小原型(Jakarta Bean Validation 就是这么工作的)。
// 原因一:忘了 @Retention(RUNTIME)
@Target(ElementType.FIELD)
@interface TooShort { } // 默认是 CLASS,反射读不到
field.getAnnotation(TooShort.class); // 返回 null
// 原因二:@Target 不匹配
@Target(ElementType.METHOD)
@interface ForField { }
@ForField // 编译错误:注解不适用于字段
private String name;
// 原因三:注解在类上,却去字段上找(或反之)排查顺序:① @Retention 是 RUNTIME 吗?② @Target 覆盖目标元素吗?③ 读取代码在找的元素位置对吗?
文件:labs/common-classes/src/main/java/cn/kb/learn/common/AnnotationLesson.java
package cn.kb.learn.common;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
public class AnnotationLesson {
/** 类级注解:给类贴一个"表名" */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Table {
String name();
}
/** 字段级注解:非空校验 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface NotBlank {
String message() default "不能为空";
int minLength() default 1;
}
/** 字段级注解:正数校验 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Positive {
String message() default "必须为正数";
}
@Table(name = "products")
static class ProductForm {
@NotBlank(message = "商品名不能为空", minLength = 2)
private String name;
@Positive(message = "价格必须大于 0")
private long priceInCents;
@NotBlank(message = "分类不能为空")
private String category = "默认分类";
ProductForm(String name, long priceInCents) {
this.name = name;
this.priceInCents = priceInCents;
}
}
/** 一个由注解驱动的校验器 */
static List<String> validate(Object target) throws IllegalAccessException {
List<String> errors = new ArrayList<>();
for (Field field : target.getClass().getDeclaredFields()) {
field.setAccessible(true);
Object value = field.get(target);
NotBlank notBlank = field.getAnnotation(NotBlank.class);
if (notBlank != null) {
String text = (String) value;
if (text == null || text.isBlank()) {
errors.add(field.getName() + ": " + notBlank.message());
} else if (text.length() < notBlank.minLength()) {
errors.add(field.getName() + ": 长度不能少于 " + notBlank.minLength() + " 个字符");
}
}
Positive positive = field.getAnnotation(Positive.class);
if (positive != null && ((Number) value).longValue() <= 0) {
errors.add(field.getName() + ": " + positive.message());
}
}
return errors;
}
public static void main(String[] args) throws Exception {
// 1. 注解的保留策略
System.out.println("Override 是注解: " + Override.class.isAnnotation());
System.out.println("Deprecated 保留到: "
+ Deprecated.class.getAnnotation(Retention.class).value());
// 2. 读取类上的注解
Table table = ProductForm.class.getAnnotation(Table.class);
System.out.println("类上的注解 @Table: " + table.name());
// 3. 读取字段上的注解及其属性
Field nameField = ProductForm.class.getDeclaredField("name");
NotBlank notBlank = nameField.getAnnotation(NotBlank.class);
System.out.println("字段注解属性: message=" + notBlank.message() + ", minLength=" + notBlank.minLength());
// 4. 注解驱动校验
ProductForm invalid = new ProductForm("A", 0);
System.out.println("校验失败的表单: " + validate(invalid));
ProductForm valid = new ProductForm("键盘", 19_900);
System.out.println("校验通过的表单: " + validate(valid).isEmpty() + "(错误列表为空)");
// 5. 没有注解的字段会被跳过
Field categoryField = ProductForm.class.getDeclaredField("category");
System.out.println("category 字段标注了 @NotBlank: " + categoryField.isAnnotationPresent(NotBlank.class));
}
}运行命令:
cd labs
./mvnw -pl common-classes compile
java -cp common-classes/target/classes cn.kb.learn.common.AnnotationLesson预期输出:
Override 是注解: true
Deprecated 保留到: RUNTIME
类上的注解 @Table: products
字段注解属性: message=商品名不能为空, minLength=2
校验失败的表单: [name: 长度不能少于 2 个字符, priceInCents: 价格必须大于 0]
校验通过的表单: true(错误列表为空)
category 字段标注了 @NotBlank: true两处值得注意:
| 输出 | 含义 |
|---|---|
Deprecated 保留到: RUNTIME | @Deprecated 之所以能被 IDE 和反射识别,是因为它保留到了运行时 |
校验失败的表单: [...] | 校验器的输出是结构化的错误列表,而不是一句提示——这就是注解驱动的好处 |
field.getAnnotation(NotBlank.class) 处断点,展开返回的 notBlank 对象,观察它的 message 与 minLength 两个属性值——注解在运行时就是一个代理对象,属性值随取随用;validate(invalid) 的循环里观察 field 变量,能看到它遍历了 name、priceInCents、category 三个字段;@Retention(RetentionPolicy.RUNTIME) 改成 RetentionPolicy.CLASS,重跑程序,观察 getAnnotation 返回 null 并抛出 NullPointerException——这是"注解不生效"最经典的现场;ProductForm.class.getAnnotations(),观察类上所有注解的列表。@Retention(RUNTIME) @Target(ElementType.FIELD)
@interface NotBlank { } // 默认 CLASS,反射读不到症状:getAnnotation 返回 null,后续 NPE。修复:加上 @Retention(RetentionPolicy.RUNTIME)。
@Target 与使用位置不匹配 @Target(ElementType.METHOD)
@interface NotBlank { }
@NotBlank
private String name; // 编译错误:注解类型不适用于此处好消息是:这类错误编译期就会暴露,不会留到运行时。
@interface Config {
Object value(); // 编译错误:属性类型不支持 Object
}修复:属性只能用基本类型、String、Class、枚举、注解或它们的数组。需要复杂结构时,用字符串编码(如 "a=1,b=2")或拆成多个属性。
@NotBlank
private String name; // 没有校验器读它,什么都不会发生修复:确认框架/工具是否支持这个注解;自己写的注解必须自己写读取逻辑。
目标:扩展校验器,体会"注解 + 反射"的威力。
步骤:
@Range(min, max),用于校验数值范围;给 priceInCents 加上"必须在 1 到 1000000 之间"的约束;@Range,并对超范围的输入输出错误信息;@Pattern(regexp),校验 name 只能包含中文、字母与数字;字段名: 错误描述 的格式,并思考:如果要支持"错误码"而不是文字描述,注解该怎么设计?验收方式:能解释第 5 步中"把描述换成错误码"对注解属性的影响。
扩展任务:给校验器加一个方法 boolean isValid(Object target),内部复用 validate——说明为什么"错误列表"比"布尔值"更有用(提示:调用方要展示具体哪里错了)。
基础题
@Retention 的三种取值分别是什么?框架用的注解应该是哪种?代码阅读题
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Loggable {
String value() default "执行";
}
class Service {
@Loggable("查询用户")
public String find() { return "用户数据"; }
@Loggable
public String save() { return "已保存"; }
}
Method method = Service.class.getMethod("find");
System.out.println(method.getAnnotation(Loggable.class).value());
Method save = Service.class.getMethod("save");
System.out.println(save.getAnnotation(Loggable.class).value());save 方法没有写属性值,为什么能拿到?编程题
@JsonField(name = "..."):指定字段序列化后的名字,默认用字段名;@JsonIgnore:序列化时跳过该字段;String toJson(Object target):把对象转成 {"key":"value","num":1} 形式的字符串(字符串加引号、数字不加);综合任务
@RequiresRole("ADMIN") 可标注在方法上;Object invokeWithCheck(Object target, String methodName, String currentRole): @RequiresRole 且角色不匹配,抛 IllegalStateException("权限不足");@PreAuthorize 与这个简化版有什么共同点?提示 1(方向):第 4 题的关键是 default 属性值——没写就用默认值。
提示 2(关键点):第 5 题判断值类型时用 value instanceof Number 决定要不要加引号。
提示 3(接近答案):第 6 题的 invokeWithCheck 用 getMethod(methodName) 找方法(无参),读注解后决定是否放行,再 invoke。
@Retention(RetentionPolicy.RUNTIME)、@Target。SOURCE(源码,编译后丢弃)、CLASS(保留到 class 文件,反射读不到,默认值)、RUNTIME(保留到运行时,可被反射读取)。框架用的注解必须是 RUNTIME。查询用户
执行find() 显式写了 @Loggable("查询用户"),取到 "查询用户";save() 只写了 @Loggable,于是取到 default "执行"。注意 @Loggable("查询用户") 的写法:因为属性名是 value,所以可以省略属性名。如果属性叫 name,就必须写 @Loggable(name = "查询用户")。
package cn.kb.learn.common;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
public class MiniJson {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface JsonField {
String name() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface JsonIgnore {
}
static class User {
@JsonField(name = "user_name")
private String name = "张三";
private int age = 30;
@JsonIgnore
private String password = "secret";
private boolean active = true;
}
static String toJson(Object target) throws IllegalAccessException {
List<String> parts = new ArrayList<>();
for (Field field : target.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(JsonIgnore.class)) {
continue;
}
field.setAccessible(true);
JsonField jsonField = field.getAnnotation(JsonField.class);
String key = (jsonField != null && !jsonField.name().isEmpty()) ? jsonField.name() : field.getName();
Object value = field.get(target);
if (value instanceof Number || value instanceof Boolean) {
parts.add("\"" + key + "\":" + value);
} else {
parts.add("\"" + key + "\":\"" + value + "\"");
}
}
return "{" + String.join(",", parts) + "}";
}
public static void main(String[] args) throws IllegalAccessException {
System.out.println(toJson(new User()));
}
}预期输出:
{"user_name":"张三","age":30,"active":true}package cn.kb.learn.common;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
public class PermissionChecker {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface RequiresRole {
String value();
}
static class AdminService {
@RequiresRole("ADMIN")
public String deleteAll() {
return "已删除全部数据";
}
public String query() {
return "查询结果";
}
}
static Object invokeWithCheck(Object target, String methodName, String currentRole) throws Exception {
Method method = target.getClass().getMethod(methodName);
RequiresRole requiresRole = method.getAnnotation(RequiresRole.class);
if (requiresRole != null && !requiresRole.value().equals(currentRole)) {
throw new IllegalStateException("权限不足:需要 " + requiresRole.value() + ",当前 " + currentRole);
}
return method.invoke(target);
}
public static void main(String[] args) throws Exception {
AdminService service = new AdminService();
System.out.println("普通用户查询: " + invokeWithCheck(service, "query", "USER"));
try {
invokeWithCheck(service, "deleteAll", "USER");
} catch (IllegalStateException exception) {
System.out.println("预期内的失败: " + exception.getMessage());
}
System.out.println("管理员删除: " + invokeWithCheck(service, "deleteAll", "ADMIN"));
}
}与 Spring Security 的 @PreAuthorize 的共同点:
预期输出:
普通用户查询: 查询结果
预期内的失败: 权限不足:需要 ADMIN,当前 USER
管理员删除: 已删除全部数据@Retention 与 @Target;@Retention → @Target → 读取位置三步排查。《Lambda 表达式》:另一种"声明式表达"——用函数作为参数传递。
暂无