Spring之拦截器(HandlerInterceptor)

前言

在web开发中,拦截器是经常用到的功能,用于拦截请求进行预处理和后处理,一般用于以下场景:

  1. 日志记录,可以记录请求信息的日志,以便进行信息监控、信息统计、计算PV(Page View)等等。

  2. 权限检查:如登陆检测,进入处理器检测是否登陆,如果没有直接返回到登陆页面。

  3. 性能监控:有时候系统在某段时间莫名其妙的慢,可以通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间(如果有反向代理,如apache可以自动记录);

  4. 通用行为:读取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

想要自己配置一个拦截器,就必须用到HandlerInterceptorWebMvcConfigurer这两个接口。

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/"); //配置静态资源路径
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值