基于注解实现接口幂等性校验
1. 解决RequestBody参数无法重复读取问题
如果多次读取HTTP请求体会报错,因为会调用HttpServletRequest的getInputStream()方法和getReader()方法,而这两个方法总共只能被调用一次,第二次调用就会报错。
这里用RepeatedlyRequestWrapper 对HttpServletRequest重新包装。将HttpServletRequest的字节流的数据,保存到一个变量中,重写getInputStream()方法和getReader()方法,从变量中读取数据,返回给调用者。
新建HttpHelper,用于读取body
public class HttpHelper {
public static String getBodyString(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = request.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString();
}
}
新建RepeatedlyRequestWrapper,防止流丢失
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException
{
super(request);
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
body = HttpHelper.getBodyString(request).getBytes("UTF-8");
}
@Override
public BufferedReader getReader() throws IOException
{
return new BufferedReader(new InputStreamReader(getInputStream()));
}
// 重写了,核心:final ByteArrayInputStream bais = new ByteArrayInputStream(body);
@Override
public ServletInputStream getInputStream() throws IOException
{
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream()
{
@Override
public int read() throws IOException
{
return bais.read();
}
@Override
public int available() throws IOException
{
return body.length;
}
@Override
public boolean isFinished()
{
return false;
}
@Override
public boolean isReady()
{
return false;
}
@Override
public void setReadListener(ReadListener readListener)
{
}
};
}
}
实现过滤器,过滤要处理请求
public class RepeatableFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException
{
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException
{
ServletRequest requestWrapper = null;
if (request instanceof HttpServletRequest
&& StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE))
{
//包装了HttpServletRequest
requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);
}
if (null == requestWrapper)
{
chain.doFilter(request, response);
}
else
{
chain.doFilter(requestWrapper, response);
}
}
@Override
public void destroy()
{
}
}
注册过滤器
@Configuration
public class RepeatableFilterConfiguration {
@Bean
public FilterRegistrationBean httpServletRequestReplacedRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new RepeatableFilter());
registration.addUrlPatterns("/*");
registration.addInitParameter("paramName", "paramValue");
registration.setName("repeatableFilter");
registration.setOrder(1);
return registration;
}
}
2. 添加注解实现幂等性校验
新增Idempotent注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
/**
* 间隔时间(s),小于此时间视为重复提交
*/
long interval() default 60L;
/**
* 提示消息
*/
String message() default "不允许重复提交,请稍候再试";
}
幂等性校验
@Aspect
@Component
@AllArgsConstructor
@Slf4j
public class IdempotentAspect {
private RedisTemplate redisTemplate;
/**
* 缓存key
*/
private final String idempotentCacheKey = "idempotent:";
@Pointcut("@annotation(xxx.xxx.xxx.common.aop.Idempotent)")
public void pointCut() {
}
@Before("pointCut()")
public void checkIsRepeatSubmit(JoinPoint joinPoint) {
log.info("********开始幂等性校验********");
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Idempotent annotation = signature.getMethod().getAnnotation(Idempotent.class);
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
//判断是否重复提交
if (this.isRepeatSubmit(request, annotation.interval())) {
throw new RuntimeException(annotation.message());
}
}
/**
* 判断是否重复执行
* @param request 请求
* @param interval 过期时间
* @return 是否重复执行
*/
private boolean isRepeatSubmit(HttpServletRequest request, Long interval) {
String nowParams = "";
if (request instanceof RepeatedlyRequestWrapper)
{
RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
nowParams = HttpHelper.getBodyString(repeatedlyRequest);
}
// body参数为空,获取Parameter的数据
if (StrUtil.isEmpty(nowParams))
{
nowParams = JSONObject.toJSONString(request.getParameterMap());
}
String submitKey = DigestUtils.md5DigestAsHex(nowParams.getBytes());
// 唯一标识(指定key + 消息头)
String cacheRepeatKey = idempotentCacheKey + submitKey;
Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(cacheRepeatKey, "1", interval, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(setIfAbsent)) {
return false;
} else {
return true;
}
}
}