【Spring MVC】Spring MVC拦截器(Interceptor)

目录

一、拦截器介绍

二、拦截器 Interceptor 定义

2.1 HandlerInterceptor接口

2.2 Spring MVC中提供的一些HandlerInterceptor接口实现类

1、AsyncHandlerInterceptor

2、WebRequestInterceptor

3、MappedInterceptor

4、ConversionServiceExposingInterceptor

三、拦截器 Interceptor 使用及配置

3.1 实现拦截器

3.2 配置拦截器

3.2.1 xml 文件配置

3.2.2 注解配置

3.2.3 API 配置

四、拦截器 Interceptor 的执行顺序

五、拦截器 Interceptor 原理分析

5.1 applyPreHandle():执行拦截器 preHandle 方法

5.2 applyPostHandle(): 执行拦截器 postHandle 方法

5.3 processDispatchResult(): 执行拦截器 afterCompletion 方法

六、拦截器的应用

6.1 性能监控

6.2 登陆检测

七、总结


一、拦截器介绍

Spring MVC中提供了处理器拦截器组件(Interceptor),拦截器在 Spring MVC 中的地位等同于 Servlet 规范中的过滤器 过滤器(Filter),用于对处理器进行预处理和后处理。

拦截器拦截的是处理器的执行,由于是全局行为,因此常用于做一些通用的功能,例如:

  1. 日志记录:记录请求信息的日志,以便进行信息监控、信息统计、计算PV(Page View)等。
  2. 权限检查:如登录检测,进入处理器检测检测是否登录,如果没有直接返回到登录页面;
  3. 性能监控:有时候系统在某段时间莫名其妙的慢,可以通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间(如果有反向代理,如apache可以自动记录);
  4. 通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取Locale、Theme信息等,只要是多个处理器都需要的即可使用拦截器实现。
  5. OpenSessionInView:如Hibernate,在进入处理器打开Session,在完成后关闭Session。

拦截器本质也是AOP(面向切面编程),也就是说符合横切关注点的所有功能都可以放入拦截器实现。

我们把 Spring MVC DispatcherServlet 请求处理流程这张图拿出来。

当浏览器发起的请求到达 Servlet 容器,DispatcherServlet 先根据处理器映射器 HandlerMapping 获取处理器,这时候获取到的是一个包含处理器和拦截器的处理器执行链,处理器执行之前将会先执行拦截器。

不包含拦截器的情况下,DispatcherServlet 处理请求的流程可以简化如下:

添加了拦截器做登录检查后,DispatcherServlet 请求处理的流程可以简化如下:

二、拦截器 Interceptor 定义

2.1 HandlerInterceptor接口

事实上拦截器的执行流程远比上述 DispatcherServelt 简化后的流程图复杂,它不仅可以在处理器之前执行,还可以在处理器之后执行。先看拦截器 Interceptor 在 Spring MVC 中的定义,Spring MVC拦截器的顶级接口为HandleInterceptor,定义了三个方法:1、preHandle(请求前) 2、postHandle(请求提交) 3、afterCompletion(请求完成后拦截)

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

我们可能注意到拦截器一共有3个回调方法,而一般的过滤器Filter才两个,这是怎么回事呢?马上分析。

  • preHandle:预处理回调方法,实现处理器的预处理(如登录检查),第三个参数为响应的处理器(如Controller实现);
    • 返回值:true表示继续流程(如调用下一个拦截器或处理器);false表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理器,此时我们需要通过response来产生响应;
  • postHandle:后处理回调方法,实现处理器的后处理(但在渲染视图之前),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。
  • afterCompletion:整个请求处理完毕回调方法,即在视图渲染完毕时回调,如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中preHandle返回true的拦截器的afterCompletion。

通过源码我们还可以发现,这三个方法都有的 handler 参数表示处理器,通常情况下可以表示我们使用注解 @Controller 定义的控制器。

对上面的流程图继续细化:

三个方法具体的执行流程如下:

  1. preHandle:处理器执行之前执行,如果返回 false 将跳过处理器、拦截器 postHandle 方法、视图渲染等,直接执行拦截器 afterCompletion 方法。
  2. postHandle:处理器执行后,视图渲染前执行,如果处理器抛出异常,将跳过该方法直接执行拦截器 afterCompletion 方法。
  3. afterCompletion:视图渲染后执行,不管处理器是否抛出异常,该方法都将执行。

