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

看本篇博客前应当先看完前面三篇,这一篇是基于前面三篇的知识点的整合。所以很多重复的代码这里就不写出了
后台通过拦截器和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开启
redis相关工具类详见:SpringBoot通过JedisCluster连接Redis集群(分布式项目)
读取body中的内容工具类详见:获取requestBody解决java.io.IOException: Stream closed

实现拦截器

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

SpringBoot之HandlerInterceptor拦截器的使用 ——(一)
SpringBoot之HandlerInterceptor拦截器的使用 ——(二)自定义注解
SpringBoot之HandlerInterceptor拦截器的使用 ——(三)获取请求参数解决java.io.IOException: Stream closed

  • 8
    点赞
  • 57
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
好的,以下是一个关于Spring Boot中的HandlerInterceptor拦截器的源码解析及使用的经验案例: 在Spring Boot中,HandlerInterceptor拦截器同样是一个非常重要的组件,它可以在请求处理之前或之后对请求进行拦截,并进行一些处理。下面我们来详细介绍HandlerInterceptor的源码和使用方法。 首先,我们需要实现HandlerInterceptor接口,该接口包含三个方法:preHandle、postHandle和afterCompletion。其中,preHandle方法在请求处理之前被调用,postHandle方法在请求处理之后被调用,而afterCompletion方法在视图渲染之后被调用。 在preHandle方法中,我们可以进行一些前置处理,比如对请求进行权限验证、登录验证等。如果验证失败,我们可以返回false,使请求不会被处理。如果验证成功,我们可以返回true,使请求继续被处理。 在postHandle方法中,我们可以对请求的响应进行一些处理,比如添加一些自定义的头信息、设置响应的状态码等。在afterCompletion方法中,我们可以进行一些资源的清理工作,比如关闭数据库连接、删除临时文件等。 接下来,我们需要将HandlerInterceptor注册到Spring Boot中,以便它能够被正确的调用。我们可以通过配置类的方式来实现。 在配置类中,我们需要继承WebMvcConfigurerAdapter类,并重写addInterceptors方法,如下所示: ``` @Configuration public class AppConfig extends WebMvcConfigurerAdapter { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**"); } } ``` 以上配置的含义是:将所有请求的路径都映射到MyInterceptor拦截器上。 最后,我们需要测试HandlerInterceptor是否生效。我们可以在拦截器中添加一些输出语句,比如System.out.println("Interceptor preHandle..."),以便我们观察到拦截器的工作情况。 在本案例中,我们介绍了Spring Boot中HandlerInterceptor拦截器的源码和使用方法,包括实现HandlerInterceptor接口、配置拦截器、添加拦截器等步骤。希望这个案例能够帮助你更好地理解和使用HandlerInterceptor拦截器
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhibo_lv

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值