HandlerInterceptorブロッキングの使用(4)-重複コミット防止
まずfastjsonを導入
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.35</version>
</dependency>
べき乗等検査の注釈を追加
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。 。
*
* :
* 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に設定してredis関連ツールクラスを開く詳細:SpringBoot JedisCluster接続Redisクラスタ(分散プロジェクト)でbodyの内容を読み取るツールクラス詳細:requestBodyを取得してjavaを解決する.io.IOException: Stream closed
ブロッキングの実装
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ツールクラス
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;
}