注意:自从前后端分离之后,Spring MVC 中的处理器方法执行后通常不会再返回视图,而是返回表示 json 或 xml 的对象,@Controller 方法返回值类型如果为 ResponseEntity 或标注了 @ResponseBody 注解,此时处理器方法一旦执行结束,Spring 将使用 HandlerMethodReturnValueHandler 对返回值进行处理,具体来说会将返回值转换为 json 或 xml,然后写入响应,后续也不会进行视图渲染,这时postHandle 将没有机会修改响应体内容

如果需要更改响应内容,可以定义一个实现 ResponseBodyAdvice 接口的类,然后将这个类直接定义到 RequestMappingHandlerAdapter 中的 requestResponseBodyAdvice 或通过 @ControllerAdvice 注解添加到 RequestMappingHandlerAdapter。

2.2 Spring MVC中提供的一些HandlerInterceptor接口实现类

1AsyncHandlerInterceptor

继承HandlerInterceptor的接口,额外提供了afterConcurrentHandlingStarted方法,该方法是用来处理异步请求。当Controller中有异步请求方法的时候会触发该方法。 经过测试,异步请求先支持preHandle、然后执行afterConcurrentHandlingStarted。异步线程完成之后执行postHandle、afterCompletion。 有兴趣的读者可自行研究。

2WebRequestInterceptor

与HandlerInterceptor接口类似,区别是WebRequestInterceptor的preHandle没有返回值。还有WebRequestInterceptor是针对请求的,接口方法参数中没有response。

public interface WebRequestInterceptor {
    
    void preHandle(WebRequest request) throws Exception;
    void postHandle(WebRequest request, @Nullable ModelMap model) throws Exception;
    void afterCompletion(WebRequest request, @Nullable Exception ex) throws Exception;
}

AbstractHandlerMapping内部的interceptors是个Object类型集合。处理的时候判断为MappedInterceptor[加入到mappedInterceptors集合中];HandlerInterceptor、WebRequestInterceptor(适配成WebRequestHandlerInterceptorAdapter)[加入到adaptedInterceptors中]

3MappedInterceptor

一个包括includePatterns和excludePatterns字符串集合并带有HandlerInterceptor的类。 很明显,就是对于某些地址做特殊包括和排除的拦截器。

4ConversionServiceExposingInterceptor

默认的<annotation-driven/>标签初始化的时候会初始化ConversionServiceExposingInterceptor这个拦截器,并被当做构造方法的参数来构造MappedInterceptor。之后会被加入到AbstractHandlerMapping的mappedInterceptors集合中。该拦截器会在每个请求之前往request中丢入ConversionService。主要用于spring:eval标签的使用。

三、拦截器 Interceptor 使用及配置

3.1 实现拦截器

使用拦截器需要实现 HandlerInterceptor 接口,为了避免实现该接口的所有方法,Spring 5 之前提供了一个抽象的实现 HandlerInterceptorAdapter,Java 8 接口默认方法新特性出现后,我们直接实现 HandlerInterceptor 接口即可。

我们实现一个拦截器,示例如下:

public class LogInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("请求来了");
		 // ture表示放行
        return true;
    }
}

Spring 配置通常有三种方式,分别是传统的 xml 、最新的注解配置以及通过 API 配置,拦截器也不例外。

3.2 配置拦截器

3.2.1 xml 文件配置

springmvc.xml 配置方式如下:

<mvc:interceptors >
    <!-- 将拦截器类添加到Spring容器 -->
    <bean class="com.zzuhkp.mvc.interceptor.LogInterceptor"/>
    <!-- 设置拦截器 -->
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <mvc:exclude-mapping path="/login"/>
        <bean class="com.zzuhkp.mvc.interceptor.LoginInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>
  • bean:mvc:interceptors 标签下的拦截器 bean 将应用到所有的处理器。
  • mvc:interceptor:这个标签下的子标签可以指定拦截器应用到哪些请求路径。
    • mvc:mapping:指定要处理的请求路径。
    • mvc:exclude-mapping:指定要排除的请求路径。
    • bean:指定要应用到给定路径的拦截器 bean。

<mvc:interceptors>这个标签是被InterceptorsBeanDefinitionParser类解析。

这里配置的每个<mvc:interceptor>都会被解析成MappedInterceptor类型的Bean。

其中

  • 子标签<mvc:mapping path="/**"/>会被解析成MappedInterceptor的includePatterns属性;
  • <mvc:exclude-mapping path="/**"/>会被解析成MappedInterceptor的excludePatterns属性;
  • <bean/>会被解析成MappedInterceptor的interceptor属性。

3.2.2 注解配置

对于注解配置来说,需要将 MappedInterceptor 配置为 Spring 的 bean,和上述 xml 配置等价的注解配置如下:

