HandlerInterceptorブロッキングの使用(4)-重複コミット防止

52671 ワード

このブログを見る前に、前の3編を見なければならない.この1編は前の3編の知識点の統合に基づいている.したがって,多くの重複コードはここではHandlerInterceptorブロッキングの使用(1)HandlerInterceptorブロッキングの使用(2)−カスタム注釈HandlerInterceptorブロッキングの使用(3)−要求パラメータbodyの情報を複数回取得したバックグラウンドはブロッキングとredisを介して重複コミット防止を実現する.ネットワークの原因で複数回の要求が同時に業務システムに入ることを回避し、データの乱れを招き、第三者に外部に露出したインタフェースが業務がまだ処理されていない場合に繰り返し呼び出されることを防止することもできる.
まず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;
	}