Java学习笔记之注解

前言

注解(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 {};

特殊规则

  1. 属性名不能是 classinterface(关键字)
  2. 属性不能有参数、不能抛异常
  3. 默认值不能为 null,用空字符串或 UNSPECIFIED 枚举代替
  4. 嵌套注解默认值:NotNull notNull() default @NotNull(message = "default required");
  5. 不指定 @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,浪费内存 编译时处理器用 SOURCECLASS

七、速查清单

问题 答案
注解本身做事吗? 不------靠注解处理器读取并触发逻辑
@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() 高效判断存在性,不创建代理实例。

相关推荐
ι:1 小时前
Codex 接管嘉立创EDA 并复现 STM32 Blue Pill 学习底板的完整教学
stm32·嵌入式硬件·学习
BossFriday1 小时前
【手撸IM】SycllaDB 消息存储基础
java·分布式·中间件
霸道流氓气质1 小时前
导入历史跟踪机制实战指南
java·linux·服务器
Xeon_CC1 小时前
vs2026远程开发debian12容器的C++程序笔记
开发语言·c++·笔记
日取其半万世不竭1 小时前
Uptime Kuma 应该放哪台机器?
java·docker·容器·https
消失的旧时光-19431 小时前
Kotlin 协程设计思想(四):launch、async、withContext 到底有什么区别?
java·kotlin·async·launch·withcontext·deferred
夜白宋1 小时前
【Redis深入】二、高性能
java·前端·redis
空圆小生1 小时前
Vue3 + Spring Boot 全栈实战:从零搭建在线彩票模拟系统
java·spring boot·后端
devpotato1 小时前
ArrayList 扩容机制:从源码细节到工程实践
java·list