接口幂等性设计-拦截器+过滤器+redis
所需依赖:
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
一、Idempotent-接口幂等性自定义注解
package com.hanjuzi.hotel.common.utils.NoRepeatSubmit.annotate;
import java.lang.annotation.*;
/**
* @Classname NoRepeatSubmit
* @Description:接口幂等性自定义注解:使用拦截器+过滤器的方式实现
* @Date: 2023/4/14 0014 14:38
* @AUTHOR: 无泪之城
* @Version 1.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
/**
* 是否把 require 数据用来计算幂等key
*
* @return
*/
boolean require() default false;
/**
* 参与幂等性计算的字段,默认所有字段
*/
String[] values() default {};
/**
* 幂等性校验失效时间(毫秒)
*/
long expiredTime() default 10000;
}
二、RequestWrapper
package com.hanjuzi.hotel.common.utils.NoRepeatSubmit.annotate;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* @Classname RequestWrapper
* @Description:HttpServletRequest实现类
* 实现 HttpServletRequestWrapper ,将 request 中的 @RequestBody 取出来,
* 并重写 getReader() 和 getInputStream(),
* 重写 getInputStream() 方法时,
* 将标志位还原,让系统无感知我们取过值;
* @Date: 2023/4/14 0014 16:49
* @AUTHOR: 无泪之城
* @Version 1.0
*/
public class RequestWrapper extends HttpServletRequestWrapper {
private String body;
public RequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
InputStream inputStream = null;
try {
inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} else {
stringBuilder.append("");
}
} catch (IOException ex) {
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
body = stringBuilder.toString();
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream(), StandardCharsets.UTF_8));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(
body.getBytes(StandardCharsets.UTF_8));
ServletInputStream servletInputStream = new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
return servletInputStream;
}
public String getBody() {
return this.body;
}
}
三、ResponseResultInterceptor
package com.hanjuzi.hotel.common.utils.NoRepeatSubmit.annotate;
import com.alibaba.fastjson.JSONObject;
import com.hanjuzi.hotel.common.exception.RenException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @Classname ResponseResultInterceptor
* @Description:HandlerIntercepter的实现类
* 这一步,做的就是幂等性校验的具体步骤;
* 结合注解,把请求中的参数、IP,通过摘要加密的方式,生成一个唯一的key,保存到 redis 中,并设置过期时间,这样就可以在过期时间内,保证该请求指挥出现一次;
* @Date: 2023/4/14 0014 16:50
* @AUTHOR: 无泪之城
* @Version 1.0
*/
@Slf4j
@Component
public class ResponseResultInterceptor implements HandlerInterceptor {
@Resource
private RedisTemplate<String, String> redisTemplate;
private final static String REQUEST_URL = "url";
/**
* Controller逻辑执行之前 可以幂等性校验,防止重复提交
* <p>
* 注意:ServletRequest 中 getReader() 和 getInputStream() 只能调用一次,也就是 request 值取了一次,就无法再取
*
* @param request
* @param response
* @param handler
* @return boolean
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (request instanceof RequestWrapper) {
// 获取@RequestBody注解参数
RequestWrapper requestWrapper = (RequestWrapper) request;
String body = requestWrapper.getBody();
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Class<?> clazz = handlerMethod.getBeanType();
Method method = handlerMethod.getMethod();
if (clazz.isAnnotationPresent(Idempotent.class)) {
Idempotent annotation = clazz.getAnnotation(Idempotent.class);
validUrl(body, request, annotation);
} else if (method.isAnnotationPresent(Idempotent.class)) {
Idempotent annotation = method.getAnnotation(Idempotent.class);
validUrl(body, request, annotation);
}
}
}
return true;
}
/**
* 校验url,重复提交
*
* @param body
* @param request
* @param annotation
**/
private void validUrl(String body, HttpServletRequest request, Idempotent annotation) {
if (annotation.require()) {
String[] values = annotation.values();
Map<String, String[]> parameterMap = request.getParameterMap();
JSONObject jsonObject = JSONObject.parseObject(body);
jsonObject.put(REQUEST_URL, request.getRequestURL());
jsonObject.putAll(parameterMap);
Map<String, Object> stringObjectMap = sortByKey(jsonObject, values);
// 摘要加密
long expiredTime = annotation.expiredTime();
Boolean bool = redisTemplate.opsForValue().setIfAbsent(stringObjectMap.toString(), "1", expiredTime, TimeUnit.MILLISECONDS);
if (!bool){
throw new RenException("提交太频繁了,请勿重复提交!");
}
}
}
/**
* map 按 key 升序排序,只取 values 字段,values为空时,代表全部字段
*
* @param map
* @param values
*/
private Map<String, Object> sortByKey(Map<String, Object> map, String[] values) {
Boolean bool = values.length < 1;
Map<String, Object> result = new LinkedHashMap<>(map.size());
map.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEachOrdered(e -> {
if (bool || isCheckKey(e.getKey(), values)) {
result.put(e.getKey(), e.getValue());
}
});
return result;
}
/**
* 校验 key 是否存在 keys数组中
*
* @param key
* @param keys
* @return java.lang.Boolean
**/
private Boolean isCheckKey(String key, String[] keys) {
for (String value : keys) {
if (key.equals(value) || key.equals(REQUEST_URL)) {
return true;
}
}
return false;
}
}
四、ResubmitFilter
package com.hanjuzi.hotel.common.utils.NoRepeatSubmit.annotate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Objects;
/**
* @Classname ResubmitFilter
* @Description:Filter实现类
*这一步的作用,就是拦截请求,将我们自定义的 Request 注入进去,避开 getReader() 和 getInputStream() 只能调用一次的情况,因为做拦截,只是取值校验,不能影响后面的实际业务;
* @Date: 2023/4/14 0014 16:49
* @AUTHOR: 无泪之城
* @Version 1.0
*/
@Slf4j
public class ResubmitFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void destroy() {
Filter.super.destroy();
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
RequestWrapper requestWrapper = null;
try {
HttpServletRequest req = (HttpServletRequest) request;
// 排除GET请求,不做幂等性校验
if (!HttpMethod.GET.name().equals(req.getMethod())) {
requestWrapper = new RequestWrapper(req);
}
} catch (Exception e) {
log.warn("RequestWrapper Error:", e);
}
chain.doFilter((Objects.isNull(requestWrapper) ? request : requestWrapper),
response);
}
}
五、WebConfigurer
package com.hanjuzi.hotel.common.utils.NoRepeatSubmit.annotate;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/**
* @Classname WebConfigurer
* @Description:将过滤器、拦截器注入到系统
* @Date: 2023/4/14 0014 17:01
* @AUTHOR: 无泪之城
* @Version 1.0
*/
@Configuration
public class WebConfigurer implements WebMvcConfigurer {
@Resource
private ResponseResultInterceptor responseResultInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加自定义拦截器
registry.addInterceptor(responseResultInterceptor).addPathPatterns("/**");
}
@Bean
public FilterRegistrationBean servletRegistrationBean() {
//通过FilterRegistrationBean实例设置优先级可以生效
ResubmitFilter resubmitFilter = new ResubmitFilter();
FilterRegistrationBean<ResubmitFilter> bean = new FilterRegistrationBean<>();
//注册自定义过滤器
bean.setFilter(resubmitFilter);
//过滤器名称
bean.setName("resubmitFilter");
//过滤所有路径
bean.addUrlPatterns("/*");
//优先级,越低越优先
bean.setOrder(Ordered.LOWEST_PRECEDENCE);
return bean;
}
}
六、使用示例
@Idempotent(require = true)
@PostMapping("/add")
@ApiOperation("【酒店】-新增")
@Idempotent(require = true)
public Result<String> add(@RequestBody HotelDTO dto) throws IOException {
String msg=hotelService.add(dto);
return new Result<String>().ok(msg);
}
@PutMapping("/update")
@ApiOperation("【酒店】-修改")
@Idempotent(require = true)
public Result<String> update(@RequestBody HotelDTO dto) throws IOException {
String msg=hotelService.updateHotel(dto);
return new Result<String>().ok(msg);
}
标签:拦截器,return,request,redis,annotation,new,过滤器,import,public
From: https://www.cnblogs.com/xiaoguo-java/p/17328435.html