springboot基于redis,使用 AOP实现防重复提交

背景:

同一条表单提交被用户点击了多次,导致数据冗余,需要防止弱网络等环境下的重复点击

场景:

  1. 前端提交按钮点击后未做禁止点击
  2. 通过其他方式直接调用接口

目标

通过在指定的接口处添加一个注解,实现防重复点击

方案

springboot、spring-data-redis、aop

实例

  1. 创建aop自定义注解RedisSynchronized
package com.demo.duplication.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 基于redis单线程实现防重复提交
 *
 * @author 勤恳且善良的程序猿
 * @date 2021/7/2
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisSynchronized {
    /**
     * 唯一key,多个参数使用'-'拼接
     */
    String key();

    /**
     * 几秒时间内不可重复提交(redis过期时间,默认1秒)
     */
    long second() default 1L;

    /**
     * 重复提交情况下的返回提示
     */
    String msg() default "请勿重复提交!";

    /**
     * 是否在key上拼接用户ID
     */
    boolean splicingUserId() default false;
}

  1. 针对上面自定义注解的key字段进行的参数解析
package com.demo.duplication.expression;

import org.springframework.aop.support.AopUtils;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.context.expression.CachedExpressionEvaluator;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.SpelEvaluationException;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * SpEL 表达式的解析
 *
 * @author 勤恳且善良的程序猿
 * @date 2021/7/2
 */
public class ExpressionEvaluator<T> extends CachedExpressionEvaluator {
    private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer();
    private final Map<ExpressionKey, Expression> conditionCache = new ConcurrentHashMap<>(64);
    private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64);


    public EvaluationContext createEvaluationContext(Object object, Class<?> targetClass, Method method, Object[] args) {
        Method targetMethod = getTargetMethod(targetClass, method);
        ExpressionRootObject root = new ExpressionRootObject(object, args);
        return new MethodBasedEvaluationContext(root, targetMethod, args, this.paramNameDiscoverer);
    }


    public T condition(String conditionExpression, AnnotatedElementKey elementKey, EvaluationContext evalContext, Class<T> clazz) {
        try {
            return getExpression(this.conditionCache, elementKey, conditionExpression).getValue(evalContext, clazz);
        } catch (SpelEvaluationException spelEvaluationException) {
            return (T) conditionExpression;
        }
    }

    private Method getTargetMethod(Class<?> targetClass, Method method) {
        AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
        Method targetMethod = this.targetMethodCache.get(methodKey);
        if (targetMethod == null) {
            targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
            if (targetMethod == null) {
                targetMethod = method;
            }
            this.targetMethodCache.put(methodKey, targetMethod);
        }
        return targetMethod;
    }
}
package com.demo.duplication.expression;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * SpEL 表达式计算上下文根对象
 *
 * @author 勤恳且善良的程序猿
 * @date 2021/7/2
 */
@Getter
@AllArgsConstructor
public class ExpressionRootObject {
    private final Object object;
    private final Object[] args;
}
  1. 切面
package com.demo.duplication.aspect;

import com.alibaba.fastjson.JSONObject;
import com.demo.duplication.annotation.RedisSynchronized;
import com.demo.duplication.expression.ExpressionEvaluator;
import com.demo.util.RedisUtil;
import lombok.SneakyThrows;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.expression.EvaluationContext;
import org.springframework.http.HttpHeaders;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

/**
 * 基于redis单线程实现防重复提交切面类
 *
 * @author 勤恳且善良的程序猿
 * @date 2021/7/2
 */
@Aspect
public class RedisSynchronizedAspect {
    @Autowired
    private RedisUtil redisUtil;
    private ExpressionEvaluator<String> evaluator = new ExpressionEvaluator<>();

    /**
     * 方法上有@RedisSynchronized注解均会调用此方法
     *
     * @param point
     * @param redisSynchronized
     * @return
     */
    @Around("@annotation(redisSynchronized)")
    @SneakyThrows
    public Object around(ProceedingJoinPoint point, RedisSynchronized redisSynchronized) {
        // 获取自定义key,作为redis的key
        String key = getKey(point);
        // 获取请求设置多少秒不可重复提交(redis过期时间)
        long second = redisSynchronized.second();
        // 获取异常提示信息
        String msg = redisSynchronized.msg();
        // 获取是否需要在key上拼接用户ID
        boolean splicingUserId = redisSynchronized.splicingUserId();
        if (splicingUserId) {
            // 获取用户ID,根据项目的登录模块,自行定义获取用户的方法
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            Integer userId = getUserId(request.getHeader(HttpHeaders.AUTHORIZATION));
            key += "-" + userId;
        }
        // 根据当前key查询redis是否有值
        Object o = redisUtil.get(key);
        if (o == null) {
            // 没值,继续进入接口,将当前key存入redis,指定过期时间
            redisUtil.set(key, "arbitrarily", second);
            return point.proceed();
        } else {
            // 有值,此处抛出全局异常
            throw new CustomException(msg);
        }
    }

    /**
     * 获取用户ID
     *
     * @param authHeader token
     * @return
     */
    public Integer getUserId(String authHeader) {
        String tokenValue = authHeader.replace("Bearer", "").trim();
        String json = TokenUtils.unToken(tokenValue);
        JSONObject info = JSONObject.parseObject(json);
        Integer userId = info.getInteger("id");
        if (userId == null)
            return null;
        return userId;
    }

    /**
     * SpEL 表达式的解析
     *
     * @param point
     * @return
     */
    public String getKey(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        RedisSynchronized redisSynchronized = method.getAnnotation(RedisSynchronized.class);
        if (point.getArgs() == null) {
            return null;
        }
        EvaluationContext evaluationContext = evaluator.createEvaluationContext(point.getTarget(), point.getTarget().getClass(), ((MethodSignature) point.getSignature()).getMethod(), point.getArgs());
        AnnotatedElementKey methodKey = new AnnotatedElementKey(((MethodSignature) point.getSignature()).getMethod(), point.getTarget().getClass());
        return evaluator.condition(redisSynchronized.key(), methodKey, evaluationContext, String.class);
    }

}


  1. 注入Bean
package com.demo.duplication;

import com.demo.duplication.aspect.RedisSynchronizedAspect;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 注入Bean
 *
 * @author 勤恳且善良的程序猿
 * @date 2021/7/2
 */
@Configuration
@ConditionalOnWebApplication
public class RedisSynchronizedAutoConfiguration {

    @Bean
    public RedisSynchronizedAspect redisSynchronizedAspect() {
        return new RedisSynchronizedAspect();
    }
}

  1. 接口控制器
package com.demo.controller;

import com.demo.duplication.annotation.RedisSynchronized;
import org.springframework.web.bind.annotation.*;

/**
* 测试控制器
*
* @author 勤恳且善良的程序猿
* @date 2021/7/2
*/
@RestController
public class DemoController {

   @GetMapping("/{id}")
   @RedisSynchronized(key = "#id", second = 2L)
   public Object index(@PathVariable("id") Long id) {
       return "index";
   }

   @PostMapping
   @RedisSynchronized(key = "#user.name", msg = "大哥,手速请慢一点哦。。。")
   public Object post(@RequestBody User user) {
       return "post";
   }
}
  1. 运行结果

{
“code”: 500,
“msg”: “请勿重复提交!”,
“data”: null
}

{
“code”: 500,
“msg”: “大哥,手速请慢一点哦。。。”,
“data”: null
}

END!

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值