// Configuration配置类
@Configuration
public class MvcConfig {
    // 将logInterceptor拦截器添加到Spring容器
    @Bean
    public MappedInterceptor logInterceptor() {
        return new MappedInterceptor(null, new LoginInterceptor());
    }

    // 将loginInterceptor拦截器添加到Spring容器
    @Bean
    public MappedInterceptor loginInterceptor() {
        // 在MappedInterceptor构造方法中可以传入拦截器的配置信息
        return new MappedInterceptor(new String[]{"/**"}, new String[]{"/login"}, new LoginInterceptor());
    }
}

MappedInterceptor实现了HandlerInterceptor接口。可用来设置拦截器的配置信息。

3.2.3 API 配置

拦截器与 Spring MVC 环境紧密结合,并且是作用范围通常是全局性的,因此大多数情况建议使用这种方式配置。

与 xml 配置对应的 API 配置如下:

@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
    
    // 重写添加拦截器的方法,来注册拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册拦截器
        registry.addInterceptor(new LogInterceptor());
        // 可传入拦截器配置信息
        registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**").excludePathPatterns("/login");
    }
}

这里在配置类上添加了@EnableWebMvc注解开启了 Spring MVC 中的某些特性,然后就可以实现 WebMvcConfigurer 接口中的 addInterceptors 方法向 Spring MVC 中添加拦截器。如果你使用了 spring-boot-starter-web,不再需要手工添加 @EnableWebMvc 注解。

四、拦截器 Interceptor 的执行顺序

通常情况下,我们并不需要关心多个拦截器的执行顺序,然而,如果一个拦截器依赖于另一个拦截器的执行结果,那么就需要注意了。使用多个拦截器后的 DispatcherServlet 请求处理流程可以简化为如下的流程图。

多个拦截器方法执行顺序如下:

  1. preHandle 按照拦截器的顺序先后执行。如果任意一次调用返回 false 则直接跳到拦截器的 afterCompletion 执行。
  2. postHandle 按照拦截器的逆序先后执行,也就说后面的拦截器先执行 postHandle。
  3. afterCompletion 也按照拦截器的逆序先后执行,后面的拦截器先执行 afterCompletion。

中断情况实例:

1 正常流程 

2 中断流程

中断流程中,比如是HandlerInterceptor2中断的流程(preHandle返回false),此处仅调用它之前拦截器的preHandle返回true的afterCompletion方法。 这个底层原理看后面的源码分析就明白了,主要是由this.interceptorIndex这个变量控制的。

那么拦截器的顺序是如何指定的呢?

  • 对于 xml 配置来说,Spring 将记录 bean 声明的顺序,先声明的拦截器将排在前面。
  • 对于注解配置来说,由于通过反射读取方法无法保证顺序,因此需要在方法上添加@Order注解指定 bean 的声明顺序。
  • 对应API配置来说,拦截器的顺序并非和添加顺序完全保持一致,为了控制先后顺序,需要自定义的拦截器实现Ordered接口。

注解配置指定顺序示例如下:

@Configuration
public class MvcConfig {
    @Order(2)
    @Bean
    public MappedInterceptor loginInterceptor() {
        return new MappedInterceptor(new String[]{"/**"}, new String[]{"/login"}, new LoginInterceptor());
    }
    
    @Order(1)
    @Bean
    public MappedInterceptor logInterceptor() {
        return new MappedInterceptor(null, new LoginInterceptor());
    }
    
}

此时虽然登录拦截器写在前面,但因为 @Order 注解指定的值较大,因此将排在日志拦截器的后面。

API配置指定顺序示例如下:

public class LoginInterceptor implements HandlerInterceptor, Ordered {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("已登录");
        return true;
    }

    @Override
    public int getOrder() {
        return 2;
    }
}

public class LogInterceptor implements HandlerInterceptor, Ordered {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("请求来了");
        return true;
    }

    @Override
    public int getOrder() {
        return 1;
    }
}

LogInterceptor 指定的排序号较 LoginInterceptor 来说比较小,因此 LogInterceptor 将排在前面。

五、拦截器 Interceptor 原理分析

