springboot结合redis防止重复提交订单


项目地址:

利用redis+token+拦截器+注解(只在需要防止重复的接口上添加该注解即可)实现防止重复订单,在跳转到提交订单的页面时就通过getNotRepeatToken获取到本次订单的唯一标识(以业务加上用户id加上uuid作为key)存入redis,并返回给前端。在提交订单时,将该标识设置在请求头中,第一次提交订单时,在redis中删除该key。后续再次提交订单时redis中已经不存在该key,则给出重复提交订单的友好提示。
springboot 拦截器注入service为null的问题,可以通过bean工厂来完成手动注入。

定义注解和拦截器

注解:NotRepeat.java

package com.demo.aop;

import java.lang.annotation.*;

/**
 * 禁止重复提交的注解
 * @Author: zlw
 * @Date: 2019/7/16 10:55
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NotRepeat {
}

拦截器:NotRepeatInterceptor.java

package com.demo.config;

import com.demo.aop.NotRepeat;
import com.demo.enums.ExceptionEnum;
import com.demo.exception.ServiceException;
import com.demo.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @Author: zlw
 * @Date: 2019/7/16 11:09
 */
@Slf4j
public class NotRepeatInterceptor implements HandlerInterceptor {


    @Autowired
    private RedisUtils redisUtils;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //解决service为null   因为拦截器是在springContext之前加载的,通过@component注解的bean还没有注入到容器中
        if (redisUtils == null) {
            BeanFactory factory= WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
             redisUtils = (RedisUtils) factory.getBean("redisUtils");
        }

        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        NotRepeat annotation = handlerMethod.getMethodAnnotation(NotRepeat.class);
        //如果存在该注解,则进行幂等性校验,若校验不通过则抛出自定义异常
        if (annotation != null) {
            checkNotRepeat(request);
        }
        return true;
    }

    /**
     * 重复校验
     * @param request
     *
     *
     * 注意: 在删除key时需要判断是否删除成功,即redisUtils.deleteKey()的返回值是否小于1。
     *      若删除不成功则视为重复提交,防止网络延迟等故障时,两次请求
     *          在redisUtils.hasKey(notRepeat)时都为true,但是还没有进行到删除步骤(删除操作只能删除一次,不存在多次删除)
     */
    private void checkNotRepeat(HttpServletRequest request) throws ServiceException {
        String notRepeat = request.getHeader("notRepeat");
        //校验请求头中是否携带了notRepeat的key
        if (StringUtils.isBlank(notRepeat)) {
            throw new ServiceException(ExceptionEnum.BAD_PARAM);
        }
        //校验是否存在该key
        if (!redisUtils.hasKey(notRepeat)) {
            throw new ServiceException(ExceptionEnum.REPEAT_REQUEST);
        }
        //校验是否删除成功
        if ((redisUtils.deleteKey(notRepeat) < 1)) {
            throw new ServiceException(ExceptionEnum.SERVER_ERROR);
        }
    }


}

请求头中添加参数的方法:

$.ajax({
    url: prefix + '/addBill',
    type: 'POST',
    dataType: "json",
    headers:{"notRepeat":"NOT_REPEAT:13135541171:67fc4069-5041-4e35-91a0-8b850aff9ea5"}
    
    })

工具类

package com.demo.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

/**
 * @author zlw
 * Date : 2019/7/16
 * Description : Redis工具
 */
@Component
@Slf4j
public class RedisUtils {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    /**
     * 设置redis String类型数据
     * @param key
     * @param value
     */
    public void setStringKeyValue(String key,String value,Long timeout) {
        if (timeout > 0) {
            stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
        }else {
            stringRedisTemplate.opsForValue().set(key, value);
        }
    }

    /**
     * 根据key获取redis String类型的value
     * @param key
     * @return
     */
    public String getStringKeyValue(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }



    /**
     * 删除key   
     * @param key
     */
    public Long deleteKey(String key) {
      return stringRedisTemplate.execute(connection -> connection.del(key.getBytes()), true);
    }

    /**
     * 删除key  
     * @param key
     */
    public Long deleteKeys(String ...key) {
        return stringRedisTemplate.delete(Arrays.asList(key));
    }

    /**
     * 获得过期时间  默认秒
     * @param key
     * @return
     */
    public Long getExpireTime(String key) {
        return stringRedisTemplate.getExpire(key);
    }


    public Boolean hasKey(String key) {
        try {
            return stringRedisTemplate.hasKey(key);
        } catch (Exception e) {
            log.info("msg:{}",e.getMessage());
            return false;
        }
    }

}

配置类:MyMvcConfig.java

package com.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @Author: zlw
 * @Date: 2019/7/16 13:37
 */
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new NotRepeatInterceptor()).addPathPatterns("/**");
    }
}

其他自定义异常和枚举类等可以在项目中查看或自定义。

测试类

TestController.java

package com.demo.controller;

import com.demo.aop.NotRepeat;
import com.demo.pojo.User;
import com.demo.service.ITestService;
import com.demo.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

/**
 * @author zlw
 * @date 2019/7/16
 */
@RestController
@RequestMapping("test")
@Slf4j
public class TestController {

    private static final String NOT_REPEAT_PREFIX = "NOT_REPEAT";

    @Autowired
    private ITestService testService;


    @Autowired
    private RedisUtils redisUtils;

    /**
     * 通过在mapper.xml中写sql进行查询
     * @param username
     * @return
     */
    @GetMapping("userByUsername")
    public ResponseEntity<User> getUserByUsername(@RequestParam("username") String username){
        User user = testService.getUserByUsername(username);
        return ResponseEntity.ok(user);
    }

    /**
     * 通过通用mapper自带的方法进行查询
     * @param id
     * @return
     */
    @NotRepeat
    @GetMapping("userById2")
    public ResponseEntity<User> getUserById(@RequestParam("id") Integer id){
        User user = testService.getUserById(id);
        return ResponseEntity.ok(user);
    }

    /**
     * 获取重复提交标识
     * @param key
     * @return
     */
    @GetMapping("getNotRepeatToken")
    public ResponseEntity<String> getToken(@RequestParam("key") String key) {
        UUID uuid = UUID.randomUUID();
        StringBuilder str = new StringBuilder();
        String newKey = str.append(NOT_REPEAT_PREFIX).append(":" + key).append(":" + uuid).toString();
        redisUtils.setStringKeyValue(newKey,"1",30L);
        return ResponseEntity.ok(newKey);
    }




}

常见异常

  • 拦截器中无法注入service,利用BeanFactory手动注入service
java.lang.NullPointerException: null
	at com.demo.config.NotRepeatInterceptor.checkNotRepeat(NotRepeatInterceptor.java:63) ~[classes/:na]
	at com.demo.config.NotRepeatInterceptor.preHandle(NotRepeatInterceptor.java:43) ~[classes/:na]
	at org.springframework.web.servlet.HandlerExecutionChain.applyPreHandle(HandlerExecutionChain.java:136) ~[spring-webmvc-5.0.10.RELEASE.jar:5.0.10.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:986) ~[spring-webmvc-5.0.10.RELEASE.jar:5.0.10.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925) ~[spring-webmvc-5.0.10.RELEASE.jar:5.0.10.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974) ~[spring-webmvc-5.0.10.RELEASE.jar:5.0.10.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:866) ~[spring-webmvc-5.0.10.RELEASE.jar:5.0.10.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:635) ~[tomcat-embed-core-8.5.34.jar:8.5.34]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851) ~[spring-webmvc-5.0.10.RELEASE.jar:5.0.10.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) ~[tomcat-embed-core-8.5.34.jar:8.5.34]
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值