java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot AOP防重复提交

SpringBoot使用AOP实现防重复提交功能

作者:AjaxZhan

这篇文章主要为大家详细介绍了SpringBoot如何使用AOP实现防重复提交功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

防重幂等的概念

防重幂等指的是我们的业务需要防止两条相同的数据重复提交导致脏数据或业务错乱。需要注意的是,重复提交属于小概率事件,这和并发压测不是同一个概念。

我们的目标是通过防重幂等的设计,让系统支持业务失败或异常快速释放限制。业务处理成功后,会在指定时间限定内限制同一条数据的提交。本文将介绍如何在SpringBoot开发中,使用AOP+Redis实现一个防重幂等功能。

防重幂等设计思路

目标:防止同一个用户在同一个业务下提交同一个数据。

策略:将用户路径+请求参数+Token生成唯一ID,存入Redis。具体流程如下:

自定义注解@RepeatSubmit

首先我们定义一个注解@RepeatSubmit,作用于方法上,设置如下参数,用于设置AOP切点。

@Inherited  
@Target(ElementType.METHOD)  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
public @interface RepeatSubmit {  
  
    /**  
     * 间隔时间(ms),小于此时间视为重复提交  
     */  
    int interval() default 5000;  
  
    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;  
  
    /**  
     * 提示消息 支持国际化 格式为 [code]  
     */  
     String message() default "{repeat.submit.message}";  
  
}

自定义切面@RepeatSubmitAspect

定义一个切面@RepeatSubmitAspect,作为防重幂等的模块化,用于横切标记上@RepeatSubmit注解的方法。

我们需要定义三个通知:前置通知、后置通知、抛出异常时的通知,他们执行的业务如下,基本上是按照上述防重幂等设计的策略来写的。

doBefore:使用@Before("@annotation(repeatSubmit)")定义前置通知,切点是加了注解的方法。

doAfterReturning:使用@AfterReturning(pointcut = "@annotation(repeatSubmit)", returning = "jsonResult")定义后置通知并拿到jsonResult。

doAfterThrowing

删除key,移除ThreadLocal本地变量

PS:这里用到了ThreadLocal,ThreadLocal是一个 Java 类,可以用来定义只由创建它们的线程访问的变量,常用于我们需要存储不在线程之间共享的数据。

@Aspect  
@Component  
public class RepeatSubmitAspect {  
  
    private static final ThreadLocal<String> KEY_CACHE = new ThreadLocal<>();  
  
    @Before("@annotation(repeatSubmit)")  
    public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {  
        // 如果注解不为0 则使用注解数值  
        long interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());  
  
        if (interval < 1000) {  
            throw new ServiceException("重复提交间隔时间不能小于'1'秒");  
        }  
        HttpServletRequest request = ServletUtils.getRequest();  
        String nowParams = argsArrayToString(point.getArgs());  
  
        // 请求地址(作为存放cache的key值)  
        String url = request.getRequestURI();  
  
        // 唯一值(没有消息头则使用请求地址)  
        String submitKey = StringUtils.trimToEmpty(request.getHeader(SaManager.getConfig().getTokenName()));  
  
        submitKey = SecureUtil.md5(submitKey + ":" + nowParams);  
        // 唯一标识(指定key + url + 消息头)  
        String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey;  
        if (RedisUtils.setObjectIfAbsent(cacheRepeatKey, "", Duration.ofMillis(interval))) {  
            KEY_CACHE.set(cacheRepeatKey);  
        } else {  
            String message = repeatSubmit.message();  
            if (StringUtils.startsWith(message, "{") && StringUtils.endsWith(message, "}")) {  
                message = MessageUtils.message(StringUtils.substring(message, 1, message.length() - 1));  
            }  
            throw new ServiceException(message);  
        }  
    }  
  
    /**  
     * 处理完请求后执行  
     *  
     * @param joinPoint 切点  
     */  
    @AfterReturning(pointcut = "@annotation(repeatSubmit)", returning = "jsonResult")  
    public void doAfterReturning(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Object jsonResult) {  
        if (jsonResult instanceof R) {  
            try {  
                R<?> r = (R<?>) jsonResult;  
                // 成功则不删除redis数据 保证在有效时间内无法重复提交  
                if (r.getCode() == R.SUCCESS) {  
                    return;  
                }  
                RedisUtils.deleteObject(KEY_CACHE.get());  
            } finally {  
                KEY_CACHE.remove();  
            }  
        }  
    }  
  
    /**  
     * 拦截异常操作  
     *  
     * @param joinPoint 切点  
     * @param e         异常  
     */  
    @AfterThrowing(value = "@annotation(repeatSubmit)", throwing = "e")  
    public void doAfterThrowing(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Exception e) {  
        RedisUtils.deleteObject(KEY_CACHE.get());  
        KEY_CACHE.remove();  
    }  
  
    /**  
     * 参数拼装  
     */  
    private String argsArrayToString(Object[] paramsArray) {  
        StringJoiner params = new StringJoiner(" ");  
        if (ArrayUtil.isEmpty(paramsArray)) {  
            return params.toString();  
        }  
        for (Object o : paramsArray) {  
            if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {  
                params.add(JsonUtils.toJsonString(o));  
            }  
        }  
        return params.toString();  
    }  
  
    /**  
     * 判断是否需要过滤的对象。  
     *  
     * @param o 对象信息。  
     * @return 如果是需要过滤的对象,则返回true;否则返回false。  
     */  
    @SuppressWarnings("rawtypes")  
    public boolean isFilterObject(final Object o) {  
        Class<?> clazz = o.getClass();  
        if (clazz.isArray()) {  
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);  
        } else if (Collection.class.isAssignableFrom(clazz)) {  
            Collection collection = (Collection) o;  
            for (Object value : collection) {  
                return value instanceof MultipartFile;  
            }  
        } else if (Map.class.isAssignableFrom(clazz)) {  
            Map map = (Map) o;  
            for (Object value : map.values()) {  
                return value instanceof MultipartFile;  
            }  
        }  
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse  
            || o instanceof BindingResult;  
    }  
  
}

简单测试

我们创建一个接口用于测试防重幂等,我的系统中使用Sa-Token权限框架,为了方便,我们通过@SaIngore放行接口。

/**  
 * @author AjaxZhan  
 */@RestController  
@RequestMapping("/repeat")  
@Slf4j  
@SaIgnore  
public class RepeatController {  
  
    @PostMapping  
    @RepeatSubmit(interval = 2000)  
    public R<Void> repeat1(String info){  
        log.info("请求成功,信息" + info);  
        return R.ok("请求成功");  
    }  
}

使用Apifox测试结果如下:

当我们在2s内连续提交就会触发异常:

至此,我们就成功地使用AOP+Redis的方式设计了一个防重幂等功能。

到此这篇关于SpringBoot使用AOP实现防重复提交功能的文章就介绍到这了,更多相关SpringBoot AOP防重复提交内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

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