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

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
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
相关文章
|
15天前
|
XML Java 数据格式
SpringBoot入门(8) - 开发中还有哪些常用注解
SpringBoot入门(8) - 开发中还有哪些常用注解
36 0
|
1月前
|
Java Spring 容器
如何解决spring EL注解@Value获取值为null的问题
本文探讨了在使用Spring框架时,如何避免`@Value(&quot;${xxx.xxx}&quot;)`注解导致值为null的问题。通过具体示例分析了几种常见错误场景,包括类未交给Spring管理、字段被`static`或`final`修饰以及通过`new`而非依赖注入创建对象等,提出了相应的解决方案,并强调了理解框架原理的重要性。
139 4
|
1月前
|
Java Spring
在使用Spring的`@Value`注解注入属性值时,有一些特殊字符需要注意
【10月更文挑战第9天】在使用Spring的`@Value`注解注入属性值时,需注意一些特殊字符的正确处理方法,包括空格、引号、反斜杠、新行、制表符、逗号、大括号、$、百分号及其他特殊字符。通过适当包裹或转义,确保这些字符能被正确解析和注入。
|
22天前
|
XML JSON Java
SpringBoot必须掌握的常用注解!
SpringBoot必须掌握的常用注解!
45 4
SpringBoot必须掌握的常用注解!
|
1月前
|
XML Java 数据格式
Spring从入门到入土(bean的一些子标签及注解的使用)
本文详细介绍了Spring框架中Bean的创建和使用,包括使用XML配置文件中的标签和注解来创建和管理Bean,以及如何通过构造器、Setter方法和属性注入来配置Bean。
74 9
Spring从入门到入土(bean的一些子标签及注解的使用)
|
24天前
|
存储 缓存 Java
Spring缓存注解【@Cacheable、@CachePut、@CacheEvict、@Caching、@CacheConfig】使用及注意事项
Spring缓存注解【@Cacheable、@CachePut、@CacheEvict、@Caching、@CacheConfig】使用及注意事项
79 2
|
24天前
|
JSON Java 数据库
SpringBoot项目使用AOP及自定义注解保存操作日志
SpringBoot项目使用AOP及自定义注解保存操作日志
35 1
|
1月前
|
架构师 Java 开发者
得物面试:Springboot自动装配机制是什么?如何控制一个bean 是否加载,使用什么注解?
在40岁老架构师尼恩的读者交流群中,近期多位读者成功获得了知名互联网企业的面试机会,如得物、阿里、滴滴等。然而,面对“Spring Boot自动装配机制”等核心面试题,部分读者因准备不足而未能顺利通过。为此,尼恩团队将系统化梳理和总结这一主题,帮助大家全面提升技术水平,让面试官“爱到不能自已”。
得物面试:Springboot自动装配机制是什么?如何控制一个bean 是否加载,使用什么注解?
|
19天前
|
存储 安全 Java
springboot当中ConfigurationProperties注解作用跟数据库存入有啥区别
`@ConfigurationProperties`注解和数据库存储配置信息各有优劣,适用于不同的应用场景。`@ConfigurationProperties`提供了类型安全和模块化的配置管理方式,适合静态和简单配置。而数据库存储配置信息提供了动态更新和集中管理的能力,适合需要频繁变化和集中管理的配置需求。在实际项目中,可以根据具体需求选择合适的配置管理方式,或者结合使用这两种方式,实现灵活高效的配置管理。
13 0
|
1月前
|
XML Java 数据库
Spring boot的最全注解
Spring boot的最全注解
下一篇
无影云桌面