【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源码分析

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

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

相关文章

力扣题单(小白友好)

力扣题单 算法小白自用题单,目前对于一些简单的数据结构感觉掌握的还可以,但是力扣很多题还是需要看题解,不够熟练;故整理了一份题单,用于巩固练习; 网上确实有很多对于算法分类讲解的网站,but:有一丢丢选择困难症,每天不知道该刷什么题,再加上网站对于一类题一般就有十几道题目…

人工智能技术的不当利用与风险

目录 前言1 视频篡改技术的滥用1.1 虚假信息传播与社会动荡1.2 对公众信任的破坏与舆论混乱 2 隐私泄露与监视风险2.1 个人信息安全与数据滥用风险2.2 社会稳定与个人自由权利的平衡 3 虚假评论与信息传播3.1 舆论操纵与社会意识形态的影响3.2 对信息可信度与公众信任的威胁 结…

雷龙发展Nand flash芯片试用体验

一、项目背景 最近自己开始准备了一个智能家居控制系统项目,需要包含室内的温湿度、空气质量、烟雾浓度以及气体含量,能够存储相应的数据,并进行显示。 Nand-flash存储器是flash存储器的一种,其内部采用非线性宏单元模式,为固态大容量内存的实现提供了廉价有效的解决方案…

阿里云ecs服务器配置反向代理上传图片

本文所有软件地址&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/12OSFilS-HNsHeXTOM47iaA 提取码&#xff1a;dqph 为什么要使用阿里云服务器&#xff1f; 项目想让别人通过外网进行访问就需要部署到我们的服务器当中 1.国内知名的服务器介绍 国内比较知名的一些…

一招让你的Mac重获新生!CleanMyMac专业软件助你清理系统垃圾,保障安全!

一、Mac用户必备神器——CleanMyMac 随着Mac的使用时间越来越长&#xff0c;系统中的垃圾文件也会越来越多&#xff0c;不仅占用宝贵的硬盘空间&#xff0c;还会影响系统的运行速度。这时候&#xff0c;一款专业的清理软件就显得尤为重要。CleanMyMac作为荣获多项大奖的清理软件…

录视频的软件推荐,助力视频内容创作

随着网络技术的发展和在线教育的兴起&#xff0c;录制视频教程的需求日益增加。无论是制作教学课程、分享办公技巧&#xff0c;还是录制游戏过程&#xff0c;一款好用的录屏软件都至关重要。本文将深入介绍三款录视频的软件&#xff0c;帮助读者了解它们的特点和操作步骤&#…

【Linux】回顾 C 文件接口

文章目录 1. 写文件2. 读文件3. 输出信息到显示器的几个方法4. stdin / stdout / stderr5. 打开文件的方式 1. 写文件 int main() {FILE* fp fopen("myfile", "w");if (!fp){printf("fopen error!\n");}const char* msg "hello world!\n…

自己录的视频怎么配上字幕?推荐几种方法

自己录的视频怎么配上字幕&#xff1f;在数字化时代&#xff0c;视频已经成为人们获取信息、娱乐消遣的重要形式。而对于许多内容创作者来说&#xff0c;为自己的视频添加字幕不仅能提升观众的观看体验&#xff0c;还能增加视频的专业度和吸引力。那么&#xff0c;如何为自己的…

初次文件包含漏洞

1.文件包含漏洞介绍 1.1.文件包含漏洞解释   文件包含漏洞就是使用函数去包含任意文件的时候&#xff0c;当包含的文件来源过滤不严谨的时候&#xff0c;当存在包含恶意文件后&#xff0c;就可以通过这个恶意的文件来达到相应的目的。 1.2.文件包含漏洞原理    其实原理就…

数据之谜:解读Facebook的用户行为

在当今数字化时代&#xff0c;社交媒体平台已经成为人们生活中不可或缺的一部分&#xff0c;而Facebook作为全球最大的社交网络之一&#xff0c;其背后隐藏着许多数据之谜。本文将深入探讨Facebook的用户行为&#xff0c;并试图解读其中的奥秘。 用户行为数据的收集 Facebook作…

【Python】进阶学习:一文解决如何从指定的源目录中,挑选出符合条件的文件,并将这些文件复制到目标目录中

【Python】进阶学习&#xff1a;一文解决如何从指定的源目录中&#xff0c;挑选出符合条件的文件&#xff0c;并将这些文件复制到目标目录中 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化…

spotify怎么购买会员?

Spotify&#xff0c;是一家瑞典在线音乐流媒体平台&#xff0c;主要服务除音乐外&#xff0c;包含播客、有声书及影片流服务。 Spotify于2006年4月&#xff0c;由Daniel Ek和Martin Lorentzon在瑞典创立&#xff0c;以数字版权管理保护的音乐为主要业务&#xff0c;用户规模截至…

出彩不停息!创维汽车SKYHOME又获国际大奖

祝贺&#xff01;创维汽车SKYHOME又获国际缪斯设计大奖&#xff01;进一步获得国际认可&#xff01; 卓越的意识、优秀的审美、无与伦比的专注&#xff0c;不仅是缪斯奖所看重的独特品质&#xff0c;也是SKYHOME设计团队在传递品牌故事中所秉持的优秀品格。作为缪斯奖青睐的设计…

ISP技术综述

原文来自技术前沿&#xff1a;ISP芯片终极进化——VP芯片&#xff08;AI视觉处理器&#xff09; 目录 1.计算机视觉的定义 2.与计算机视觉密切相关的概念与计算机视觉密切相关的概念有机器视觉&#xff0c;图像处理与分析&#xff0c;图像和视频理解。 3.计算机视觉的应用 …

【Python】flask框架请求体数据,文件上传,请求头信息获取方式案例汇总

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

uniapp打包H5,nginx部署无法读取/static目录下index.xxxxx.css

1、修改静态文件index.xxxxx.css名字&#xff0c;如a1.css 2、修改index.html对应的css引入文件 完事大工告成

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

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

camelot pdf提取表格实践(记录)

参考&#xff1a; 巧用Python的camelot库批量提取PDF发票信息 关于文本pdf的表格抽取 AttributeError: module ‘camelot‘ has no attribute ‘read_pdf‘及类似问题解决办法 camelot 参数 https://blog.csdn.net/INTSIG/article/details/123000010 报错解决&#xff1a; Mod…

初识JavaScript

1、JavaScript实现 JavaScript包含一下几个部分&#xff1a; 核心&#xff08;ECMAScript&#xff09;文档对象模型&#xff08;DOM&#xff09;游览器对象模型&#xff08;BOM&#xff09; 1.1ECMScript ECMAScript,即ECMA-262定义的语言&#xff0c;并不局限于web游览器&…

腾讯在线文档下载文档html格式

腾讯在线文档下载文档html格式 步骤 chrome 浏览器打开该文档&#xff08;edge不行&#xff09; 同时按住ctrlp快捷键调出腾讯文档内置的打印页面&#xff0c;打印范围要选择整个工作薄&#xff0c;纸张建议调大一点&#xff0c;边距建议较窄&#xff0c;缩放要选择宽度撑满&…