前言
在web开发中,拦截器是经常用到的功能,用于拦截请求进行预处理和后处理,一般用于以下场景:
-
日志记录,可以记录请求信息的日志,以便进行信息监控、信息统计、计算PV(Page View)等等。
-
权限检查:如登陆检测,进入处理器检测是否登陆,如果没有直接返回到登陆页面。
-
性能监控:有时候系统在某段时间莫名其妙的慢,可以通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间(如果有反向代理,如apache可以自动记录);
-
通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取Locale、Theme信息等,只要是多个处理器都需要的即可使用拦截器实现。
过滤器跟拦截器的区别
在说拦截器之前,不得不说一下过滤器,有时候往往被这两个词搞的头大。
其实我们最先接触的就是过滤器,还记得web.xml中配置的或者继承Filter
接口的类吗~
你应该知道spring mvc的拦截器是只拦截controller而不拦截jsp,html 页面文件的,如果想要拦截那怎么办?
这就用到过滤器filter了,filter是在servlet前执行的,你也可以理解成过滤器中包含拦截器,一个请求过来 ,先进行过滤器处理,看程序是否受理该请求 。 过滤器放过后,程序中的拦截器进行处理 。
(1)过滤器(Filter):当你有一堆东西的时候,你只希望选择符合你要求的某一些东西。定义这些要求的工具,就是过滤器。(理解:就是一堆字母中取一个B)
(2)拦截器(Interceptor):在一个流程正在进行的时候,你希望干预它的进展,甚至终止它进行,这是拦截器做的事情。(理解:就是一堆字母中,干预他,通过验证的少点,顺便干点别的东西)。
HandlerInterceptor简介
HandlerInterceptor是springMVC项目中的拦截器,它拦截的目标是请求的地址,比MethodInterceptor先执行。
拦截器是相对于Spring中来说的,它和过滤器不一样,过滤器的范围更广一些是相对于Tomcat容器来说的。拦截器可以对用户进行拦截过滤处理。
但是并不是说拦截器只对请求进入Controller控制器之前起作用,它也分为3个部分:
- 请求进入Controller之前,通过拦截器执行代码逻辑
- Controller执行之后(只是Controller执行完毕,视图还没有开始渲染),通过拦截器执行代码逻辑
- Controller完全执行完毕(整个请求全部结束),通过拦截器执行代码逻辑,可用于清理资源等。
HandlerInterceptor和WebMvcConfigurer
想要自己配置一个拦截器,就必须用到HandlerInterceptor
和WebMvcConfigurer
这两个接口。
HandlerInterceptor
作用:自定义拦截器
如何创建:自定义一个类实现HandlerInterceptor接口**并将它注入到Spring容器中。加上@Component注解或者在含有@Configuration注解的类中使用@Bean注入。
HandlerInterceptor源码如下:
package org.springframework.web.servlet;
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}
这三个方法都是干什么的,有什么作用,什么时候调用,不同的拦截器之间是怎样的调用顺序呢?先补一张图:
-
preHandle:此方法的作用是在请求进入到Controller进行拦截,有返回值。(返回true则将请求放行进入Controller控制层,false则请求结束返回错误信息)。可用于登录验证(判断用户是否登录);权限验证:判断用户是否有权访问资源(校验token)等。
-
postHandle:该方法是在Controller控制器执行完成但是还没有返回模板进行渲染拦截。没有返回值。就是Controller----->拦截------>ModelAndView。因此我们可以将Controller层返回来的参数进行一些修改,它就包含在ModelAndView中,所以该方法多了一个ModelAndView参数。
-
afterCompletion:该方法是在ModelAndView返回给前端渲染后执行。例如登录的时候,我们经常把用户信息放到ThreadLocal中,为了防止内存泄漏,就需要将其remove掉,该操作就是在这里执行的。
DispatcherServlet的doDispatch方法封装了springMVC处理请求的整个过程。首先根据请求找到对应的HandlerExecutionChain,它包含了处理请求的handler和所有的HandlerInterceptor拦截器;然后在调用hander之前分别调用每个HandlerInterceptor拦截器的preHandle方法,若有一个拦截器返回false,则会调用triggerAfterCompletion方法,并且立即返回不再往下执行;若所有的拦截器全部返回true并且没有出现异常,则调用handler返回ModelAndView对象;再然后分别调用每个拦截器的postHandle方法;最后,即使是之前的步骤抛出了异常,也会执行triggerAfterCompletion方法。关于拦截器的处理到此为止,接下来看看triggerAfterCompletion做了什么。
triggerAfterCompletion做的事情就是从当前的拦截器开始逆向调用每个拦截器的afterCompletion方法,并且捕获它的异常,也就是说每个拦截器的afterCompletion方法都会调用。
根据以上的代码,分析一下不同拦截器及其方法的执行顺序。假设有5个拦截器编号分别为12345,若一切正常则方法的执行顺序是12345的preHandle,54321的postHandle,54321的afterCompletion。若编号3的拦截器的preHandle方法返回false或者抛出了异常,接下来会执行的是21的afterCompletion方法。这里要注意的地方是,我们在写一个拦截器的时候要谨慎的处理preHandle中的异常,因为这里一旦有异常抛出就不会再受到这个拦截器的控制。12345的preHandle的方法执行过之后,若handler出现了异常或者某个拦截器的postHandle方法出现了异常,则接下来都会执行54321的afterCompletion方法,因为只要12345的preHandle方法执行完,当前拦截器的拦截器就会记录成编号5的拦截器,而afterCompletion总是从当前的拦截器逆向的向前执行。
WebMvcConfigurer
作用:添加拦截规则
如何创建:自定义一个类,实现WebMvcConfigurer并将它注入到Spring容器中。根据需求实现里面方法。
WebMvcConfigurer源码如下**:**
package org.springframework.web.servlet.config.annotation;
public interface WebMvcConfigurer {
default void configurePathMatch(PathMatchConfigurer configurer) {
}
default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
}
default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
}
default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
}
default void addFormatters(FormatterRegistry registry) {
}
default void addInterceptors(InterceptorRegistry registry) {
}
default void addResourceHandlers(ResourceHandlerRegistry registry) {
}
default void addCorsMappings(CorsRegistry registry) {
}
default void addViewControllers(ViewControllerRegistry registry) {
}
default void configureViewResolvers(ViewResolverRegistry registry) {
}
default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
}
default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
}
default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
}
default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
@Nullable
default Validator getValidator() {
return null;
}
@Nullable
default MessageCodesResolver getMessageCodesResolver() {
return null;
}
}
根据源码发现,这里面有很多方法,但是通常我们只用到了里面的两种方法如下:
addInterceptors
:添加拦截器,拦截器需要拦截的路径和需要排除拦截的路径都需要在其中配置
addResourceHandlers
:配置静态资源路径,即某些请求需要读取某个路径下的静态资源内容,需要配置该静态资源的路径,通过该方法可以统一给这些请求配置指定静态资源路径
拦截器实现流程
根据第二部分,我们知道了配置拦截器的准备工作。
1)自定义类实现HandlerInterceptor
下例是通过MDC给request中设置traceId。
@Component
public class TraceIdInterceptor implements HandlerInterceptor {
private static final String MDC_TRACE_ID_KEY = "traceId";
private static final String HEADER_TRACE_ID = "X-Trace-Id";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// MDC中已经有traceId了,那就不再重复设置
if (StringUtils.hasText(MDC.get(MDC_TRACE_ID_KEY))) {
return true;
}
// 如果外面过来的请求头中包含traceId,则直接使用它的
String traceId = request.getHeader(HEADER_TRACE_ID);
if (StringUtils.hasText(traceId)) {
MDC.put(MDC_TRACE_ID_KEY, traceId);
return true;
}
// 如果外面过来的请求中没有requestId,自己生成一个
MDC.put(MDC_TRACE_ID_KEY, generateTraceId());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) {
// 请求处理结束后,删掉traceId
MDC.remove(MDC_TRACE_ID_KEY);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("进入拦截器=======执行后,暂不增加业务逻辑处理========");
}
}
// 生成traceId
private String generateTraceId() {
String uuid = UUID.randomUUID().toString().replace("-", "");
long time = System.currentTimeMillis();
return uuid + "." + time;
}
2)自定义类实现WebMvcConfigurer
/**
* 此处也可以继承WebMvcConfigurationSupport
*/
@Configuration
public class WebAppConfig implements WebMvcConfigurer {
@Resource
private TraceIdInterceptor traceIdInterceptor;
/**
* 添加拦截规则, registry可以添加多个拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
List<String> patterns = new ArrayList<>();
patterns.add("/login");
registry.addInterceptor(traceIdInterceptor)
.addPathPatterns("/**") //所有的请求都要拦截。
.excludePathPatterns(patterns); //将不需要拦截的接口请求排除在外
}
/**
* 下面代码意思是:配置一个拦截器,如果访问路径是 addResourceHandler 中的这个路径(这里/**表示所有的路径),
* 那么就映射到访问本地的addResourceLocations这个路径上,这样就可以看到该路径上的资源了
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**") //配置需要添加静态资源请求的url
.addResourceLocations("classpath:/resources/static/"); //配置静态资源路径
}
}