该切面功能适用场景
解决方案
- 前端解决:限制点击下单按钮为1次后失效。不足:用户体验下降,能绕过前端
- 后端解决:防重提交切面解决,自定义注释实现该功能(如下)
- 步骤:
- 自定义注释类RepeatSubmit
- 创建切面并有该注释绑定,在切面类实现防重提交功能:
- 方式一:引入redission进行加锁5秒,原理redis的setAbsent
- 方式二:将token存入redis中,下单成功删除token,下单前需要调用获取token接口才能成功下单(类似于加锁,和方式一原理相同)
- RepeatSubmit
- /**
- * 自定义防重提交
- */
- @Documented
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface RepeatSubmit {
- /**
- * 防重提交类型。 方法、令牌
- */
- enum Type {PARAM, TOKEN}
- /**
- * 默认防重提交,是方法参数
- * @return
- */
- Type limitType() default Type.PARAM;
- /**
- * 加锁过期时间,默认5秒
- * @return
- */
- long lockTime() default 5;
- }
复制代码- /**
- * 定义一个切面类
- */
- @Aspect
- @Component
- @Slf4j
- public class RepeatSubmitAspect {
- @Autowired
- private StringRedisTemplate redisTemplate;
- @Autowired
- private RedissonClient redissonClient;
- /**
- * 定义 @Pointcut注解表达式,
- * 方式一:@annotation:当执行的方法上拥有指定的注解时生效(我们采用这)
- */
- @Pointcut("@annotation(repeatSubmit)")
- public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {
- }
- /**
- * 环绕通知, 围绕着方法执行
- *
- * @param joinPoint
- * @param repeatSubmit
- * @return
- * @throws Throwable
- * @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。
- */
- @Around("pointCutNoRepeatSubmit(repeatSubmit)")
- public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
- HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
- Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
- // 记录成功或者失败
- Boolean res = false;
- // 防重提交类型
- String type = repeatSubmit.limitType().name();
- if (type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {
- //方式一,参数形式防重提交
- long lockTime = repeatSubmit.lockTime();
- String ipAddr = CommonUtil.getIpAddr(request);
- MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
- Method method = methodSignature.getMethod();
- String className = method.getDeclaringClass().getName();
- String key = "order-server:repeat_submit"+CommonUtil.MD5(String.format("%s-%s-%s-%s", ipAddr, className, method, accountNo));
- // 加锁
- //res = redisTemplate.opsForValue().setIfAbsent(key,"1",lockTime, TimeUnit.SECONDS);
- RLock lock = redissonClient.getLock(key);
- // 尝试加锁,最多等待2秒,上锁以后5秒自动解锁 [lockTime默认为5s, 可以自定义]
- res = lock.tryLock(2, lockTime, TimeUnit.SECONDS);
- } else if (type.equalsIgnoreCase(RepeatSubmit.Type.TOKEN.name())) {
- //方式二,令牌形式防重提交
- String requestToken = request.getHeader("request-token");
- if (StringUtils.isBlank(requestToken)) {
- throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL);
- }
- String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, accountNo, requestToken);
- /**
- * 提交表单的token key
- * key是 order:submit:accountNo:token,然后直接删除成功则完成
- */
- res = redisTemplate.delete(key);
- }
- if (!res) {
- log.error("订单请求重复提交");
- return null;
- }
- log.info("环绕通知执行前");
- Object obj = joinPoint.proceed();
- log.info("环绕通知执行后");
- return obj;
- }
- }
复制代码
- RedissionConfiguration配置类(用于加锁)
- @Configuration
- public class RedissionConfiguration {
- @Value("${spring.redis.host}")
- private String redisHost;
- @Value("${spring.redis.port}")
- private String redisPort;
- @Value("${spring.redis.password}")
- private String redisPwd;
- /**
- * 配置分布式锁的redisson
- * @return
- */
- @Bean
- public RedissonClient redissonClient(){
- Config config = new Config();
- //单机方式
- config.useSingleServer().setPassword(redisPwd).setAddress("redis://"+redisHost+":"+redisPort);
- //集群
- //config.useClusterServers().addNodeAddress("redis://192.31.21.1:6379","redis://192.31.21.2:6379")
- RedissonClient redissonClient = Redisson.create(config);
- return redissonClient;
- }
- /**
- * 集群模式
- * 备注:可以用"rediss://"来启用SSL连接
- */
- /*@Bean
- public RedissonClient redissonClusterClient() {
- Config config = new Config();
- config.useClusterServers().setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒
- .addNodeAddress("redis://127.0.0.1:7000")
- .addNodeAddress("redis://127.0.0.1:7002");
- RedissonClient redisson = Redisson.create(config);
- return redisson;
- }*/
- }
复制代码
- 使用说明:在下单接口标注@RepeatSubmit(limitType = RepeatSubmit.Type.TOKEN)
- 或者@RepeatSubmit(limitType = RepeatSubmit.Type.PARAM)
- /**
- * 下单前获取令牌,用于防重提交
- * @return
- */
- @GetMapping("token")
- public JsonData getOrderToken() {
- Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
- String token = CommonUtil.getStringNumRandom(32);
- String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, accountNo, token);
- // token 过期时间30分钟
- redisTemplate.opsForValue().set(key, String.valueOf(Thread.currentThread().getId()), 30, TimeUnit.MINUTES);
- return JsonData.buildSuccess(token);
- }
- @PostMapping("confirm")
- @RepeatSubmit(limitType = RepeatSubmit.Type.TOKEN)
- public void confirmOrder(@RequestBody ConfirmOrderRequest orderRequest, HttpServletResponse response) {
- // TODO 下单业务
- }
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |