java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot自定义注解

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

作者:Micro麦可乐

自定义注解是一种强大的元编程工具,允许在不修改原有代码逻辑的情况下,为程序添加额外的功能,本文将分析5个常见的案例,希望对大家有所帮助

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

自定义注解有哪些好处?

自定义注解的原理

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

自定义注解的实现步骤

引入依赖

首先确保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(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自定义注解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文