背景

本文是《Java 后端从小白到大神》修仙系列第十七篇,正式进入Java后端世界,本篇文章主要聊Java基础中的注解。若想详细学习请点击 首篇博文,我们开始吧。

文章概览

  1. 注解的核心概念
  2. 元注解详解
  3. 自定义注解与使用
  4. 注解处理器(APT)
  5. 注解在框架中的应用
  6. 代码示例与实战

一、注解的核心概念

1.1 什么是注解?

注解(Annotation)是 Java 的元数据机制,用于为代码附加描述信息。 注解本身不改变代码逻辑,但可被编译器、工具、框架解析使用。

1.2 核心价值

  • 声明式编程:用标记表达意图,简化代码
  • 配置与业务分离:避免硬编码
  • 编译时检查:提前发现错误
  • 自动代码生成:减少重复劳动

1.3 注解的本质

注解本质是一个接口,默认继承 java.lang.annotation.Annotation,无需手动继承,JVM 自动实现。

1.4 注解常见应用场景

场景 说明 示例
代码标记 标识方法/类的特殊含义 @Override、@Deprecated
编译检查 编译器辅助校验 @SuppressWarnings
代码生成 编译时生成代码 Lombok @Data
运行时逻辑 框架动态处理 Spring @Autowired

1.5 Java 内置核心注解

注解 作用
@Override 标记方法重写
@Deprecated 标记已过时
@SuppressWarnings 抑制编译器警告
@FunctionalInterface 标记函数式接口

二、元注解详解

2.1 什么是元注解?

元注解(Meta-Annotation) 是用于定义其他注解的“注解”,核心作用是控制普通注解的行为、特性(比如注解保留多久、能用到哪些地方)。

2.2 核心元注解

元注解 作用 参数
@Retention 指定注解的保留策略 RetentionPolicy 枚举
@Target 指定注解能用在什么代码上 ElementType 枚举数组,限定注解贴在哪
@Documented 标记注解是否包含在 Javadoc 中
@Inherited 标记注解是否可被子类继承
@Repeatable 标记注解可重复使用 包含注解的类型

2.3 @Retention 详解

保留策略 决定了注解在哪个阶段仍然可用:

