SpringBoot之HandlerInterceptor拦截器的使用

SpringBoot之HandlerInterceptor拦截器的使用

过滤器和拦截器的理解

拦截器是在servlet之前运行的

HandlerInterceptor简介

拦截器我想大家都并不陌生,最常用的登录拦截、或是权限校验、或是防重复提交、或是根据业务像12306去校验购票时间,总之可以去做很多的事情。
我仔细想了想
这里我分三篇博客来介绍HandlerInterceptor的使用,从基本的使用、到自定义注解、最后到读取body中的流解决无法多次读取的问题。

1、定义实现类

定义一个Interceptor 非常简单方式也有几种,我这里简单列举两种

一个是通过实现接口,一个是通过继承.(这两种方式就是我们最常用的)

1、类要实现Spring 的HandlerInterceptor 接口
2、类继承实现了HandlerInterceptor 接口的类,例如 已经提供的实现了HandlerInterceptor 接口的抽象类HandlerInterceptorAdapter

这里我们通过实现Interceptor接口来进行举例

2、HandlerInterceptor方法介绍


	boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception;

	void postHandle(
			HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
			throws Exception;

	void afterCompletion(
			HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception;

SpringBoot之HandlerInterceptor拦截器的使用_第1张图片

  • preHandle:在业务处理器处理请求之前被调用。预处理,可以进行编码、安全控制、权限校验等处理;
  • postHandle:在业务处理器处理请求执行完成后,生成视图之前执行。后处理(调用了Service并返回ModelAndView,但未进行页面渲染),有机会修改ModelAndView (这个博主就基本不怎么用了);
  • afterCompletion:在DispatcherServlet完全处理完请求后被调用,可用于清理资源等。返回处理(已经渲染了页面);

接下来让我们来实现一个登陆 and 访问权限校验的拦截器吧

另一个人对该方法的解释

HandlerInterceptor接口主要定义了三个方法:
**1. boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handle)方法:**该方法将在请求处理之前进行调用,只有该方法返回true,才会继续执行后续的Interceptor和Controller,当返回值为true 时就会继续调用下一个Interceptor的preHandle 方法,如果已经是最后一个Interceptor的时候就会是调用当前请求的Controller方法;
**2.void postHandle (HttpServletRequest request, HttpServletResponse response, Object handle, ModelAndView modelAndView)方法:**该方法将在请求处理之后,DispatcherServlet进行视图返回渲染之前进行调用,可以在这个方法中对Controller 处理之后的ModelAndView 对象进行操作。
**3.void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex)方法:**该方法也是需要当前对应的Interceptor的preHandle方法的返回值为true时才会执行,该方法将在整个请求结束之后,也就是在DispatcherServlet 渲染了对应的视图之后执行。用于进行资源清理。

拦截器实现

Interceptor是SpringMVC的标准组件,Interceptor在被创建之后是天然运行在IOC容器之中的。

这句话的意思不就是我们使用拦截器要导入SpringMVC的依赖的意思嘛

Interceptor底层就是基于SpringAOP面向切面编程实现。

package com.xxx.core.filter;

import com.xxx.common.exception.FastRuntimeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class TestFilter extends HandlerInterceptorAdapter {
	private final Logger logger = LoggerFactory.getLogger(TestFilter.class);
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		logger.info("request请求地址path[{}] uri[{}]", request.getServletPath(),request.getRequestURI());
		//request.getHeader(String) 从请求头中获取数据
		//从请求头中获取用户token(登陆凭证根据业务而定)
		Long userId= getUserId(request.getHeader("H-User-Token"));
		if (userId != null && checkAuth(userId,request.getRequestURI())){
			return true;
		}
		//这里的异常是我自定义的异常,系统抛出异常后框架捕获异常然后转为统一的格式返回给前端, 其实这里也可以返回false
		throw new FastRuntimeException(20001,"No access");
	}

	/**
	 * 根据token获取用户ID
	 * @param userToken
	 * @return
	 */
	private Long getUserId(String userToken){
		Long userId = null;
		return userId;
	}

	/**
	 * 校验用户访问权限
	 * @param userId
	 * @param requestURI
	 * @return
	 */
	private boolean checkAuth(Long userId,String requestURI){
		return true;
	}

	@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 {}
}

新建WebAppConfigurer 实现WebMvcConfigurer接口

其实以前都是继承WebMvcConfigurerAdapter类 不过springBoot2.0以上 WebMvcConfigurerAdapter 方法过时,有两种替代方案:
1、继承WebMvcConfigurationSupport
2、实现WebMvcConfigurer
但是继承WebMvcConfigurationSupport会让Spring-boot对mvc的自动配置失效。根据项目情况选择。现在大多数项目是前后端分离,并没有对静态资源有自动配置的需求所以继承WebMvcConfigurationSupport也未尝不可。

@Configuration
public class WebAppConfigurer implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 可添加多个
        registry.addInterceptor(new TestFilter()).addPathPatterns("/**");
    }

    ....
}