DispatcherServlet 处理请求的代码位于 DispatcherServlet#doDispatch 方法,关于处理器和拦截器简化后的代码如下:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    //...
       HandlerExecutionChain mappedHandler = null;
 
    try {
        try {
            ModelAndView mv = null;
            Object dispatchException = null;
            try {
                processedRequest = this.checkMultipart(request);
                multipartRequestParsed = processedRequest != request;
                // 1.获取处理器执行链(从 HandlerMapping 获取处理器链)
                mappedHandler = this.getHandler(processedRequest);
                if (mappedHandler == null) {
                    this.noHandlerFound(processedRequest, response);
                    return;
                }
                // 2.获取处理器适配器
                HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                //...
                //【3】.执行前置拦截器(拦截器 preHandle 执行(正序))
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }
                // 4.执行业务handler
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }
                this.applyDefaultViewName(processedRequest, mv);
                //【5】.执行后置拦截器(拦截器 postHandle 执行(逆序))
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            } catch (Exception var20) {
                dispatchException = var20;
            } catch (Throwable var21) {
                dispatchException = new NestedServletException("Handler dispatch failed", var21);
            }
            //【6】.渲染视图,处理页面响应,同时也会去执行最终拦截器(拦截器 afterCompletion 执行(逆序))
            this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
        } catch (Exception var22) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
        } catch (Throwable var23) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
        }
    }finally {
        //...
    }
}

可以看到,整体流程和我们前面描述是保持一致的。【3】、【5】、【6】步骤是对拦截器的执行处理,现在分别来查看第【3】、【5】、【6】步骤执行的具体方法的源码

5.1 applyPreHandle()执行拦截器 preHandle 方法

以拦截器预执行 preHandle 为例,看一下处理器执行链是怎么调用拦截器方法的。

HandlerExecutionChain.java

// 3.执行前置拦截器中的详细代码
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 获得本次请求对应的所有拦截器
    // getInterceptors()是HandlerExecutionChain中的方法,获取到的是当前处理器执行链中所有的拦截器,也就是和当前请求的处理器映射器绑定在一起的所有拦截器
    // 说明获得的拦截器都是用来拦截本次请求的,不会有别的请求的拦截器
    HandlerInterceptor[] interceptors = this.getInterceptors();

    if (!ObjectUtils.isEmpty(interceptors)) {
        // 按照拦截器顺序依次执行每个拦截器的preHandle方法。
        // 并且,interceptorIndex值会一次 + 1 (该值是给后面的最终拦截器使用的)
        for(int i = 0; i < interceptors.length; this.interceptorIndex = i++) {
            HandlerInterceptor interceptor = interceptors[i];
            // 只要每个拦截器不返回false,则继续执行,否则执行最终拦截器
            if (!interceptor.preHandle(request, response, this.handler)) {
                // 返回 false 则直接执行 afterCompletion
                this.triggerAfterCompletion(request, response, (Exception)null);
                return false;
            }
        }
    }
    // 最终返回true
    return true;
}

处理器链拿到拦截器列表后按照顺序(拦截器1、拦截器2)调用了拦截器的 preHandle 方法,如果返回 false 则跳到 afterCompletion 执行。

那处理器链中的拦截器的列表从哪来的呢?继续跟踪获取处理器链的方法DispatcherServlet#getHandler,可以发现获取处理器链的核心代码如下:

public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
        implements HandlerMapping, Ordered, BeanNameAware {

    protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {

        HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
                (HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
        String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, LOOKUP_PATH);
        // 将与当前处理器映射器绑定在一起的拦截器添加到处理器执行链中。this.adaptedInterceptors是AbstractHandlerMapping中的拦截器列表
        for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
            // 将通过注解创建的拦截器添加到处理器执行链中(MappedInterceptor类型的拦截器)
            if (interceptor instanceof MappedInterceptor) {
                MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
                if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
                    // 将与该处理器映射器绑定在一起的拦截器都加入到当前处理器执行链中
                    chain.addInterceptor(mappedInterceptor.getInterceptor());
                }
            // 将通过其他方式创建的拦截器添加到处理器执行链中
            } else {
                // 将与该处理器映射器绑定在一起的拦截器都加入到当前处理器执行链中
                chain.addInterceptor(interceptor);
            }
        }
        return chain;
    }
}

上面的源码显示Spring 创建处理器执行链 HandlerExecutionChain 后将 AbstractHandlerMapping 中拦截器列表 adaptedInterceptors 中的拦截器添加到了处理器执行链,那 AbstractHandlerMapping 中的拦截器列表中的拦截器又从哪来呢?

