SpringBoot集成SpirngMVC之拦截器

SpringMVC中的拦截器

一、概念

1.1、什么是拦截器

SpringMVC的处理器拦截器,类似于Servlet开发中的过滤器Filter,用于对处理器进行预处理和后处理。

依赖于web框架,在实现上基于Java的反射机制,属于面向切面编程(AOP)的一种运用。由于拦截器是基于web框架的调用,因此可以使用Spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个 controller生命周期之内可以多次调用。

拦截器作用

SpringMVC 框架中的拦截器用于对处理器进行预处理和后处理的技术。可以定义拦截器链,连接器链就是将拦截器按着顺序结成一条链,在访问被拦截的方法时,拦截器链中的拦截器会按着定义的顺序执行。

拦截器和过滤器的功能比较类似,有以下区别:

过滤器是 Servlet 规范的一部分,任何框架都可以使用过滤器技术;拦截器是 SpringMVC 框架独有的。

过滤器配置了 /*,可以拦截任何资源;
拦截器只会对控制器中的方法进行拦截。

拦截器也是 AOP 思想的一种实现方式。

拦截器与过滤器的区别

1)过滤器:
依赖于servlet容器。在实现上基于函数回调,可以对几乎所有请求进行过滤,但是缺点是一个过滤器实例只能在容器初始化时调用一次。使用过滤器的目的是用来做一些过滤操作,比如:在过滤器中修改字符编码;在过滤器中修改HttpServletRequest的一些参数,包括:过滤低俗文字、危险字符等。

2)拦截器:
依赖于web框架,在实现上基于Java的反射机制,属于面向切面编程(AOP)的一种运用。由于拦截器是基于web框架的调用,因此可以使用Spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个controller生命周期之内可以多次调用。

过滤器(filter):

1) filter属于Servlet技术,只要是web工程都可以使用
2) filter主要对所有请求过滤
3) filter的执行时机早于Interceptor

拦截器(interceptor)

1) interceptor属于SpringMVC技术,必须要有SpringMVC环境才可以使用
2) interceptor通常对处理器Controller进行拦截
3) interceptor只能拦截dispatcherServlet处理的请求

应用场景

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

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

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

4)通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取Locale、Theme信息等,只要是多个Controller中的处理方法都需要的,我们就可以使用拦截器实现。

二、HandlerInterceptor 定义

2.1、接口描述

直接看下SpringMVC中的接口:

public interface HandlerInterceptor {

/**
* 预处理回调方法,实现处理器的预处理,第三个参数为响应的处理器,自定义Controller
* 返回值:true表示继续流程(如调用下一个拦截器或处理器);false表示流程中断
* 不会继续调用其他的拦截器或处理器,此时我们需要通过response来产生响应
*/
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {

return true;
}

/**
* 后处理回调方法,实现处理器的后处理(渲染视图之前)
* 此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理
* modelAndView也可能为null,如API接口返回JSON数据时
*/
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}

/**
* 整个请求处理完毕回调方法,即在视图渲染完毕时回调
* 如性能监控中我们可以在此记录结束时间并输出消耗时间
* 还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中
*/
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}

}

2.2、MappedInterceptor

MappedInterceptor类是HandlerInterceptor接口的包装类,也是其子类。可以说是装饰器模式的一种应用

根据类说明:

包含并委托对HandlerInterceptor的调用,以及拦截器应应用的包含(和可选排除)路径模式。还提供匹配逻辑来测试拦截器是否应用于给定的请求路径。

在MappedInterceptor中可以看到几个属性的属性:

// 包含进来的路径
private final String[] includePatterns;

// 需要排除的路径
private final String[] excludePatterns;
// 拦截器
private final HandlerInterceptor interceptor;

// 路径匹配器。有针对性的对路径来进行匹配
private PathMatcher pathMatcher;

2.3、初始化过程

在RequestMappingHandlerMapping进行创建的时候,可以看到有在其中进行设置:

@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
  RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();
  mapping.setOrder(0);
  mapping.setInterceptors(getInterceptors());
  // ......
  return mapping;
}

可以看到获取得到拦截器。直接来到org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#getInterceptors方法中来:

