基于Redis的Spring Boot 幂等性插件模块封装

幂等性注解定义

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * @author jeckxu
 */
@Inherited
@Target(ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Idempotent {

   /**
    * 幂等操作的唯一标识,使用spring el表达式 用#来引用方法参数
    * @return Spring-EL expression
    */
   String key() default "";

   /**
    * 有效期 默认:1 有效期要大于程序执行时间,否则请求还是可能会进来
    * @return expireTime
    */
   int expireTime() default 1;

   /**
    * 时间单位 默认:s
    * @return TimeUnit
    */
   TimeUnit timeUnit() default TimeUnit.SECONDS;

   /**
    * 提示信息,可自定义
    * @return String
    */
   String info() default "ROOT.IDEMPOTENT.DO_NOT_REPEAT_THE_OPERATION";

   /**
    * 是否在业务完成后删除key true:删除 false:不删除
    * @return boolean
    */
   boolean delKey() default true;

}

幂等性配置类

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @author Jeckxu
 */
@ConfigurationProperties(MagicalIdempotentProperties.PREFIX)
public class MagicalIdempotentProperties {
    public static final String PREFIX = "magical.idempotent";
    /**
     * 是否开启:默认为:false,便于生成配置提示。
     */
    private Boolean enabled = Boolean.FALSE;
    /**
     * 单机配置:redis 服务地址
     */
    private String address = "redis://127.0.0.1:6379";
    /**
     * 密码配置
     */
    private String password;
    /**
     * db
     */
    private Integer database = 0;
    /**
     * 连接池大小
     */
    private Integer poolSize = 20;
    /**
     * 最小空闲连接数
     */
    private Integer idleSize = 5;
    /**
     * 连接空闲超时,单位:毫秒
     */
    private Integer idleTimeout = 60000;
    /**
     * 连接超时,单位:毫秒
     */
    private Integer connectionTimeout = 3000;
    /**
     * 命令等待超时,单位:毫秒
     */
    private Integer timeout = 10000;
    /**
     * 集群模式,单机:single,主从:master,哨兵模式:sentinel,集群模式:cluster
     */
    private Mode mode = Mode.single;
    /**
     * 主从模式,主地址
     */
    private String masterAddress;
    /**
     * 主从模式,从地址
     */
    private String[] slaveAddress;
    /**
     * 哨兵模式:主名称
     */
    private String masterName;
    /**
     * 哨兵模式地址
     */
    private String[] sentinelAddress;
    /**
     * 集群模式节点地址
     */
    private String[] nodeAddress;

    public enum Mode {
        /**
         * 集群模式,单机:single,主从:master,哨兵模式:sentinel,集群模式:cluster
         */
        single, master, sentinel, cluster;
    }

    /**
     * 是否开启:默认为:false,便于生成配置提示。
     */

    public Boolean getEnabled() {
        return this.enabled;
    }

    /**
     * 单机配置:redis 服务地址
     */

    public String getAddress() {
        return this.address;
    }

    /**
     * 密码配置
     */

    public String getPassword() {
        return this.password;
    }

    /**
     * db
     */

    public Integer getDatabase() {
        return this.database;
    }

    /**
     * 连接池大小
     */

    public Integer getPoolSize() {
        return this.poolSize;
    }

    /**
     * 最小空闲连接数
     */

    public Integer getIdleSize() {
        return this.idleSize;
    }

    /**
     * 连接空闲超时,单位:毫秒
     */

    public Integer getIdleTimeout() {
        return this.idleTimeout;
    }

    /**
     * 连接超时,单位:毫秒
     */

    public Integer getConnectionTimeout() {
        return this.connectionTimeout;
    }

    /**
     * 命令等待超时,单位:毫秒
     */

    public Integer getTimeout() {
        return this.timeout;
    }

    /**
     * 集群模式,单机:single,主从:master,哨兵模式:sentinel,集群模式:cluster
     */

    public Mode getMode() {
        return this.mode;
    }

    /**
     * 主从模式,主地址
     */

    public String getMasterAddress() {
        return this.masterAddress;
    }

    /**
     * 主从模式,从地址
     */

    public String[] getSlaveAddress() {
        return this.slaveAddress;
    }

    /**
     * 哨兵模式:主名称
     */

    public String getMasterName() {
        return this.masterName;
    }

    /**
     * 哨兵模式地址
     */

    public String[] getSentinelAddress() {
        return this.sentinelAddress;
    }

    /**
     * 集群模式节点地址
     */

    public String[] getNodeAddress() {
        return this.nodeAddress;
    }

    /**
     * 是否开启:默认为:false,便于生成配置提示。
     */

    public void setEnabled(final Boolean enabled) {
        this.enabled = enabled;
    }

    /**
     * 单机配置:redis 服务地址
     */

    public void setAddress(final String address) {
        this.address = address;
    }

    /**
     * 密码配置
     */

    public void setPassword(final String password) {
        this.password = password;
    }

    /**
     * db
     */

    public void setDatabase(final Integer database) {
        this.database = database;
    }

    /**
     * 连接池大小
     */

    public void setPoolSize(final Integer poolSize) {
        this.poolSize = poolSize;
    }

    /**
     * 最小空闲连接数
     */

    public void setIdleSize(final Integer idleSize) {
        this.idleSize = idleSize;
    }

    /**
     * 连接空闲超时,单位:毫秒
     */

    public void setIdleTimeout(final Integer idleTimeout) {
        this.idleTimeout = idleTimeout;
    }

    /**
     * 连接超时,单位:毫秒
     */

    public void setConnectionTimeout(final Integer connectionTimeout) {
        this.connectionTimeout = connectionTimeout;
    }

    /**
     * 命令等待超时,单位:毫秒
     */

    public void setTimeout(final Integer timeout) {
        this.timeout = timeout;
    }

    /**
     * 集群模式,单机:single,主从:master,哨兵模式:sentinel,集群模式:cluster
     */

    public void setMode(final Mode mode) {
        this.mode = mode;
    }

    /**
     * 主从模式,主地址
     */

    public void setMasterAddress(final String masterAddress) {
        this.masterAddress = masterAddress;
    }

    /**
     * 主从模式,从地址
     */

    public void setSlaveAddress(final String[] slaveAddress) {
        this.slaveAddress = slaveAddress;
    }

    /**
     * 哨兵模式:主名称
     */

    public void setMasterName(final String masterName) {
        this.masterName = masterName;
    }

    /**
     * 哨兵模式地址
     */

    public void setSentinelAddress(final String[] sentinelAddress) {
        this.sentinelAddress = sentinelAddress;
    }

    /**
     * 集群模式节点地址
     */

    public void setNodeAddress(final String[] nodeAddress) {
        this.nodeAddress = nodeAddress;
    }

}

幂等性切面类

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
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.jeckxu.magical.core.idempotent.annotation.Idempotent;
import org.jeckxu.magical.core.idempotent.exception.IdempotentException;
import org.jeckxu.magical.core.idempotent.expression.KeyResolver;
import org.redisson.api.RMapCache;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author jeckxu
 */
@Aspect
public class IdempotentAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(IdempotentAspect.class);
    private ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal();
    private static final String RMAPCACHE_KEY = "magical:idempotent";
    private static final String KEY = "key";
    private static final String DELKEY = "delKey";
    private final RedissonClient redissonClient;
    private final KeyResolver keyResolver;


    @Pointcut("@annotation(org.jeckxu.magical.core.idempotent.annotation.Idempotent)")
    public void pointCut() {
    }

    @Before("pointCut()")
    public void beforePointCut(JoinPoint joinPoint) throws Exception {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        if (!method.isAnnotationPresent(Idempotent.class)) {
            return;
        }
        Idempotent idempotent = method.getAnnotation(Idempotent.class);
        String key;
        // 若没有配置 幂等 标识编号,则使用 url + 参数列表作为区分
        if (StringUtils.isEmpty(idempotent.key())) {
            String url = request.getRequestURL().toString();
            String argString = Arrays.asList(joinPoint.getArgs()).toString();
            key = url + argString;
        } else {
            // 使用jstl 规则区分
            key = keyResolver.resolver(idempotent, joinPoint);
        }
        long expireTime = idempotent.expireTime();
        String info = idempotent.info();
        TimeUnit timeUnit = idempotent.timeUnit();
        boolean delKey = idempotent.delKey();
        // do not need check null
        RMapCache<String, Object> rMapCache = redissonClient.getMapCache(RMAPCACHE_KEY);
        String value = LocalDateTime.now().toString().replace("T", " ");
        Object v1;
        if (null != rMapCache.get(key)) {
            // had stored
            throw new IdempotentException(info);
        }
        synchronized (this) {
            v1 = rMapCache.putIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
            if (null != v1) {
                throw new IdempotentException(info);
            } else {
                LOGGER.info("[idempotent]:has stored key={},value={},expireTime={}{},now={}", key, value, expireTime, timeUnit, LocalDateTime.now().toString());
            }
        }
        Map<String, Object> map = CollectionUtils.isEmpty(threadLocal.get()) ? new HashMap<>(4) : threadLocal.get();
        map.put(KEY, key);
        map.put(DELKEY, delKey);
        threadLocal.set(map);
    }

    @After("pointCut()")
    public void afterPointCut(JoinPoint joinPoint) {
        Map<String, Object> map = threadLocal.get();
        if (CollectionUtils.isEmpty(map)) {
            return;
        }
        RMapCache<Object, Object> mapCache = redissonClient.getMapCache(RMAPCACHE_KEY);
        if (mapCache.size() == 0) {
            return;
        }
        String key = map.get(KEY).toString();
        boolean delKey = (boolean) map.get(DELKEY);
        if (delKey) {
            mapCache.fastRemove(key);
            LOGGER.info("[idempotent]:has removed key={}", key);
        }
        threadLocal.remove();
    }


    public IdempotentAspect(final RedissonClient redissonClient, final KeyResolver keyResolver) {
        this.redissonClient = redissonClient;
        this.keyResolver = keyResolver;
    }
}

