【智能排班系统】基于AOP和自定义注解实现接口幂等性

文章目录

  • 接口幂等性介绍
    • 为何重视接口幂等性
    • 幂等性实现策略
      • token机制
        • 存在问题: 先删除token还是执行完业务再删除
      • 乐观锁与版本号
      • 分布式锁
      • 唯一约束
      • 防重表
  • 实现
    • 定义注解
    • handler处理器
      • handler接口定义
        • 接口实现:基于token机制
          • Service
          • Controller
          • 实现
        • 接口实现:参数param作为分布式锁的键
          • Service
          • 实现
        • 接口实现:SPEL表达式的值作为分布式锁的键
          • Service
          • 实现
          • 工具类
      • handler工厂
    • AOP
      • 幂等相关参数包装
      • 切面
    • 配置类
  • 使用
    • SpEL方式
    • param方式
  • 说明

接口幂等性介绍

接口幂等性,源自数学中的幂运算性质,意指一个操作无论执行一次还是多次,对系统状态的影响均等同于执行一次的效果。在编程和API设计语境中,幂等性意味着对同一接口发起的多次相同请求,不论请求次数如何,其最终结果都是确定的且对系统资源状态的改变是一致的

为何重视接口幂等性

接口幂等性的价值主要体现在以下几个方面:

  • 容错与重试机制在网络环境不稳定或系统短暂故障时,客户端的请求可能丢失、超时或重复发送。幂等接口允许客户端在未收到明确响应或响应不可信时,安全地重新发起请求,而不必担心导致数据不一致或资源的重复消耗。例如,支付系统中的扣款操作,即使因网络问题重复发送扣款请求,幂等设计确保只扣除一次费用,避免用户资金被重复扣减。

  • 系统一致性维护: 在分布式环境中,系统间的通信延迟、节点故障等情况可能导致操作的重复执行。幂等接口确保即使在这些异常情况下,系统状态也能保持一致,不会因为重复请求而出现逻辑混乱或数据冲突。

  • 安全性提升防止恶意攻击或用户误操作导致的资源滥用。例如,用户无意中连续点击“提交订单”按钮,幂等设计能确保仅创建一份有效的订单,而不是生成多个重复订单。

幂等性实现策略

实现接口幂等性通常采用以下几种策略:

token机制

服务端提供一个获取token的接口。在执行操作之前,先去访问token接口来获取一个token,该token会被存储到Redis中。在发起操作请求的时候带上token,在执行操作逻辑之前,先判断token存在,如果存在,先删除token则执行操作逻辑;否则说明同样的请求已经被处理过,不再执行处理逻辑

存在问题: 先删除token还是执行完业务再删除
  • 先删除:可能导致业务由于服务器宕机没有执行,重试还带上之前token,由于防重设计,请求不能再执行
  • 后删除:业务处理成功,但是服务闪断,没有成功删除token。发起同样的请求,导致业务被执行两遍

个人看法:不好保证删除token和执行业务的原子性,本人还没有找到可行的方案

乐观锁与版本号

在更新资源时,使用乐观锁机制,通过比较在更新资源的时候比较版本号来判断操作是否为重复执行。若版本号不匹配(即资源已被其他操作更新),则拒绝此次更新请求。

个人看法:需要对数据库进行改造,比较麻烦,除非一开始在设计数据库的时候就考虑

分布式锁

通过获取方法参数或使用SpEL表达式来生成分布式锁的键,在执行业务之前获取分布式锁,获取锁成功才可以执行业务,业务执行完毕删除锁,保证同一时间同一个请求只能处理一次

唯一约束

  • 数据库唯一约束:给MySQL表的字段添加唯一约束,例如给username添加唯一约束之后,就算用户同时发起了多次注册请求,也只有一个请求可以注册成功
  • Redis Set 防重:将方法参数的MD5码添加到set中,每次执行请求时,判断md5是否在set中,如果在,说明已经执行过,直接返回失败(不建议,set会越来越多)

防重表

类似上面的唯一约束,只是单独用一个表来存储响应标识,并对该表示设置唯一约束。能插入标识说明是第一次执行

实现

定义注解

