自定义spring-boot-starter 实现 幂等注解 防止重复提交

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 自定义spring-boot-starter 实现 幂等注解 防止重复提交
一般遇见这种需求,大体思路思路我想基本是这样的,
1.自定义一个spring-boot-starter
2.启动一个拦截器实现拦截自定义注解
3.根据注解的一些属性进行拼接一个key
4.判断key是否存在
4.1 不存在 存入redis,然后设置一个过期时间(一般过期时间也是注解的一个属性)
4.2 存在则抛出一个重复提交异常 

闲话少说,先来一个使用端代码以及结果


使用方式

1.png

key = "T(cn.goswan.orient.common.security.util.SecurityUtils).getUser().getUsername()+#test.id"

这部分 的key就是拦截器里面用到的判断的key,具体可以根据自己业务用el表达式去定义

我用的是class fullpanth+用户名+业务主键 当作判定key

expireTime = 3

设置为了 3

timeUnit = TimeUnit.SECONDS

设置为了秒,即为3秒后这个key从缓存中消失,使用端一定注意这个时常一定要大于自己的业务处理耗时


好了下面上结果,连续发送两次请求(postman 发送)第一次请求并没有报错


第二次请求抛出如下错误(自定义的错误)

exception.IdempotentException: classUrl public cn.goswan.orient.common.core.util.R com..demo.controller.TestController.save(com.demo.entity.Test) not allow repeat submit 

好了,说了这么多,下面上源码


目录结构

1.png

pom 文件(这里的comm-data实际上内部是对redis 的引用配置可以忽略,大家可以替换成自己的redis 配置即可,如果有不明白的可以看看我之前的文件,redis templete 哨兵配置代码参考一下)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>cn.goswan</groupId>
        <artifactId>orient-common</artifactId>
        <version>3.9.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>basal-common-idempotent</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.goswan</groupId>
            <artifactId>orient-common-data</artifactId>
        </dependency>
    </dependencies>
</project>

Idempotent.java

package com.basal.common.idempotent.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
 * @Author alan.wang
 * 
 * @desc: 定义注解
 */
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    /**
     * 幂等操作的唯一标识,使用spring el表达式 用#来引用方法参数
     * @return Spring-EL expression
     */
    String key() default "";
//    /**
//     * 是否作用域是所有请求(根据请求ip)
//     * 默认:false
//     *  false:只做用在当前请求人(限定同意时间段只对当前访问ip拦截)
//     *  ture:  作用在所有人(同一时间对所有ip进行拦截)
//     *
//     * @return isWorkOnAll
//     **/
//    boolean isWorkOnAll() default false;
    /**
     * 有效期 默认:1 有效期要大于程序执行时间,否则请求还是可能会进来
     * @return expireTime
     */
    int expireTime() default 1;
    /**
     * 时间单位 默认:s
     * @return TimeUnit
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}

IdempotentAspect.java

package com.basal.common.idempotent.aspect;
import cn.goswan.orient.common.data.util.StringUtils;
import com.basal.common.idempotent.annotation.Idempotent;
import com.basal.common.idempotent.exception.IdempotentException;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.Redisson;
import org.redisson.api.RMapCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;
import java.util.Objects;
/**
 * @Author alan.wang
 *
 * @desc:
 * 防止重复提交注解拦截器,具体流程就是拦截带@Idempotent的方法,然后从redis取出key
 * 如果key 已经存在:抛出自定义异常
 * 如果key不存在:则存入
 */
@Aspect
public class IdempotentAspect {
    final SpelExpressionParser PARSER = new SpelExpressionParser();
    final LocalVariableTableParameterNameDiscoverer DISCOVERER = new LocalVariableTableParameterNameDiscoverer();
    private static final String RMAPCACHE_KEY = "idempotent";
    @Autowired
    private Redisson redisson;
    @Pointcut("@annotation(com.basal.common.idempotent.annotation.Idempotent)")
    public void pointCut() {
    }
    @Before("pointCut()")
    public void beforeCut(JoinPoint joinPoint) {
        //获取切面拦截的方法
        Object[] arguments = joinPoint.getArgs();
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        if (!methodSignature.getMethod().isAnnotationPresent(Idempotent.class)) {
            return;
        }
        Method method = ((MethodSignature) signature).getMethod();
        if (method.getDeclaringClass().isInterface()) {
            try {
                method = joinPoint.getTarget().getClass().getDeclaredMethod(joinPoint.getSignature().getName(),
                        method.getParameterTypes());
            } catch (SecurityException | NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
        }
        //获取切面拦截的方法的参数并放入值context中
        StandardEvaluationContext context = new StandardEvaluationContext();
        String[] params = DISCOVERER.getParameterNames(method);
        if (params != null && params.length > 0) {
            for (int len = 0; len < params.length; len++) {
                context.setVariable(params[len], arguments[len]);
            }
        }
        //获取类全路径作为根key
        String classUrl = method.toString();
        Idempotent idempotent = methodSignature.getMethod().getAnnotation(Idempotent.class);
        String idKey = "";
        if (StringUtils.isEmpty(idempotent.key())) {
            idKey = classUrl;
        } else {
            //将annotation中的key 获取到并通过spelExpression 转为具体值
            SpelExpression spelExpression = PARSER.parseRaw(idempotent.key());
            String key = spelExpression.getValue(context, String.class);
            idKey = classUrl + key;
        }
        //判断map 中是否已经存在key
        RMapCache rMapCache = redisson.getMapCache(RMAPCACHE_KEY);
        //存在则抛出重复提交异常
        if (rMapCache.containsKey(idKey)) {
            throw new IdempotentException("classUrl " + classUrl + " not allow repeat submit ");
        } else {
            //不存在则存入cache map,如果存入过程中又有操作以至于存在key,则同样抛出异常
            Object idObj = rMapCache.putIfAbsent(idKey, System.currentTimeMillis(), idempotent.expireTime(), idempotent.timeUnit());
            if (Objects.nonNull(idObj)) {
                throw new IdempotentException("classUrl " + classUrl  + " not allow repeat submit ");
            }
        }
    }
}

IdempotentConfig.java

package com.basal.common.idempotent.config;
import com.basal.common.idempotent.aspect.IdempotentAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * @Author alan.wang
 *
 * @desc: 将IdempotentAspect 拦截器注入到spring 容器中
 */
@Configuration
public class IdempotentConfig {
    @Bean
    public IdempotentAspect IdempotentAspect(){
        IdempotentAspect idempotentAspect = new IdempotentAspect();
        return idempotentAspect;
    }
}

IdempotentException.java

package com.basal.common.idempotent.exception;
/**
 * @Author alan.wang
 * 
 * @desc: Idempotent 重复提交异常
 */
public class IdempotentException extends RuntimeException {
    public IdempotentException() {
        super();
    }
    public IdempotentException(String message) {
        super(message);
    }
    public IdempotentException(String message, Throwable cause) {
        super(message, cause);
    }
    public IdempotentException(Throwable cause) {
        super(cause);
    }
    protected IdempotentException(String message, Throwable cause, boolean enableSuppression,
                                  boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.basal.common.idempotent.config.IdempotentConfig


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
XML Java 数据格式
SpringBoot入门(8) - 开发中还有哪些常用注解
SpringBoot入门(8) - 开发中还有哪些常用注解
56 0
|
8天前
|
Java Spring
【Spring】方法注解@Bean,配置类扫描路径
@Bean方法注解,如何在同一个类下面定义多个Bean对象,配置扫描路径
131 73
|
3天前
|
Java Spring 容器
【SpringFramework】Spring IoC-基于注解的实现
本文主要记录基于Spring注解实现IoC容器和DI相关知识。
34 21
|
9天前
|
XML Java 数据格式
使用idea中的Live Templates自定义自动生成Spring所需的XML配置文件格式
本文介绍了在使用Spring框架时,如何通过创建`applicationContext.xml`配置文件来管理对象。首先,在resources目录下新建XML配置文件,并通过IDEA自动生成部分配置。为完善配置,特别是添加AOP支持,可以通过IDEA的Live Templates功能自定义XML模板。具体步骤包括:连续按两次Shift搜索Live Templates,配置模板内容,输入特定前缀(如spring)并按Tab键即可快速生成完整的Spring配置文件。这样可以大大提高开发效率,减少重复工作。
使用idea中的Live Templates自定义自动生成Spring所需的XML配置文件格式
|
9天前
|
设计模式 XML Java
【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能
本文详细介绍了Spring框架的核心功能,并通过手写自定义Spring框架的方式,深入理解了Spring的IOC(控制反转)和DI(依赖注入)功能,并且学会实际运用设计模式到真实开发中。
【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能
|
8天前
|
存储 Java Spring
【Spring】获取Bean对象需要哪些注解
@Conntroller,@Service,@Repository,@Component,@Configuration,关于Bean对象的五个常用注解
|
8天前
|
Java Spring
【Spring配置】idea编码格式导致注解汉字无法保存
问题一:对于同一个项目,我们在使用idea的过程中,使用汉字注解完后,再打开该项目,汉字变成乱码问题二:本来a项目中,汉字注解调试好了,没有乱码了,但是创建出来的新的项目,写的注解又成乱码了。
|
16天前
|
NoSQL Java Redis
Spring Boot 自动配置机制:从原理到自定义
Spring Boot 的自动配置机制通过 `spring.factories` 文件和 `@EnableAutoConfiguration` 注解,根据类路径中的依赖和条件注解自动配置所需的 Bean,大大简化了开发过程。本文深入探讨了自动配置的原理、条件化配置、自定义自动配置以及实际应用案例,帮助开发者更好地理解和利用这一强大特性。
64 14
|
2月前
|
前端开发 Java Spring
Spring MVC核心:深入理解@RequestMapping注解
在Spring MVC框架中,`@RequestMapping`注解是实现请求映射的核心,它将HTTP请求映射到控制器的处理方法上。本文将深入探讨`@RequestMapping`注解的各个方面,包括其注解的使用方法、如何与Spring MVC的其他组件协同工作,以及在实际开发中的应用案例。
47 4
|
2月前
|
前端开发 Java 开发者
Spring MVC中的请求映射:@RequestMapping注解深度解析
在Spring MVC框架中,`@RequestMapping`注解是实现请求映射的关键,它将HTTP请求映射到相应的处理器方法上。本文将深入探讨`@RequestMapping`注解的工作原理、使用方法以及最佳实践,为开发者提供一份详尽的技术干货。
131 2