背景
本文是《Java 后端从小白到大神》修仙系列第十七篇,正式进入Java后端世界,本篇文章主要聊Java基础中的注解。若想详细学习请点击 首篇博文,我们开始吧。
文章概览
- 注解的核心概念
- 元注解详解
- 自定义注解与使用
- 注解处理器(APT)
- 注解在框架中的应用
- 代码示例与实战
一、注解的核心概念
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 定义注解
核心规则
- 用
@interface 定义注解
- 注解里写的 “方法”本质就是注解属性
- 为什么是方法?因为注解本质是接口,接口只能定义方法
- 没有
default → 属性必填
- 有
default → 属性可选
- 必须配合元注解(
@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
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 反射解析注解
注解通过反射解析才能真正生效,核心规则:
isAnnotationPresent(xxx.class) → 判断是否有该注解
getAnnotation(xxx.class) → 获取注解实例
- 通过“方法调用”的写法获取属性值
- 这是 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 的工作原理
- 编译器扫描源码中的注解
- 调用对应的注解处理器
- 注解处理器生成新的源文件
- 编译器编译生成的源文件
4.3 自定义注解处理器
- 运行时解析注解 → 用反射
- 编译时解析注解 → 用 Elements + Types
- 整体核心关系图
1
2
3
4
5
6
|
ProcessingEnvironment (全局上下文:发工具)
↓ 提供四大工具
├── Elements (操作元素的工具)
├── Types (处理类型的工具)
├── Filer (生成代码文件)
└── Messager (编译日志/报错)
|
- Elements 工具能干什么
1
2
3
4
5
6
|
Elements (元素操作工具)
↓ 帮你获取/查询
├── getTypeElement(类名) → TypeElement (代表一个类/接口)
├── getPackageOf(元素) → PackageElement (代表包)
├── getEnclosedElements() → 一堆 Element
└── 各种判断/查询方法
|
- Element 家族结构(最重要)
1
2
3
4
5
6
|
Element (所有代码元素的父类)
├─ TypeElement → 类、接口、枚举
├─ ExecutableElement → 方法、构造方法
├─ VariableElement → 字段、方法参数、局部变量
├─ PackageElement → 包
└─ TypeParameterElement → 泛型类型参数
|
- 一轮注解处理的完整流程
1
2
3
4
5
6
7
8
9
10
|
RoundEnvironment (本轮注解环境)
↓ 查询
└── getElementsAnnotatedWith(某注解)
↓
得到 Set<Element>
↓
强转为具体子类
├─ TypeElement (类)
├─ MethodElement (方法)
└─ VariableElement(字段)
|
- 一句话串完所有
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 的应用场景
- 代码生成:自动生成重复代码,如 Builder 模式、DTO 转换
- 框架开发:如 Dagger、ButterKnife 等依赖注入框架
- 静态分析:在编译时检测潜在问题
- 文档生成:自动生成 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
标记主键
@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 自动拦截
↓
打印【操作名称】
打印【方法参数】
↓
执行业务代码
↓
成功 → 打印【返回值】
异常 → 打印【异常堆栈】
↓
打印【耗时】
|
七、总结
- 注解本质
- 注解就是代码元数据,用于给类、方法、字段附加配置与标记
- 底层是特殊接口,由
@Target、@Retention 等元注解控制行为
- 分三种生效阶段:源码级(
SOURCE)、编译级(CLASS)、运行级(RUNTIME)
- 核心用法
- 运行时:通过反射读取注解,配合 AOP 实现日志、缓存、权限等切面功能
- 编译时:使用 APT(注解处理器)生成代码,如 Lombok、Builder 自动生成
- 框架内置:Spring、JPA、JUnit 大量使用注解替代 XML,实现声明式编程
- 关键技术点
- 自定义注解 + 反射 = 运行时逻辑扩展
- 自定义注解 + AOP = 统一切面增强(日志、缓存、校验)
- 自定义注解 + APT = 编译时代码生成
@Valid + ConstraintValidator = 参数校验扩展
- 务实建议
- 运行时注解避免在高频循环中滥用反射,必要时缓存结果
- 简单配置优先用注解,复杂逻辑仍用代码实现
- 注解命名见名知意,必填属性不配默认值,可选属性合理默认
- 优先使用成熟框架注解,少重复造轮子
- 一句话总结
- 注解 = 代码标记 + 元信息
- 运行时靠反射/AOP,编译时靠 APT
- 多用在统一切面、配置简化、自动代码生成,少做过度设计
掌握注解的使用和原理,对于编写高质量、可维护的 Java 代码至关重要。通过合理使用注解,可以大幅提高开发效率,减少冗余代码,使代码更加清晰和优雅。