SpringBoot之HandlerInterceptor拦截器的使用(四)防重复提交

看本篇博客前应当先看完前面三篇,这一篇是基于前面三篇的知识点的整合。所以很多重复的代码这里就不写出了
后台通过拦截器和redis实现防重复提交,避免因为网络原因导致多次请求同时进入业务系统,导致数据错乱,也可以防止对外暴露给第三方的接口在业务尚未处理完的情况下重复调用。

1.首先引入fastjson

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.35</version>
</dependency>

2.新增一个幂等校验的注解

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开启
redis相关工具类详见:SpringBoot通过JedisCluster连接Redis集群(分布式项目)
读取body中的内容工具类详见:获取requestBody解决java.io.IOException: Stream closed

 3.实现拦截器

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);
	}
}

4.新建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;
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值