protected final Object[] getInterceptors() {
  if (this.interceptors == null) {
    // 创建拦截器集合
    InterceptorRegistry registry = new InterceptorRegistry();
    // 添加到DelegatingWebMvcConfiguration对象中的configurers集合中来
    addInterceptors(registry);
    // 默认添加了两个拦截器对象
    registry.addInterceptor(new ConversionServiceExposingInterceptor(mvcConversionService()));
    registry.addInterceptor(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider()));
    this.interceptors = registry.getInterceptors();
  }
  return this.interceptors.toArray();
}

说明了RequestMappingHandlerMapping和Interceptor是存在着关系的。

其实不止这一处,因为RequestMappingHandlerMapping实现了ApplicationContextAware接口,那么必定重写setApplicationContext方法。

所以看下RequestMappingHandlerMapping类中的setApplicationContext方法:

public final void setApplicationContext(ApplicationContext context) throws BeansException {
  // ...
  this.initApplicationContext(context);
 // ...
}

直接来到:org.springframework.web.servlet.handler.AbstractDetectingUrlHandlerMapping#initApplicationContext方法中

public void initApplicationContext() throws ApplicationContextException {
    // 访问父类方法
    super.initApplicationContext();
    // 检测Handler
    detectHandlers();
}

先来看下父类中的initApplicationContext()方法

protected void initApplicationContext() throws BeansException {
    // 扩展方法!空实现
    extendInterceptors(this.interceptors);
    // 从IOC容器中获取得到所有MappedInterceptor类型的bean
    detectMappedInterceptors(this.adaptedInterceptors);
    // 初始化Interceptor
    initInterceptors();
}

看一下如何初始化Interceptor的

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");
            }
            // 添加到集合中来
            this.adaptedInterceptors.add(adaptInterceptor(interceptor));
        }
    }
}
protected HandlerInterceptor adaptInterceptor(Object interceptor) {
    // 通常都是这个类型
    if (interceptor instanceof HandlerInterceptor) {
        return (HandlerInterceptor) interceptor;
    }else if (interceptor instanceof WebRequestInterceptor) {
        return new WebRequestHandlerInterceptorAdapter((WebRequestInterceptor) interceptor);
    }else {
        throw new IllegalArgumentException("Interceptor type not supported" )
    }
}

此时将所有的拦截器保存到AbstractHandlerMapping类中的属性adaptedInterceptors集合中来

2.4、MappedInterceptor和HandlerInterceptor比较

关系 作用 特点 如何添加拦截器
HandlerInterceptor 一旦添加,表示拦截所有的HandlerMethod 1、创建RequestMappingHandlerMapping对象时,添加拦截器;
2、重写extendInterceptors方法添加拦截器;
3、添加配置MappedInterceptor类型的bean到容器中;
MappedInterceptor MappedInterceptor是HandlerInterceptor的子类 可以针对指定URL的HandlerMethod进行拦截 类是用final关键字修饰的,无法继承 配置成为web容器中的一个bean即可。

其实99%的情况下,我们在web中配置的都是MappedInterceptor类型的拦截器,但是为什么我们在写的时候都是在直接实现HandlerInterceptor接口呢?

而且在上面分析的时候,可以看到MappedInterceptor使用final关键字修饰的,无法进行继承,那么也就无法自己创建MappedInterceptor类型的bean了。

所以这个时候得利用Web中的配置类来配置拦截器了。

@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new XxxxIntecepter())
                // 需要包含的路径
                .addPathPatterns()
                // 需要排除的路径
                .excludePathPatterns()
                // 添加拦截器顺序
                .order(1);
    }
}

2.5、拦截器使用

2.5.1、MappedInterceptor

因为MappedInterceptor无法继承,但是又因为在初始化的时候需要找到MappedInterceptor类型的bean,所以肯定是在某个过程中间是将HandlerInterceptor给封装成了MappedInterceptor。

2.5.2、HandlerInterceptor

虽然步骤是一样的,但是使用起来还是很爽的。

下面先来列出步骤,然后在HandlerExecutionChain讲解完成之后来完成对应的案例。

2.5.2.1、创建拦截器
public class XxxxIntecepter implements HandlerInterceptor {
  // 有需要的重写三个方法
}
2.5.2.2、添加拦截器到容器中
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new XxxxIntecepter())
                // 需要包含的路径
                .addPathPatterns()
                // 需要排除的路径
                .excludePathPatterns()
                // 添加拦截器顺序
                .order(1);
    }
}

然后在HandlerExecutionChain执行阶段的时候,会遍历所有的拦截器,将符合路径的拦截器筛选出来,执行其中的方法。

所以有必要先来看下HandlerExecutionChain中的拦截器。

三、HandlerExecutionChain

HandlerExecutionChain是由HandlerMethod和HandlerIntecepter组成的。

3.1、拦截器的筛选

在根据请求找到handler的时候,发现返回的是HandlerExecutionChain。那么这里为什么呢?

因为找到了handler的时候,想要在在handler方法执行前后添加一些逻辑!这也是一种常理,有点类似AOP原理。

那么下面就来看下如何根据请求找到HandlerExecutionChain的。

直接来到org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler方法中来,找到下面这行代码:

Object handler = getHandlerInternal(request);
// ......
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

然后看下如何获取得到HandlerExecutionChain的:

protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
  // 判断handler,当然不是!所以创建一个执行器链条
  HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
  (HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
  // 获取得到URL,根据URL来进行筛选
  String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, LOOKUP_PATH);
  // 获取得到拦截器集合,循环判断
  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;
}

也就是说找到了handler之后,还需要找到所有的方法拦截器进行筛选,符合条件的才会添加到当前的HandlerExecutionChain中来。

具体的筛选规则:

  • 1、如果配置了路径过滤规则,那么就进行匹配;
  • 2、如果没有配置路径,那么就是拦截所有的;

3.2、拦截器的执行

直接来到org.springframework.web.servlet.DispatcherServlet#doDispatch方法中来,找到下面这行代码:

// 执行拦截器中的preHandle方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
  return;
}

// Actually invoke the handler.
// 执行目标方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// ......

// 执行拦截器中的postHandle方法
mappedHandler.applyPostHandle(processedRequest, response, mv);

也就是说,拦截器中的preHandle方法是在执行目标方法之前执行的,而拦截器中的postHandle方法是在执行目标方法之后执行的。

那么到此,还有一个方法是在哪里执行的呢?

对于controller中的HandlerMethod来说,是方法,只要是方法执行,那么就可能会出现问题。那么就有了两种情况:

  • 1、HandlerMethod执行正常;
  • 2、HandlerMethod执行异常;

所以springmvc分别对这两种情况做了处理:

HandlerMethod执行正常

这是在大的try...catch...中执行完成之后执行的。

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

HandlerMethod执行异常

这是在大的try...catch...finally中的catch和finally中的执行的。

通过这两种情况分析,也就是说,HandlerInterceptor类中的afterCompletion总是会执行的。

小结

1、如果拦截器的preHandle(前置方法)拦截(返回值为false),那么对于postHandle方法将不会执行;

2、如果拦截器的前置方法放行,那么后置和最终方法都将会执行;

四、拦截器执行顺序

如果存在多个拦截器,那么对应的执行顺序是怎样的呢?

preHandle方法执行顺序

直接看下源码:

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
  HandlerInterceptor[] interceptors = getInterceptors();
  if (!ObjectUtils.isEmpty(interceptors)) {
    for (int i = 0; i < interceptors.length; i++) {
      HandlerInterceptor interceptor = interceptors[i];
      if (!interceptor.preHandle(request, response, this.handler)) {
        triggerAfterCompletion(request, response, null);
        return false;
      }
      // 计数器!执行到了第几个拦截器记录下标
      this.interceptorIndex = i;
    }
  }
  return true;
}

获取得到拦截器集合,然后从第一个开始来进行执行。而一旦其中一个拦截器拦截了,那么将会直接执行对应的triggerAfterCompletion方法。

那么看一下对应的triggerAfterCompletion方法

void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex)
  throws Exception {
  // 获取得到拦截器!	
  HandlerInterceptor[] interceptors = getInterceptors();
  if (!ObjectUtils.isEmpty(interceptors)) {
    // 直接执行拦截器的最终方法!
    // 看下这里的for循环是反转着执行的
    for (int i = this.interceptorIndex; i >= 0; i--) {
      HandlerInterceptor interceptor = interceptors[i];
      try {
        interceptor.afterCompletion(request, response, this.handler, ex);
      }
      catch (Throwable ex2) {
        logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
      }
    }
  }
}

