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

news2024/9/20 16:26:43

目录

一、拦截器介绍

二、拦截器 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.coloradmin.cn/o/1527995.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python之Web开发中级教程----ubuntu安装MySQL

Python之Web开发中级教程----ubuntu安装MySQL 进入/opt目录 cd /opt 更新软件源 sudo apt-get upgrade sudo apt-get update 3、安装Mysql server sudo apt-get install mysql-server 4、启动Mysql service mysql start 5、确认Mysql的状态 service mysql status 6、安装My…

天眼销批量查询功能上线

天眼销是一款提供企业线索的产品&#xff0c;致力于帮助客户获取最新的企业联系方式、工商信息等关键数据。 数据库收录全国 3.3亿及以上企业(含个体)线索&#xff0c;涵盖企业名称、企业状态、注册时间、注册资本、经营范围、法人信息、联系方式等维度&#xff0c;为用户提供…

免费SSL证书哪个更好

当下为了实现网站的https访问&#xff0c;很多的站点都会在自己的网站上部署使用SSL证书。 从2018年7月1日开始&#xff0c;Chrome将显示所有未使用SSL证书的网站标记为“不安全”&#xff0c;SSL证书&#xff0c;用于加密HTTP协议&#xff0c;也就是HTTPS。随着https的普及度…

智能合约 - ERC20介绍

什么是ERC20 ERC20全称为Ethereum Request for Comment 20&#xff0c;是一种智能合约标准&#xff0c;用于以太坊网络上的代币发行 姊妹篇 - 如何部署ERC20 ERC20的应用场景 代币化资产&#xff0c;例如&#xff1a;USDT 是一种以美元为背书的ERC20代币&#xff0c;每个USDT代…

adobe animate 时间轴找不到编辑多个帧按钮

如题&#xff0c;找了半天&#xff0c;在时间轴上找不到编辑多个帧按钮,导致无法批量处理帧 然后搜索发现原来是有些版本被隐藏了&#xff0c;需要再设置一下 勾选上就好了

一款基于 SpringCloud 开发的AI聊天机器人系统,已对接GPT-4.0,非常强大

简介 一个基于SpringCloud的Chatgpt机器人&#xff0c;已对接GPT-3.5、GPT-4.0、百度文心一言、stable diffusion AI绘图、Midjourney绘图。用户可以在界面上与聊天机器人进行对话&#xff0c;聊天机器人会根据用户的输入自动生成回复。同时也支持画图&#xff0c;用户输入文本…

赛昉(starFive)星光2 多媒体框架分析与功能验证

开发板 开发板长这个样子: 串口调试接口如下: 整体支持情况 驱动&firmware&API jh7110/soft_3rdpart/wave511 : H.264&H.265 Decoder (Chips&Media 芯媒)jh7110/soft_3rdpart/wave521 : H.264&H.265 Encoder (Chips&Media 芯媒)jh7110/soft_3rdp…

什么是CPU?CPU的性能指标是什么?

我们在就看一台笔记本电脑配置时&#xff0c;必然要关注CPU的型号与性能&#xff0c;那么你知道什么是CPU吗&#xff1f;CPU的性能指标又是什么呢&#xff1f;如何来衡量这款CPU的性能是不是很强大&#xff1f;我们来一起看一下&#xff01; 什么是CPU CPU&#xff0c;全称中央…

C++手写链表、反转链表、删除链表节点、遍历、为链表增加迭代器

本篇博客介绍如何使用C实现链表&#xff0c;首先编写一个简单的链表&#xff0c;然后增加模板&#xff0c;再增加迭代器。 简单链表的实现 链表的结构如下&#xff1a; 首先需要定义链表的节点&#xff1a; struct ListNode {int data;ListNode* pNext;ListNode(int value …

[Linux]互斥锁(什么是锁,为什么需要锁,怎么使用锁(接口),演示代码)

目录 一、锁的概念 一些需要了解的概念 什么是锁&#xff1f;为什么需要锁&#xff1f;什么时候使用锁&#xff1f;怎么定义锁&#xff1f; 二、锁的接口 1.初始化锁 2.加锁 3.申请锁 4.解锁 5.销毁锁 三、实践&#xff08;写代码&#xff09;&#xff1a;黄牛抢票 M…