幂等性异常定义

/**
 * Idempotent Exception If there is a custom global exception, you need to inherit the
 * custom global exception.
 *
 * @author jeckxu
 */
public class IdempotentException extends Exception {

   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);
   }

}


幂等性唯一标志处理器

import org.aspectj.lang.JoinPoint;
import org.jeckxu.magical.core.idempotent.annotation.Idempotent;

/**
 * @author jeckxu
 * @date 2020/11/10
 * <p>
 * 唯一标志处理器
 */
public interface KeyResolver {

   /**
    * 解析处理 key
    * @param idempotent 接口注解标识
    * @param point 接口切点信息
    * @return 处理结果
    */
   String resolver(Idempotent idempotent, JoinPoint point);

}
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.jeckxu.magical.core.idempotent.annotation.Idempotent;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.lang.reflect.Method;

/**
 * @author jeckxu
 */
public class ExpressionResolver implements KeyResolver {

   private static final SpelExpressionParser PARSER = new SpelExpressionParser();

   private static final LocalVariableTableParameterNameDiscoverer DISCOVERER = new LocalVariableTableParameterNameDiscoverer();

   @Override
   public String resolver(Idempotent idempotent, JoinPoint point) {
      Object[] arguments = point.getArgs();
      String[] params = DISCOVERER.getParameterNames(getMethod(point));
      StandardEvaluationContext context = new StandardEvaluationContext();

      for (int len = 0; len < params.length; len++) {
         context.setVariable(params[len], arguments[len]);
      }

      Expression expression = PARSER.parseExpression(idempotent.key());
      return expression.getValue(context, String.class);
   }

