项目地址:
利用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]