Spring Boot接口注解实现:分布式锁、限流、记录、日志、防抖、防重

whdahanh 发布于 2024-08-19 1142 次阅读


分布式锁:CacheLock


1、自定义注解
import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheLock {
/**
锁前缀
**/
    String lockedPrefix() default "";
/**
有效时间,默认100
锁的有效期 (毫秒)
**/
    long expireTime() default 100;
}

2、AOP实现缓存锁

@Aspect
@Component
@Slf4j
public class CacheLockAspect {
    private static final String LOCK_VALUE = "locked";

    @Autowired
    private RedissonClient redissonClient;

    // 更改注入方式,使得配置更加清晰
    private final String profile;

    public CacheLockAspect(@Value("${spring.profiles.active}") String profile) {
        this.profile = profile;
    }

    @Pointcut("@annotation(com.cjhb.original.swam.annotation.CacheLock)")
    public void cacheLock() {
    }

    @Around("cacheLock()")
    public Object cacheLockPoint(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        CacheLock cacheLock = method.getAnnotation(CacheLock.class);
        String lockKey = cacheLock.lockedPrefix() + "-" + profile;
        RLock rLock = redissonClient.getLock(lockKey);
        long timeOut = cacheLock.expireTime();
        
        // 添加重试机制
        int maxAttempts = 3; // 最大尝试次数
        for (int i = 0; i < maxAttempts; i++) {
            try {
                boolean isSuccess = rLock.tryLock(1L, timeOut, TimeUnit.MILLISECONDS);
                if (isSuccess) {
                    log.info("method:{} 获取锁:{},开始执行", method.getName(), lockKey);
                    return pjp.proceed(); // 直接返回方法执行结果
                } else {
                    log.warn("method:{} 第{}次尝试获取锁失败,停止执行或重试", method.getName(), i + 1);
                    if (i == maxAttempts - 1) {
                        log.error("method:{} 所有尝试获取锁均失败,停止执行", method.getName());
                        break;
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("Interrupted while trying to acquire lock", e);
            } catch (Exception e) {
                log.error("method:{} 运行错误", method.getName(), e);
                throw e;
            } finally {
                if (rLock.isHeldByCurrentThread()) {
                    rLock.unlock();
                    log.info("method:{} 释放锁:{}", method.getName(), lockKey);
                }
            }
        }
        return null;
    }
}


3、使用示例代码
    /**
     * 心跳检测
     */
    @Scheduled(cron = "0/1 * * * * ?")
    @CacheLock(lockedPrefix = "heartBeatDetection")
    public void startHeartBeatDetection() {
        mqttHeartBeatService.startHeartBeatDetection();
    }

接口请求记录注解:InterfaceRecord

1、自定义注解
import java.lang.annotation.*;

/**
 * 接口请求记录注解
 * @author Jeffrey
 * @version V1.0
 * @Package com.original.swam.annotation
 * @date 2024/4/20 22:19
 * @Copyright © 2018-2024
 */
@Target({ElementType.METHOD,ElementType.PARAMETER,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface InterfaceRecord {

}

2、AOP实现接口记录
import com.cjhb.original.swam.common.constants.KeyToolsConstants;
import com.cjhb.original.swam.common.constants.ReqHeaderConstants;
import com.cjhb.original.swam.common.core.Result;
import com.cjhb.original.swam.common.enums.HttpTypeEnums;
import com.cjhb.original.swam.common.pjo.dto.v1.RequestLogAddDTO;
import com.cjhb.original.swam.common.pjo.dto.v1.RequestLogEditorDTO;
import com.cjhb.original.swam.common.pjo.sysTools.SystemToolsBo;
import com.cjhb.original.swam.common.utils.http.IpUtils;
import com.cjhb.original.swam.common.utils.http.ReqUtils;
import com.cjhb.original.swam.common.utils.json.JsonUtils;
import com.cjhb.original.swam.common.utils.lang.StringUtils;
import com.cjhb.original.swam.common.utils.randomNumber.UniqueIdUtils;
import com.cjhb.original.swam.common.utils.randomNumber.UuidUtils;
import com.cjhb.original.swam.common.utils.time.DateUtils;
import com.cjhb.original.swam.common.utils.tools.KeyToolUtils;
import com.cjhb.original.swam.server.other.v1.RequestLogService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;

/**
 * 请求日志切面记录
 *
 * @author Jeffrey
 * @version V1.0
 * @Package com.original.swam.aspect
 * @date 2024/4/20 22:26
 * @Copyright © 2018-2024
 */
@Aspect
@Component
public class InterfaceRecordAspect {

    private static Logger logger = LoggerFactory.getLogger(WebLogAspect.class);

    @Resource
    private RequestLogService requestLogService;

    private String uuid = "";

    private Long startTime = 0L;

    @Resource
    private UniqueIdUtils uniqueIdUtils;

    @Pointcut("@annotation(com.cjhb.original.swam.annotation.InterfaceRecord)")
    public void interfaceRecord() {
    }

    @Before("interfaceRecord()")
    public void doBefore(JoinPoint joinPoint) throws IOException {
        LocalDateTime requestTime = DateUtils.getTodayToLocalTime();
        LocalDateTime createTime = DateUtils.getTodayToLocalTime();
        startTime = DateUtils.getToLocalTime().getTime();
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            return;
        }
        HttpServletRequest request = attributes.getRequest();
        String platform = ReqUtils.getReqHeaderParm(request, ReqHeaderConstants.HEADER_PLATFORM);
        String authorizationKey = ReqUtils.getReqHeaderParm(request, ReqHeaderConstants.HEADER_PREFIX);
        String extractTokenInfo = KeyToolUtils.extractTokenInfo(authorizationKey, KeyToolsConstants.Secret_Key);
        SystemToolsBo systemToolsBo = KeyToolUtils.decodeAnalyzeExtractSubject(extractTokenInfo);
        uuid = uniqueIdUtils.generatorRequestLogSystemId();
        // url
        String requestUrl = request.getRequestURI().trim().toString();
        // Http Method
        String requestMethod = request.getMethod();
        // Class Method
        String classMethod = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
        // IP
        String requestIp = IpUtils.getReqIp(request);
        // 请求参数
        String requestParams = "";
        if (requestMethod.equalsIgnoreCase(HttpTypeEnums.GET_TYPE.getCode())) {
            Map<String, String[]> parameterMap = request.getParameterMap();
            for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
                for (String value : entry.getValue()) {
                    requestParams += entry.getKey() + "=" + value + "&";
                }
            }
            // 移除最后一个"&"符号
            if (!requestParams.isEmpty()) {
                requestParams = requestParams.substring(0, requestParams.length() - 1);
            }
        } else if (requestMethod.equalsIgnoreCase(HttpTypeEnums.POST_TYPE.getCode())) {
            Object[] args = joinPoint.getArgs();
            String params = "";
            if (args != null) {
                params = Arrays.toString(args);
            }
            requestParams = params;
        }
        RequestLogAddDTO requestLog = RequestLogAddDTO.getInstance().addRequestLog(systemToolsBo.getSystemUniqueId(), uuid, platform, requestIp, requestUrl, requestMethod, requestParams, requestTime, createTime, "", classMethod);
        Result result = requestLogService.addRequestLog(requestLog);
        uuid = (StringUtils.checkNull(result) ? uuid : result.getData().toString());
    }

    @After("interfaceRecord()")
    public void doAfter() {
//        logger.info("After advice is executed.");
    }

    @Around("interfaceRecord()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object proceed = proceedingJoinPoint.proceed();
        if (proceed != null) {
            String result = JsonUtils.toJson(proceed);
            Long timeNums = System.currentTimeMillis() - startTime;
            RequestLogEditorDTO requestLog = RequestLogEditorDTO.getInstance().editorRequestLog(uuid, result, timeNums.floatValue(), DateUtils.getTodayToLocalTime());
            requestLogService.editorRequestLog(requestLog);
        }
        return proceed;
    }


}

3、示例代码
    @ApiOperation(value = "开放指挥中心人员设备信息(组织架构id)", notes = "开放指挥中心人员设备信息(组织架构id)", nickname = "开放指挥中心人员设备信息(组织架构id)", httpMethod = "GET")
    @GetMapping("/queryComCenOg")
    @ResponseBody
    @InterfaceRecord
//    @RestrictionRequest(key = "queryDeviceManageSideList", times = 2)
    public Result<List<CommandCenterDeviceInfoVo>> queryComCenOg(@RequestParam("orgId") String orgId) {
        return openCommandCenterService.queryComCenOg(orgId);
    }

接口限制请求拦截:RestrictionRequest

1、自定义注解
import java.lang.annotation.*;

/**
 * 接口限制请求拦截
 * @deprecated 加上这个注解可以将参数设置为key
 * @Package IntelliJ IDEA
 * @Author Jeffrey
 * @Date 2024-03-13 17:16
 * @Version V1.0
 * @Copyright © 2018-2024
 **/
@Target({ElementType.METHOD,ElementType.PARAMETER,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RestrictionRequest {

    /**
     * 限制访问的key
     * @return
     */
    String key();

    /**
     * 限制访问时间 秒
     * @return
     */
    int times();

}

2、AOP实现限流请求
import com.cjhb.original.swam.annotation.RestrictionRequest;
import com.cjhb.original.swam.common.constants.CacheConstants;
import com.cjhb.original.swam.common.constants.KeyToolsConstants;
import com.cjhb.original.swam.common.constants.ReqHeaderConstants;
import com.cjhb.original.swam.common.enums.ResultCode;
import com.cjhb.original.swam.common.exception.BizException;
import com.cjhb.original.swam.common.pjo.sysTools.SystemToolsBo;
import com.cjhb.original.swam.common.utils.http.IpUtils;
import com.cjhb.original.swam.common.utils.http.ReqUtils;
import com.cjhb.original.swam.common.utils.lang.StringUtils;
import com.cjhb.original.swam.common.utils.redis.RedisUtilsService;
import com.cjhb.original.swam.common.utils.tools.KeyToolUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;

/**
 * 接口限制请求拦截
 *
 * @Author Jeffrey
 * @Date 2024-03-15 10:05
 * @Version V1.0
 * @Copyright © 2018-2024
 **/
@Aspect
@Component
public class RestrictionRequestAspect {

    @Resource
    private RedisUtilsService redisUtilsService;

    @Pointcut("@annotation(com.cjhb.original.swam.annotation.RestrictionRequest)")
    public void pt() {
    }

    @Around("pt()")
    public Object aopAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        RestrictionRequest limitAccess = methodSignature.getMethod().getAnnotation(RestrictionRequest.class);
        if (limitAccess.times() < 0){
            return joinPoint.proceed();
        }
        String key = CacheConstants.REQUEST_RESTRICTION_KEY + limitAccess.key();
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            String requestIp = IpUtils.getReqIp(request);
            String platform = ReqUtils.getReqHeaderParm(request, ReqHeaderConstants.HEADER_PLATFORM);
            String authorization = ReqUtils.getReqHeaderParm(request, ReqHeaderConstants.HEADER_PREFIX);
            String extractTokenInfo = KeyToolUtils.extractTokenInfo(authorization, KeyToolsConstants.Secret_Key);
            SystemToolsBo systemToolsBo = KeyToolUtils.decodeAnalyzeExtractSubject(extractTokenInfo);
            key = key + ":" + requestIp + "&" + platform + "&" + systemToolsBo.getSystemUniqueId();
        }
        Object obj = redisUtilsService.get(key);
        if (StringUtils.checkNull(obj)) {
            int times = limitAccess.times();
            redisUtilsService.set(key, key, times, TimeUnit.SECONDS);
            return joinPoint.proceed();
        } else {
            throw new BizException(ResultCode.INTERFACE_RESTRICTION_LIMIT_ERROR);
        }
    }

}

3、使用示例代码
    @ApiOperation(value = "请求记录分页", notes = "请求记录分页", nickname = "请求记录分页", httpMethod = "POST")
    @PostMapping("/pageList")
//    @InterfaceRecord
    @ResponseBody
    @RestrictionRequest(key = "queryPersonnelPageList", times = 2)
    public Result<PageVo<RequestLogPageVo>> queryPersonnelPageList(@RequestBody @Validated RequestLogPageDTO dto) {
        Result<PageVo<RequestLogPageVo>> pageVoResult = requestLogService.getRequestLogPageList(dto);
        return pageVoResult;
    }

防重复提交:RepeatSubmit

接口防重复提交是防止用户在短时间内多次点击提交按钮或重复发送相同请求导致的多次执行同一操作的问题,这对于保护数据一致性、避免资源浪费非常重要.

1、自定义接口防重注解@RepeatSubmit 

* @author 吴农软语
* 自定义接口防重注解类
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {

     * 定义了两种防止重复提交的方式,PARAM 表示基于方法参数来防止重复,TOKEN 则可能涉及生成和验证token的机制
     */
    enum Type { PARAM, TOKEN }

     * 设置默认的防重提交方式为基于方法参数。开发者可以不指定此参数,使用默认值。
     * @return Type
     */
    Type limitType() default Type.PARAM;

     * 允许设置加锁的过期时间,默认为5秒。这意味着在第一次请求之后的5秒内,相同的请求将被视为重复并被阻止
     * @return
     */
    long lockTime() default 5;


    String serviceId() default ""; 
}

2、利用AOP实现接口防重处理
/**
* @author 吴农软语
* 利用AOP实现接口防重处理
*/
@Aspect
@Slf4j
public class RepeatSubmitAspect {
    @Resource
    private RedisRepository redisRepository

    @Resource
    private RedissonClient redissonClient

    @Pointcut("@annotation(repeatSubmit)")
    public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {

    }

    /**
     * 环绕通知, 围绕着方法执行
     * 两种方式
     * 方式一:加锁 固定时间内不能重复提交
     * 方式二:先请求获取token,这边再删除token,删除成功则是第一次提交
     */
    @Around("pointCutNoRepeatSubmit(repeatSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest()
        String serviceId = repeatSubmit.serviceId()
        //用于记录成功或者失败
        boolean res = false
        //防重提交类型
        String type = repeatSubmit.limitType().name()
        if (type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {
            //方式一,参数形式防重提交
            //通过 redissonClient 获取分布式锁,基于IP地址、类名、方法名和服务ID生成唯一key
            long lockTime = repeatSubmit.lockTime()
            String ipAddr = AddrUtil.getRemoteAddr(request)
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature()
            Method method = methodSignature.getMethod()
            String className = method.getDeclaringClass().getName()
            String key = repeatSubmit.serviceId() + ":repeat_submit:" + AddrUtil.MD5(String.format("%s-%s-%s-%s", ipAddr, className, method, serviceId))

            //使用 tryLock 尝试获取锁,如果无法获取(即锁已被其他请求持有),则认为是重复提交,直接返回null
            RLock lock = redissonClient.getLock(key)
            //锁自动过期时间为 lockTime 秒,确保即使程序异常也不会永久锁定资源,尝试加锁,最多等待0秒,上锁以后5秒自动解锁 [lockTime默认为5s, 可以自定义]
            res = lock.tryLock(0, lockTime, TimeUnit.SECONDS)

        } else {
            //方式二,令牌形式防重提交
            //从请求头中获取 request-token,如果不存在,则抛出异常
            String requestToken = request.getHeader("request-token")
            if (StringUtils.isBlank(requestToken)) {
                throw new RuntimeException("请求未包含令牌")
            }
            //使用 request-token 和 serviceId 构造Redis的key,尝试从Redis中删除这个键。如果删除成功,说明是首次提交;否则认为是重复提交
            String key = String.format(CommonConstants.SERVICE_SUBMIT_TOKEN_KEY, repeatSubmit.serviceId(), requestToken)
            res = redisRepository.del(key)
        }
        if (!res) {
            log.error("请求重复提交")
            return null
        }
        //在环绕通知的前后记录日志,有助于跟踪方法执行情况和重复提交的检测
        log.info("环绕通知执行前")
        Object obj = joinPoint.proceed()
        log.info("环绕通知执行后")
        return obj
    }
}
3、注册自定义自动配置类:RepeatAutoConfiguration 
@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RepeatAutoConfiguration {
    @Bean
    public RepeatSubmitAspect repeatSubmitAspect() {
        return new RepeatSubmitAspect();
    }
}
4、使用示例代码
  @PostMapping("/users/save")
    @RepeatSubmit(serviceId = "saveUser", limitType = RepeatSubmit.Type.TOKEN, lockTime = 5)
    public ResponseEntity<String> saveUser(@RequestBody User user) {
        userService.save(user);
        return ResponseEntity.ok("用户保存成功");
    }

接口防抖

接口防抖是一种常见的前端性能优化策略,用于限制在一定时间内连续触发的函数只会执行一次,常用于搜索框的输入监听、按钮防连击等情况,以减少不必要的计算或网络请求。
后端接口防抖处理主要是为了避免在短时间内接收到大量相同的请求,特别是由于前端操作(如快速点击按钮)、网络重试或异常情况导致的重复请求。后端接口防抖通常涉及记录最近的请求信息,并在特定时间窗口内拒绝处理相同或相似的请求。

1、自定义接口防重注解类:@AntiShake
@Target(ElementType.METHOD) 

@Retention(RetentionPolicy.RUNTIME) 
public @interface AntiShake {

    long value() default 1000L; 
}


2、实现AOP切面处理防抖
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect 
@Component 
public class AntiShakeAspect {

    private ThreadLocal<Long> lastInvokeTime = new ThreadLocal<>();

    @Around("@annotation(antiShake)") 
    public Object aroundAdvice(ProceedingJoinPoint joinPoint, AntiShake antiShake) throws Throwable {
        long currentTime = System.currentTimeMillis();
        long lastTime = lastInvokeTime.get() != null ? lastInvokeTime.get() : 0;

        if (currentTime - lastTime < antiShake.value()) {

            return null; 
        }

        lastInvokeTime.set(currentTime);
        return joinPoint.proceed(); 
    }
}


3、注册自定义自动配置类:AntiShakeAspect
@Configuration(proxyBeanMethods = false)
public class AntiShakeAutoConfiguration {
    @Bean
    public AntiShakeAspect antiShakeAspect() {
        return new AntiShakeAspect();
    }
}


4、使用示例代码
@Service 
public class SomeService {
   @AntiShake(value = 2000) 
   public String someMethodThatNeedsToBeDebounced(String param) {

        return "Result";
    }
}

控制台记录输出


import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

/**
 * @author mjeffrey
 */
@Aspect
@Component
public class WebLogAspect {

    private static Logger logger = LoggerFactory.getLogger(WebLogAspect.class);

    @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)" +
            "|| @annotation(org.springframework.web.bind.annotation.GetMapping)" +
            "|| @annotation(org.springframework.web.bind.annotation.PostMapping)" +
            "|| execution(* com.cjhb.original.swam.controller.*.*(..))")
    public void webLog() {
    }

    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) {
//        logger.info("Before advice is executed.");
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            return;
        }
        HttpServletRequest request = attributes.getRequest();
        String params = "";
        Object[] args = joinPoint.getArgs();
        if (args != null) {
            params = Arrays.toString(args);
        }
        logger.info("【url】:[{}],【Http Method】:[{}],【Class Method】:[{}].[{}], 【IP】:[{}],【请求参数】:[{}]",
                request.getRequestURI().toString(), request.getMethod(),
                joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(), request.getRemoteAddr(), params);
    }

    @After("webLog()")
    public void doAfter() {
//        logger.info("After advice is executed.");
    }

    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//        logger.info("Around advice is executed before method call.");
        Long startTime = System.currentTimeMillis();
        Object proceed = proceedingJoinPoint.proceed();
        if (proceed != null) {
            logger.info("【返回参数】:[{}],【耗时】:[{}]ms", JSONObject.toJSONString(proceed), System.currentTimeMillis() - startTime);
        }
//        logger.info("Around advice is executed after method call.");
        return proceed;
    }


}


微信扫描下方的二维码阅读本文

此作者没有提供个人介绍
最后更新于 2024-08-23