项目简介
●springboot
●redis
●@ApiIdempotentAnn注解 + 拦截器对请求进行拦截
●压测工具: jmeter
实现思路
为需要保证幂等性的每一次请求创建一个唯一标识token, 先获取token, 并将此token存入redis, 请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token:
●如果存在, 正常处理业务逻辑, 并从redis中删除此token, 那么, 如果是重复请求, 由于token已被删除, 则不能通过校验, 返回重复提交
●如果不存在, 说明参数不合法或者是重复请求, 返回提示即可
请求流程
当页面加载的时候通过接口获取token
当访问接口时,会经过拦截器,如果发现该接口有自定义的幂等性注解,说明该接口需要验证幂等性(查看请求头里是否有key=token的值,如果有,并且删除成功,那么接口就访问成功,否则为重复提交);如果发现该接口没有自定义的幂等性注解,放行。
代码
pom依赖
添加redis依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
自定义注解
即添加了该注解的接口要实现幂等性验证
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ApiIdempotentAnn { boolean value() default true; }
幂等性拦截器
package com.example.springbootdemointerfacemideng.intceptor; import com.example.springbootdemointerfacemideng.annotation.ApiIdempotentAnn; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter; import java.lang.reflect.Method; /** * @author CBeann * @create 2020-07-04 18:06 */ @Component public class ApiIdempotentInceptor extends HandlerInterceptorAdapter { @Autowired private StringRedisTemplate stringRedisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } final HandlerMethod handlerMethod = (HandlerMethod) handler; final Method method = handlerMethod.getMethod(); // 有这个注解 boolean methodAnn = method.isAnnotationPresent(ApiIdempotentAnn.class); if (methodAnn && method.getAnnotation(ApiIdempotentAnn.class).value()) { // 需要实现接口幂等性 boolean result = checkToken(request); if (result) { return super.preHandle(request, response, handler); } else { response.setContentType("application/json; charset=utf-8"); PrintWriter writer = response.getWriter(); writer.print("重复调用"); writer.close(); response.flushBuffer(); return false; } } return super.preHandle(request, response, handler); } @Override public void postHandle( HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { super.postHandle(request, response, handler, modelAndView); } private boolean checkToken(HttpServletRequest request) { String token = request.getHeader("token"); if (null == token || "".equals(token)) { // 没有token,说明重复调用或者 return false; } // 返回是否删除成功 return stringRedisTemplate.delete(token); } }
MVC配置文件
/** * @author chaird * @create 2020-09-23 16:13 */ @Configuration public class MVCConfig extends WebMvcConfigurerAdapter { @Autowired private ApiIdempotentInceptor apiIdempotentInceptor; @Override public void addInterceptors(InterceptorRegistry registry) { // 获取http请求拦截器 registry.addInterceptor(apiIdempotentInceptor).addPathPatterns("/*"); } }
接口层
package com.example.springbootdemointerfacemideng.controller; import com.example.springbootdemointerfacemideng.annotation.ApiIdempotentAnn; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; /** * @author chaird * @create 2020-09-23 15:47 */ @RestController public class ApiController { AtomicInteger num = new AtomicInteger(100); @Autowired private StringRedisTemplate stringRedisTemplate; /** * 前端获取token,然后把该token放入请求的header中 * * @return */ @GetMapping("/getToken") public String getToken() { String token = UUID.randomUUID().toString().substring(1, 9); stringRedisTemplate.opsForValue().set(token, "1"); return token; } /** * 主业务逻辑,num--,并且加了自定义接口 * * @return */ @GetMapping("/submit") @ApiIdempotentAnn public String rushB() { // num-- num.decrementAndGet(); return "success"; } /** * 查看num的值 * * @return */ @GetMapping("/getNum") public String getNum() { return String.valueOf(num.get()); } }
测试
(1)首先调用http://localhost:8080/getToken 获取token
(2)用JMeter测试
配置一百个线程在1秒内访问
(3)分析结果
说明只成功调用了一次http://localhost:8080/submit 接口
代码下载
Demooo/springboot-demo-interface-mideng at master · cbeann/Demooo · GitHub




