前言
注解(Annotation)是给类、方法、字段贴标签的元数据机制。
**元数据(Metadata)**即"描述数据的数据"------好比快递包裹上的运单标签:包裹本身是数据,标签上的收件人、地址、重量是元数据,快递员靠标签分拣而不拆包裹。注解同理:
@Override不改变方法逻辑,只是告诉编译器"这个方法改写了父类方法,帮我检查一下"。
注解本身不做事,靠注解处理器发挥作用:运行时用反射(Spring、JUnit),编译时用 APT(下文) 生成代码(MapStruct)。Lombok 不同------它并非标准 APT,而是直接操作编译器内部 AST,属于非标准手段。这也是 Lombok 需要 IDE 插件的原因------IDE 需要识别这些非标准修改后的 AST 才能正确补全和编译。
AST(Abstract Syntax Tree,抽象语法树) :编译器将源码解析成树状结构,每个节点是一个语法单元(类声明、方法调用、变量赋值等)。好比把一篇中文文章拆成主谓宾语法树------编译器后续的类型检查、代码生成都在这棵树上操作。Lombok 直接修改这棵树来"注入" getter/setter,跳过了生成新
.java文件的步骤。
一、概念
1.1 为什么需要注解
没有注解时,配置靠 XML、标记靠命名约定(JUnit3 方法必须以 test 开头),本质问题:配置与代码分离。注解把配置贴在代码旁,改代码即改配置,无需两边同步。
1.2 注解的定义
注解是给代码打标签------用 @ 附加的元数据,不改变代码行为,给工具/框架/编译器提供信息。一句话:"不说话的标签",贴在代码上等人来读。
1.3 注解与注释的区别
注解会被编译器保留到指定阶段,可被机器读取;注释编译后彻底消失,只是给人看的。
二、内置注解与元注解
2.1 JDK 内置注解
| 注解 | 作用 | 关键限制 |
|---|---|---|
@Override |
编译期检查是否真正重写 | --- |
@Deprecated |
标记过时 API,调用时警告 | --- |
@SuppressWarnings |
抑制特定编译器警告 | --- |
@FunctionalInterface |
标记函数式接口 | 只能有一个抽象方法,默认/静态方法不限 |
@SafeVarargs |
抑制可变参数泛型警告 | 只能用于 static/final/private 方法,或 final 类中的实例方法 |
@Native |
标记 JNI 可引用字段 | 防 JIT 内联优化 |
2.2 元注解
| 元注解 | 作用 |
|---|---|
@Retention |
控制保留策略:SOURCE(编译后丢弃)/ CLASS(字节码保留,运行时不读)/ RUNTIME(运行时反射可读) |
@Target |
限定可贴位置:TYPE / FIELD / METHOD / PARAMETER / CONSTRUCTOR / LOCAL_VARIABLE / ANNOTATION_TYPE / PACKAGE / TYPE_PARAMETER / TYPE_USE |
@Documented |
出现在 Javadoc 中 |
@Inherited |
子类继承父类注解(仅类层面,不是注解继承关系) |
@Repeatable |
允许同一位置重复使用,需配合容器注解 |
Lombok 注解使用 SOURCE 策略,编译后
.class文件不留痕迹。
三、自定义注解
3.1 基本语法
java
@Retention(RetentionPolicy.RUNTIME) // 运行时可通过反射读取------Spring AOP 等框架依赖于此
@Target(ElementType.METHOD) // 只能贴在方法上,避免误贴在类/字段
public @interface Log {
// value 是特殊属性名:使用时若只传 value,可以省略属性名直接写 @Log("描述")
String value() default ""; // 默认空串避免 NPE,注解属性默认值不能为 null
LogLevel level() default LogLevel.INFO; // 不指定则默认 INFO 级别
enum LogLevel { DEBUG, INFO, WARN, ERROR } // 内部枚举,比字符串常量更安全(编译期校验)
}
// 使用示例:@Log("创建订单") 或 @Log(value="创建订单", level=Log.LogLevel.WARN)
3.2 继承限制
注解不能 extends,@interface 隐式继承 Annotation。注解间无继承体系,复用靠组合或 @Inherited(仅让子类继承父类标注)。
3.3 属性类型与特殊规则
| 支持类型 | 示例 |
|---|---|
| 基本类型、String、Class、枚举、注解 | String name() default ""; |
| 以上类型的一维数组 | String[] tags() default {}; |
特殊规则:
- 属性名不能是
class或interface(关键字) - 属性不能有参数、不能抛异常
- 默认值不能为
null,用空字符串或UNSPECIFIED枚举代替 - 嵌套注解默认值:
NotNull notNull() default @NotNull(message = "default required"); - 不指定
@Target时默认允许早期 8 个位置,不含TYPE_USE/TYPE_PARAMETER
四、注解处理器
4.1 运行时处理(反射)
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogExecution { String value() default ""; }
java
public class LogProxy {
// 泛型方法:<T> 声明类型参数,T 返回类型------保持代理对象与原目标类型一致
public static <T> T createProxy(T target) {
// Proxy.newProxyInstance 三个参数:类加载器、目标接口数组、InvocationHandler(函数式接口)
return (T) Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 使用目标类的加载器,确保类一致性
target.getClass().getInterfaces(), // JDK 动态代理基于接口,目标必须实现接口
(proxy, method, args) -> { // lambda 实现 InvocationHandler
// 从当前被调用的方法上获取 @LogExecution 注解
LogExecution ann = method.getAnnotation(LogExecution.class);
if (ann != null) {
// 注解有描述则用描述,否则退化为方法名作为日志标识
String desc = ann.value().isEmpty() ? method.getName() : ann.value();
long start = System.currentTimeMillis(); // 纳秒精度用 System.nanoTime()
Object result = method.invoke(target, args); // invoke:在 target 上以 args 调用 method
System.out.println(desc + " 耗时 " + (System.currentTimeMillis() - start) + "ms");
return result;
}
// 无注解的方法直接透传调用,不增加额外开销
return method.invoke(target, args);
});
}
}
// 限制:目标类必须实现接口(JDK 动态代理强制要求),无接口时改用 CGLIB 字节码子类代理
反射 API 速查:
| 方法 | 用途 |
|---|---|
getAnnotation(Class) |
获取指定注解 |
getAnnotations() |
获取所有注解(含继承) |
getDeclaredAnnotations() |
仅直接声明的注解 |
isAnnotationPresent(Class) |
判断是否存在(不创建代理实例,更高效) |
4.2 编译时处理(APT)
**APT(Annotation Processing Tool)**是 javac 内置的编译期注解处理框架,在编译阶段扫描注解并调用处理器生成新代码。与 4.1 的运行时反射处理相反------APT 在编译期完成所有工作,生成的代码直接参与后续编译,运行时零开销。
4.2.1 处理流程:轮(Round)机制
APT 不是"扫一遍就完",而是多轮(Round)迭代:
第 1 轮:扫描源码 → 发现 @Builder 注解 → 调用 process() → 处理器生成新 .java 文件
第 2 轮:扫描新生成的 .java → 如果新文件也有注解 → 再次调用 process()
第 N 轮:直到没有任何新文件生成,编译结束
为什么需要多轮? 因为注解处理器可能生成带注解的新源文件(如 MapStruct 生成的 Impl 类可能又被其他处理器处理),必须反复扫描直到"无新增"。
每轮结束时编译器检查:
- 本轮是否有新文件生成 → 有则开启下一轮
- 本轮是否没有任何处理器被调用 → 结束
- 是否已到最大轮数(防止死循环)→ 超限报错
4.2.2 编写处理器:AbstractProcessor
java
// @SupportedAnnotationTypes 声明本处理器要处理的注解全限定名,支持通配符 "*"
@SupportedAnnotationTypes("net.feixiang.Builder")
// @SupportedSourceVersion 声明支持的 Java 源码版本,处理器会匹配当前编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_8)
// 继承 AbstractProcessor 而非直接实现 Processor------框架已处理 SPI 注册和生命周期
public class BuilderProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) { // 每轮编译回调:一个 round 处理一批注解
// 遍历本轮所有被 @Builder 标注的元素(类/方法/字段等)
for (Element e : roundEnv.getElementsAnnotatedWith(Builder.class))
// processingEnv 是 AbstractProcessor 的受保护字段,编译器自动注入
// 通过 Messager 向编译日志输出 NOTE 级别信息(非错误,不会中断编译)
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "发现: " + e);
return true; // true = 声明已处理完毕,后续处理器不再处理这批注解
}
}
@SupportedAnnotationTypes:声明本处理器关注哪些注解(全限定名),支持"*"匹配所有注解。@SupportedSourceVersion:声明能处理的 Java 版本,编译器匹配后才启用。process()的参数 :annotations:本轮请求处理的注解类型集合。roundEnv:本轮环境,可查询本轮有哪些元素被标注。
- 返回值含义 :
true= 声明"这批注解我处理了",其他处理器不再接收;false= 未处理,交给下一个处理器。 getSupportedAnnotationTypes()/getSupportedSourceVersion():注解方式的替代方案------覆写这两个方法可动态决定支持范围。
4.2.3 四个核心 API
AbstractProcessor 提供 processingEnv 受保护字段(编译器自动注入),通过它获取四个工具:
| API | 获取方式 | 职责 | 一句话 |
|---|---|---|---|
| Filer | processingEnv.getFiler() |
生成新的源文件、类文件、资源文件 | "造文件的"------APT 的核心产出工具 |
| Messager | processingEnv.getMessager() |
向编译日志输出信息 | "喊话的"------NOTE/WARNING/ERROR,ERROR 会中断编译 |
| Elements | processingEnv.getElementUtils() |
操作程序元素(包、类、方法、字段) | "查户口的"------获取包名、父类、注解等 |
| Types | processingEnv.getTypeUtils() |
操作类型系统(类型转换、父子关系判断) | "验血型的"------判断 A 是否是 B 的子类等 |
Filer 才是 APT 的价值核心 :运行时反射是"读"注解,APT 是"写"新代码。Filer 通过
createSourceFile()生成.java文件,编译器会在后续轮中编译这些新文件。
4.2.4 注册处理器
编译器通过 SPI(ServiceLoader)机制 发现处理器,三种注册方式:
- 手动创建
META-INF/services/javax.annotation.processing.Processor文件,内容为处理器全限定类名(一行一个) - 用 Google AutoService 的
@AutoService(Processor.class)注解,编译时自动生成上述 SPI 文件 - Java 9+ 模块化项目:在
module-info.java中用provides Processor with XxxProcessor;
4.2.5 关键限制
标准 APT 只能生成新文件,不能修改已有源码。这是 Java 语言规范的设计约束------注解处理器不应改变原始代码的语义。
Lombok 之所以能"修改"代码(自动生成 getter/setter),是因为它绕过了标准 APT,直接操作 javac 编译器内部的 AST(抽象语法树),属于非标准手段。这也是 Lombok 需要 IDE 插件的原因------IDE 需要识别这些非标准修改才能正确补全和编译。
五、举例
5.1 参数校验框架
java
// 文件:NotNull.java
@Retention(RetentionPolicy.RUNTIME) // 运行时通过反射读取,做校验
@Target(ElementType.FIELD) // 仅字段------校验的是字段值,不是方法或类
public @interface NotNull {
// 校验失败时的提示信息,支持自定义文案
String message() default "不能为空";
}
java
// 文件:Length.java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Length {
int min() default 0; // 最小长度,默认不限制
int max() default Integer.MAX_VALUE; // 最大长度,默认不限(Integer.MAX_VALUE = 2147483647)
String message() default "长度不合法";
}
java
// 文件:Validator.java
public class Validator {
// 返回所有不满足校验的错误信息列表,空列表表示全部通过
public static List<String> validate(Object obj) throws Exception {
List<String> errors = new ArrayList<>();
for (Field f : obj.getClass().getDeclaredFields()) { // getDeclaredFields:所有字段含 private
f.setAccessible(true); // 突破 private 访问控制,否则 get(obj) 抛 IllegalAccessException
Object v = f.get(obj); // 反射获取当前对象该字段的实际值
NotNull nn = f.getAnnotation(NotNull.class); // 读取 @NotNull,不存在则返回 null
if (nn != null && v == null)
errors.add(f.getName() + ": " + nn.message());
Length len = f.getAnnotation(Length.class); // 读取 @Length,同样不存在则 null
if (len != null && v instanceof String s // Java 16+ 模式匹配:instanceof 同时赋值
&& (s.length() < len.min() || s.length() > len.max()))
errors.add(f.getName() + ": " + len.message());
}
return errors;
}
}
// 注意:@NotNull 对基本类型(int/long 等)无效------基本类型默认值为 0 而非 null,应改用 @Range
5.2 简易 ORM
ORM(Object-Relational Mapping,对象关系映射):将 Java 对象与数据库表建立映射关系------类对应表、字段对应列、对象对应行,从而用操作对象的方式替代手写 SQL 字符串拼接。Hibernate、MyBatis 都是成熟的 ORM 框架,此处用注解实现一个极简版演示原理。
java
// 文件:Table.java
@Retention(RetentionPolicy.RUNTIME) // 运行时反射读取以拼接 SQL
@Target(ElementType.TYPE) // 类级别,标记一个类对应一张数据库表
public @interface Table {
String name(); // 表名,必填------无法确定合理默认值
}
java
// 文件:Column.java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD) // 字段级别,标记一个字段对应一列
public @interface Column {
String name(); // 列名,必填
boolean primaryKey() default false; // 主键标记,默认不是
}
java
// 文件:SimpleOrm.java
public class SimpleOrm {
// 根据实体对象自动生成 INSERT 语句
public static String generateInsert(Object entity) throws Exception {
Class<?> c = entity.getClass();
Table t = c.getAnnotation(Table.class); // 读取类上的 @Table 注解获取表名
if (t == null)
throw new IllegalArgumentException("缺少 @Table 注解"); // 没有 @Table 无法确定目标表
StringBuilder cols = new StringBuilder(); // 列名拼接
StringBuilder vals = new StringBuilder(); // 值拼接
for (Field f : c.getDeclaredFields()) { // 遍历所有字段(含 private)
Column col = f.getAnnotation(Column.class); // 读取字段上的 @Column
if (col == null) continue; // 没有 @Column 说明此字段不映射到数据库列
f.setAccessible(true); // 突破 private 以便反射取值
// 非首列时添加逗号分隔符
if (!cols.isEmpty()) { cols.append(", "); vals.append(", "); }
cols.append(col.name()); // 拼接列名
Object v = f.get(entity); // 反射获取字段值
vals.append(v instanceof String ? "'" + v + "'" : v); // 字符串值需加单引号包裹
}
return "INSERT INTO " + t.name() + " (" + cols + ") VALUES (" + vals + ")";
}
}
// 扩展方向:generateSelect / generateUpdate / generateDelete
// ⚠ 仅教学演示:字符串拼接存在 SQL 注入风险,生产环境必须使用 PreparedStatement
六、反例
| # | 反例行为 | 正确做法 |
|---|---|---|
| 6.1 注解当注释 | @ThisMethodCreatesUser 描述过程 |
注解标记"是什么"(@Transactional),注释写"为什么" |
| 6.2 属性过复杂 | 一个注解十几个必填属性 | 拆分职责(@Timeout/@Retry 独立),提供合理默认值 |
| 6.3 滥用 RUNTIME | 自定义 @RuntimeGetter + 反射动态生成,每次调用都反射 |
编译时 APT 处理(Lombok)零运行时开销 |
| 6.4 处理器有状态 | static Map cache 多次调用间污染 |
处理器保持无状态,或每次新建实例 |
| 6.5 硬编码配置 | @CacheConfig(ttl=600) 写死参数 |
配置外部化:@Value("${cache.ttl}") 从配置文件读取 |
| 6.6 忽略保留策略 | 编译时注解却用 RUNTIME,浪费内存 |
编译时处理器用 SOURCE 或 CLASS |
七、速查清单
| 问题 | 答案 |
|---|---|
| 注解本身做事吗? | 不------靠注解处理器读取并触发逻辑 |
@Retention 三种策略? |
SOURCE 丢弃 / CLASS 保留但不反射 / RUNTIME 可反射 |
@Target 默认范围? |
早期 8 个位置,不含 TYPE_USE/TYPE_PARAMETER |
| 注解属性支持哪些类型? | 基本类型、String、Class、枚举、注解、以上的一维数组 |
| 注解可以继承吗? | 不能 extends,@Inherited 仅让子类继承父类标注 |
@Repeatable 用途? |
同一位置重复使用,需配合容器注解 |
@SafeVarargs 限制? |
仅 static/final/private 方法或 final 类实例方法 |
| 属性默认值能是 null? | 不能,用空字符串或 UNSPECIFIED 枚举 |
| 运行时 vs 编译时处理? | 运行时反射灵活有开销;编译时 APT 生成代码零开销 |
| APT 能改已有代码? | 标准不能(只生成新文件),Lombok 非标准手段绕过 |
getAnnotation() vs getDeclaredAnnotation()? |
前者含继承的,后者仅直接声明的 |
八、面试口述
注解是 Java 5 引入的元数据机制,用 @ 给代码贴标签,本身不改变行为,靠处理器发挥作用。内置注解:@Override 编译期检查重写,@Deprecated 标记过时,@SuppressWarnings 抑制警告。元注解:@Retention 控制生命周期,@Target 限制位置,@Inherited 子类继承(类层面),@Repeatable 可重复。
自定义注解用 @interface,属性可有默认值,value 属性可省略名。处理分两种:运行时反射(Spring/JUnit,灵活有开销)和编译时 APT(MapStruct/Lombok,零运行时开销)。核心价值:"配置即代码"------配置贴近代码,IDE 自动补全。与配置文件对比:注解适合与代码强绑定的场景,外部配置文件(YAML)适合环境差异大的场景,Spring Boot 的 @ConfigurationProperties 将两者结合。注解不能替代接口:注解是元数据标记,接口是类型系统核心------注解不能定义方法签名、不能强制实现、不能用于多态。反射读取注解:getAnnotation() 包含继承的(受 @Inherited 影响),getDeclaredAnnotation() 仅直接声明的;isAnnotationPresent() 高效判断存在性,不创建代理实例。