public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
        implements HandlerMapping, Ordered, BeanNameAware {
    private final List<Object> interceptors = new ArrayList<>();
    private final List<HandlerInterceptor> adaptedInterceptors = new ArrayList<>();
    
    // 该方法在创建HandlerMapping时,Spring会自动回调
    @Override
    protected void initApplicationContext() throws BeansException {
        extendInterceptors(this.interceptors);
        // 从Spring容器中获取全部拦截器
        detectMappedInterceptors(this.adaptedInterceptors);
        // 对拦截器进行适配,并且将其添加到AbstractHandlerMapping的adaptedInterceptors列表中
        initInterceptors();
    }

    // 从容器中获取拦截器
    protected void detectMappedInterceptors(List<HandlerInterceptor> mappedInterceptors) {
        mappedInterceptors.addAll(
                BeanFactoryUtils.beansOfTypeIncludingAncestors(
                        obtainApplicationContext(), MappedInterceptor.class, true, false).values());
    }

    // 拦截器适配
    protected void initInterceptors() {
        if (!this.interceptors.isEmpty()) {
            for (int i = 0; i < this.interceptors.size(); i++) {
                Object interceptor = this.interceptors.get(i);
                if (interceptor == null) {
                    throw new IllegalArgumentException("Entry number " + i + " in interceptors array is null");
                }
                // 将拦截器添加到AbstractHandlerMapping的adaptedInterceptors列表中
                this.adaptedInterceptors.add(adaptInterceptor(interceptor));
            }
        }
    }       
}

各种 HandlerMapping 的实现都继承了 AbstractHandlerMapping,HandlerMapping 被容器创建时将回调#initApplicationContext方法,这个方法回调时会从容器中查找类型为 MappedInterceptor 的拦截器,然后对拦截器进行适配,这个流程是针对使用注解来实现的拦截器(MappedInterceptor类型)。Spring MVC 中如果使用了 @EnableWebMvc ,HandlerMapping bean 被创建时会回调WebMvcConfigurer#addInterceptors方法直接将拦截器设置到 AbstractHandlerMapping 中的 interceptors成员属性中。

MappedInterceptor类型的拦截器会被加到mappedInterceptors集合中,HandlerInterceptor类型的会被加到adaptedInterceptors集合中,WebRequestInterceptor类型的会被适配成WebRequestHandlerInterceptorAdapter加到adaptedInterceptors集合中。

5.2 applyPostHandle()执行拦截器 postHandle 方法

HandlerExecutionChain.java

// 5.执行后置拦截器
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
    // 获得本次请求对应的所有拦截器
    HandlerInterceptor[] interceptors = this.getInterceptors();
    if (!ObjectUtils.isEmpty(interceptors)) {
        // 按逆序执行每个拦截器的postHandle方法,所以我们看到先执行的拦截器2的postHandle,再执行拦截器1的postHandle
        for(int i = interceptors.length - 1; i >= 0; --i) {
            HandlerInterceptor interceptor = interceptors[i];
            // 执行拦截器的postHandle方法
            interceptor.postHandle(request, response, this.handler, mv);
        }
    }
}

后置处理是按照拦截器顺序逆序处理的。

5.3 processDispatchResult()执行拦截器 afterCompletion 方法

DispatcherServlet.java

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
    //...
    if (mv != null && !mv.wasCleared()) {
        // 处理响应,进行视图渲染
        this.render(mv, request, response);
        if (errorView) {
            WebUtils.clearErrorRequestAttributes(request);
        }
    } else if (this.logger.isDebugEnabled()) {
        this.logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + this.getServletName() + "': assuming HandlerAdapter completed request handling");
    }

    if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
        if (mappedHandler != null) {
            // 6、执行最终拦截器
            mappedHandler.triggerAfterCompletion(request, response, (Exception)null);
        }
    }
}

其中,有一个render()方法,该方法会直接处理完response。然后则是触发triggerAfterCompletion方法去执行本次请求对应的所有拦截器的afterCompletion 方法:

HandlerExecutionChain.java

// 6、执行拦截器的最终方法
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) throws Exception {
    // 获得本次请求对应的所有拦截器
    HandlerInterceptor[] interceptors = this.getInterceptors();
    if (!ObjectUtils.isEmpty(interceptors)) {
        // 逆序执行每个拦截器(interceptorIndex为前置拦截器动态计算)的afterCompletion方法
        // 由于this.interceptorIndex当前标记着最后一个执行到的前置拦截器下表,然后这里又是逆序遍历,所以只有成功执行了preHandle方法的拦截器,才回去执行其对应的afterCompletion方法
        for(int i = this.interceptorIndex; i >= 0; --i) {
            HandlerInterceptor interceptor = interceptors[i];
            try {
                // 执行拦截器的afterCompletion方法
                interceptor.afterCompletion(request, response, this.handler, ex);
            } catch (Throwable var8) {
                logger.error("HandlerInterceptor.afterCompletion threw exception", var8);
            }
        }
    }
}

