• 手摸手系列之 - 什么是接口的幂等性以及 AOP+Redis 基于注解实现接口幂等性校验


    接口的幂等性是指在分布式系统中,一个操作或者请求无论执行多少次,其结果都是相同的。换句话说,即使多次执行同一个操作,它也不会产生副作用,或者不会改变系统的状态。幂等性是设计 RESTful API 时的一个重要原则。

    幂等性通常适用于以下两种情况:

    1. 安全操作: 例如,GET 请求用于获取资源,不论执行多少次,都不会改变资源的状态,因此是幂等的。
    2. 状态改变操作: 例如,PUT 请求用于更新资源,如果资源已经处于请求中描述的状态,再次执行相同的 PUT 请求不会对资源造成进一步的改变,因此也是幂等的。

    幂等性对于确保分布式系统的一致性和可靠性非常重要,特别是在网络请求可能会因为各种原因被重复发送的情况下。例如,如果一个用户提交了一个表单,但由于网络问题,表单被提交了两次,幂等性可以保证系统不会因为重复的提交而产生错误的状态或数据。

    如何实现幂等性?

    前端控制,在前端做拦截,比如按钮点击一次之后就置灰或者隐藏。但是往往前端并不可靠,还是得后端处理才更放心。

    现在我们在后端通过一个注解Idempotent来一步步实现接口的幂等性。

    1. 首先定义一个幂等注解Idempotent,用于标注方法为幂等操作。
    package org.jeecg.common.idempotent.annotation;
    
    import org.jeecg.common.idempotent.keyresolver.IdempotentKeyResolver;
    import org.jeecg.common.idempotent.keyresolver.impl.DefaultIdempotentKeyResolver;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 幂等注解,用于标注方法为幂等操作。
     * 幂等性意味着无论调用多少次,结果都相同,不会产生副作用。
     * 通过此注解,可以实现对重复请求的拦截,提高系统稳定性和效率。
     *
     * @author ZHANGCHAO
     * @version 1.0.0
     * @date 2023/5/15 14:23
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Idempotent {
    
        /**
         * 幂等的超时时间,默认为 1 秒
         * 

    * 注意,如果执行时间超过它,请求还是会进来 */ int timeout() default 1; /** * 时间单位,默认为 SECONDS 秒 */ TimeUnit timeUnit() default TimeUnit.SECONDS; /** * 提示信息,正在执行中的提示 */ String message() default "重复请求,请稍后重试"; /** * 使用的 Key 解析器 * 设置用于生成幂等键的解析器类。 * 幂等键用于唯一标识一个幂等操作,通过解析器可以从方法参数等中提取出此键。 * 默认解析器为DefaultIdempotentKeyResolver,它根据方法参数生成幂等键。 */ Class<? extends IdempotentKeyResolver> keyResolver() default DefaultIdempotentKeyResolver.class; /** * 使用的 Key 参数 * 设置用于生成幂等键的参数名。 * 此参数名应对应方法的一个参数,解析器将根据此参数值生成幂等键。 * 如果不设置,默认解析器将根据所有参数生成幂等键。 * 注意,如果设置了keyResolver为自定义解析器,此参数可能被忽略。 * * @return 用于生成幂等键的参数名。 */ String keyArg() default ""; }

    1. 定义幂等性切面类,用于对标注了{@link Idempotent}注解的方法进行幂等性校验。
    package org.jeecg.common.idempotent.aop;
    
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.jeecg.common.idempotent.CollectionUtils;
    import org.jeecg.common.idempotent.IdempotentRedisDAO;
    import org.jeecg.common.idempotent.annotation.Idempotent;
    import org.jeecg.common.idempotent.keyresolver.IdempotentKeyResolver;
    import org.springframework.util.Assert;
    
    import java.util.List;
    import java.util.Map;
    
    /**
     * 幂等性切面类,用于对标注了{@link Idempotent}注解的方法进行幂等性校验。
     * 通过在方法执行前检查是否已处理过相同的请求,来防止重复操作。
     * @author ZHANGCHAO
     * @version 1.0.0
     * @date 2023/5/15 14:28
     */
    @Slf4j
    @Aspect
    public class IdempotentAspect {
    
        /**
         * IdempotentKeyResolver 集合
         * 幂等键解析器的映射,用于根据注解中指定的类名获取对应的解析器实例。
         */
        private final Map<Class<? extends IdempotentKeyResolver>, IdempotentKeyResolver> keyResolvers;
    
        /**
         * Redis操作DAO,用于在Redis中进行幂等键的设置和查询。
         */
        private final IdempotentRedisDAO idempotentRedisDAO;
    
        /**
         * 构造函数,初始化幂等键解析器映射和Redis DAO。
         *
         * @param keyResolvers 幂等键解析器列表。
         * @param idempotentRedisDAO 幂等性Redis操作DAO。
         */
        public IdempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
            this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass);
            this.idempotentRedisDAO = idempotentRedisDAO;
        }
    
        /**
         * 在方法执行前的切面逻辑,用于实现幂等性校验。
         * 通过注解@annotation(idempotent)来标识需要进行幂等性校验的方法。
         *
         * @param joinPoint 切点,用于获取方法参数和签名等信息。
         * @param idempotent 幂等性注解实例,包含幂等键解析器的类名、锁的超时时间等信息。
         * @throws RuntimeException 如果key已存在,即重复请求,抛出运行时异常。
         */
        @Before("@annotation(idempotent)")
        public void beforePointCut(JoinPoint joinPoint, Idempotent idempotent) {
            // 根据注解中指定的幂等键解析器类名,获取对应的幂等键解析器
            // 获得 IdempotentKeyResolver
            IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver());
            // 确保幂等键解析器不为空,否则抛出异常
            Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver");
            // 使用幂等键解析器解析出请求的幂等键
            // 解析 Key
            String key = keyResolver.resolver(joinPoint, idempotent);
            // 日志记录解析出的幂等键
            log.info("key: {}", key);
            // 尝试在Redis中设置幂等键,如果不存在则设置成功,表示该请求是第一次到来
            // 锁定 Key。
            boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit());
            // 如果设置失败,表示幂等键已存在,即该请求是重复的,抛出运行时异常
            // 锁定失败,抛出异常
            if (!success) {
                log.info("[beforePointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs());
                throw new RuntimeException(idempotent.message());
            }
        }
    
    }
    
    1. Key 解析器接口
    package org.jeecg.common.idempotent.keyresolver;
    
    import org.aspectj.lang.JoinPoint;
    import org.jeecg.common.idempotent.annotation.Idempotent;
    
    /**
     * 幂等性键解析器接口。
     * 该接口用于解析方法调用的幂等性键,以确保重复调用的处理符合幂等性原则。
     * @author ZHANGCHAO
     * @version 1.0.0
     * @date 2023/5/15 14:21
     */
    public interface IdempotentKeyResolver {
    
        /**
         * 解析幂等性键 key
         *
         * @param joinPoint 切点,包含方法调用的相关信息。
         * @param idempotent 幂等性注解,用于配置幂等性处理的相关属性。
         * @return 解析得到的幂等性键。
         * @description 该方法通过分析方法参数和注解属性,生成一个唯一的幂等性键,用于标识一个幂等操作。
         */
        String resolver(JoinPoint joinPoint, Idempotent idempotent);
    }
    
    1. 定义两个 Key 解析器接口的实现类,一个默认的根据方法名和参数生成幂等性 key,一个基于 Spring Expression Language (SpEL)
      首先是 DefaultIdempotentKeyResolver
    /**
     * 默认的幂等性关键字解析器,实现了IdempotentKeyResolver接口。
     * 该解析器用于根据方法名和参数生成幂等性关键字。
     */
    package org.jeecg.common.idempotent.keyresolver.impl;
    
    import cn.hutool.core.util.StrUtil;
    import cn.hutool.crypto.SecureUtil;
    import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
    import org.aspectj.lang.JoinPoint;
    import org.jeecg.common.idempotent.annotation.Idempotent;
    import org.jeecg.common.idempotent.keyresolver.IdempotentKeyResolver;
    
    /**
     * 默认幂等性关键字解析器类。
     */
    public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver {
    
        /**
         * 根据切面连接点和幂等注解,解析并返回幂等性关键字。
         *
         * @param joinPoint 切面连接点,包含目标方法和其参数信息。
         * @param idempotent 幂等注解,用于配置幂等性相关属性。
         * @return 生成的幂等性关键字。
         */
        /**
         * 解析一个 Key
         *
         * @param joinPoint  AOP 切面
         * @param idempotent 幂等注解
         * @return Key
         */
        @Override
        public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
            // 获取目标方法名
            String methodName = joinPoint.getSignature().toString();
    
            // 创建一个数组,用于存储除ShiroHttpServletRequest外的所有参数
            Object[] objects = new Object[joinPoint.getArgs().length];
            for (int i = 0; i < joinPoint.getArgs().length; i++) {
                // 排除ShiroHttpServletRequest类型的参数,因为它们不参与幂等性关键字的生成
                if (!(joinPoint.getArgs()[i] instanceof ShiroHttpServletRequest)) {
                    objects[i] = joinPoint.getArgs()[i];
                }
            }
    
            // 将参数数组转换为字符串,使用逗号分隔
            String argsStr = StrUtil.join(",", objects);
    
            // 使用methodName和argsStr拼接后的字符串进行MD5加密,生成幂等性关键字
            return SecureUtil.md5(methodName + argsStr);
        }
    }
    

    ExpressionIdempotentKeyResolver:

    package org.jeecg.common.idempotent.keyresolver.impl;
    
    import cn.hutool.core.util.ArrayUtil;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.jeecg.common.idempotent.annotation.Idempotent;
    import org.jeecg.common.idempotent.keyresolver.IdempotentKeyResolver;
    import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
    import org.springframework.core.ParameterNameDiscoverer;
    import org.springframework.expression.Expression;
    import org.springframework.expression.ExpressionParser;
    import org.springframework.expression.spel.standard.SpelExpressionParser;
    import org.springframework.expression.spel.support.StandardEvaluationContext;
    
    import java.lang.reflect.Method;
    
    /**
     * 基于Spring EL表达式
     *
     * 实现幂等性Key的解析,基于Spring Expression Language (SpEL)。
     * 该解析器通过评估给定的SpEL表达式来生成幂等性Key。
     *
     * @author ZHANGCHAO
     * @version 1.0.0
     * @date 2023/5/15 14:26
     */
    public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver {
    
        /**
         * 用于发现方法参数名称的工具。
         */
        private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
    
        /**
         * SpEL表达式解析器。
         */
        private final ExpressionParser expressionParser = new SpelExpressionParser();
    
        /**
         * 获取实际的方法对象,处理接口和实现类之间的映射。
         *
         * @param point 切点,包含方法调用的信息。
         * @return 方法对象。
         */
        private static Method getMethod(JoinPoint point) {
            // 处理,声明在类上的情况
            MethodSignature signature = (MethodSignature) point.getSignature();
            Method method = signature.getMethod();
            if (!method.getDeclaringClass().isInterface()) {
                return method;
            }
    
            // 处理,声明在接口上的情况
            try {
                return point.getTarget().getClass().getDeclaredMethod(
                        point.getSignature().getName(), method.getParameterTypes());
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
        }
    
        /**
         * 解析一个 Key
         * 根据SpEL表达式解析出幂等性Key。
         *
         * @param joinPoint 切面连接点,包含当前的Method调用信息。
         * @param idempotent 幂等注解实例,包含SpEL表达式。
         * @return 解析出的幂等性Key。
         */
        @Override
        public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
            // 获取实际调用的方法
            Method method = getMethod(joinPoint);
            // 获取方法参数
            Object[] args = joinPoint.getArgs();
            // 获取方法参数名称
            String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method);
            // 创建SpEL表达式的评估上下文
            // 准备 Spring EL 表达式解析的上下文
            StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
            // 设置参数名称和值到评估上下文中
            if (ArrayUtil.isNotEmpty(parameterNames)) {
                for (int i = 0; i < parameterNames.length; i++) {
                    evaluationContext.setVariable(parameterNames[i], args[i]);
                }
            }
    
            // 解析注解中定义的SpEL表达式,获取幂等性Key
            // 解析参数
            Expression expression = expressionParser.parseExpression(idempotent.keyArg());
            return expression.getValue(evaluationContext, String.class);
        }
    }
    
    1. 幂等性配置类,用于初始化幂等性相关的Bean
    package org.jeecg.common.idempotent;
    
    import org.jeecg.common.idempotent.aop.IdempotentAspect;
    import org.jeecg.common.idempotent.keyresolver.IdempotentKeyResolver;
    import org.jeecg.common.idempotent.keyresolver.impl.DefaultIdempotentKeyResolver;
    import org.jeecg.common.idempotent.keyresolver.impl.ExpressionIdempotentKeyResolver;
    import org.jeecg.common.modules.redis.config.RedisConfig;
    import org.springframework.boot.autoconfigure.AutoConfigureAfter;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.core.StringRedisTemplate;
    
    import java.util.List;
    
    /**
     * 幂等性配置类,用于初始化幂等性相关的Bean。
     *
     * @author ZHANGCHAO
     * @version 1.0.0
     * @date 2023/5/15 14:40
     */
    @Configuration
    @AutoConfigureAfter(RedisConfig.class) // 依赖Redis配置,确保在Redis配置之后初始化
    public class IdempotentConfiguration {
    
        /**
         * 初始化幂等性切面。
         *
         * @param keyResolvers 幂等性键解析器列表,用于生成唯一的幂等性键。
         * @param idempotentRedisDAO 幂等性Redis操作DAO,用于存储和查询幂等性键。
         * @return 初始化后的幂等性切面实例。
         */
        @Bean
        public IdempotentAspect idempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
            return new IdempotentAspect(keyResolvers, idempotentRedisDAO);
        }
    
        /**
         * 初始化幂等性Redis DAO。
         *
         * @param stringRedisTemplate 字符串Redis模板,用于操作Redis。
         * @return 初始化后的幂等性Redis DAO实例。
         */
        @Bean
        public IdempotentRedisDAO idempotentRedisDAO(StringRedisTemplate stringRedisTemplate) {
            return new IdempotentRedisDAO(stringRedisTemplate);
        }
    
        // ========== 各种 IdempotentKeyResolver Bean ==========
        /**
         * 初始化默认幂等性键解析器。
         *
         * @return 默认幂等性键解析器实例。
         */
        @Bean
        public DefaultIdempotentKeyResolver defaultIdempotentKeyResolver() {
            return new DefaultIdempotentKeyResolver();
        }
    
        /**
         * 初始化基于表达式幂等性键解析器。
         *
         * @return 基于表达式幂等性键解析器实例。
         */
        @Bean
        public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() {
            return new ExpressionIdempotentKeyResolver();
        }
    }
    
    1. 幂等性 Redis 数据访问对象
    package org.jeecg.common.idempotent;
    
    import lombok.AllArgsConstructor;
    import org.springframework.data.redis.core.StringRedisTemplate;
    
    import java.util.concurrent.TimeUnit;
    
    import static org.jeecg.common.idempotent.RedisKeyDefine.KeyTypeEnum.STRING;
    
    /**
     * 幂等性Redis数据访问对象,用于实现操作的幂等性。
     * 通过在Redis中设置和检查键值对,确保相同操作在重复请求时不会被多次执行。
     *
     * @author ZHANGCHAO
     * @version 1.0.0
     * @date 2023/5/15 14:36
     */
    @AllArgsConstructor
    public class IdempotentRedisDAO {
    
        /**
         * RedisKeyDefine对象,预定义了幂等键的模板和类型。
         */
        private static final RedisKeyDefine IDEMPOTENT = new RedisKeyDefine("幂等操作",
                "idempotent:%s", // 参数为 uuid
                STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
    
        /**
         * Redis模板,用于操作Redis数据库。
         */
        private final StringRedisTemplate redisTemplate;
    
        /**
         * 格式化Redis键。
         *
         * @param key 原始键。
         * @return 格式化后的Redis键。
         */
        private static String formatKey(String key) {
            return String.format(IDEMPOTENT.getKeyTemplate(), key);
        }
    
        /**
         * 如果键不存在,则设置键的值并返回true;如果键已存在,则返回false。
         *
         * @param key 键的标识。
         * @param timeout 键的过期时间。
         * @param timeUnit 时间单位。
         * @return 如果键被设置,则返回true;否则返回false。
         */
        public Boolean setIfAbsent(String key, long timeout, TimeUnit timeUnit) {
            String redisKey = formatKey(key);
            return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit);
        }
    }
    
    1. Redis Key 定义类,用于定义和管理 Redis 键的相关属性
    package org.jeecg.common.idempotent;
    
    import com.fasterxml.jackson.annotation.JsonValue;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.Getter;
    
    import java.time.Duration;
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * Redis Key定义类
     *
     * 用于定义和管理Redis键的相关属性,如键模板、键类型、值类型、超时类型和超时时间等。
     *
     * @author ZHANGCHAO
     * @version 1.0.0
     * @date 2023/5/15 14:30
     */
    @Data
    public class RedisKeyDefine {
    
        /**
         * Redis RedisKeyDefine 数组
         */
        private static final List<RedisKeyDefine> DEFINES = new ArrayList<>();
        /**
         * Key 模板
         */
        private final String keyTemplate;
        /**
         * Key 类型的枚举
         */
        private final KeyTypeEnum keyType;
        /**
         * Value 类型
         * 

    * 如果是使用分布式锁,设置为 {@link java.util.concurrent.locks.Lock} 类型 */ private final Class<?> valueType; /** * 超时类型 */ private final TimeoutTypeEnum timeoutType; /** * 过期时间 */ private final Duration timeout; /** * 备注 */ private final String memo; private RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, TimeoutTypeEnum timeoutType, Duration timeout) { this.memo = memo; this.keyTemplate = keyTemplate; this.keyType = keyType; this.valueType = valueType; this.timeout = timeout; this.timeoutType = timeoutType; // 添加注册表 add(this); } public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, Duration timeout) { this(memo, keyTemplate, keyType, valueType, TimeoutTypeEnum.FIXED, timeout); } public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, TimeoutTypeEnum timeoutType) { this(memo, keyTemplate, keyType, valueType, timeoutType, Duration.ZERO); } public static void add(RedisKeyDefine define) { DEFINES.add(define); } /** * 格式化 Key *

    * 注意,内部采用 {@link String#format(String, Object...)} 实现 * * @param args 格式化的参数 * @return Key */ public String formatKey(Object... args) { return String.format(keyTemplate, args); } @Getter @AllArgsConstructor public enum KeyTypeEnum { STRING("String"), LIST("List"), HASH("Hash"), SET("Set"), ZSET("Sorted Set"), STREAM("Stream"), PUBSUB("Pub/Sub"); /** * 类型 */ @JsonValue private final String type; } @Getter @AllArgsConstructor public enum TimeoutTypeEnum { FOREVER(1), // 永不超时 DYNAMIC(2), // 动态超时 FIXED(3); // 固定超时 /** * 类型 */ @JsonValue private final Integer type; } }

    1. 依赖的其他的一些工具类

    CollectionUtils:

    package org.jeecg.common.idempotent;
    
    import cn.hutool.core.collection.CollUtil;
    import cn.hutool.core.collection.CollectionUtil;
    import com.google.common.collect.ImmutableMap;
    
    import java.util.*;
    import java.util.function.BinaryOperator;
    import java.util.function.Function;
    import java.util.function.Predicate;
    import java.util.function.Supplier;
    import java.util.stream.Collectors;
    
    /**
     * Collection 工具类
     */
    public class CollectionUtils {
    
        public static boolean containsAny(Object source, Object... targets) {
            return Arrays.asList(targets).contains(source);
        }
    
        public static boolean isAnyEmpty(Collection<?>... collections) {
            return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty);
        }
    
        public static <T> List<T> filterList(Collection<T> from, Predicate<T> predicate) {
            if (CollUtil.isEmpty(from)) {
                return new ArrayList<>();
            }
            return from.stream().filter(predicate).collect(Collectors.toList());
        }
    
        public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper) {
            if (CollUtil.isEmpty(from)) {
                return new ArrayList<>();
            }
            return distinct(from, keyMapper, (t1, t2) -> t1);
        }
    
        public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper, BinaryOperator<T> cover) {
            if (CollUtil.isEmpty(from)) {
                return new ArrayList<>();
            }
            return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values());
        }
    
        public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func) {
            if (CollUtil.isEmpty(from)) {
                return new ArrayList<>();
            }
            return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList());
        }
    
        public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
            if (CollUtil.isEmpty(from)) {
                return new ArrayList<>();
            }
            return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList());
        }
    
        public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) {
            if (CollUtil.isEmpty(from)) {
                return new HashSet<>();
            }
            return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet());
        }
    
        public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
            if (CollUtil.isEmpty(from)) {
                return new HashSet<>();
            }
            return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet());
        }
    
        public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc) {
            if (CollUtil.isEmpty(from)) {
                return new HashMap<>();
            }
            return convertMap(from, keyFunc, Function.identity());
        }
    
        public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc, Supplier<? extends Map<K, T>> supplier) {
            if (CollUtil.isEmpty(from)) {
                return supplier.get();
            }
            return convertMap(from, keyFunc, Function.identity(), supplier);
        }
    
        public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
            if (CollUtil.isEmpty(from)) {
                return new HashMap<>();
            }
            return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1);
        }
    
        public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction) {
            if (CollUtil.isEmpty(from)) {
                return new HashMap<>();
            }
            return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new);
        }
    
        public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, Supplier<? extends Map<K, V>> supplier) {
            if (CollUtil.isEmpty(from)) {
                return supplier.get();
            }
            return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier);
        }
    
        public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction, Supplier<? extends Map<K, V>> supplier) {
            if (CollUtil.isEmpty(from)) {
                return new HashMap<>();
            }
            return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier));
        }
    
        public static <T, K> Map<K, List<T>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc) {
            if (CollUtil.isEmpty(from)) {
                return new HashMap<>();
            }
            return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList())));
        }
    
        public static <T, K, V> Map<K, List<V>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
            if (CollUtil.isEmpty(from)) {
                return new HashMap<>();
            }
            return from.stream()
                    .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList())));
        }
    
        // 暂时没想好名字,先以 2 结尾噶
        public static <T, K, V> Map<K, Set<V>> convertMultiMap2(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) {
            if (CollUtil.isEmpty(from)) {
                return new HashMap<>();
            }
            return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet())));
        }
    
        public static <T, K> Map<K, T> convertImmutableMap(Collection<T> from, Function<T, K> keyFunc) {
            if (CollUtil.isEmpty(from)) {
                return Collections.emptyMap();
            }
            ImmutableMap.Builder<K, T> builder = ImmutableMap.builder();
            from.forEach(item -> builder.put(keyFunc.apply(item), item));
            return builder.build();
        }
    
        public static boolean containsAny(Collection<?> source, Collection<?> candidates) {
            return org.springframework.util.CollectionUtils.containsAny(source, candidates);
        }
    
        public static <T> T getFirst(List<T> from) {
            return !CollectionUtil.isEmpty(from) ? from.get(0) : null;
        }
    
        public static <T> T findFirst(List<T> from, Predicate<T> predicate) {
            if (CollUtil.isEmpty(from)) {
                return null;
            }
            return from.stream().filter(predicate).findFirst().orElse(null);
        }
    
        public static <T, V extends Comparable<? super V>> V getMaxValue(List<T> from, Function<T, V> valueFunc) {
            if (CollUtil.isEmpty(from)) {
                return null;
            }
            assert from.size() > 0; // 断言,避免告警
            T t = from.stream().max(Comparator.comparing(valueFunc)).get();
            return valueFunc.apply(t);
        }
    
        public static <T, V extends Comparable<? super V>> V getMinValue(List<T> from, Function<T, V> valueFunc) {
            if (CollUtil.isEmpty(from)) {
                return null;
            }
            assert from.size() > 0; // 断言,避免告警
            T t = from.stream().min(Comparator.comparing(valueFunc)).get();
            return valueFunc.apply(t);
        }
    
        public static <T, V extends Comparable<? super V>> V getSumValue(List<T> from, Function<T, V> valueFunc, BinaryOperator<V> accumulator) {
            if (CollUtil.isEmpty(from)) {
                return null;
            }
            assert from.size() > 0; // 断言,避免告警
            return from.stream().map(valueFunc).reduce(accumulator).get();
        }
    
        public static <T> void addIfNotNull(Collection<T> coll, T item) {
            if (item == null) {
                return;
            }
            coll.add(item);
        }
    
        public static <T> Collection<T> singleton(T deptId) {
            return deptId == null ? Collections.emptyList() : Collections.singleton(deptId);
        }
    
    }
    

    你自己项目中的RedisConfig

    package org.jeecg.common.modules.redis.config;
    
    import com.fasterxml.jackson.annotation.JsonAutoDetect;
    import com.fasterxml.jackson.annotation.PropertyAccessor;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.extern.slf4j.Slf4j;
    import org.jeecg.common.constant.CacheConstant;
    import org.jeecg.common.constant.GlobalConstants;
    import org.jeecg.common.modules.redis.receiver.RedisReceiver;
    import org.jeecg.common.modules.redis.writer.JeecgRedisCacheWriter;
    import org.springframework.cache.CacheManager;
    import org.springframework.cache.annotation.CachingConfigurerSupport;
    import org.springframework.cache.annotation.EnableCaching;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.cache.RedisCacheConfiguration;
    import org.springframework.data.redis.cache.RedisCacheManager;
    import org.springframework.data.redis.cache.RedisCacheWriter;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.listener.ChannelTopic;
    import org.springframework.data.redis.listener.RedisMessageListenerContainer;
    import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.RedisSerializationContext;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    import javax.annotation.Resource;
    import java.time.Duration;
    
    import static java.util.Collections.singletonMap;
    
    /**
     * 开启缓存支持
     *
     * @author zyf
     * @Return:
     */
    @Slf4j
    @EnableCaching
    @Configuration
    public class RedisConfig extends CachingConfigurerSupport {
    
        //不同的频道名
        //业务消息
        private static final String channel = "BusinessNews";
    
        @Resource
        private LettuceConnectionFactory lettuceConnectionFactory;
    
        /**
         * RedisTemplate配置
         *
         * @param lettuceConnectionFactory
         * @return
         */
        @Bean
        public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
            log.info(" --- redis config init --- ");
            Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = jacksonSerializer();
            RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
            redisTemplate.setConnectionFactory(lettuceConnectionFactory);
            RedisSerializer<String> stringSerializer = new StringRedisSerializer();
    
            // key序列化
            redisTemplate.setKeySerializer(stringSerializer);
            // value序列化
            redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
            // Hash key序列化
            redisTemplate.setHashKeySerializer(stringSerializer);
            // Hash value序列化
            redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
            redisTemplate.afterPropertiesSet();
            return redisTemplate;
        }
    
        /**
         * 缓存配置管理器
         *
         * @param factory
         * @return
         */
        @Bean
        public CacheManager cacheManager(LettuceConnectionFactory factory) {
            Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = jacksonSerializer();
            // 配置序列化(解决乱码的问题),并且配置缓存默认有效期 6小时
            RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(6));
            RedisCacheConfiguration redisCacheConfiguration = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
            //.disableCachingNullValues();
    
            // 以锁写入的方式创建RedisCacheWriter对象
            //update-begin-author:taoyan date:20210316 for:注解CacheEvict根据key删除redis支持通配符*
            RedisCacheWriter writer = new JeecgRedisCacheWriter(factory, Duration.ofMillis(50L));
            //RedisCacheWriter.lockingRedisCacheWriter(factory);
            // 创建默认缓存配置对象
            /* 默认配置,设置缓存有效期 1小时*/
            //RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(1));
            // 自定义配置test:demo 的超时时间为 5分钟
            RedisCacheManager cacheManager = RedisCacheManager.builder(writer).cacheDefaults(redisCacheConfiguration)
                    .withInitialCacheConfigurations(singletonMap(CacheConstant.SYS_DICT_TABLE_CACHE,
                            RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(10)).disableCachingNullValues()
                                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))))
                    .withInitialCacheConfigurations(singletonMap(CacheConstant.TEST_DEMO_CACHE, RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(5)).disableCachingNullValues()))
                    .withInitialCacheConfigurations(singletonMap(CacheConstant.PLUGIN_MALL_RANKING, RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(24)).disableCachingNullValues()))
                    .withInitialCacheConfigurations(singletonMap(CacheConstant.PLUGIN_MALL_PAGE_LIST, RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(24)).disableCachingNullValues()))
                    .transactionAware().build();
            //update-end-author:taoyan date:20210316 for:注解CacheEvict根据key删除redis支持通配符*
            return cacheManager;
        }
    
        /**
         * redis 监听配置
         *
         * @param redisConnectionFactory redis 配置
         * @return
         */
        @Bean
        public RedisMessageListenerContainer redisContainer(RedisConnectionFactory redisConnectionFactory,
                                                            MessageListenerAdapter commonListenerAdapter) {
            RedisMessageListenerContainer container = new RedisMessageListenerContainer();
            container.setConnectionFactory(redisConnectionFactory);
            container.addMessageListener(commonListenerAdapter, new ChannelTopic(GlobalConstants.REDIS_TOPIC_NAME));
            //listenerAdapter的通道
    //     container.addMessageListener(businessListenerAdapter, new PatternTopic(RedisConfig.channel));
            return container;
        }
    
    
        @Bean
        MessageListenerAdapter commonListenerAdapter(RedisReceiver redisReceiver) {
            MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(redisReceiver, "onMessage");
            messageListenerAdapter.setSerializer(jacksonSerializer());
            return messageListenerAdapter;
        }
    
    //  @Bean
    //  MessageListenerAdapter businessListenerAdapter(RedisReceiver redisReceiver) {
    //     MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(redisReceiver, "receiveMessage");
    //     messageListenerAdapter.setSerializer(jacksonSerializer());
    //     return messageListenerAdapter;
    //  }
    
        private Jackson2JsonRedisSerializer jacksonSerializer() {
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
            jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
            return jackson2JsonRedisSerializer;
        }
    }
    

    最终的项目结构:

    在这里插入图片描述

    测试接口幂等性

    在相应的需要幂等性的接口上加Idempotent注解,如:
    在这里插入图片描述

    这里设置的默认超时时间是 5 秒,即 5 秒内只允许相同参数的请求进来一次,前端重复点击审核按钮测试:

    在这里插入图片描述

    可以看到,请求已被拦截:

    在这里插入图片描述

    总结

    我们定义了Idempotent注解,它允许我们标记方法为幂等操作,并提供了超时时间、提示信息和Key解析器等配置。然后,通过创建幂等性切面类IdempotentAspect,利用AOP在方法执行前进行幂等性校验。在实际测试中,通过在需要幂等性的接口上添加Idempotent注解,并设置适当的超时时间,可以观察到重复请求被成功拦截,证明了实现的有效性。
    通过在项目中应用这些类和注解,可以有效地防止因重复请求导致的系统状态错误或数据不一致问题,从而提高系统的稳定性和可靠性。

  • 相关阅读:
    电脑出现错误代码0x80004005的解决方法
    基于FPGA的图像指数对比度增强算法实现,包括tb测试文件和MATLAB辅助验证
    TCP网络编程心得体会浅谈
    Spring Boot集成Hibernate Envers自定义修订实体使用多个数据源
    【算法】莫队
    内存模型以及如何判定对象已死问题
    协程gevent模块的使用
    第三章 Linux多线程开发 线程取消 属性 同步 互斥锁 死锁 读写锁 生产者消费者 信号量
    JS深入学习笔记 - 第一章.构造函数原型与原型链
    Java学习路线图
  • 原文地址:https://blog.csdn.net/qq_26030541/article/details/139622681