Spring Boot 自定义注解实战案例详解

 更新时间:2026年05月23日 16:03:40   作者:Micro麦可乐  
自定义注解在SpringBoot中的强大应用,通过AOP与注解结合实现代码复用、业务解耦与声明式编程,简化业务逻辑,本文详细解析了日志记录、参数校验、权限校验与分布式限流等注解的定义与实现,助你提升代码质量与开发效率

@RestController@Autowired@Transactional

自定义注解是一种强大的元编程工具,允许在不修改原有代码逻辑的情况下,为程序添加额外的功能。通过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("提交成功");
    }
}

❺ 自定义加解密注解

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

总结

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

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

相关文章

  • Java使用JCommander实现解析命令行参数

    Java使用JCommander实现解析命令行参数

    jcommander 是一个只有几十 kb 的 Java 命令行参数解析工具,可以通过注解的方式快速实现命令行参数解析,本文就来和大家介绍一下JCommander是如何解析命令行参数吧
    2023-06-06
  • IDEA项目如何取消git版本管控并添加svn版本控制

    IDEA项目如何取消git版本管控并添加svn版本控制

    在公司内部服务器环境下,将代码仓库从Gitee的Git迁移到SVN可以避免外部版本控制的风险,迁移过程中,先删除项目的.git文件夹,再通过Eclipse的设置界面删除原Git配置并添加SVN配置,之后,将项目提交到SVN仓库,确保使用ignore列表过滤不必要的文件
    2024-10-10
  • Tomcat集群和Session复制应用介绍

    Tomcat集群和Session复制应用介绍

    本文将详细介绍Tomcat集群和Session复制应用,需要了解的朋友可以参考下
    2012-11-11
  • mybatis如何批量添加一对多中间表

    mybatis如何批量添加一对多中间表

    这篇文章主要介绍了mybatis如何批量添加一对多中间表,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-02-02
  • Spring Boot 通过注解实现数据校验的方法

    Spring Boot 通过注解实现数据校验的方法

    这篇文章主要介绍了Spring Boot 通过注解实现数据校验的方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-09-09
  • 一文教你如何使用Java灵活读取Excel内容

    一文教你如何使用Java灵活读取Excel内容

    这篇文章主要给大家介绍了关于如何使用Java灵活读取Excel内容的相关资料,读取Excel数据是Java开发中常见的任务,文中通过代码示例介绍的非常详细,需要的朋友可以参考下
    2023-09-09
  • Java C++题解leetcode消失的两个数字实例

    Java C++题解leetcode消失的两个数字实例

    这篇文章主要介绍了Java C++题解leetcode消失的两个数字实例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • java远程调用接口、URL的方式代码

    java远程调用接口、URL的方式代码

    我们都知道接口有自己本地的,也有远程别人写好的,而调用远程接口的就需要使用远程调用啦,这篇文章主要给大家介绍了关于java远程调用接口、URL的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2023-11-11
  • java byte数组与16进制间相互转换的示例

    java byte数组与16进制间相互转换的示例

    这篇文章主要介绍了java byte数组与16进制间相互转换的示例,帮助大家更好的理解和使用Java,感兴趣的朋友可以了解下
    2020-10-10
  • 解读tk.mybatis的通用批量更新方式

    解读tk.mybatis的通用批量更新方式

    这篇文章主要介绍了关于tk.mybatis的通用批量更新方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-08-08

最新评论