在 Spring 框架中,如何自定义一个注解?
作者:Java 后端开发工程师・8 年实战经验
时间:2025 年 6 月 23日
标签:Spring、自定义注解、AOP、元编程、框架扩展
一、引言
在我参与的一个金融系统重构项目中,遇到了一个典型的场景:需要对所有敏感接口进行统一的权限校验。最初的实现是在每个 Controller 方法中添加重复的权限判断代码,导致代码冗余且难以维护。后来通过自定义 Spring 注解结合 AOP,完美解决了这个问题,不仅消除了重复代码,还让权限控制变得灵活可配置。
自定义注解是 Spring 框架中非常强大的扩展机制,它允许我们在不改变原有代码结构的前提下,通过声明式的方式为程序添加额外的功能。本文将结合我多年的开发经验,从实际场景出发,深入讲解如何在 Spring 中自定义一个注解。
二、哪些场景适合使用自定义注解?
在实际开发中,我总结了以下几类典型场景:
- 权限控制:如接口访问权限校验、角色验证
- 日志记录:自动记录方法调用参数、返回值、执行时间
- 参数校验:如非空检查、格式验证、范围校验
- 事务控制:自定义事务传播行为、隔离级别
- 缓存处理:自定义缓存策略、过期时间
- 分布式锁:方法级的分布式锁实现
- 异步处理:标记方法为异步执行
- 数据脱敏:自动对敏感数据进行脱敏处理
三、自定义 Spring 注解的核心步骤
1. ✅ 定义注解(@interface)
首先需要使用 Java 的 @interface 语法定义注解,并使用元注解(@Retention、@Target 等)指定注解的保留策略和作用目标。
示例代码:定义一个权限校验注解
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD) // 注解作用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时保留注解信息
@Documented // 包含在JavaDoc中
public @interface PermissionCheck {
String value() default ""; // 权限标识,默认空字符串
boolean required() default true; // 是否必须校验,默认true
}
2. ✅ 创建切面(@Aspect)处理注解
使用 Spring AOP 创建切面,在切面中实现注解的业务逻辑。
示例代码:实现权限校验切面
java
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Component
public class PermissionCheckAspect {
@Before("@annotation(com.example.annotation.PermissionCheck)")
public void before(JoinPoint joinPoint) throws Throwable {
// 获取方法上的注解
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
PermissionCheck annotation = method.getAnnotation(PermissionCheck.class);
// 获取注解参数
String requiredPermission = annotation.value();
boolean required = annotation.required();
// 如果不需要校验,直接返回
if (!required) {
return;
}
// 获取当前用户权限(实际项目中可能从SecurityContext或Token中获取)
String currentUserPermission = getCurrentUserPermission();
// 校验权限
if (!hasPermission(currentUserPermission, requiredPermission)) {
throw new PermissionDeniedException("权限不足");
}
}
// 模拟获取当前用户权限
private String getCurrentUserPermission() {
// 实际项目中可能从SecurityContextHolder或Token中获取
return "user:read";
}
// 权限校验逻辑
private boolean hasPermission(String currentPermission, String requiredPermission) {
// 简单实现,实际项目中可能涉及复杂的权限表达式计算
return currentPermission.equals(requiredPermission);
}
}
3. ✅ 在目标方法上应用注解
在需要进行权限校验的方法上添加我们自定义的注解。
示例代码:在 Controller 中使用注解
less
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
@PermissionCheck("user:read") // 标记需要user:read权限
public User getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
@PostMapping
@PermissionCheck("user:create") // 标记需要user:create权限
public User createUser(@RequestBody User user) {
return userService.createUser(user);
}
@DeleteMapping("/{id}")
@PermissionCheck("user:delete") // 标记需要user:delete权限
public void deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
}
}
4. ✅ 启用 AOP 自动代理
确保在 Spring 配置中启用 AOP 自动代理,可以通过 @EnableAspectJAutoProxy 注解实现。
示例代码:启用 AOP
less
@SpringBootApplication
@EnableAspectJAutoProxy // 启用AOP自动代理
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
四、实战案例:实现一个方法执行时间统计注解
需求背景
在性能优化过程中,我们需要统计某些关键方法的执行时间,找出性能瓶颈。使用自定义注解可以优雅地实现这一需求,而不需要在每个方法中添加重复的计时代码。
实现代码
1. 定义注解
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExecutionTime {
String value() default ""; // 方法描述,可选
}
2. 创建切面
java
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;
@Aspect
@Component
public class ExecutionTimeAspect {
private static final Logger logger = LoggerFactory.getLogger(ExecutionTimeAspect.class);
@Around("@annotation(com.example.annotation.ExecutionTime)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
// 执行目标方法
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
// 获取方法信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
ExecutionTime annotation = method.getAnnotation(ExecutionTime.class);
String methodDescription = annotation.value();
// 记录方法执行时间
logger.info("方法 [{}] {} 执行时间: {}ms",
method.getName(),
methodDescription.isEmpty() ? "" : "(" + methodDescription + ")",
executionTime);
return result;
}
}
3. 在目标方法上使用注解
kotlin
@Service
public class OrderService {
@ExecutionTime("创建订单")
public Order createOrder(Order order) {
// 模拟业务处理
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return order;
}
@ExecutionTime("查询订单")
public Order getOrderById(Long orderId) {
// 模拟业务处理
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new Order(orderId, "TEST_ORDER");
}
}
4. 日志输出示例
ini
INFO [ExecutionTimeAspect] 方法 [createOrder] (创建订单) 执行时间: 502ms
INFO [ExecutionTimeAspect] 方法 [getOrderById] (查询订单) 执行时间: 301ms
五、自定义注解的进阶技巧
1. 与 Spring 的其他特性结合
自定义注解可以与 Spring 的各种特性结合使用,例如:
- 条件加载:结合 @Conditional 注解,根据条件决定是否加载某个组件
- 属性注入:通过 @Value 或 @ConfigurationProperties 注入配置值到注解属性
- 事件监听:在注解处理中发布 Spring 事件,实现松耦合
- 国际化:注解中的错误消息支持国际化
2. 处理注解参数
注解参数可以是基本类型、字符串、枚举、Class 等,也可以是数组。例如:
less
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retryable {
int maxAttempts() default 3; // 最大重试次数
Class<? extends Throwable>[] include() default {}; // 需要重试的异常类型
long delay() default 1000; // 重试延迟时间(ms)
}
3. 处理注解继承
如果需要注解在子类中生效,可以使用 @Inherited 元注解:
less
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited // 允许子类继承该注解
public @interface MyAnnotation {
String value() default "";
}
4. 性能考虑
虽然注解很强大,但过度使用 AOP 可能会影响性能。对于性能敏感的系统,可以考虑:
- 减少切面拦截的范围,只对必要的方法使用注解
- 使用 @Around 增强时,避免在切面中执行耗时操作
- 考虑使用编译时 AOP(如 AspectJ)代替运行时 AOP
六、总结:自定义注解的本质是 "元编程"
自定义注解是 Spring 框架中 "约定大于配置" 理念的最佳实践,它让我们可以通过声明式的方式为程序添加额外的行为。
从 8 年开发经验来看,合理使用自定义注解可以带来以下好处:
- 代码简洁:消除重复代码,使业务逻辑更清晰
- 可维护性:集中管理横切关注点,修改时只需改动一处
- 扩展性:可以根据需求随时添加新的注解和功能
- 松耦合:通过注解将特定功能与业务逻辑解耦
七、常见问题与解决方案
1. 注解不生效怎么办?
- 检查是否启用了 AOP 自动代理(@EnableAspectJAutoProxy)
- 确保切面类被 Spring 容器管理(@Component 或 @Aspect 注解)
- 检查注解的 RetentionPolicy 是否为 RUNTIME
- 避免在同一个类中调用被注解的方法(AOP 代理失效)
2. 如何在注解中获取 Spring Bean?
可以通过 ApplicationContextAware 接口获取 ApplicationContext,然后从中获取 Bean:
java
@Component
public class BeanUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
applicationContext = context;
}
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
}
然后在切面中使用:
less
@Aspect
@Component
public class MyAspect {
@Around("@annotation(com.example.MyAnnotation)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取Spring Bean
MyService myService = BeanUtils.getBean(MyService.class);
// ...
}
}
3. 如何处理注解参数的校验?
可以在切面中添加参数校验逻辑,例如:
ini
@Before("@annotation(com.example.Validated)")
public void before(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Validated validated = method.getAnnotation(Validated.class);
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0) {
for (Object arg : args) {
if (arg == null && validated.notNull()) {
throw new IllegalArgumentException("参数不能为空");
}
// 其他校验逻辑...
}
}
}
🚀 你的项目中使用了哪些自定义注解?
自定义注解是 Spring 框架中非常灵活且强大的特性,合理使用可以极大提升代码质量和开发效率。欢迎在评论区分享你在项目中自定义注解的经验和案例,让我们一起学习和进步!