开发者

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

目录
  • 自定义注解的原理
  • 自定义注解的实现步骤
    • 引入依赖
    • 定义自定义注解
  • 常见的自定义注解案例
    • 自定义日志注解
    • 自定义参数校验注解
    • 自定义权限校验注解
    • 自定义分布式限流注解
    • 自定义加解密注解
  • 总结

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

    自定义注解有哪些好处?

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

    自定义注解的原理

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

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

    自定义注解的实现步骤

    引入依赖

    首先确保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 ConstraintValidatorjavascript<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 U编程客栈serDTO {
            @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()));
            
            ifjavascript (!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) {
            thi编程s.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自定义注解内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