策略 描述 应用场景
SOURCE 仅源码保留,编译后丢弃 编译检查(如 @Override
CLASS 保留到类文件,运行时丢弃(默认) 字节码分析工具
RUNTIME 运行时可通过反射读取 运行时框架(如 Spring)

2.4 @Target 详解

目标类型 指定了注解可以应用的代码元素:

类型 描述
TYPE 类、接口、枚举
FIELD 字段(包括枚举常量)
METHOD 方法
PARAMETER 方法参数
CONSTRUCTOR 构造器
LOCAL_VARIABLE 局部变量
ANNOTATION_TYPE 注解类型
PACKAGE
TYPE_PARAMETER 类型参数
TYPE_USE 类型使用

2.5 元注解使用示例

通过一个完整示例,演示核心元注解的实际用法(结合 @Retention @Target 等,覆盖常用场景):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
 * 测试用例注解(元注解应用示例)
 */
@Retention(RetentionPolicy.RUNTIME)  // 运行时保留,支持反射读取
@Target({ElementType.METHOD, ElementType.TYPE})  // 可应用于类、方法
@Documented  // 生成Javadoc时包含该注解
@Inherited  // 子类可继承该注解
public @interface TestCase {
    String id();          // 必须赋值的属性(无默认值)
    String desc() default "暂无描述";  // 可选属性(有默认值)
    int priority() default 1;  // 数值类型可选属性
}

三、自定义注解与使用

3.1 定义注解

核心规则

  1. @interface 定义注解
  2. 注解里写的 “方法”本质就是注解属性
  3. 为什么是方法?因为注解本质是接口,接口只能定义方法
  4. 没有 default → 属性必填
  5. default → 属性可选
  6. 必须配合元注解@Retention / @Target
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 元注解:控制注解生命周期与使用位置
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ApiEndpoint {

    String path();                   // 无 default → 必填属性,这是【属性】,不是普通方法
    HttpMethod method() default HttpMethod.GET;  // 有 default → 可选
    String description() default "";             // 可选
    boolean requiresAuth() default true;        // 可选
    String version() default "1.0";             // 可选
}

// 枚举类型也可以作为注解属性
enum HttpMethod { GET, POST, PUT, DELETE, PATCH }

3.2 使用注解

核心规则

  1. 在类/方法/字段上直接写 @注解名
  2. 必填属性必须赋值
  3. 有默认值的可以不写
  4. 注解本身只是标记,不会自动生效
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class UserController {

    // 使用注解,必填项必须传参
    @ApiEndpoint(
        path = "/users/{id}",
        method = HttpMethod.GET,
        description = "根据ID获取用户信息"
    )
    public User getUser(long id) {
        return new User(id, "John Doe");
    }

    // 部分属性使用默认值
    @ApiEndpoint(
        path = "/users",
        method = HttpMethod.POST,
        description = "创建新用户"
    )
    public User createUser(User user) {
        user.setId(1001);
        return user;
    }
}

3.3 反射解析注解

注解通过反射解析才能真正生效,核心规则:

  1. isAnnotationPresent(xxx.class) → 判断是否有该注解
  2. getAnnotation(xxx.class) → 获取注解实例
  3. 通过“方法调用”的写法获取属性值
  4. 这是 Spring、MyBatis 等框架处理注解的底层原理
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class ApiDocGenerator {
    public static void generate(Class<?> clazz) {
        for (Method method : clazz.getDeclaredMethods()) {

            // 1. 判断方法上是否有 @ApiEndpoint
            if (method.isAnnotationPresent(ApiEndpoint.class)) {

                // 2. 获取注解对象
                ApiEndpoint api = method.getAnnotation(ApiEndpoint.class);

                // 3. 获取注解属性(像调用方法一样)
                System.out.println(api.path());
                System.out.println(api.method());
                System.out.println(api.requiresAuth());
            }
        }
    }
}

3.4 注解支持的属性类型

注解属性只支持以下 6 类,不能是任意对象:

  • 基本类型(int、boolean 等)
  • String
  • Class
  • 枚举
  • 其他注解
  • 以上类型的数组
1
2
3
4
5
6
7
8
public @interface ComplexAnnotation {
    int value();
    String name();
    Class<?> target();
    Status status() default Status.ACTIVE;
    SubAnnotation meta();
    String[] tags();
}

四、注解处理器(APT)

4.1 什么是 APT?

注解处理器(Annotation Processing Tool,APT) 是 Java 编译器的一个工具,用于在编译时处理注解并生成代码。

4.2 APT 的工作原理

  1. 编译器扫描源码中的注解
  2. 调用对应的注解处理器
  3. 注解处理器生成新的源文件
  4. 编译器编译生成的源文件

4.3 自定义注解处理器

  • 运行时解析注解 → 用反射
  • 编译时解析注解 → 用 Elements + Types
  1. 整体核心关系图
1
2
3
4
5
6
ProcessingEnvironment  (全局上下文:发工具)
       ↓ 提供四大工具
   ├── Elements      (操作元素的工具)
   ├── Types         (处理类型的工具)
   ├── Filer         (生成代码文件)
   └── Messager      (编译日志/报错)
  1. Elements 工具能干什么
1
2
3
4
5
6
Elements (元素操作工具)
       ↓ 帮你获取/查询
   ├── getTypeElement(类名)  → TypeElement (代表一个类/接口)
   ├── getPackageOf(元素)    → PackageElement (代表包)
   ├── getEnclosedElements() → 一堆 Element
   └── 各种判断/查询方法
  1. Element 家族结构(最重要)
1
2
3
4
5
6
Element (所有代码元素的父类)
   ├─ TypeElement        → 类、接口、枚举
   ├─ ExecutableElement  → 方法、构造方法
   ├─ VariableElement    → 字段、方法参数、局部变量
   ├─ PackageElement     → 包
   └─ TypeParameterElement → 泛型类型参数
  1. 一轮注解处理的完整流程
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
RoundEnvironment (本轮注解环境)
       ↓ 查询
   └── getElementsAnnotatedWith(某注解)
           得到 Set<Element>
           强转为具体子类
           ├─ TypeElement   (类)
           ├─ MethodElement (方法)
           └─ VariableElement(字段)
  1. 一句话串完所有
1
2
3
4
5
6
7
ProcessingEnvironment 给你工具
Elements 工具帮你找 Element
Element 代表具体代码(类/方法/字段)
RoundEnvironment 帮你按注解查 Element
  • Element = 代码本身
  • Elements = 操作代码的工具
  • ProcessingEnvironment = 给工具的上下文
  • RoundEnvironment = 按注解查代码的工具

代码示例

注解定义

1
2
3
4
5
6
7
import java.lang.annotation.*;

@Target(ElementType.TYPE)     // 只能作用在类上
@Retention(RetentionPolicy.SOURCE)  // 只在编译期有效
public @interface AutoBuilder {
    // 空注解 → 仅作为标记
}

注解处理器

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.tools.JavaFileObject;
import java.io.Writer;
import java.util.List;
import java.util.Set;

// 声明支持的注解 & Java 版本
@SupportedAnnotationTypes("com.example.AutoBuilder")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class AutoBuilderProcessor extends AbstractProcessor {

    // 工具对象(从 ProcessingEnvironment 获取)
    private Filer filer;          // 生成文件
    private Elements elementUtils;// 操作类/字段/包信息

    // ==========================
    // 初始化:拿到所有工具
    // ==========================
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.filer = processingEnv.getFiler();
        this.elementUtils = processingEnv.getElementUtils();
    }

    // ==========================
    // 核心处理方法
    // ==========================
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        // 1. 找到所有加了 @AutoBuilder 的类
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoBuilder.class);

        for (Element element : elements) {
            // 只处理类
            if (element.getKind() == ElementKind.CLASS) {
                TypeElement targetClass = (TypeElement) element;
                generateBuilderClass(targetClass);
            }
        }
        return true;
    }

    // ==========================
    // 生成 Builder 类
    // ==========================
    private void generateBuilderClass(TypeElement targetClass) {
        try {
            // 类信息
            String className = targetClass.getSimpleName().toString();
            String packageName = elementUtils.getPackageOf(targetClass).getQualifiedName().toString();
            String builderName = className + "Builder";

            // 创建源文件
            JavaFileObject file = filer.createSourceFile(packageName + "." + builderName);

            try (Writer writer = file.openWriter()) {
                writer.write(generateCode(packageName, className, builderName, targetClass));
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // ==========================
    // 拼接 Builder 代码
    // ==========================
    private String generateCode(String packageName, String className, String builderName, TypeElement targetClass) {
        StringBuilder sb = new StringBuilder();

        // 包名
        sb.append("package ").append(packageName).append(";\n\n");

        // Builder 类
        sb.append("public class ").append(builderName).append(" {\n");

        // ==========================
        // 遍历字段,生成成员变量
        // ==========================
        List<? extends Element> enclosedElements = targetClass.getEnclosedElements();

        for (Element element : enclosedElements) {
            if (element.getKind() == ElementKind.FIELD) {
                VariableElement field = (VariableElement) element;
                String fieldType = field.asType().toString();
                String fieldName = field.getSimpleName().toString();

                sb.append("    private ").append(fieldType).append(" ").append(fieldName).append(";\n");
            }
        }

        // ==========================
        // 生成 withXXX 方法
        // ==========================
        for (Element element : enclosedElements) {
            if (element.getKind() == ElementKind.FIELD) {
                VariableElement field = (VariableElement) element;
                String fieldType = field.asType().toString();
                String fieldName = field.getSimpleName().toString();
                String withName = "with" + capitalize(fieldName);

                sb.append("\n    public ").append(builderName).append(" ").append(withName)
                  .append("(").append(fieldType).append(" ").append(fieldName).append(") {\n");
                sb.append("        this.").append(fieldName).append(" = ").append(fieldName).append(";\n");
                sb.append("        return this;\n");
                sb.append("    }\n");
            }
        }

        // ==========================
        // 生成 build() 方法
        // ==========================
        sb.append("\n    public ").append(className).append(" build() {\n");
        sb.append("        return new ").append(className).append("(this);\n");
        sb.append("    }\n");

        sb.append("}\n");
        return sb.toString();
    }

    // 小工具:首字母大写
    private String capitalize(String s) {
        return Character.toUpperCase(s.charAt(0)) + s.substring(1);
    }
}

最终生成的 Builder 类结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.example;

public class UserBuilder {

    // 1. 自动生成:和 User 类一模一样的字段
    private String name;
    private int age;
    private String email;

    // 2. 自动生成:链式调用的 withXXX 方法
    public UserBuilder withName(String name) {
        this.name = name;
        return this;
    }

    public UserBuilder withAge(int age) {
        this.age = age;
        return this;
    }

    public UserBuilder withEmail(String email) {
        this.email = email;
        return this;
    }

    // 3. 自动生成:build() 方法,创建目标对象
    public User build() {
        return new User(this);
    }
}

4.4 APT 的应用场景

  1. 代码生成:自动生成重复代码,如 Builder 模式、DTO 转换
  2. 框架开发:如 Dagger、ButterKnife 等依赖注入框架
  3. 静态分析:在编译时检测潜在问题
  4. 文档生成:自动生成 API 文档

五、注解在框架中的应用

5.1 Spring 核心注解

@Component
标记类为 Spring 组件,让 Spring 管理

1
2
@Component
public class UserService {}

@Autowired
自动从 Spring 容器注入依赖

1
2
@Autowired
private UserService userService;

@Value
从配置文件读取值注入

1
2
@Value("${app.name}")
private String appName;

@Scope
指定 Bean 作用域

1
2
3
@Scope("prototype")
@Component
public class UserService {}

5.2 Spring Web 注解

@Controller
标记为 MVC 控制器

1
2
@Controller
public class UserController {}

@RestController
REST 接口专用,直接返回 JSON

1
2
@RestController
public class UserApi {}

@RequestMapping
统一配置请求路径

1
2
@RequestMapping("/user")
public class UserController {}

@RequestParam
接收 URL 查询参数

1
String getUser(@RequestParam Long id) {}

@PathVariable
接收路径中的变量

1
2
@GetMapping("/user/{id}")
User get(@PathVariable Long id) {}

5.3 Spring Boot 注解

@SpringBootApplication
启动类核心注解

1
2
@SpringBootApplication
public class App {}

@Configuration
标记配置类

1
2
@Configuration
public class WebConfig {}

@Bean
手动向容器注册对象

1
2
3
4
@Bean
public RestTemplate restTemplate() {
    return new RestTemplate();
}

5.4 JPA / Hibernate 注解

@Entity
标记数据库实体类

1
2
@Entity
public class User {}

@Table
指定映射表名

1
2
3
@Table(name = "t_user")
@Entity
public class User {}

@Id
标记主键

1
2
@Id
private Long id;

@GeneratedValue
主键自增策略

1
2
3
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column
自定义数据库列

1
2
@Column(name = "user_name", nullable = false)
private String username;

@ManyToOne / @OneToMany
关联关系映射

1
2
@ManyToOne
private Dept dept;

5.5 JUnit 注解

@Test
标记测试方法

1
2
@Test
void testUser() {}

@BeforeEach
每个测试方法前执行

1
2
@BeforeEach
void setUp() {}

@AfterEach
每个测试方法后执行

1
2
@AfterEach
void tearDown() {}

5.6 Lombok 注解(最常用)

@Data
一键生成 getter/setter/toString/hashCode/equals

1
2
@Data
public class User {}

@Getter / @Setter
只生成 getter 或 setter

1
2
@Getter @Setter
private String name;

@Builder
生成 Builder 模式

1
2
@Builder
public class User {}

@NoArgsConstructor / @AllArgsConstructor
无参 / 全参构造器

1
2
3
@NoArgsConstructor
@AllArgsConstructor
public class User {}

六、代码示例与实战

6.1 ConstraintValidator 接口

它是 Java Validation(JSR380)的校验器接口,专门用来写 自定义注解校验逻辑 的。简单说:你想自己写个注解 like @Phone@IdCard@MyNotNull 做参数校验,业务逻辑就写在这个接口里。

它长什么样

1
2
3
4
5
6
7
public interface ConstraintValidator<A extends Annotation, T> {
    // 初始化:可以拿到注解里的属性
    void initialize(A constraintAnnotation);

    // 核心:校验逻辑,返回 true=通过,false=失败
    boolean isValid(T value, ConstraintValidatorContext context);
}
  • A:你自定义的校验注解(比如 @Phone
  • T:要校验的数据类型(String / Integer / 自定义对象等)

① 注解类型
② 要校验的字段的数据类型

ConstraintValidator<A extends Annotation, T>参数含义:<给谁校验,校验什么类型>

1
2
ConstraintValidator<Phone, String>
                            

第一个:Phone
表示:这个校验器是给哪个注解服务的
也就是:给谁写的校验逻辑

1
2
3
ConstraintValidator<Phone, String>
                    
            绑定 @Phone 注解

第二个:String
表示:这个校验器要校验什么类型的字段值

也就是说:
这个 @Phone 注解,只能加在 String 类型的字段上!

1
2
3
ConstraintValidator<Phone, String>
                             
                只能校验 String 类型的字段

定义校验注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class) //绑定真正的校验器
public @interface Phone {

    String message() default "手机号格式不正确"; //校验失败提示信息
    Class<?>[] groups() default {}; //分组(必须写,框架要求)
    Class<? extends Payload>[] payload() default {}; //负载(必须写,框架要求)
    boolean allowNull() default false; // 添加属性,演示在 initialize 中读取
}

实现校验逻辑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class PhoneValidator implements ConstraintValidator<Phone, String> {

    // 定义成员变量,保存从注解里读取的配置
    private boolean allowNull;

    /**
     * 初始化方法:
     * 框架创建校验器时会自动调用,用来读取注解上的属性
     * initialize 只在启动时执行一次
     */
    @Override
    public void initialize(Phone constraintAnnotation) {
        // 从注解中获取 allowNull 属性的值
        this.allowNull = constraintAnnotation.allowNull();
    }

    /**
     * 核心校验逻辑
     */
    @Override
    public boolean isValid(String phone, ConstraintValidatorContext context) {
        // phone = 传入的手机号的值,比如 "13812345678"
        // 如果允许 null,并且传进来的是 null,直接校验通过
        if (allowNull) {
            if (phone == null) {
                return true;
            }
        }

        // 不允许 null 时,null 直接失败
        if (phone == null) {
            return false;
        }

        // 手机号格式校验
        return phone.matches("1[3-9]\\d{9}");

        // ConstraintValidatorContext context,自定义错误提示、覆盖默认提示、多字段关联校验、精细化报错。
        // if (phone == null) {
        // 自己拼错误信息
        // context.buildConstraintViolationWithTemplate("手机号不能为空!")
        //       .addConstraintViolation();
        // return false;
        // }
    }
}

使用注解:加到字段上

1
2
3
4
5
6
7
8
9
@Data
public class UserDTO {

    private String name;

    // 使用自定义注解,并设置属性
    @Phone(allowNull = true, message = "手机号格式不正确哦")
    private String phone;
}

触发校验:接口层加 @Valid

1
2
3
4
5
6
7
@RestController
public class UserController {
    @PostMapping("/user")
    public String addUser(@RequestBody @Valid UserDTO userDTO) {
        return "校验通过";
    }
}

最直观的流程图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Valid  → 触发校验
框架扫描 UserDTO 所有字段
发现字段带 @Phone
查看 @Phone 上的 @Constraint(validatedBy = 谁)
找到 PhoneValidator
new PhoneValidator()
调用 initialize(注解属性)
调用 isValid(字段值, 上下文)  ← 就是这么找到的!

6.2 自定义缓存注解

自定义缓存注解 @Cacheable
作用:标记需要缓存的方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import java.lang.annotation.*;

/**
 * 自定义缓存注解
 * 作用:标记方法,让切面自动缓存返回结果
 */
@Retention(RetentionPolicy.RUNTIME)     // 运行时生效
@Target(ElementType.METHOD)            // 只能加在方法上
public @interface Cacheable {

    // 缓存 key 前缀
    String key() default "";

    // 过期时间(秒)
    int expire() default 3600;

    // 是否把方法参数拼进缓存 key
    boolean useParams() default true;
}

AOP 切面:缓存核心逻辑
作用:拦截带 @Cacheable 的方法,实现自动缓存

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Component
public class CacheAspect {

    // Spring 缓存管理器
    private final CacheManager cacheManager;

    // 构造注入
    public CacheAspect(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    /**
     * 环绕通知:拦截所有带 @Cacheable 注解的方法
     */
    @Around("@annotation(com.example.Cacheable)")
    public Object aroundCache(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 获取当前方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        // 2. 获取方法上的 @Cacheable 注解
        Cacheable cacheable = method.getAnnotation(Cacheable.class);

        // 3. 生成缓存 key
        String cacheKey = generateCacheKey(cacheable, joinPoint);

        // 4. 获取缓存对象,methodCache = 缓存分组 / 区域名,它是给缓存分组用的名字,你写什么Spring就会创建什么缓存组,类比文件夹名称,缓存则是文件名称
        Cache cache = cacheManager.getCache("methodCache");

        // 5. 先查缓存
        if (cache != null) {
            Cache.ValueWrapper valueWrapper = cache.get(cacheKey);
            if (valueWrapper != null) {
                // 缓存命中 → 直接返回,不执行目标方法
                return valueWrapper.get();
            }
        }

        // 6. 缓存未命中 → 执行目标方法(调用业务代码)
        Object result = joinPoint.proceed();

        // 7. 将结果放入缓存
        if (cache != null) {
            cache.put(cacheKey, result);
        }

        // 8. 返回结果
        return result;
    }

    /**
     * 生成缓存 key
     * 规则:类名.方法名:自定义key:参数值
     */
    private String generateCacheKey(Cacheable cacheable, ProceedingJoinPoint joinPoint) {
        StringBuilder keyBuilder = new StringBuilder();

        // 类名.方法名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        keyBuilder.append(signature.getDeclaringTypeName())
                  .append(".")
                  .append(signature.getMethod().getName());

        // 追加自定义 key 前缀
        String customKey = cacheable.key();
        if (!customKey.isEmpty()) {
            keyBuilder.append(":").append(customKey);
        }

        // 追加方法参数(如果开启)
        if (cacheable.useParams()) {
            Object[] args = joinPoint.getArgs();
            for (Object arg : args) {
                keyBuilder.append(":").append(arg);
            }
        }

        return keyBuilder.toString();
    }
}

使用示例(业务层)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import org.springframework.stereotype.Service;

@Service
public class UserService {

    /**
     * 给方法开启缓存
     * 缓存 key 前缀:user
     * 过期时间:2小时
     */
    @Cacheable(key = "user", expire = 7200)
    public User getUserById(Long id) {
        System.out.println("执行数据库查询...");
        return userRepository.findById(id).orElse(null);
    }
}

执行业务方法

1
userService.getUserById(1001L);

最终生成的缓存 key 长这样

1
2
规则:类名.方法名:自定义key:参数值
com.example.service.UserService.getUserById:user:1001

流程图

1
2
3
4
5
6
7
8
9
@Cacheable 标记方法
AOP 拦截
生成缓存 key
查缓存 → 有 → 直接返回
无 → 执行方法 → 存缓存 → 返回

6.3 自定义日志注解

自定义日志注解 @Log

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import java.lang.annotation.*;

/**
 * 自定义日志注解
 * 作用:标记需要自动打印日志的方法
 */
@Retention(RetentionPolicy.RUNTIME)   // 运行时生效
@Target(ElementType.METHOD)          // 只允许作用在方法上
public @interface Log {

    // 操作描述(例如:创建订单、删除用户)
    String value() default "";

    // 是否记录方法入参
    boolean logParams() default true;

    // 是否记录方法返回值
    boolean logResult() default true;
}

AOP 日志切面(核心逻辑)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;

@Aspect
@Component
public class LogAspect {

    private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);

    /**
     * 环绕通知:拦截所有带 @Log 注解的方法
     */
    @Around("@annotation(com.example.Log)")
    public Object aroundLog(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 获取当前执行的方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        // 2. 获取方法上的 @Log 注解
        Log logAnno = method.getAnnotation(Log.class);

        // 3. 获取操作名称
        String operation = logAnno.value().isEmpty() 
            ? method.getName() 
            : logAnno.value();

        // 4. 打印:开始执行
        logger.info("===== 开始操作:{} =====", operation);

        // 5. 打印:方法参数(如果开启)
        if (logAnno.logParams()) {
            Object[] args = joinPoint.getArgs();
            logger.info("操作参数:{}", Arrays.toString(args));
        }

        long start = System.currentTimeMillis();
        Object result = null;

        try {
            // 6. 执行目标方法
            result = joinPoint.proceed();

            // 7. 打印:返回结果(如果开启)
            if (logAnno.logResult()) {
                logger.info("返回结果:{}", result);
            }

        } catch (Exception e) {
            // 8. 打印:异常信息
            logger.error("操作异常:{}", e.getMessage(), e);
            throw e;

        } finally {
            // 9. 打印:耗时
            long cost = System.currentTimeMillis() - start;
            logger.info("===== 操作结束:{},耗时:{}ms =====\n", operation, cost);
        }

        return result;
    }
}

业务层使用示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    /**
     * 完整日志:记录操作 + 参数 + 返回值
     */
    @Log("创建订单")
    public Order createOrder(OrderRequest request) {
        System.out.println("执行创建订单逻辑...");
        return new Order();
    }

    /**
     * 不打印返回值
     */
    @Log(value = "查询订单", logResult = false)
    public Order getOrder(long orderId) {
        System.out.println("执行查询订单逻辑...");
        return new Order();
    }
}

运行日志输出

1
2
3
4
5
===== 开始操作:创建订单 =====
操作参数:OrderRequest(id=1001, name=测试订单)
执行创建订单逻辑...
返回结果:Order(id=1001, status=CREATED)
===== 操作结束:创建订单,耗时:12ms =====

核心流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Log 标记方法
AOP 自动拦截
打印【操作名称】
打印【方法参数】
执行业务代码
成功 → 打印【返回值】
异常 → 打印【异常堆栈】
打印【耗时】

七、总结

  1. 注解本质
  • 注解就是代码元数据,用于给类、方法、字段附加配置与标记
  • 底层是特殊接口,由 @Target@Retention 等元注解控制行为
  • 分三种生效阶段:源码级(SOURCE)、编译级(CLASS)、运行级(RUNTIME)
  1. 核心用法
  • 运行时:通过反射读取注解,配合 AOP 实现日志、缓存、权限等切面功能
  • 编译时:使用 APT(注解处理器)生成代码,如 Lombok、Builder 自动生成
  • 框架内置:Spring、JPA、JUnit 大量使用注解替代 XML,实现声明式编程
  1. 关键技术点
  • 自定义注解 + 反射 = 运行时逻辑扩展
  • 自定义注解 + AOP = 统一切面增强(日志、缓存、校验)
  • 自定义注解 + APT = 编译时代码生成
  • @Valid + ConstraintValidator = 参数校验扩展
  1. 务实建议
  • 运行时注解避免在高频循环中滥用反射,必要时缓存结果
  • 简单配置优先用注解,复杂逻辑仍用代码实现
  • 注解命名见名知意,必填属性不配默认值,可选属性合理默认
  • 优先使用成熟框架注解,少重复造轮子
  1. 一句话总结
  • 注解 = 代码标记 + 元信息
  • 运行时靠反射/AOP,编译时靠 APT
  • 多用在统一切面、配置简化、自动代码生成,少做过度设计

掌握注解的使用和原理,对于编写高质量、可维护的 Java 代码至关重要。通过合理使用注解,可以大幅提高开发效率,减少冗余代码,使代码更加清晰和优雅。