在 Spring 框架中,如何自定义一个注解?

在 Spring 框架中,如何自定义一个注解?

作者:Java 后端开发工程师・8 年实战经验

时间:2025 年 6 月 23日

标签:Spring、自定义注解、AOP、元编程、框架扩展


一、引言

在我参与的一个金融系统重构项目中,遇到了一个典型的场景:需要对所有敏感接口进行统一的权限校验。最初的实现是在每个 Controller 方法中添加重复的权限判断代码,导致代码冗余且难以维护。后来通过自定义 Spring 注解结合 AOP,完美解决了这个问题,不仅消除了重复代码,还让权限控制变得灵活可配置。

自定义注解是 Spring 框架中非常强大的扩展机制,它允许我们在不改变原有代码结构的前提下,通过声明式的方式为程序添加额外的功能。本文将结合我多年的开发经验,从实际场景出发,深入讲解如何在 Spring 中自定义一个注解。


二、哪些场景适合使用自定义注解?

在实际开发中,我总结了以下几类典型场景:

  1. 权限控制:如接口访问权限校验、角色验证
  2. 日志记录:自动记录方法调用参数、返回值、执行时间
  3. 参数校验:如非空检查、格式验证、范围校验
  4. 事务控制:自定义事务传播行为、隔离级别
  5. 缓存处理:自定义缓存策略、过期时间
  6. 分布式锁:方法级的分布式锁实现
  7. 异步处理:标记方法为异步执行
  8. 数据脱敏:自动对敏感数据进行脱敏处理

三、自定义 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. 代码简洁:消除重复代码,使业务逻辑更清晰
  2. 可维护性:集中管理横切关注点,修改时只需改动一处
  3. 扩展性:可以根据需求随时添加新的注解和功能
  4. 松耦合:通过注解将特定功能与业务逻辑解耦

七、常见问题与解决方案

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 框架中非常灵活且强大的特性,合理使用可以极大提升代码质量和开发效率。欢迎在评论区分享你在项目中自定义注解的经验和案例,让我们一起学习和进步!

相关推荐
midsummer_woo1 小时前
基于spring boot的医院挂号就诊系统(源码+论文)
java·spring boot·后端
Olrookie2 小时前
若依前后端分离版学习笔记(三)——表结构介绍
笔记·后端·mysql
沸腾_罗强2 小时前
Bugs
后端
一条GO2 小时前
ORM中实现SaaS的数据与库的隔离
后端
京茶吉鹿2 小时前
"if else" 堆成山?这招让你的代码优雅起飞!
java·后端
长安不见2 小时前
从 NPE 到高内聚:Spring 构造器注入的真正价值
后端
你我约定有三2 小时前
RabbitMQ--消息丢失问题及解决
java·开发语言·分布式·后端·rabbitmq·ruby
程序视点3 小时前
望言OCR 2025终极评测:免费版VS专业版全方位对比(含免费下载)
前端·后端·github
rannn_1113 小时前
Java学习|黑马笔记|Day23】网络编程、反射、动态代理
java·笔记·后端·学习
一杯科技拿铁3 小时前
Go 的时间包:理解单调时间与挂钟时间
开发语言·后端·golang