由此可以看到,拦截器的最终方法的执行也是按照倒叙来执行的,而且是在视图渲染之后。

六、拦截器的应用

这里我们讲几个拦截器最常见的应用。

6.1 性能监控

如记录一下请求的处理时间,得到一些慢请求(如处理时间超过500毫秒),从而进行性能改进,一般的反向代理服务器如apache都具有这个功能,但此处我们演示一下使用拦截器怎么实现。 

实现分析:

  1. 在进入处理器之前记录开始时间,即在拦截器的preHandle记录开始时间;
  2. 在结束请求处理之后记录结束时间,即在拦截器的afterCompletion记录结束实现,并用结束时间-开始时间得到这次请求的处理时间。

问题:

我们的拦截器是单例的,因此不管用户请求多少次都只有一个拦截器实现,即线程不安全,那我们应该怎么记录时间呢?

解决方案是使用ThreadLocal,它是线程绑定的变量,提供线程局部变量(一个线程一个ThreadLocal,A线程的ThreadLocal只能看到A线程的ThreadLocal,不能看到B线程的ThreadLocal)。 

代码实现:

public class StopWatchHandlerInterceptor extends HandlerInterceptorAdapter {  
    
    private NamedThreadLocal<Long>  startTimeThreadLocal =  new NamedThreadLocal<Long>("StopWatch-StartTime");  
    @Override  
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1、开始时间    
        long beginTime = System.currentTimeMillis();
        // 线程绑定变量(该数据只有当前请求的线程可见)  
        startTimeThreadLocal.set(beginTime);
        // 继续流程  
        return true;
    }  
      
    @Override  
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 2、结束时间    
        long endTime = System.currentTimeMillis();
        // 得到线程绑定的局部变量(开始时间)  
        long beginTime = startTimeThreadLocal.get();
        // 3、消耗的时间  
        long consumeTime = endTime - beginTime;
        // 此处认为处理时间超过500毫秒的请求为慢请求  
        if(consumeTime > 500) {
            // TODO 记录到日志文件  
            System.out.println(String.format("%s consume %d millis", request.getRequestURI(), consumeTime));  
        }          
    }  
}

NamedThreadLocal:Spring提供的一个命名的ThreadLocal实现。 

在测试时需要把stopWatchHandlerInterceptor拦截器的排序设置成1,也就是放在拦截器链的第一个,这样得到的时间才是比较准确的。 

6.2 登陆检测

在访问某些资源时(如订单页面),需要用户登录后才能查看,因此需要进行登录检测。 

流程:

  1. 访问需要登录的资源时,由拦截器重定向到登录页面;
  2. 如果访问的是登录页面,拦截器不应该拦截;
  3. 用户登录成功后,往cookie/session添加登录成功的标识(如用户编号);
  4. 下次请求时,拦截器通过判断cookie/session中是否有该标识来决定继续流程还是到登录页面;
  5. 在此拦截器还应该允许游客访问的资源。 

拦截器代码如下所示:

@Override  
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
    // 1、请求到登录页面则放行  
    if(request.getServletPath().startsWith(loginUrl)) {  
        return true;  
    }  
          
    // 2、TODO 比如退出、首页等页面无需登录,即此处要放行 允许游客的请求  
          
    // 3、如果用户已经登录则放行    
    if(request.getSession().getAttribute("username") != null) {  
        // 更好的实现方式是使用cookie  
        return true;  
    }  
          
    // 4、非法请求,即这些请求需要登录后才能访问  
    // 重定向到登录页面  
    response.sendRedirect(request.getContextPath() + loginUrl);  
    return false;  
}

提示:推荐能使用servlet规范中的过滤器Filter实现相关功能的话,最好用Filter实现,因为HandlerInteceptor只有在Spring Web MVC环境下才能使用,因此Filter是最通用的、最先应该使用的。如登录这种拦截器最好使用Filter来实现。

七、总结

拦截器常用于初始化资源,权限监控,会话设置,资源清理等的功能设置。我们通过源码可以看到,拦截器类似于对我们业务方法的环绕通知效果,并且是通过循环收集好的拦截器集合来控制每个拦截器方法的执行顺序。要熟练运用拦截器,就需要我们对它的执行顺序完全掌握,做到深入掌握拦截器的执行机制!