   /**
    * 根据切点解析方法信息
    * @param joinPoint 切点信息
    * @return Method 原信息
    */
   private Method getMethod(JoinPoint joinPoint) {
      MethodSignature signature = (MethodSignature) joinPoint.getSignature();
      Method method = 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);
         }
      }
      return method;
   }

}

幂等性异常统一处理

import org.jeckxu.magical.core.idempotent.exception.IdempotentException;
import org.jeckxu.magical.core.launch.destroy.MagicalDestroying;
import org.jeckxu.magical.core.launch.utils.I18nMessageUtils;
import org.jeckxu.magical.core.tool.api.R;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.DispatcherServlet;

import javax.servlet.Servlet;

@Order(3)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class})
@RestControllerAdvice
public class MagicalIdempotentExceptionTranslator implements MagicalDestroying {

    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MagicalIdempotentExceptionTranslator.class);

    @ExceptionHandler(IdempotentException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public R<Object> handleError(IdempotentException e) {
      e.printStackTrace();
      return R.fail(1999, I18nMessageUtils.get(e.getMessage()));
    }
}

幂等插件自动配置【初始化】

import org.jeckxu.magical.core.idempotent.aspect.IdempotentAspect;
import org.jeckxu.magical.core.idempotent.expression.ExpressionResolver;
import org.jeckxu.magical.core.idempotent.expression.KeyResolver;
import org.jeckxu.magical.core.idempotent.prop.MagicalIdempotentProperties;
import org.jeckxu.magical.core.launch.destroy.MagicalDestroying;
import org.jeckxu.magical.core.tool.utils.StringUtil;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.*;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 幂等插件初始化
 * @author jeckxu
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedissonClient.class)
@EnableConfigurationProperties(MagicalIdempotentProperties.class)
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnProperty(value = "magical.idempotent.enabled", havingValue = "true")
public class IdempotentAutoConfiguration implements MagicalDestroying {

