在 Spring 项目中,我们经常看到 @Transactional、@Cacheable、@PreAuthorize 等注解——它们简洁优雅,却能自动完成事务、缓存、权限校验等复杂逻辑。
这些“魔法”的背后,正是 自定义注解 + AOP(面向切面编程) 的组合。
本文将手把手带你实现一个用于方法日志记录的自定义注解,并深入理解其原理与扩展可能。
一、什么是自定义注解?
Java 注解(Annotation)本质上是一种元数据,它不直接参与程序逻辑,但可以被编译器、运行时或框架读取并执行相应行为。
要让注解“活”起来,关键在于:
- 正确使用元注解(如
@Target、@Retention); - 结合 AOP 或反射机制 在运行时拦截并处理。
二、实战:实现一个日志注解
1. 定义基础实体与服务(略)
假设已有:
User实体类UserDAO数据访问层UserService业务层UserController控制器
@RestController public class UserController { @Autowired private UserService userService; @GetMapping("/user/{id}") public User findUser(@PathVariable Integer id) { return userService.findUserById(id); } }
现在,我们希望在调用 findUser 时自动打印日志,但不想写重复的 log.info(...)。
2. 创建自定义注解
import java.lang.annotation.*; @Documented @Retention(RetentionPolicy.RUNTIME) // 运行时保留,可通过反射获取 @Target(ElementType.METHOD) // 仅用于方法 public @interface CustomAnnotation { String name() default ""; // 注解属性1 String value() default ""; // 注解属性2(习惯命名为 value,可简写) }
关键元注解说明:
| 元注解 | 作用 |
@Documented |
生成 Javadoc 时包含该注解 |
@Retention(RUNTIME) |
必须为 RUNTIME,否则 AOP 无法通过反射读取 |
@Target(METHOD) |
限定只能用在方法上 |
💡 注解中的方法 = 属性。使用时:
@CustomAnnotation(name = "xxx", value = "yyy")若只有
value属性,可简写为:@CustomAnnotation("yyy")
3. 使用注解标记方法
@CustomAnnotation(name = "findUser", value = "根据ID查找用户") @GetMapping("/user/{id}") public User findUser(@PathVariable Integer id) { return userService.findUserById(id); }
此时注解只是“贴标签”,还没任何行为。
4. 用 AOP 拦截注解并执行逻辑
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Aspect @Component public class LogAspect { // 定义切入点:所有被 @CustomAnnotation 标记的方法 @Pointcut("@annotation(cn.example.demo.CustomAnnotation)") public void annotatedMethod() {} // 在方法执行前拦截 @Before("annotatedMethod() && @annotation(annotation)") public void logBefore(JoinPoint joinPoint, CustomAnnotation annotation) { String className = joinPoint.getSignature().getDeclaringTypeName(); String methodName = joinPoint.getSignature().getName(); System.out.println("=== 日志拦截 ==="); System.out.println("类名: " + className); System.out.println("方法: " + methodName); System.out.println("功能: " + annotation.value()); System.out.println("标识: " + annotation.name()); } }
✅ 注意:
- 切点表达式
@annotation(...)必须写全注解的完整类名;- 方法参数中
CustomAnnotation annotation会自动注入注解实例。
5. 启动项目,测试效果
访问:http://localhost:8080/user/1
控制台输出:
=== 日志拦截 === 类名: cn.example.demo.UserController 方法: findUser 功能: 根据ID查找用户 标识: findUser
✅ 成功!无需修改业务代码,日志自动增强。
三、自定义注解还能做什么?
上述模式可轻松扩展至多种场景:
| 场景 | 实现思路 |
| 参数校验 | 如 @Phone、@IdCard,在 AOP 中验证参数合法性 |
| 权限控制 | 如 @RequireRole("ADMIN"),拦截无权限请求 |
| 缓存操作 | 如 @CachePut(key = "#id"),自动写入 Redis |
| 操作审计 | 记录谁在何时做了什么操作 |
| 限流熔断 | 结合 Sentinel 或自定义计数器 |
所有这些,底层逻辑都一样:定义注解 → AOP 拦截 → 执行增强逻辑。
四、注意事项
@Retention必须是RUNTIME,否则反射拿不到;- Spring AOP 默认只对 Spring Bean 生效,确保被注解的类由 Spring 管理;
- 注解不能继承(
@Inherited仅对类注解有效,对方法无效); - 性能敏感场景慎用:AOP 本质是代理,有轻微开销。
五、总结
自定义注解不是“语法糖”,而是解耦业务与横切关注点的强大工具。
通过 “注解 + AOP” 模式,你可以:
- 让代码更声明式(Declarative);
- 避免重复样板代码;
- 提升系统可维护性与扩展性。
下次当你想“在某些方法前后统一做点事”时,不妨试试自定义注解——
让代码自己描述意图,而不是堆满 if-else 和 log。