总结 Spring MVC 整个拦截器相关的流程如下:

  1. HandlerMapping 被容器实例化并初始化。
    1. 初始化时默认从容器中查找类型为 MappedInterceptor 的拦截器添加到 HandlerMapping 中的拦截器列表,这种默认行为支持了 xml 和注解配置拦截器。
    2. 使用 @EnableWebMvc 注解后,Spring 通过 @Bean 创建 HandlerMapping bean,实例化后回调 WebMvcConfigurer#addInterceptors 将拦截器提前设置到 HandlerMapping 中的拦截器列表,这种行为支持了 API 配置拦截器。
  2. 客户端发起请求,DispatcherServlet 使用 HandlerMapping 查找处理器执行链,将 HandlerMapping 中的拦截器添加到处理器执行链 HandlerExecutionChain 中的拦截器列表。
  3. DispatcherServlet 按照拦截器的顺序依次调用拦截器中的回调方法。

相关文章:【Spring MVC】Spring MVC的执行流程与源码分析

                  【Spring MVC】处理器映射器:AbstractHandlerMethodMapping源码分析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.kler.cn/a/273248.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《我的AUTOSAR之路》ECUM(二) 唤醒处理

ECUM唤醒 1 EcuM 唤醒源2 EcuM 唤醒源配置3 Can 通道唤醒源调用解析1 EcuM 唤醒源 AUTOSAR 唤醒过程包含的步骤 检查唤醒源和上报唤醒时间唤醒源保护唤醒过程是独立于 EcuM 休眠阶段的,但是唤醒时间可以用于休眠阶段 在整个 Ecu 所有阶段,唤醒事件都可以存在唤醒不单单指 Ecu …

【Java】高级篇1:异常处理

异常&#xff1a;程序在执行过程中出现的非正常情况&#xff0c;如果不处理最终会导致JVM的非正常停止。 Java的异常抛出机制 Java异常体系 1、Throwable 2、Error和Exception 异常处理方式 1、try-catch-finally&#xff08;捕获异常&#xff09; 基本结构&#xff1a; 使用…

小迪安全42WEB攻防-通用漏洞文件包含LFIRFI伪协议

#知识点: 1、解释什么是文件包含 2、分类-本地LFI&远程RFI 3、利用-配合上传&日志&会话 4、利用-伪协议&编码&算法等 #核心知识: 1、本地包含LFI&远程包含RF1-区别 一个只能包含本地&#xff0c;一个可以远程加载 具体形成原因由代码和环境配置文件决定…

机器学习----特征缩放

目录 一、什么是特征缩放&#xff1a; 二、为什么要进行特征缩放&#xff1f; 三、如何进行特征缩放&#xff1a; 1、归一化&#xff1a; 2、均值归一化&#xff1a; 3、标准化&#xff08;数据需要符合正态分布&#xff09;&#xff1a; 一、什么是特征缩放&#xff1a; 通…

Mysql增删改查(详解)

1.新增 insert into 表名 values 新增字段 。 如图&#xff1a; 这里我一共添加了三条数据。 2.查询 2.1 全列查询 select * from 表名 。 如图&#xff1a; 这里的全列查询可以展示一个表中的全部的数据。 2.2 指定查询 select 要查询的字段名 from 表名 。 比如…

摄影第一课

色彩 红色绿色黄色 红色蓝色洋红 蓝色绿色青色 冷暖色 摄影基础 选择合适的前景&#xff0c;增加照片层次感 测光拍摄&#xff0c;照片有亮和暗的地方&#xff0c;立体感更强 拍摄技巧 拍摄倒影 手机靠近水面&#xff0c;距离越近拍到的倒影越多适当降低曝光、获得更加准…

阳光保险MySQL数据库平稳迁移OceanBase,稳定运营超700天

作者简介&#xff1a; 车东兴&#xff1a;于阳光保险就职&#xff0c;深耕保险行业的 IT 领域长达12 年&#xff0c;对保险领域的基础架构实践有深刻的理解与掌握。熟悉多款数据库&#xff0c;具有丰富的数据库运维经验。 王华城&#xff1a;于阳光保险就职&#xff0c;10多年一…

XDAG节点版本更新(0.6.5升级到0.7.0)

1、拉取最新的xdagj源码 mkdir /root/xdagj-0.7.0 && cd /root/xdagj-0.7.0 git clone https://github.com/XDagger/xdagj.git cd xdagj mvn clean package -Dmaven.test.skiptrue2、创建新的数据目录并解压程序包 mkdir /data/docker-compose/xdagj-7.0/bin -p cd /…

SpringBoot异常:类文件具有错误的版本 61.0, 应为 52.0的解决办法

