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

 更新时间:2024年03月08日 15:04:05   作者:AjaxZhan  
这篇文章主要为大家详细介绍了SpringBoot如何使用AOP实现防重复提交功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

防重幂等的概念

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

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

防重幂等设计思路

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

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

  • 用户从前端发送请求,我们通过切面拦截,拿到请求地址、请求参数和token,生成一个唯一ID。
  • 判断Redis中是否已存在数据以及数据是否有效
  • 如果不存在:正常执行业务。如果存在:抛出异常,提示重复提交。
  • 通过AOP拦截方法执行结果,如果结果正常,就放行;否则就删掉存入Redis的Key,说明本次业务异常,下次提交可以放行。

自定义注解@RepeatSubmit

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

  • interval:间隔时间
  • timeUnit:时间单位,ms
  • message:支持国际化的提示消息
@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)")定义前置通知,切点是加了注解的方法。

  • 从注解拿到间隔时间
  • 从切点拿到请求参数,从ServletRequest拿到请求地址和请求头的用户token。
  • 拼接SubmitKey:对token:请求参数做MD5加密。
  • 拼接CacheKey(存到ThreadLocal里面):将缓存常量SUBMIT_KEY,拼接URL,SubmitKey三者拼接作为Cachekey。(这意味着,如果请求的地址相同、参数相同、token相同,就认为是相同提交。)
  • 判断Redis中是否已存在Key,如果存在,抛出异常。否则将CacheKey设置到Redis。

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

  • 拿到jsonResult,转为R类型
  • 判断code是否为成功,如果是就返回,如果不是就代表业务失败,于是我们就删掉CacheKey,因为这次业务并未处理成功,下一次请求是可以接纳的。
  • 删除ThreadLocal本地变量

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防重复提交内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring Boot 整合 Mybatis Annotation 注解的完整 Web 案例

    Spring Boot 整合 Mybatis Annotation 注解的完整 Web 案例

    这篇文章主要介绍了Spring Boot 整合 Mybatis Annotation 注解的完整 Web 案例,需要的朋友可以参考下
    2017-05-05
  • Java中LocalDate的详细方法举例总结

    Java中LocalDate的详细方法举例总结

    这篇文章主要给大家介绍了关于Java中LocalDate详细方法举例的相关资料,LocalDate主要是用来处理日期的类,文中通过代码示例介绍的非常详细,需要的朋友可以参考下
    2023-09-09
  • 使用Spring Cache和Redis实现查询数据缓存

    使用Spring Cache和Redis实现查询数据缓存

    在现代应用程序中,查询缓存的使用已经变得越来越普遍,它不仅能够显著提高系统的性能,还能提升用户体验,在这篇文章中,我们将探讨缓存的基本概念、重要性以及如何使用Spring Cache和Redis实现查询数据缓存,需要的朋友可以参考下
    2024-07-07
  • Java导入导出csv格式文件完整版详解(附代码)

    Java导入导出csv格式文件完整版详解(附代码)

    在Java中你可以使用不同的库来导出CSV格式的文件,这篇文章主要给大家介绍了关于Java导入导出csv格式文件的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-07-07
  • 在实践中了解Java反射机制应用

    在实践中了解Java反射机制应用

    当程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言。我们认为java并不是动态语言,但是它却有一个非常突出的动态相关机制,俗称:反射。下面我们来简单学习一下吧
    2019-05-05
  • Spring中字段格式化的使用小结

    Spring中字段格式化的使用小结

    Spring提供的一个core.convert包 是一个通用类型转换系统。它提供了统一的 ConversionService  API和强类型的Converter SPI,用于实现从一种类型到另一种类型的转换逻辑,这篇文章主要介绍了Spring中字段格式化的使用详解,需要的朋友可以参考下
    2022-06-06
  • Java如何通过属性名获取Object对象属性值

    Java如何通过属性名获取Object对象属性值

    这篇文章主要介绍了Java如何通过属性名获取Object对象属性值问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-07-07
  • spring mvc中的@PathVariable动态参数详解

    spring mvc中的@PathVariable动态参数详解

    这篇文章主要介绍了spring mvc中的@PathVariable动态参数详解,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • 一篇文章带你了解Java泛型的super和extends

    一篇文章带你了解Java泛型的super和extends

    这篇文章主要介绍了Java泛型extends及super区别实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2021-08-08
  • SpringSecurity+jwt+captcha登录认证授权流程总结

    SpringSecurity+jwt+captcha登录认证授权流程总结

    本文介绍了SpringSecurity、JWT和验证码在Spring Boot 3.2.0中的应用,包括登录认证和授权流程的详细步骤,本文给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧
    2024-11-11

最新评论