C#开发中方法使用的问题注意

C#开发中&#xff0c;我们在进行方法内嵌时&#xff0c;需要注意方法回传带值时&#xff0c;我们需要对方法回传的值进行一个赋值传递 如下所示 console.WriteLine("请输入你的爱好&#xff1a;"); string aihao Console.ReadLine(); name ChangeData(name);同时在…

Legacy|电脑Windows系统如何迁移到新安装的硬盘?系统迁移详细教程!

前言 前面讲了很多很多关于安装系统、重装系统的教程。但唯独没有讲到电脑换了新的硬盘之后&#xff0c;怎么把旧系统迁移到新的硬盘上。 今天小白就来跟各位小伙伴详细唠唠&#xff1a; 开始之前需要把系统迁移的条件准备好&#xff0c;意思就是在WinPE系统下&#xff0c;可…

B004-springcloud alibaba 服务容错 Sentinel

目录 高并发带来的问题服务雪崩效应常见容错方案常见的容错思路隔离超时限流熔断降级 常见的容错组件 Sentinel入门什么是Sentinel微服务项目集成Sentinel核心库安装Sentinel控制台实现一个接口的限流 Sentinel的概念和功能基本概念重要功能 Sentinel规则流控规则三种流控模式三…

拒绝云测,热门猫主食冻干对比测评,希喂、SC、VE谁实力更强?

在当今的科学养宠时代&#xff0c;主食冻干已经成为了猫日常饮食不可或缺的一部分。高肉含量的主食冻干不仅易吸收、好消化&#xff0c;更能给猫提供其他猫粮所不能提供的微量物质&#xff0c;更满足猫的全面营养需求。然而&#xff0c;在众多品牌和口味的主食冻干中&#xff0…

抖音找人推广要给多少推广费?CloudNEO:9000+网红资源,助您品牌代言

抖音作为中国最受欢迎的短视频平台之一&#xff0c;吸引了众多用户的关注&#xff0c;也成为了企业推广的热门渠道。然而&#xff0c;很多人对于在抖音上找人推广需要支付多少推广费并不了解。下面让我们来解析一下抖音推广费用的计算方式。 1. 推广形式&#xff1a; 首先&…

ISP技术综述

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

[自研开源] MyData v0.7.3 更新日志

开源地址&#xff1a;gitee | github 详细介绍&#xff1a;MyData 基于 Web API 的数据集成平台 部署文档&#xff1a;用 Docker 部署 MyData 使用手册&#xff1a;MyData 使用手册 试用体验&#xff1a;https://demo.mydata.work 交流Q群&#xff1a;430089673 介绍 MyData …

git常见使用

1. 概念 分布式&#xff0c;有远程仓库和本地仓库的概念&#xff0c;因此要注意同步问题git是面向对象的&#xff0c;本质是内容寻址系统。.git目录下有个文件夹objects&#xff0c;存储git库中的对象&#xff0c;git就是根据object建立一种树形结构&#xff0c;将文件和通过h…

数据库应用:Linux 部署 GaussDB

目录 一、实验 1.环境 2.Linux 部署 GaussDB 3.Linux 使用 GaussDB 4.使用 GaussDB 进行表与索引操作 5.使用 GaussDB 进行视图操作 6.使用 GaussDB 进行联表查询 7.使用 GaussDB 进行外键关联 二、问题 1.运行python脚本报错 2. 安装GaussDB 报错 3. install 安装…

广州大彩科技新品发布:大彩科技COF系列2.4寸串口屏发布!

一、产品介绍 此次发布的是S系列平台2.4寸COF超薄结构串口屏&#xff0c;分辨率为240*320&#xff0c;该平台采用了Cortex-M3内核的处理器&#xff0c;内置了2Mbyte PSRAM和64Mbit FLASH&#xff0c;是专为小尺寸串口屏设计的MCU&#xff0c;精简了外围电路。 该平台默认支持大…