   private static Config singleConfig(MagicalIdempotentProperties properties) {
      Config config = new Config();
      SingleServerConfig serversConfig = config.useSingleServer();
      serversConfig.setAddress(properties.getAddress());
      String password = properties.getPassword();
      if (StringUtil.isNotBlank(password)) {
         serversConfig.setPassword(password);
      }
      serversConfig.setDatabase(properties.getDatabase());
      serversConfig.setConnectionPoolSize(properties.getPoolSize());
      serversConfig.setConnectionMinimumIdleSize(properties.getIdleSize());
      serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
      serversConfig.setConnectTimeout(properties.getConnectionTimeout());
      serversConfig.setTimeout(properties.getTimeout());
      return config;
   }

   private static Config masterSlaveConfig(MagicalIdempotentProperties properties) {
      Config config = new Config();
      MasterSlaveServersConfig serversConfig = config.useMasterSlaveServers();
      serversConfig.setMasterAddress(properties.getMasterAddress());
      serversConfig.addSlaveAddress(properties.getSlaveAddress());
      String password = properties.getPassword();
      if (StringUtil.isNotBlank(password)) {
         serversConfig.setPassword(password);
      }
      serversConfig.setDatabase(properties.getDatabase());
      serversConfig.setMasterConnectionPoolSize(properties.getPoolSize());
      serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize());
      serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize());
      serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize());
      serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
      serversConfig.setConnectTimeout(properties.getConnectionTimeout());
      serversConfig.setTimeout(properties.getTimeout());
      return config;
   }

   private static Config sentinelConfig(MagicalIdempotentProperties properties) {
      Config config = new Config();
      SentinelServersConfig serversConfig = config.useSentinelServers();
      serversConfig.setMasterName(properties.getMasterName());
      serversConfig.addSentinelAddress(properties.getSentinelAddress());
      String password = properties.getPassword();
      if (StringUtil.isNotBlank(password)) {
         serversConfig.setPassword(password);
      }
      serversConfig.setDatabase(properties.getDatabase());
      serversConfig.setMasterConnectionPoolSize(properties.getPoolSize());
      serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize());
      serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize());
      serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize());
      serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
      serversConfig.setConnectTimeout(properties.getConnectionTimeout());
      serversConfig.setTimeout(properties.getTimeout());
      return config;
   }

   private static Config clusterConfig(MagicalIdempotentProperties properties) {
      Config config = new Config();
      ClusterServersConfig serversConfig = config.useClusterServers();
      serversConfig.addNodeAddress(properties.getNodeAddress());
      String password = properties.getPassword();
      if (StringUtil.isNotBlank(password)) {
         serversConfig.setPassword(password);
      }
      serversConfig.setMasterConnectionPoolSize(properties.getPoolSize());
      serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize());
      serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize());
      serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize());
      serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
      serversConfig.setConnectTimeout(properties.getConnectionTimeout());
      serversConfig.setTimeout(properties.getTimeout());
      return config;
   }

   /**
    * 切面 拦截处理所有 @Idempotent
    * @return Aspect
    */
   @Bean
   public IdempotentAspect idempotentAspect(MagicalIdempotentProperties properties) {
      return new IdempotentAspect(redissonClient(properties), new ExpressionResolver());
   }

   /**
    * key 解析器
    * @return KeyResolver
    */
   @Bean
   @ConditionalOnMissingBean(KeyResolver.class)
   public KeyResolver keyResolver() {
      return new ExpressionResolver();
   }

   private static RedissonClient redissonClient(MagicalIdempotentProperties properties) {
      MagicalIdempotentProperties.Mode mode = properties.getMode();
      Config config;
      switch (mode) {
         case sentinel:
            config = sentinelConfig(properties);
            break;
         case cluster:
            config = clusterConfig(properties);
            break;
         case master:
            config = masterSlaveConfig(properties);
            break;
         case single:
            config = singleConfig(properties);
            break;
         default:
            config = new Config();
            break;
      }
      return Redisson.create(config);
   }
}

yml配置项

magical:
  idempotent:
    enabled: false
    address: redis://192.168.0.1:26379
    password: ${YOUR_PASSWORD}
上一篇:Spring.NET 学习笔记(1)


下一篇:Javascript语法基础