过滤器和拦截器的理解
拦截器是在servlet之前运行的
拦截器我想大家都并不陌生,最常用的登录拦截、或是权限校验、或是防重复提交、或是根据业务像12306去校验购票时间,总之可以去做很多的事情。
我仔细想了想
这里我分三篇博客来介绍HandlerInterceptor的使用,从基本的使用、到自定义注解、最后到读取body中的流解决无法多次读取的问题。
定义一个Interceptor 非常简单方式也有几种,我这里简单列举两种
一个是通过实现接口,一个是通过继承.(这两种方式就是我们最常用的)
1、类要实现Spring 的HandlerInterceptor 接口
2、类继承实现了HandlerInterceptor 接口的类,例如 已经提供的实现了HandlerInterceptor 接口的抽象类HandlerInterceptorAdapter
这里我们通过实现Interceptor接口来进行举例
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;
接下来让我们来实现一个登陆 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,之后通过对应的注解取值。
这句话的意思就是加了拦截注解以后,只要有注解的当我发送请求的时候都要经过拦截器
根据需求其实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等信息我这里就加上了
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的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
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) {
}
};
}
}
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);
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);
}
}
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