import com.dam.enums.IdempotentSceneEnum;
import com.dam.enums.IdempotentTypeEnum;import java.lang.annotation.*;/*** 幂等注解*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {/*** 幂等Key,只有在 {@link Idempotent#type()} 为 {@link IdempotentTypeEnum#SPEL} 时生效*/String key() default "";/*** 触发幂等失败逻辑时,返回的错误提示信息*/String message() default "您操作太快,请稍后再试";/*** 验证幂等类型,支持多种幂等方式* RestAPI 建议使用 {@link IdempotentTypeEnum#TOKEN} 或 {@link IdempotentTypeEnum#PARAM}* 其它类型幂等验证,使用 {@link IdempotentTypeEnum#SPEL}*/IdempotentTypeEnum type() default IdempotentTypeEnum.PARAM;/*** 验证幂等场景,支持多种 {@link IdempotentSceneEnum}*/IdempotentSceneEnum scene() default IdempotentSceneEnum.RESTAPI;/*** 设置防重令牌 Key 前缀,MQ 幂等去重可选设置* {@link IdempotentSceneEnum#MQ} and {@link IdempotentTypeEnum#SPEL} 时生效*/String uniqueKeyPrefix() default "";/*** 设置防重令牌 Key 过期时间,单位秒,默认 1 小时,MQ 幂等去重可选设置* {@link IdempotentSceneEnum#MQ} and {@link IdempotentTypeEnum#SPEL} 时生效*/long keyTimeout() default 3600L;
}

handler处理器

handler接口定义

注意,接口定义了两个空方法,因为不需要所有处理器都需要处理那两个方法。该接口默认实现了execute方法,即每个处理器的共同之处都是先调用buildParam构建包装参数,再调用handler方法来进行幂等逻辑判断,