其实下面还有很多方法我这里就省略了,过滤器可以添加多个,可以指定Path,这里的/**是对所有的请求都做拦截。

这里的一句话说/**是对所有的请求做拦截,但是我们要思考一下会不会拦截静态资源呢?

这里我觉得是不会的。

通过注解拦截一系列的任务

功能简介

拦截所有添加了我们自定义的注解的方法,并将userId和userMobile放入HttpServletRequest,之后通过对应的注解取值。

这句话的意思就是加了拦截注解以后,只要有注解的当我发送请求的时候都要经过拦截器

包格式

SpringBoot之HandlerInterceptor拦截器的使用_第2张图片

首先我们来先定义三个注解

根据需求其实UserId和UserMobile可以不要,不影响拦截器的使用

package com.xxx.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 UserAuthenticate
{
	/**
	 * 是否需要校验访问权限 默认不校验
	 *
	 * @return
	 */
	boolean permission() default false;

}

package com.xxx.core.annotation;

import java.lang.annotation.*;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UserId {
}

package com.xxx.core.annotation;

import java.lang.annotation.*;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UserMobile {
}

常量类

package com.xxx.core.handler;

public class HeaderCons {

    /**
     * 用户ID
     */
    public static final String USER_ID = "H-User-Id";

    /**
     * 用户手机号
     */
    public static final String USER_MOBILE = "H-User-Mobile";
}

拦截器

package com.xxx.core.filter;

import com.xxx.exception.FastRuntimeException;
import com.xxx.core.annotation.UserAuthenticate;
import com.xxx.core.handler.HeaderCons;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Objects;


public class TestFilter extends HandlerInterceptorAdapter {
	private final Logger logger = LoggerFactory.getLogger(TestFilter.class);
	@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();
		UserAuthenticate userAuthenticate = method.getAnnotation(UserAuthenticate.class);
		//如果没有加注解则userAuthenticate为null
		if (Objects.nonNull(userAuthenticate)) {
			Long userId= getUserId(request);
			//userAuthenticate.permission()取出permission判断是否需要校验权限
			if (userId == null || (userAuthenticate.permission() && !checkAuth(userId,request.getRequestURI()))){
				throw new FastRuntimeException(20001,"No access");
			}
		}
		return true;
	}

	/**
	 * 根据token获取用户ID
	 * @param request
	 * @return
	 */
	private Long getUserId(HttpServletRequest request){
		//添加业务逻辑根据token获取用户UserId
		request.getHeader("H-User-Token");
		Long userId = 1L;
		String userMobile = "18888888888";
		request.setAttribute(HeaderCons.USER_ID,userId);
		request.setAttribute(HeaderCons.USER_MOBILE,userMobile);
		return userId;
	}

	/**
	 * 校验用户访问权限
	 * @param userId
	 * @param requestURI
	 * @return
	 */
	private boolean checkAuth(Long userId,String requestURI){
		//添加业务逻辑根据UserId获取用户的权限组然后校验访问权限
		return true;
	}

	@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 {}
}

package com.xxx.core;

import com.xxx.core.filter.TestFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class WebAppConfigurer extends WebMvcConfigurerAdapter {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 可添加多个,这里选择拦截所有请求地址,进入后判断是否有加注解即可
            registry.addInterceptor(new TestFilter()).addPathPatterns("/**");
    }
}

如果不需要使用UserId和UserMobile这两个注解到这里已经结束了。不过为了方便业务层的使用直接获取用户的id、mobile等信息我这里就加上了

添加如下类即可取出我们在拦截器中set进去的值

package com.xxx.core.handler;

import com.xxx.core.annotation.UserId;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletRequest;


public class UserIdMethodArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(UserId.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
        return servletRequest.getAttribute(HeaderCons.USER_ID);
    }
}
package com.xxx.core.handler;

import com.xxx.core.annotation.UserMobile;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletRequest;


public class UserMobileMethodArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(UserMobile.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
        return servletRequest.getAttribute(HeaderCons.USER_MOBILE);
    }
}

以上类是根据你定义的注解来建设的取出放在request里面的值,如果有多个就再加就行了

package com.xxx.core.filter;


import com.xxx.core.annotation.UserId;
import com.xxx.core.annotation.UserMobile;
import com.xxx.core.handler.UserIdMethodArgumentResolver;
import com.xxx.core.handler.UserMobileMethodArgumentResolver;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import java.util.List;


@Configuration
public class FilterAutoConfiguration {


    @Configuration
    @ConditionalOnWebApplication
    @ConditionalOnClass({UserId.class, UserMobile.class})//多个用逗号隔开
    protected static class ArgumentResolverAutoConfiguration extends WebMvcConfigurerAdapter {
        protected ArgumentResolverAutoConfiguration() {
        }

        public void addArgumentResolvers(List argumentResolvers) {
        //可添加多个
            argumentResolvers.add(new UserIdMethodArgumentResolver());
            argumentResolvers.add(new UserMobileMethodArgumentResolver());
        }
    }
}

大功告成 接下来我们看看如何使用

package com.xxx.controller;