问题&#xff1a; java: 无法访问org.mybatis.spring.annotation.MapperScan 错误的类文件: /D:/Program Files/apache-maven-3.6.0/repository/org/mybatis/mybatis-spring/3.0.3/mybatis-spring-3.0.3.jar!/org/mybatis/spring/annotation/MapperScan.class 类文件具有错误的…

辐射全国、面向世界、聚焦未来——华为(深圳)全球具身智能产业创新中心正式成立

3月15日&#xff0c;深圳市前海深港现代服务业合作区管理局&#xff08;以下简称“前海管理局”&#xff09;、深圳市宝安区人民政府、华为技术有限公司&#xff08;以下简称“华为”&#xff09;共同签署合作协议&#xff0c;宣布共建华为&#xff08;深圳&#xff09;全球具身…

LeetCode刷题记录:(11)组合(初识回溯算法)

leetcode传送通道 暂时记录&#xff0c;这篇没啥营养&#xff0c;不用看了 class Solution {List<List<Integer>> result new ArrayList<>(); // 存所有组合List<Integer> path new LinkedList<>(); //存每一个组合public List<List<Int…

前端路由跳转bug

路由后面拼接了id的千万不能取相近的名字&#xff0c;浏览器分辩不出&#xff0c;只会匹配前面的路径 浏览器自动跳转到上面的路径页面&#xff0c;即使在菜单管理里面配置了正确的路由 跳转了无数次&#xff0c;页面始终不对&#xff0c;检查了路由配置&#xff0c;没有任何问…

【iOS】——Blocks

文章目录 前言一、Blocks概要1.什么是Blocks 二、Block模式1.block语法2.block类型变量3.截获自动变量值4._Block修饰符5.截获的自动变量 三、Blocks的实现1.Block的实质2.截获自动变量值3._Block说明符4.Block存储域 前言 一、Blocks概要 1.什么是Blocks Blocks是C语言的扩…

Redis 八种常用数据类型详解

夯实基础&#xff0c;这篇文章带着大家回顾一下 Redis 中的 8 种常用数据类型&#xff1a; 5 种基础数据类型&#xff1a;String&#xff08;字符串&#xff09;、List&#xff08;列表&#xff09;、Set&#xff08;集合&#xff09;、Hash&#xff08;散列&#xff09;、Zse…

IDEA直接打包Docker镜像

以下为使用IDEA打包Docker镜像并推送到远程仓库&#xff08;使用Windows打包Docker镜像并推送到远程仓库&#xff09;教程 1 安装Docker Desktop 下载地址&#xff1a;https://www.docker.com/products/docker-desktop/ 安装成功后&#xff0c;可在cmd查看版本号 2 启动Do…

基于Qt 和python 的自动升级功能

需求&#xff1a; 公司内部的一个客户端工具&#xff0c;想加上一个自动升级功能。 服务端&#xff1a; 1&#xff0c;服务端使用python3.7 &#xff0c;搭配 fastapi 和uvicorn 写一个简单的服务&#xff0c;开出一个get接口&#xff0c;用于客户端读取安装包的版本&#…

微服务:高并发带来的问题的容错方案

1.相关脚本&#xff08;陈天狼&#xff09; 启动nacos客户端&#xff1a; startup.cmd -m standalone 启动sentinel控制台&#xff1a; # 直接使⽤jar命令启动项⽬(控制台本身是⼀个SpringBoot项⽬) java -Dserver.port8080 -Dcsp.sentinel.dashboard.serverlocalhost:808…

通过点击按钮实现查看全屏和退出全屏的效果

动态效果如图&#xff1a; 可以通过点击按钮&#xff0c;或者esc键实现全屏和退出全屏的效果 实现代码&#xff1a; <template><div class"hello"><el-button click"fullScreen()" v-if"!isFullscreen">查看全屏</el-butt…

Obsidian使用200+插件与70+种主题分享

主题资源 下载方式一&#xff1a; 网盘下载 密码:a3eu 下载方式二&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1fOgP8lY29sYxkUAbTlQQCw 提取码&#xff1a;qhxa 下载解压打开红色框文件夹 上面的是插件&#xff0c;下面的是主题 以下介绍安装主题 打开Obsidi…

苍穹外卖-day08:导入地址簿功能代码(单表crud)、用户下单(业务逻辑)、订单支付(业务逻辑,cpolar软件)

苍穹外卖-day08 课程内容 导入地址簿功能代码用户下单订单支付 功能实现&#xff1a;用户下单、订单支付 用户下单效果图&#xff1a; 订单支付效果图&#xff1a; 1. 导入地址簿功能代码&#xff08;单表crud&#xff09; 1.1 需求分析和设计 1.1.1 产品原型&#xff08…
最新文章