import com.dam.annotation.Idempotent;
import com.dam.core.aop.IdempotentParam;
import org.aspectj.lang.ProceedingJoinPoint;/*** 幂等执行处理器**/
public interface IdempotentExecuteHandler {IdempotentParam buildParam(ProceedingJoinPoint joinPoint);/*** 幂等处理逻辑** @param wrapper 幂等参数包装器*/void handler(IdempotentParam wrapper);/*** 执行幂等处理逻辑** @param joinPoint  AOP 方法处理* @param idempotent 幂等注解*/default void execute(ProceedingJoinPoint joinPoint, Idempotent idempotent){// 模板方法模式:构建幂等参数包装器IdempotentParam idempotentParam = buildParam(joinPoint).setIdempotent(idempotent);// 如果不满足幂等,handler方法会通过抛出异常来使下面的程序被中断handler(idempotentParam);};/*** 幂等异常流程处理*/default void exceptionProcessing() {}/*** 执行目标方法成功的后置处理*/default void postProcessing() {}
}
接口实现:基于token机制
Service
import com.dam.core.handler.IdempotentExecuteHandler;/*** Token 实现幂等接口**/
public interface IdempotentTokenService extends IdempotentExecuteHandler {/*** 创建幂等验证Token*/String createToken();
}
Controller

使用token机制,在发送具体请求之前,需要先发送一个前置请求来向服务端获取token,因此需要开发一个Controller供前端使用

import com.dam.constant.KeyConstants;
import com.dam.model.result.R;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;/*** 基于 Token 验证请求幂等性控制器*/
@RestController
@RequiredArgsConstructor
public class IdempotentTokenController {private final IdempotentTokenService idempotentTokenService;/*** 请求申请Token*/@GetMapping("/token")public R createToken() {return R.ok().addData(KeyConstants.TOKEN_KEY, idempotentTokenService.createToken());}
}
实现
import cn.hutool.core.util.StrUtil;
import com.dam.constant.KeyConstants;
import com.dam.core.aop.IdempotentParam;
import com.dam.exception.SSSException;
import com.dam.model.enums.ResultCodeEnum;
import com.dam.properties.IdempotentTokenProperties;
import com.google.common.base.Strings;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;
import java.util.Optional;
import java.util.UUID;/*** 基于 Token 验证请求幂等性, 通常应用于 RestAPI 方法*/
@RequiredArgsConstructor
public final class IdempotentTokenExecuteHandler implements IdempotentTokenService {/*** Redis 操作模板,用于存储和验证幂等Token*/private final StringRedisTemplate redisTemplate;/*** 幂等相关配置属性*/private final IdempotentTokenProperties idempotentTokenProperties;/*** 如果配置文件里面没有写明,则使用该默认值*/private static final String TOKEN_PREFIX_KEY = KeyConstants.IDEMPOTENT_PREFIX + "token:";/*** 幂等Token在Redis中的默认过期时间(毫秒),若配置文件未指定,则使用此值*/private static final long TOKEN_EXPIRED_TIME = 6000;@Overridepublic IdempotentParam buildParam(ProceedingJoinPoint joinPoint) {return new IdempotentParam();}/*** 创建一个新的幂等Token。生成一个全局唯一的UUID,并添加前缀(从配置或默认值获取),* 将此Token作为键存入Redis,并设置一个过期时间(从配置或默认值获取)。* 值为空字符串,因为仅用于标识Token的存在,不存储额外数据。** @return 新创建的幂等Token字符串*/@Overridepublic String createToken() {String token = Optional.ofNullable(Strings.emptyToNull(idempotentTokenProperties.getPrefix())).orElse(TOKEN_PREFIX_KEY) + UUID.randomUUID();redisTemplate.opsForValue().set(token, "", Optional.ofNullable(idempotentTokenProperties.getTimeout()).orElse(TOKEN_EXPIRED_TIME));return token;}/*** 处理幂等Token验证逻辑。在接收到业务请求时,从请求头或请求参数中提取幂等Token,* 然后验证该Token在Redis中的存在性。如果存在,则删除该Token并允许业务逻辑执行;* 否则,抛出异常表示重复请求或无效Token。** @param wrapper 幂等参数包装器*/@Overridepublic void handler(IdempotentParam wrapper) {// 获取当前请求上下文中的HttpServletRequest对象HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();// 从请求头或者请求参数中获取token对应的值String token = request.getHeader(KeyConstants.TOKEN_KEY);if (StrUtil.isBlank(token)) {token = request.getParameter(KeyConstants.TOKEN_KEY);if (StrUtil.isBlank(token)) {// 如果请求中未找到Token,抛出异常throw new SSSException(ResultCodeEnum.IDEMPOTENT_TOKEN_NULL_ERROR);}}// 验证并删除TokenBoolean tokenDelFlag = redisTemplate.delete(token);if (!tokenDelFlag) {// 删除失败,说明token对应的请求已经被执行过了,不能再执行了String errMsg = StrUtil.isNotBlank(wrapper.getIdempotent().message())? wrapper.getIdempotent().message(): ResultCodeEnum.IDEMPOTENT_TOKEN_DELETE_ERROR.getMessage();throw new SSSException(ResultCodeEnum.IDEMPOTENT_TOKEN_DELETE_ERROR.getCode(), errMsg);}}
}
接口实现:参数param作为分布式锁的键
Service

注意,该Service继承接口IdempotentExecuteHandler,多写一层接口是为了定义一些属于自己的方法

import com.dam.core.handler.IdempotentExecuteHandler;/*** 参数方式幂等实现接口**/
public interface IdempotentParamService extends IdempotentExecuteHandler {
}
实现

幂等性判断逻辑:将包含请求路径当前用户ID参数MD5值组合为key来做分布式锁,如果请求能加锁成功,说明是第一次执行,执行结束之后解锁。使用该方式可以保证同一时刻,同样的请求只有一个在执行。

import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.alibaba.fastjson2.JSON;
import com.dam.constant.KeyConstants;
import com.dam.context.UserContext;
import com.dam.core.IdempotentContext;
import com.dam.core.aop.IdempotentParam;
import com.dam.exception.SSSException;
import com.dam.model.enums.ResultCodeEnum;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;/*** 基于方法参数验证请求幂等性* 基于分布式锁实现*/
@RequiredArgsConstructor//使用构造器注入
public final class IdempotentParamExecuteHandler implements IdempotentParamService {/*** 注入Redisson客户端,用于操作分布式锁*/private final RedissonClient redissonClient;/*** 获取到的分布式锁对应的key,用来给上下文使用*/private final static String CONTEXT_LOCK_KEY = KeyConstants.IDEMPOTENT_PREFIX + "lock:param:restAPI";@Overridepublic IdempotentParam buildParam(ProceedingJoinPoint joinPoint) {// 构建分布式锁的唯一key,包含请求路径、当前用户ID和参数MD5值String lockKey = String.format(KeyConstants.IDEMPOTENT_PREFIX + "lock:param:path:%s:currentUserId:%s:md5:%s", getServletPath(), getCurrentUserId(), calcArgsMD5(joinPoint));return IdempotentParam.builder().lockKey(lockKey).joinPoint(joinPoint).build();}/*** 幂等逻辑判断,如果获取分布式锁失败,说明同样的请求和参数已经在执行,返回异常** @param wrapper 幂等参数包装器*/@Overridepublic void handler(IdempotentParam wrapper) {// 从包装器中获取分布式锁对应的keyString lockKey = wrapper.getLockKey();// 通过RedissonClient获取分布式锁实例RLock lock = redissonClient.getLock(lockKey);// 尝试获取锁,如果无法立即获取(即锁已被其他请求持有),则抛出异常,表示请求重复if (!lock.tryLock()) {throw new SSSException(ResultCodeEnum.FAIL.getCode(), wrapper.getIdempotent().message());}// 将获取到的锁存储在IdempotentContext中,以便后续解锁操作IdempotentContext.put(CONTEXT_LOCK_KEY, lock);}/*** 将分布式锁进行解锁*/@Overridepublic void postProcessing() {RLock lock = null;try {// 从IdempotentContext中获取之前存储的锁实例lock = (RLock) IdempotentContext.getKey(CONTEXT_LOCK_KEY);} finally {if (lock != null) {lock.unlock();}}}/*** 获取当前线程上下文中的ServletPath(请求路径)** @return 当前线程上下文 ServletPath*/private String getServletPath() {ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();// 从ServletAttributes中获取请求的ServletPathreturn sra.getRequest().getServletPath();}/*** 获取当前操作用户的ID** @return 当前操作用户 ID*/private String getCurrentUserId() {// 从UserContext中获取当前用户的IDString userId = UserContext.getUserId();if (StrUtil.isBlank(userId)) {throw new SSSException(ResultCodeEnum.FAIL.getCode(), "用户ID获取失败,请登录");}return userId;}/*** 计算参数的MD5码** @param joinPoint 包含方法参数的ProceedingJoinPoint对象* @return 参数的MD5值,用于标识请求参数的唯一性*/private String calcArgsMD5(ProceedingJoinPoint joinPoint) {String md5 = DigestUtil.md5Hex(JSON.toJSONBytes(joinPoint.getArgs()));return md5;}
}
接口实现:SPEL表达式的值作为分布式锁的键
Service
import com.dam.core.handler.IdempotentExecuteHandler;/*** SpEL 方式幂等实现接口**/
public interface IdempotentSpELService extends IdempotentExecuteHandler {
}
实现
import com.dam.annotation.Idempotent;
import com.dam.constant.KeyConstants;
import com.dam.core.IdempotentContext;
import com.dam.core.aop.IdempotentAspect;
import com.dam.core.aop.IdempotentParam;
import com.dam.core.handler.spel.IdempotentSpELService;
import com.dam.exception.SSSException;
import com.dam.model.enums.ResultCodeEnum;
import com.dam.toolkit.SpELUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;/*** 基于 SpEL 方法验证请求幂等性,适用于 RestAPI(Restful) 场景* 基于分布式锁实现*/
@RequiredArgsConstructor
public final class IdempotentSpELByRestAPIExecuteHandler implements IdempotentSpELService {/*** Redisson 客户端,用于操作分布式锁*/private final RedissonClient redissonClient;/*** 分布式锁的基础键名,用于存储全局唯一标识的锁*/private final static String LOCK = KeyConstants.IDEMPOTENT_PREFIX + "lock:spEL:restAPI";/*** 构建幂等参数包装器,通过解析 SpEL 表达式生成请求的唯一标识(锁键)** @param joinPoint 切点对象,包含目标方法信息及参数* @return 构建好的幂等参数包装器*/@SneakyThrows@Overridepublic IdempotentParam buildParam(ProceedingJoinPoint joinPoint) {// 从切点对象中获取方法上的 @Idempotent 注解Idempotent idempotent = IdempotentAspect.getIdempotent(joinPoint);// 使用 SpEL 工具解析注解中的 key 属性表达式,生成请求的唯一标识(锁键)String lockKey = (String) SpELUtil.parseKey(idempotent.key(), ((MethodSignature) joinPoint.getSignature()).getMethod(), joinPoint.getArgs());return IdempotentParam.builder().lockKey(lockKey).joinPoint(joinPoint).build();}/*** 幂等性逻辑处理。尝试获取分布式锁,如果获取失败(即锁已被其他请求持有),说明有相同方法和参数的请求正在执行,* 此时抛出异常,拒绝当前请求。** @param wrapper 幂等参数包装器*/@Overridepublic void handler(IdempotentParam wrapper) {
//        System.out.println("wrapper.getLockKey():" + wrapper.getLockKey());String uniqueKey = KeyConstants.IDEMPOTENT_PREFIX + wrapper.getIdempotent().uniqueKeyPrefix() + wrapper.getLockKey();RLock lock = redissonClient.getLock(uniqueKey);// 尝试获取锁,如果无法立即获取(即锁已被其他请求持有),抛出异常,表示请求重复if (!lock.tryLock()) {
//            System.out.println("wrapper.getIdempotent().message():" + wrapper.getIdempotent().message());throw new SSSException(ResultCodeEnum.FAIL.getCode(), wrapper.getIdempotent().message());}// 上下文用来传递分布式锁,便于请求处理完成之后进行解锁IdempotentContext.put(LOCK, lock);}/*** 后处理,对分布式锁进行解锁*/@Overridepublic void postProcessing() {// 从 IdempotentContext 中获取之前存储的锁实例RLock lock = null;try {lock = (RLock) IdempotentContext.getKey(LOCK);} finally {// 如果锁实例不为空,进行解锁操作if (lock != null) {lock.unlock();}}}/*** 请求的方法执行过程中发生了异常,也对分布式锁进行解锁*/@Overridepublic void exceptionProcessing() {// 从 IdempotentContext 中获取之前存储的锁实例RLock lock = null;try {lock = (RLock) IdempotentContext.getKey(LOCK);} finally {// 如果锁实例不为空,进行解锁操作if (lock != null) {lock.unlock();}}}
}
工具类
import cn.hutool.core.util.ArrayUtil;
import com.google.common.collect.Lists;
import org.springframework.core.DefaultParameterNameDiscoverer;
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;
import java.util.ArrayList;
import java.util.Optional;/*** SpEL 表达式解析工具*/
public class SpELUtil {/*** 校验并返回实际使用的 SpEL 表达式** @param spEl       SpEL 表达式字符串* @param method     目标方法对象* @param contextObj 目标方法的参数数组* @return 如果传入的 SpEL 表达式包含特定符号(如 "#" 或 "T("),则解析并返回其实际值;否则直接返回传入的 SpEL 表达式字符串*/public static Object parseKey(String spEl, Method method, Object[] contextObj) {// 定义一个列表,存储 SpEL 表达式可能包含的特殊标志符ArrayList<String> spELFlag = Lists.newArrayList("#", "T(");// 查找传入 SpEL 表达式是否包含这些特殊标志符中的任意一个Optional<String> optional = spELFlag.stream().filter(spEl::contains).findFirst();// 如果找到,则需要解析 SpEL 表达式if (optional.isPresent()) {// 调用 parse 方法解析 SpEL 表达式,并返回解析后的值Object parse = parse(spEl, method, contextObj);return parse;}// 如果未找到特殊标志符,直接返回传入的 SpEL 表达式字符串return spEl;}/*** 转换参数为字符串** @param spEl       spEl 表达式* @param contextObj 上下文对象* @return 解析的字符串值*/public static Object parse(String spEl, Method method, Object[] contextObj) {DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();ExpressionParser parser = new SpelExpressionParser();Expression exp = parser.parseExpression(spEl);String[] params = discoverer.getParameterNames(method);
//        System.out.println("contextObj:" + JSON.toJSONString(contextObj));StandardEvaluationContext context = new StandardEvaluationContext();if (ArrayUtil.isNotEmpty(params)) {for (int len = 0; len < params.length; len++) {context.setVariable(params[len], contextObj[len]);}}Object value = exp.getValue(context);return value;
//        String md5Hex = DigestUtil.md5Hex(JSON.toJSONString(contextObj));
//        System.out.println("md5Hex:" + md5Hex);
//        return md5Hex;}
}

handler工厂

简单工厂模式提供专门的工厂类用于创建对象,实现了对象创建和使用的职责分离,客户端不需知道所创建的具体产品类的类名以及创建过程(在这里不需要创建,只需要从容器里面获取相应的Bean即可),只需知道具体产品类所对应的参数即可,通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。

import com.dam.ApplicationContextHolder;
import com.dam.core.handler.param.IdempotentParamService;
import com.dam.core.handler.spel.mq.IdempotentSpELByMQExecuteHandler;
import com.dam.core.handler.spel.restful.IdempotentSpELByRestAPIExecuteHandler;
import com.dam.core.handler.token.IdempotentTokenService;
import com.dam.enums.IdempotentSceneEnum;
import com.dam.enums.IdempotentTypeEnum;/*** 幂等执行处理器工厂* 简单工厂模式*/
public final class IdempotentExecuteHandlerFactory {/*** 根据枚举参数获取对应的幂等执行处理器handler** @param scene 指定幂等验证场景类型* @param type  指定幂等处理类型* @return 幂等执行处理器*/public static IdempotentExecuteHandler getInstance(IdempotentSceneEnum scene, IdempotentTypeEnum type) {IdempotentExecuteHandler result = null;switch (scene) {case RESTAPI: {switch (type) {case PARAM:result = ApplicationContextHolder.getBean(IdempotentParamService.class);break;case TOKEN:result = ApplicationContextHolder.getBean(IdempotentTokenService.class);break;case SPEL:result = ApplicationContextHolder.getBean(IdempotentSpELByRestAPIExecuteHandler.class);break;default: {}}break;}case MQ:result = ApplicationContextHolder.getBean(IdempotentSpELByMQExecuteHandler.class);break;default: {}}return result;}
}

AOP

幂等相关参数包装

import com.dam.annotation.Idempotent;
import com.dam.enums.IdempotentTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.aspectj.lang.ProceedingJoinPoint;/*** 幂等参数包装**/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public final class IdempotentParam {/*** 幂等注解*/private Idempotent idempotent;/*** AOP 处理连接点*/private ProceedingJoinPoint joinPoint;/*** 锁标识,{@link IdempotentTypeEnum#PARAM}*/private String lockKey;
}

切面

import com.dam.annotation.Idempotent;
import com.dam.core.IdempotentContext;
import com.dam.core.exception.RepeatConsumptionException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;import java.lang.reflect.Method;/*** 幂等注解 AOP 拦截器*/
@Aspect
public final class IdempotentAspect {/*** 使用Around来对Idempotent注解标记的方法进行环绕增强* @param joinPoint 使用 @Around ,自定义的切入点* @return* @throws Throwable*/@Around("@annotation(com.dam.annotation.Idempotent)")public Object idempotentHandler(ProceedingJoinPoint joinPoint) throws Throwable {// 获取注解信息Idempotent idempotent = getIdempotent(joinPoint);// 根据注解来获取相应的处理器IdempotentExecuteHandler instance = IdempotentExecuteHandlerFactory.getInstance(idempotent.scene(), idempotent.type());// 存储实际方法的执行结果Object resultObj;try {// 当不满足幂等时,execute会报错,后面的代码不会再执行instance.execute(joinPoint, idempotent);// 请求的具体方法执行resultObj = joinPoint.proceed();// 处理器进行后处理,如解除分布式锁instance.postProcessing();}  finally {IdempotentContext.clean();}return resultObj;}/*** 从给定的ProceedingJoinPoint中获取目标方法上的Idempotent注解实例** @param joinPoint 切点对象,包含方法签名等信息* @return 目标方法上的Idempotent注解实例* @throws NoSuchMethodException 如果找不到对应方法时抛出此异常*/public static Idempotent getIdempotent(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {// 获取方法签名对象,包含方法名、参数类型等信息MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();// 通过反射获取目标类(切面所拦截的对象)上与当前方法签名匹配的Method对象Method targetMethod = joinPoint.getTarget().getClass().getDeclaredMethod(methodSignature.getName(), methodSignature.getMethod().getParameterTypes());// 从Method对象上获取Idempotent注解实例return targetMethod.getAnnotation(Idempotent.class);}
}

配置类

将需要的Bean注入到容器中

import com.dam.core.aop.IdempotentAspect;
import com.dam.core.handler.param.IdempotentParamExecuteHandler;
import com.dam.core.handler.param.IdempotentParamService;
import com.dam.core.handler.spel.IdempotentSpELService;
import com.dam.core.handler.spel.mq.IdempotentSpELByMQExecuteHandler;
import com.dam.core.handler.spel.restful.IdempotentSpELByRestAPIExecuteHandler;
import com.dam.core.handler.token.IdempotentTokenExecuteHandler;
import com.dam.core.handler.token.IdempotentTokenService;
import com.dam.properties.IdempotentTokenProperties;
import org.redisson.api.RedissonClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;/*** 幂等自动装配*/
@EnableConfigurationProperties(IdempotentTokenProperties.class)
@Configuration
public class IdempotentAutoConfiguration {/*** 幂等切面*/@Beanpublic IdempotentAspect idempotentAspect() {return new IdempotentAspect();}/*** 参数方式幂等实现,基于 RestAPI 场景*/@Bean@ConditionalOnMissingBeanpublic IdempotentParamService idempotentParamExecuteHandler(RedissonClient redissonClient) {return new IdempotentParamExecuteHandler(redissonClient);}/*** Token 方式幂等实现,基于 RestAPI 场景*/@Bean@ConditionalOnMissingBeanpublic IdempotentTokenService idempotentTokenExecuteHandler(StringRedisTemplate distributedCache,IdempotentTokenProperties idempotentTokenProperties) {return new IdempotentTokenExecuteHandler(distributedCache, idempotentTokenProperties);}/*** 申请幂等 Token 控制器,基于 RestAPI 场景*/
//    @Bean
//    public IdempotentTokenController idempotentTokenController(IdempotentTokenService idempotentTokenService) {
//        return new IdempotentTokenController(idempotentTokenService);
//    }/*** SpEL 方式幂等实现,基于 RestAPI 场景*/@Bean@ConditionalOnMissingBeanpublic IdempotentSpELService idempotentSpELByRestAPIExecuteHandler(RedissonClient redissonClient) {return new IdempotentSpELByRestAPIExecuteHandler(redissonClient);}/*** SpEL 方式幂等实现,基于 MQ 场景*/@Bean@ConditionalOnMissingBeanpublic IdempotentSpELByMQExecuteHandler idempotentSpELByMQExecuteHandler(StringRedisTemplate distributedCache) {return new IdempotentSpELByMQExecuteHandler(distributedCache);}
}

使用

SpEL方式

在方法上面添加@Idempotent注解,案例如下:
想要自定义分布式锁的键,可以给key设置不同的SpEL表达式

/*** 修改*/
@PostMapping("/update")
@PreAuthorize("hasAnyAuthority('bnt.user.update','bnt.storeUser.update')")
@Idempotent(uniqueKeyPrefix = "sss-system-server:lock_userInfo_update:",key = "T(com.dam.context.UserContext).getUsername()",message = "正在执行用户信息修改流程,请稍后...",scene = IdempotentSceneEnum.RESTAPI,type = IdempotentTypeEnum.SPEL
)
public R update(@RequestBody UserEntity user) {boolean b = userService.updateById(user);return R.ok();
}

param方式

@Idempotent(message = "正在执行用户信息修改流程,请稍后...",scene = IdempotentSceneEnum.RESTAPI,type = IdempotentTypeEnum.PARAM
)

说明

本文代码来源于马哥 12306 的代码,本人只是根据自己的理解进行少量修改并应用到智能排班系统中。代码仓库为12306,该项目含金量较高,有兴趣的朋友们建议去学习一下。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/798002.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥杯22年第十三届省赛-数组切分|线性DP

题目链接&#xff1a; 蓝桥杯2022年第十三届省赛真题-数组切分 - C语言网 (dotcpp.com) 1.数组切分 - 蓝桥云课 (lanqiao.cn) 这道题C语言网数据会强一些。 说明&#xff1a; 对于一个切分的子数组&#xff0c;由于数组是1-N的一个排列&#xff0c;所以每个数唯一 可以用子…

5 两个有序链表序列的合并

分数 10 作者 DS课程组 单位 浙江大学 本题要求实现一个函数&#xff0c;将两个链表表示的递增整数序列合并为一个非递减的整数序列。 函数接口定义&#xff1a; List Merge( List L1, List L2 );其中List结构定义如下&#xff1a; typedef struct Node *PtrToNode; stru…

计算机网络——34LANs

LANs MAC地址和ARP 32bit IP地址 网络层地址用于使数据到达目标IP子网&#xff1a;前n - 1跳从而到达子网中的目标节点&#xff1a;最后一跳 LAN&#xff08;MAC/物理/以太网&#xff09;地址&#xff1a; 用于使帧从一个网卡传递到与其物理连接的另一个网卡&#xff08;在同…

数位排序(Comparator<int[]>())

题目 import java.util.Arrays; import java.util.Comparator; import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in);int n sc.nextInt();int m sc.nextInt();int[][] a new int[n][2];for(int i0;i…

MCU电子方案开发

MCU电子方案开发&#xff0c;在酷得进行MCU电子方案开发&#xff0c;可以充分利用当地的产业链资源&#xff0c;降低研发成本&#xff0c;提高研发效率。 东莞市酷得智能科技有限公司&#xff08;以下简称&#xff1a;酷得&#xff09;2018年成立于松山湖&#xff0c;且在汕头设…

C语言进阶课程学习记录-第25课 - # 和 ## 操作符使用分析

C语言进阶课程学习记录-第25课 - # 和 ## 操作符使用分析 #运算符实验-#转化字符串预处理后代码 实验-#输出函数名预处理后的代码 ##运算符实验-##定义变量预处理后代码 实验-##定义结构体预处理后的代码 小结 本文学习自狄泰软件学院 唐佐林老师的 C语言进阶课程&#xff0c;图…

【Redis】golang操作Redis基础入门

【Redis】golang操作Redis基础入门 大家好 我是寸铁&#x1f44a; 总结了一篇【Redis】golang操作Redis基础入门sparkles: 喜欢的小伙伴可以点点关注 &#x1f49d; Redis的作用 Redis&#xff08;Remote Dictionary Server&#xff09;是一个开源的内存数据库&#xff0c;它主…

深度学习中的注意力模块的添加

在深度学习中&#xff0c;骨干网络通常指的是网络的主要结构或主干部分&#xff0c;它负责从原始输入中提取高级特征。骨干网络通常由卷积神经网络&#xff08;CNN&#xff09;或者类似的架构组成&#xff0c;用于对图像、文本或其他类型的数据进行特征提取和表示学习。 注意力…

设计模式:策略模式示例

文章目录 示例 1: 排序策略示例 2: 支付策略示例 3: 压缩策略 策略模式的示例非常多样&#xff0c;下面是一些场景的示例及其代码实现&#xff1a; 示例 1: 排序策略 在需要对不同类型的数据集进行排序时&#xff0c;可以使用策略模式来选择不同的排序算法。 // 策略接口 pub…

libVLC 音频输出设备切换

libvlc_audio_output_list_get和libvlc_audio_output_device_list_get是libVLC 库中用于处理音频输出的两个函数。 libvlc_audio_output_list_get函数用于获取可用的音频输出模块列表。这个列表通常包括不同的音频输出方式&#xff0c;例如 Pulseaudio、ALSA 等。通过这个函数…

Linux——用户管理,文件压缩命令

用户管理命令 (1)系统存储用户信息的位置: /etc/passwd:存储用户的基本信息 UID:用户ID GID:组ID; (2)系统存储组信息的位置: /etc/group (3)系统存储用户密码信息的位置: /etc/shadow (2)添加用户 使用命令添加新用户:useradd newname 桌面添加:右键:设置:用户,解锁,添加用…

算法第三十九天-验证二叉树的前序序列化

验证二叉树的前序序列化 题目要求 解题思路 方法一&#xff1a;栈 栈的思路是「自底向上」的想法。下面要结合本题是「前序遍历」这个重要特点。 我们知道「前序遍历」是按照「根节点-左子树-右子树」的顺序遍历的&#xff0c;只有当根节点的所有左子树遍历完成之后&#xf…

排查Java中的OOM(Out of Memory)问题

Java的OOM&#xff08;OutOfMemoryError&#xff09;问题通常表示Java虚拟机&#xff08;JVM&#xff09;在尝试分配内存给对象时&#xff0c;无法找到足够的连续内存空间。这可能是由于内存泄漏、堆内存不足或其他原因导致的。排查OOM问题通常涉及以下几个步骤&#xff1a; 查…

使用 Docker 部署 Photopea 在线 PS 工具

1&#xff09;Photopea 介绍 GitHub&#xff1a;https://github.com/photopea/photopea 官方手册&#xff1a;https://www.photopea.com/learn/ Adobe 出品的「PhotoShop」想必大家都很熟悉啦&#xff0c;但是「PhotoShop」现在对电脑配置要求越来越高&#xff0c;体积越来越大…

逆向入门:为ctf国赛而写的笔记 day01

目录 通用寄存器&#xff1a; EAX:累加寄存器&#xff0c;是很多加法乘法指令的缺省寄存器 EBX&#xff1a;基地址寄存器&#xff0c;在内存寻址时存放基地址 ECX&#xff1a;计数器 EDX&#xff1a;数据寄存器&#xff0c;被用于来放整数除法产生的余数 变址寄存器 标志…

流行的API架构学习

几种流行的API架构风格图 SOAP&#xff08;Simple Object Access Protocol&#xff09; 优点&#xff1a;SOAP 是一种基于 XML 的通信协议&#xff0c;具有良好的跨平台和跨语言支持。它提供了丰富的安全性和事务管理功能&#xff0c;并支持复杂的消息交换模式。 缺点&#xf…

windows,web端网页唤起打开本地的客户端程序

这里写自定义目录标题 需求&#xff1a;在电脑浏览器网页唤起本地的应用程序 使用类似以下代码 <a href"myprotocol:">打开飞书</a>在客户端安装的时候在注册表会有自己的协议&#xff0c;若是没有的可自定义注册表 自定义注册表步骤 1.winr 运行 regedi…

物联网工程-系统设计作业

1.设计一套基于RFID牛场养殖信息管理系统&#xff0c;并给出系统设计思路、系统构架和控制流程图。 一、设计思想 为方便牛场养殖员鉴别和管理牛群&#xff0c;为每只牛佩戴有RFID标签的动物耳钉&#xff0c;并将牛的健康情况录入数据库中&#xff0c;随着牛的生长&#xff0c;…

关于递归和回溯的思考

完整代码: 力扣112路径总和 class Solution { private:bool traversal(TreeNode* cur, int count) {if (!cur->left && !cur->right && count 0) return true; // 遇到叶子节点&#xff0c;并且计数为0if (!cur->left && !cur->right) r…

[StartingPoint][Tier1]Funnel

Task 1 How many TCP ports are open? (打开了多少个 TCP 端口&#xff1f;) # nmap -sS -T4 10.129.224.226 --min-rate 1000 2 Task 2 What is the name of the directory that is available on the FTP server? (FTP 服务器上可用的目录名称是什么&#xff1f;) $ n…