SpringBoot自定义注解的5个实战案例分享

 更新时间:2025年09月29日 08:56:06   作者:Micro麦可乐  
自定义注解是一种强大的元编程工具,允许在不修改原有代码逻辑的情况下,为程序添加额外的功能,本文将分析5个常见的案例,希望对大家有所帮助

自定义注解是一种强大的元编程工具,允许在不修改原有代码逻辑的情况下,为程序添加额外的功能。通过AOP面向切面编程)与自定义注解的结合,我们可以实现关注点分离,让业务代码更加清晰简洁。

自定义注解有哪些好处?

  • 代码复用:将通用逻辑封装到注解中
  • 业务解耦:横切关注点与核心业务逻辑分离
  • 声明式编程:通过注解配置行为,代码更直观
  • 可维护性:通用逻辑集中管理,修改更方便

自定义注解的原理

Spring Boot 自定义注解的底层原理主要依赖于:

  • Java 注解机制(@interface 定义注解)
  • AOP(面向切面编程) 或 拦截器 结合反射来解析注解
  • Spring 容器在运行时自动识别和织入逻辑

自定义注解的实现步骤

引入依赖

首先确保pom.xml中包含必要的依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

定义自定义注解

以一个最基础的自定义注解为例:

import java.lang.annotation.*;

@Target(ElementType.METHOD) // 注解作用目标:方法
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Documented
public @interface MyAnnotation {
    String value() default "default";
}

注解说明:

  • @Target:指定注解作用的范围(类、方法、字段、参数…)
  • @Retention:指定注解生命周期(源码、编译期、运行时)
  • @Documented:生成 Javadoc 时包含注解信息

常见的自定义注解案例

下面博主讲完整演示几个日常开发中我们常见的自定义注解案例来让大家深入的了解

自定义日志注解

定义注解

效果:调用接口时,自动打印方法耗时和相关日志

