看本篇博客前应当先看完前面三篇,这一篇是基于前面三篇的知识点的整合。所以很多重复的代码这里就不写出了
后台通过拦截器和redis实现防重复提交,避免因为网络原因导致多次请求同时进入业务系统,导致数据错乱,也可以防止对外暴露给第三方的接口在业务尚未处理完的情况下重复调用。
首先引入fastjson
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.35</version>
</dependency>
新增一个幂等校验的注解
package com.xxx.util.core.annotation;
import javax.ws.rs.NameBinding;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
@NameBinding
public @interface Idempotent
{
/**
* 是否把body数据用来计算幂等key。如果没有登录信息,请设置这个值为true。主要用于第三方接入。
*
* @return
*/
boolean body() default false;
/**
* body里的哪些字段用来计算幂等key。body()为true时才有生效。如果这个为空,则计算整个body。主要用于第三方接入。<br/>
* <p>
* 字段命名规则:<br/>
* path: Like xpath, to find the specific value via path. Use :(Colon) to separate different key name or index.
* For example:
* JSON content:
* {
* "name": "One Guy",
* "details": [
* {"education_first": "xx school"},
* {"education_second": "yy school"},
* {"education_third": "zz school"},
* ...
* ],
* "loan": {"loanNumber":"1234567810","loanAmount":1000000},
* }
*
* To find the value of "name", the path="name".
* To find the value of "education_second", the path="details:0:education_second".
* To find the value of "loanNumber" , the path="loan:loanNumber".
* To find the value of "name" and "loanNumber" , the path="name","loan:loanNumber".
*
* @return
*/
String[] bodyVals() default {};
/**
* idempotent lock失效时间,in milliseconds。一些处理时间较长或者数据重复敏感的接口,可以适当设置长点时间。
*
* @return
*/
int expiredTime() default 60000;
}
默认不去读取body中的内容去做幂等,可以@Idempotent(body = true) 将body设为true开启
实现拦截器
package com.xxx.core.filter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.xxx.common.exception.FastRuntimeException;
import com.xxx.core.annotation.Idempotent;
import com.xxx.core.filter.request.HttpHelper;
import com.xxx.core.filter.request.RequestReaderHttpServletRequestWrapper;
import com.xxx.util.core.utils.SpringContextUtil;
import com.xxx.util.redis.SimpleLock;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import redis.clients.jedis.JedisCluster;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Objects;
import java.util.regex.Pattern;
public class IdempotentFilter extends HandlerInterceptorAdapter {
private final Logger logger = LoggerFactory.getLogger(IdempotentFilter.class);
private static final String IDEMPOTENT = "idempotent.info";
private static final String NAMESPACE = "idempotent";
private static final String NAMESPACE_LOCK = "idempotent.lock";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
logger.info("request请求地址path[{}] uri[{}]", request.getServletPath(),request.getRequestURI());
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
Idempotent ra = method.getAnnotation(Idempotent.class);
if (Objects.nonNull(ra)) {
logger.debug("Start doIdempotent");
int liveTime = getIdempotentLockExpiredTime(ra);
String key = generateKey(request, ra);
logger.debug("Finish generateKey:[{}]",key);
JedisCluster jedisCluster = getJedisCluster();
//上分布式锁 避免相同的请求同时进入调用jedisCluster.get(key) 都为null的情况
new SimpleLock(NAMESPACE_LOCK + key,jedisCluster).wrap(new Runnable() {
@Override
public void run() {
//判断key是否存在,如存在抛出重复提交异常,如果不存在 则新增
if (jedisCluster.get(key) == null){
jedisCluster.setex(key,liveTime,"true");
request.setAttribute(IDEMPOTENT, key);
}else {
logger.debug("the key exist : {}, will be expired after {} mils if not be cleared", key, liveTime);
throw new FastRuntimeException(20001,"请勿重复提交");
}
}
});
}
return true;
}
private int getIdempotentLockExpiredTime(Idempotent ra)
{
return ra.expiredTime();
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
try
{
//业务处理完成 删除redis中的key
afterIdempotent(request);
}
catch (Exception e)
{
// ignore it when exception
logger.error("Error after @Idempotent", e);
}
}
private void afterIdempotent(HttpServletRequest request) throws IOException
{
Object obj = request.getAttribute(IDEMPOTENT);
if (obj != null){
logger.debug("Start afterIdempotent");
String key = obj.toString();
JedisCluster jedisCluster = getJedisCluster();
if (StringUtils.isNotBlank(key) && jedisCluster.del(key) == 0)
{
logger.debug("afterIdempotent error Prepared to delete the key:[{}] ",key);
}
logger.debug("End afterIdempotent");
}
}
/**
* generate key
*
* @param request
* @param ra
* @return
*/
public String generateKey(HttpServletRequest request, Idempotent ra)
{
String requestURI = request.getRequestURI();
String requestMethod = request.getMethod();
StringBuilder result = new StringBuilder(NAMESPACE);
String token = request.getHeader("H-User-Token");
append(result, requestURI);
append(result, requestMethod);
append(result, token);
appendBodyData( request, result, ra);
logger.debug("The raw data to be generated key: {}", result.toString());
return DigestUtils.sha1Hex(result.toString());
}
private void appendBodyData(HttpServletRequest request, StringBuilder src,
Idempotent ra)
{
if (Objects.nonNull(ra))
{
boolean shouldHashBody = (boolean) ra.body();
logger.debug("Found attr for body in @Idempotent, the value is {}", shouldHashBody);
if (shouldHashBody)
{
String data = null;
try {
data = HttpHelper.getBodyString(new RequestReaderHttpServletRequestWrapper(request));
} catch (IOException e) {
logger.warn("Found attr for body in @Idempotent, but the body is blank");
return;
}
if (StringUtils.isBlank(data))
{
logger.warn("Found attr for body in @Idempotent, but the body is blank");
return;
}
String[] bodyVals = ra.bodyVals();
// bodyVals优先
if (Objects.nonNull(bodyVals) && bodyVals.length != 0)
{
logger.debug("Found attr for bodyVals in @Idempotent, the value is {}", Arrays.asList(bodyVals));
final String finalData = data;
Arrays.asList(bodyVals).stream().sorted().forEach(e -> {
String val = getEscapedVal(finalData, e);
append(src, val);
});
}
else
{
append(src, data);
}
}
}
}
private String getEscapedVal(String json, String path)
{
String[] paths = path.split(":");
JSONObject jsonObject = null;
JSONArray jsonArray = null;
String nodeVal = json;
for (String fieldName : paths)
{
if (isInteger(fieldName)){
try {
jsonArray = JSONObject.parseArray(nodeVal);
nodeVal= jsonArray.get(Integer.parseInt(fieldName)).toString();
} catch (JSONException e) {//如果无法转为jsonArray 则说明不是数组尝试转为jsonObject去取值
logger.warn("getEscapedVal JSONObject.parseArray error nodeVal:[{}] fieldName:[{}]",nodeVal,nodeVal);
jsonObject = JSONObject.parseObject(nodeVal);
nodeVal = jsonObject.get(fieldName).toString();
}
}else {
jsonObject = JSONObject.parseObject(nodeVal);
nodeVal = jsonObject.get(fieldName).toString();
}
}
return nodeVal;
}
public static boolean isInteger(String str) {
Pattern pattern = Pattern.compile("^[-\\+]?[\\d]*$");
return pattern.matcher(str).matches();
}
private void append(StringBuilder src, String str)
{
if (!StringUtils.isBlank(str))
{
src.append("#").append(str);
}
}
//手动注入
public JedisCluster getJedisCluster() {
return SpringContextUtil.getBean(JedisCluster.class);
}
}
新建SpringContextUtil工具类
package com.xxx.util.core.utils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext; // Spring应用上下文环境
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtil.applicationContext = applicationContext;
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) throws BeansException {
return (T) applicationContext.getBean(name);
}
@SuppressWarnings("unchecked")
public static <T> T getBean(Class<?> clz) throws BeansException {
return (T) applicationContext.getBean(clz);
}
}
使用方式异常简单,如果可以根据请求头的内容做区分是否重复提交则直接使用@Idempotent
即可,如果是提供给第三方的接口 请求头无法哦按段需要指定body则@Idempotent(body = true,bodyVals = {“loan:loanNumber”})即可
- 案例代码如下
-
@Idempotent(body = true,bodyVals = {"loan:loanNumber"}) @PostMapping(Urls.Test.V1_ADD) @ResponseBody @ApiOperation(value = Urls.UserProfiles.V1_GET_USER_PROFILES_BY_PAGE_DESC) public Response add(@RequestBody Test test) { return null; }
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/124438.html