import com.xxx.common.response.Response;
import com.xxx.common.Urls;
import com.xxx.core.annotation.UserAuthenticate;
import com.xxx.core.annotation.UserId;
import com.xxx.core.annotation.UserMobile;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@Validated
@RestController
public class TestAuthController {
	@UserAuthenticate
	@GetMapping(value = Urls.Test.TEST)
	public Response testAuth(@UserId Long userId,@UserMobile String userMobile) {
		System.out.println("userId : "+ userId + "  userMobile :" + userMobile);
		return new Response();
	}
}

浏览器输入地址 后台打印
userId : 1 userMobile :18888888888

SpringBoot之HandlerInterceptor拦截器的使用 ——(三)@RequestBody获取请求参数解决java.io.IOException: Stream closed

现在开发的项目是基于SpringBoot的maven项目,拦截器的使用很多时候是必不可少的,当有需要需要你对body中的值进行校验,例如加密验签、防重复提交、内容校验等等。
当你开开心心的在拦截器中通过request.getInputStream();获取到body中的信息后,你会发现你在controller中使用了@RequestBody注解获取参数报如下错误

I/O error while reading input message; nested exception is java.io.IOException: Stream closed
org.springframework.http.converter.HttpMessageNotReadableException: I/O error while reading input message; nested exception is java.io.IOException: Stream closed
	at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:229)
	at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:150)
	at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:128)
	at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)
	at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:158)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:128)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:97)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:661)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.boot.web.filter.ApplicationContextHeaderFilter.doFilterInternal(ApplicationContextHeaderFilter.java:55)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.CorsFilter.doFilterInternal(CorsFilter.java:96)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.boot.actuate.trace.WebRequestTraceFilter.doFilterInternal(WebRequestTraceFilter.java:110)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)

IO流关闭只能读取一次,接下来我们就开始解决这个BUG,有两种大的方向:

  • 这个需求咱不做了,特么有本事就把东西都放在请求头里面传过来;
  • 解决流只能读取一次的问题,让它可以被多次重复读取;

考虑到生活还要继续,妻儿老小在家等着我回去告诉他们晚上吃冷面还是鸡,要这需求不给搞定那也只能去打包收拾东西滚蛋,回家路上顺便卖点血换钱了,我选择了第二种

最简单的方案就是 先读取流,然后在将流写进去就行了

引入包


	org.apache.tomcat.embed
	tomcat-embed-core
	8.5.15


新建HttpHelper (用于读取Body)

package com.xxx.util.core.filter.request;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;

public class HttpHelper {
    public static String getBodyString(HttpServletRequest request) throws IOException {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        BufferedReader reader = null;
        try {
            inputStream = request.getInputStream();
            reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
            String line = "";
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }
}

新建RequestReaderHttpServletRequestWrapper(防止流丢失)

package com.xxx.util.core.filter.request;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class RequestReaderHttpServletRequestWrapper extends HttpServletRequestWrapper{

    private final byte[] body;

    public RequestReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        body = HttpHelper.getBodyString(request).getBytes(Charset.forName("UTF-8"));
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {

        final ByteArrayInputStream bais = new ByteArrayInputStream(body);

        return new ServletInputStream() {

            @Override
            public int read() throws IOException {
                return bais.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }
        };
    }
}

新建HttpServletRequestReplacedFilter(过滤器)

package com.xxx.util.core.filter.request;


import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;


public class HttpServletRequestReplacedFilter implements Filter {
    @Override
    public void destroy() {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if(request instanceof HttpServletRequest) {
            requestWrapper = new RequestReaderHttpServletRequestWrapper((HttpServletRequest) request);
        }
        //获取请求中的流如何,将取出来的字符串,再次转换成流,然后把它放入到新request对象中。
        // 在chain.doFiler方法中传递新的request对象
        if(requestWrapper == null) {
            chain.doFilter(request, response);
        } else {
            chain.doFilter(requestWrapper, response);
        }
    }

    @Override
    public void init(FilterConfig arg0) throws ServletException {

    }
}

最后我们只需要在Application.java中加上如下代码注入过滤器即可

@Bean
	public FilterRegistrationBean httpServletRequestReplacedRegistration() {
		FilterRegistrationBean registration = new FilterRegistrationBean();
		registration.setFilter(new HttpServletRequestReplacedFilter());
		registration.addUrlPatterns("/*");
        registration.addInitParameter("paramName", "paramValue");
		registration.setName("httpServletRequestReplacedFilter");
		registration.setOrder(1);
		return registration;
	}

如下代码即可在拦截其中获取body且保证了controller中依旧可以再次获取

 HttpHelper.getBodyString(request);

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

首先引入fastjson


	com.alibaba
	fastjson
	1.2.35

新增一个幂等校验的注解

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。主要用于第三方接入。
*

* 字段命名规则:
* 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 getBean(String name) throws BeansException {
        return (T) applicationContext.getBean(name);
    }

    @SuppressWarnings("unchecked")
    public static  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://blog.csdn.net/zhibo_lv/article/details/81699360

你可能感兴趣的:(SpringMVC)