幂等性注解定义
import java.lang.annotation.*; import java.util.concurrent.TimeUnit; /** * @author jeckxu */ ElementType.METHOD) (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 */ 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 */ 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; "@annotation(org.jeckxu.magical.core.idempotent.annotation.Idempotent)") ( public void pointCut() { } "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); } "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(); 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; 3) (proxyBeanMethods = false) (type = ConditionalOnWebApplication.Type.SERVLET) (Servlet.class, DispatcherServlet.class}) ({ public class MagicalIdempotentExceptionTranslator implements MagicalDestroying { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MagicalIdempotentExceptionTranslator.class); IdempotentException.class) ( 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 */ proxyBeanMethods = false) (RedissonClient.class) (MagicalIdempotentProperties.class) (RedisAutoConfiguration.class) (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 */ public IdempotentAspect idempotentAspect(MagicalIdempotentProperties properties) { return new IdempotentAspect(redissonClient(properties), new ExpressionResolver()); } /** * key 解析器 * @return KeyResolver */ 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配置项