/**
 * 方法日志注解
 * 用于自动记录方法入参、出参和执行时间
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodLog {
    String value() default "";
    boolean printArgs() default true;
    boolean printResult() default true;
    boolean timing() default true;
}

切面实现

这里仅仅以打印输出为案例,实际生产环境中,小伙伴们可以结合数据库、日志系统等将信息记录入库

@Aspect
@Component
@Slf4j
public class MethodLogAspect {

    @Around("@annotation(methodLog)")
    public Object around(ProceedingJoinPoint joinPoint, MethodLog methodLog) throws Throwable {
        String methodName = getMethodName(joinPoint);
        String className = joinPoint.getTarget().getClass().getSimpleName();
        
        // 记录开始时间
        long startTime = System.currentTimeMillis();
        
        if (methodLog.printArgs()) {
            Object[] args = joinPoint.getArgs();
            log.info("[{}#{}] 方法调用, 参数: {}", className, methodName, Arrays.toString(args));
        } else {
            log.info("[{}#{}] 方法调用", className, methodName);
        }
        
        try {
            Object result = joinPoint.proceed();
            
            if (methodLog.printResult()) {
                log.info("[{}#{}] 方法返回: {}", className, methodName, result);
            }
            
            if (methodLog.timing()) {
                long cost = System.currentTimeMillis() - startTime;
                log.info("[{}#{}] 方法执行耗时: {}ms", className, methodName, cost);
            }
            
            return result;
        } catch (Exception e) {
            log.error("[{}#{}] 方法执行异常: {}", className, methodName, e.getMessage());
            throw e;
        }
    }
    
    private String getMethodName(ProceedingJoinPoint joinPoint) {
        return joinPoint.getSignature().getName();
    }
}

使用示例

以用户接口为例,创建用户的时候会记录该接口会打印相关的信息日志

@RestController
@RequestMapping("/api/user")
public class UserController {
    
    @PostMapping
    @MethodLog(value = "创建用户", printArgs = true, printResult = true, timing = true)
    public User createUser(@RequestBody User user) {
        // 业务逻辑
        return userService.save(user);
    }
    
    @GetMapping("/{id}")
    @MethodLog("根据ID查询用户")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
}

自定义参数校验注解

通常我们在Controller中进行数据校验都是用validation, 可以大大节省我们参数校验的时间,虽然validation 默认的注解已经足以应付我们工作中大部分场景,但还是会有一些参数校验有其它的一些验证要求,那么就可以用到自定义参数校验注解。

你也查阅博主之前写的 【Spring Boot数据校验validation实战:写少一半代码,还更优雅!】学习Spring Boot数据校验

定义注解

效果:提交手机号不合法时,自动抛出校验异常

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class) // 绑定校验器
public @interface Phone {
    String message() default "手机号格式错误";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

实现校验器

这里就简单验证一下是否正确的手机号,小伙伴们可以加入自己需要的验证逻辑,比如仅限移动用户等

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class PhoneValidator implements ConstraintValidator<Phone, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return value != null && value.matches("^1[3-9]\\d{9}$");
    }
}

使用示例

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;

@RestController
public class RegisterController {

    @PostMapping("/register")
    public String register(@Valid @RequestBody UserDTO userDTO) {
        return "注册成功";
    }

    public static class UserDTO {
        @NotBlank
        private String name;

        @Phone
        private String phone;

        // getter/setter
    }
}

自定义权限校验注解

本次我们模拟Spring Security中的@PreAuthorize注解,想完整学习@PreAuthorize注解用法的小伙伴可以参考博主Spring Security专栏下的 【最新Spring Security实战教程(七)方法级安全控制@PreAuthorize注解的灵活运用】

这里我们就模拟一下全县校验的功能

定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPermission {
    String value(); // 权限标识
}

实现 AOP 权限校验

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class PermissionAspect {

    @Before("@annotation(checkPermission)")
    public void check(JoinPoint joinPoint, CheckPermission checkPermission) {
        String requiredPermission = checkPermission.value();
        // 模拟从上下文获取当前用户权限
        String userPermission = "USER"; 

        if (!userPermission.equals(requiredPermission)) {
            throw new RuntimeException("权限不足,缺少:" + requiredPermission);
        }
    }
}

使用示例

@RestController
public class AdminController {

    @CheckPermission("ADMIN")
    @GetMapping("/admin")
    public String adminPage() {
        return "管理员页面";
    }
}

自定义分布式限流注解

定义注解

/**
 * 限流注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    String key() default "";
    int limit() default 100;
    int timeWindow() default 60; // 时间窗口,单位:秒
    String message() default "访问过于频繁,请稍后再试";
}

切面实现

@Aspect
@Component
@Slf4j
public class RateLimitAspect {
    
    private final Map<String, RateLimiter> limiterMap = new ConcurrentHashMap<>();
    
    @Before("@annotation(rateLimit)")
    public void rateLimitCheck(RateLimit rateLimit) {
        String key = generateKey(rateLimit);
        RateLimiter limiter = limiterMap.computeIfAbsent(key, 
            k -> RateLimiter.create(rateLimit.limit() / (double) rateLimit.timeWindow()));
        
        if (!limiter.tryAcquire()) {
            throw new RuntimeException(rateLimit.message());
        }
    }
    
    private String generateKey(RateLimit rateLimit) {
        String key = rateLimit.key();
        if (StringUtils.isEmpty(key)) {
            // 可以结合用户信息、IP等生成唯一key
            return "rate_limit:" + System.identityHashCode(rateLimit);
        }
        return "rate_limit:" + key;
    }
}

// 简单的令牌桶限流器实现
class RateLimiter {
    private final double capacity;
    private final double refillTokensPerOneMillis;
    private double availableTokens;
    private long lastRefillTimestamp;
    
    public static RateLimiter create(double permitsPerSecond) {
        return new RateLimiter(permitsPerSecond);
    }
    
    private RateLimiter(double permitsPerSecond) {
        this.capacity = permitsPerSecond;
        this.refillTokensPerOneMillis = permitsPerSecond / 1000.0;
        this.availableTokens = permitsPerSecond;
        this.lastRefillTimestamp = System.currentTimeMillis();
    }
    
    public synchronized boolean tryAcquire() {
        refill();
        if (availableTokens < 1) {
            return false;
        }
        availableTokens -= 1;
        return true;
    }
    
    private void refill() {
        long currentTime = System.currentTimeMillis();
        if (currentTime > lastRefillTimestamp) {
            long millisSinceLastRefill = currentTime - lastRefillTimestamp;
            double refill = millisSinceLastRefill * refillTokensPerOneMillis;
            this.availableTokens = Math.min(capacity, availableTokens + refill);
            this.lastRefillTimestamp = currentTime;
        }
    }
}

使用示例

@RestController
@RequestMapping("/api")
public class ApiController {
    
    @GetMapping("/public/data")
    @RateLimit(limit = 10, timeWindow = 60, message = "接口调用频率超限")
    public ApiResponse getPublicData() {
        return ApiResponse.success("公开数据");
    }
    
    @PostMapping("/submit")
    @RateLimit(key = "submit_limit", limit = 5, timeWindow = 30)
    public ApiResponse submitData(@RequestBody Data data) {
        // 处理提交
        return ApiResponse.success("提交成功");
    }
}

自定义加解密注解

可参考博主之前写的 【Spring Boot中整合Jasypt 使用自定义注解+AOP实现敏感字段的加解密】进行学习,这里就不再赘述了!

总结

以上通过5个案例演示,完整讲解了Spring Boot 自定义注解的使用,通过合理使用自定义注解,我们可以大幅提升代码的可读性、可维护性和复用性。在实际项目中,可以根据业务需求灵活组合和扩展这些注解,构建更加健壮和安全的应用程序。

到此这篇关于SpringBoot自定义注解的5个实战案例分享的文章就介绍到这了,更多相关SpringBoot自定义注解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java使用JDK与Cglib动态代理技术统一管理日志记录

    Java使用JDK与Cglib动态代理技术统一管理日志记录

    这篇文章主要介绍了Java使用JDK与Cglib动态代理技术统一管理日志记录,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-05-05
  • Java四种元注解介绍

    Java四种元注解介绍

    元注解是用来修饰注解的注解,在java.lang.annotation包下,当我们需要自己定义一个注解去做某些事情的时候,我们要对该注解进行一些限制,确保我们注解的作用域,这篇文章主要介绍了Java四种元注解介绍,需要的朋友可以参考下
    2024-08-08
  • Spring Security实现验证码登录功能

    Spring Security实现验证码登录功能

    这篇文章主要介绍了Spring Security实现验证码登录功能,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-01-01
  • 浅谈springMVC接收前端json数据的总结

    浅谈springMVC接收前端json数据的总结

    下面小编就为大家分享一篇浅谈springMVC接收前端json数据的总结,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-03-03
  • Java中Object转换为List类型的实现方法

    Java中Object转换为List类型的实现方法

    这篇文章主要介绍了Java中Object转换为List类型的实现方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-03-03
  • 通过实例解析spring bean之间的关系

    通过实例解析spring bean之间的关系

    这篇文章主要介绍了通过实例解析spring bean之间的关系,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-01-01
  • SpringBoot单元测试框架Mockito介绍及使用

    SpringBoot单元测试框架Mockito介绍及使用

    与集成测试将系统作为一个整体测试不同,单元测试更应该专注于某个类。所以当被测试类与外部类有依赖的时候,尤其是与数据库相关的这种费时且有状态的类,很难做单元测试。但好在可以通过“Mockito”这种仿真框架来模拟这些比较费时的类,从而专注于测试某个类内部的逻辑
    2023-01-01
  • SpringBoot结合Quartz实现数据库存储

    SpringBoot结合Quartz实现数据库存储

    本文主要介绍了SpringBoot+Quartz+数据库存储,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • 史上最全MybatisPlus从入门到精通

    史上最全MybatisPlus从入门到精通

    MyBatis-Plus是MyBatis增强工具,简化开发并提升效率,支持自动映射表名/字段与实体类,提供条件构造器、多种查询方式(等值/范围/模糊/分页等)、主键策略(如雪花算法、UUID)及分页插件配置,实现灵活的数据操作与数据库交互,需要的朋友跟随小编一起看看吧
    2025-08-08
  • java题解LeetCode20.有效的括号

    java题解LeetCode20.有效的括号

    这篇文章主要为大家介绍了java题解LeetCode20.有效的括号示例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-10-10

最新评论