postHandle方法执行顺序

倒序执行:

void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
  throws Exception {

  HandlerInterceptor[] interceptors = getInterceptors();
  if (!ObjectUtils.isEmpty(interceptors)) {
    // 同样的倒序执行
    for (int i = interceptors.length - 1; i >= 0; i--) {
      HandlerInterceptor interceptor = interceptors[i];
      interceptor.postHandle(request, response, this.handler, mv);
    }
  }
}

afterCompletion方法执行顺序

void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex)
  throws Exception {
  // 获取得到拦截器!	
  HandlerInterceptor[] interceptors = getInterceptors();
  if (!ObjectUtils.isEmpty(interceptors)) {
    // 直接执行拦截器的最终方法!
    // 看下这里的for循环是反转着执行的
    for (int i = this.interceptorIndex; i >= 0; i--) {
      HandlerInterceptor interceptor = interceptors[i];
      try {
        interceptor.afterCompletion(request, response, this.handler, ex);
      }
      catch (Throwable ex2) {
        logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
      }
    }
  }
}

执行图

拦截器全部放行

拦截器某个不放行

五、应用

5.1、记录controller中的接口耗时时间

  • 需求:记录一下controller中添加了@Fuck注解的方法的执行时间

思路:编写一个拦截器识别方法上存在@Fuck注解的拦截器;然后再编写一个统计时间的方法拦截器;

注解

@Target(value = {ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Fuck {
}

找到HandlerMethod方法上添加@Fuck注解的拦截器

/**
 * @author lg
 * @Description 只找到controller层中的HandlerMethod方法上存在@Fuck注解的方法
 * @date 2023/3/7 10:35
 */
public class CustomFuckIntecepter implements HandlerInterceptor {

    /**
     * 如果当前handlermehod方法上存在@Fuck就放行,否则不放行
     *
     * @param request  请求封装之后的对象;
     * @param response 响应封装的对象;
     * @param handler  如果是利用注解的话,那么就是HandlerMethod对象;
     * @return true,表示放行,进行下一步处理;false表示的是拦截
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            Fuck annotation = method.getAnnotation(Fuck.class);
            return annotation != null;
        }
        return false;
    }

    /**
     * 在HandlerMethod方法执行完成之后执行的操作
     *
     * @param request      请求封装之后的对象;
     * @param response     响应封装的对象;
     * @param handler      如果是利用注解的话,那么就是HandlerMethod对象;
     * @param modelAndView HandlerMethod对象执行完成之后的返回值
     * @throws Exception 方法抛出的异常
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    /**
     * @param request  请求封装之后的对象;
     * @param response 响应封装的对象;
     * @param handler  如果是利用注解的话,那么就是HandlerMethod对象;
     * @param ex       抛出的异常信息
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

统计时间拦截器

public class CountTimeIntercepter implements HandlerInterceptor {

  private static final Logger logger = LoggerFactory.getLogger(CountTimeIntercepter.class);

  private static final ThreadLocal<Long> COUNT_TIME = new ThreadLocal<>();

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    long startTime = System.currentTimeMillis();
    COUNT_TIME.set(startTime);
    return true;
  }

  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    long endTime = System.currentTimeMillis();
    Long startTime = COUNT_TIME.get();
    long methodExecuteCostedTime = endTime - startTime;
    String requestURI = request.getRequestURI();
    String method = request.getMethod();
    logger.info("当前执行的接口路径是:{},请求方式是:{},耗时:{} ms",requestURI,method,methodExecuteCostedTime);
    COUNT_TIME.remove();
  }
}

添加到webconfig

@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    
    registry.addInterceptor(new CustomFuckIntecepter())
      // 需要包含的路径
      .addPathPatterns()
      // 需要排除的路径
      .excludePathPatterns()
      // 添加拦截器顺序
      .order(1);
    
    registry.addInterceptor(new CountTimeIntercepter())
      .addPathPatterns("/**")
      .order(2);
  }
}

六、总结

posted @ 2022-05-16 00:46  雩娄的木子  阅读(372)  评论(0编辑  收藏  举报