在拦截器里放入参数 controller_SpringBoot 拦截器解析

SpringWeb框架中的拦截器作用类似于过滤器,都可以对一个请求进行拦截处理。

我们可以用拦截器做很多事情:

  • 日志记录:记录请求信息的日志,以便进行信息监控、信息统计等;
  • 权限检查:如登录校验,在处理器处理之前先判断是否已经登录;
  • 性能监控:通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间。
  • 通用行为:读取 Cookie 得到用户信息并将用户对象放入请求,从而方便后续流程使用。还有如提取 Locale、Theme 信息等,只要是多个处理器都需要的都可以用拦截器来实现。

SpringBoot中拦截器的接口名称是HandlerInterceptor,接口里有三个方法。

public 
  • preHandle

用来拦截处理器的执行,preHandle方法将在Controller处理之前调用的。SpringMVC里可以同时存在多个interceptor,它们基于链式方式调用,每个interceptor都根据其声明顺序依次执行。这种链式结构可以中断,当方法返回false时整个请求就结束了。

方法的返回值是布尔类型,方法如果返回false,那后面的interceptor和Controller都不会执行(通常都会响应一个自定义的 Http 错误码给客户端)。如果返回值为true,则接着调用下一个interceptor的preHandle方法。如果当前是最后一个interceptor,接下来就会直接调用Controller的处理方法。

  • postHandle:

postHandle 会在Controller方法调用之后,但是在DispatcherServlet 渲染视图之前调用。因此我们可以在这个阶段,对将要返回给客户端的ModelAndView进行操作。

  • afterCompletion:

afterCompletion在当前interceptor的preHandle方法返回true时才执行。该方法会在整个请求处理完成后被调用,就是DispatcherServlet渲染视图完成以后,主要是用来进行资源清理工作。需要注意的是,afterCompletion在interceptor链式结构中以相反的顺序执行,也就是说先申明的interceptor返回会后调用。

下面来看看如何在SpringBoot项目中如何自定义一个拦截器,实现这样的两个功能:登录拦截校验、记录方法的请求耗时。

登录校验拦截器:LoginCheckHandlerInterceptor

为了方便测试简化下场景,如果请求头中能获取到userId的值代表已经登录,否则表示未登录。如果没有登录,给客户端直接响应Http的401错误码,并且输出一个"please login"字符串,整个请求就结束不往下走。如果登录了,则把请求交给下一个拦截器处理。

@Slf4j

记录方法耗时拦截器:LogProcessTimeHandlerInterceptor

因为在Spring容器里拦截器是单例的,多个线程请求进来共用一个实例,是线程非安全的。为了能准确记录每个请求的处理时间,我们需要用ThreadLocal 。ThreadLocal将变量与线程绑定,提供线程局部变量,保证每个线程只能读到自己的数据。

@Slf4j

拦截器配置

@Configuration

为了方便测试,新建一个Controller。sayHello方法体内使用Thread.sleep随机睡眠一段时间,用来模拟其他业务操作处理耗时,最后返回一个"Hello"的打招呼字符串。

@RestController

现在万事俱备了,只差临门一脚来测试一下我们配置的拦截器是否有生效。

这里先验证一下未登录的场景下拦截器是否生效,打开postman请求"/sayHello"接口,此时请求头中没有设置userId,将得到一个"please login"响应字符串,拦截成功。

58ff72a2f524e7aeb2a988462e23ca7f.png

再验证登录的场景,在请求头添加 userId=666,重新发起请求,那这次请求将会成功,可以看到"Hello zhangsan"的响应。

f35e04af20d035a31c6d56b4acec44f6.png

然后在控制台上,可以看到日志记录了本次请求处理耗时,拦截器也生效了。

2020-07-30 13:41:55.611  INFO 54873 --- [nio-8012-exec-4] s.b.e.t.LogProcessTimeHandlerInterceptor : this request process time=3006 ms

最后通过控制台的日志输出,可以观察到各个拦截器的执行顺序。

2020-07-30 13:46:13.681  INFO 54873 --- [nio-8012-exec-6] s.b.e.t.LogProcessTimeHandlerInterceptor : execute LogProcessTimeHandlerInterceptor's preHandle method
2020-07-30 13:46:13.681  INFO 54873 --- [nio-8012-exec-6] c.s.b.e.t.LoginCheckHandlerInterceptor   : execute LoginCheckHandlerInterceptor's preHandle method
2020-07-30 13:46:13.683  INFO 54873 --- [nio-8012-exec-6] c.s.b.e.t.LoginCheckHandlerInterceptor   : execute LoginCheckHandlerInterceptor's postHandle method
2020-07-30 13:46:13.683  INFO 54873 --- [nio-8012-exec-6] s.b.e.t.LogProcessTimeHandlerInterceptor : execute LogProcessTimeHandlerInterceptor's postHandle method
2020-07-30 13:46:13.683  INFO 54873 --- [nio-8012-exec-6] c.s.b.e.t.LoginCheckHandlerInterceptor   : execute LoginCheckHandlerInterceptor's afterCompletion method
2020-07-30 13:46:13.683  INFO 54873 --- [nio-8012-exec-6] s.b.e.t.LogProcessTimeHandlerInterceptor : execute LogProcessTimeHandlerInterceptor's afterCompletion method
2020-07-30 13:46:13.683  INFO 54873 --- [nio-8012-exec-6] s.b.e.t.LogProcessTimeHandlerInterceptor : this request process time=2 ms

由于前面我们配置拦截器的时候,先申明LogProcessTimeHandlerInterceptor,然后再是LoginCheckHandlerInterceptor。

通过日志打印也证实,拦截器内方法的执行顺序依次是:preHandle ---> postHandle ---> afterCompletion。

preHandle方法是按拦截器申明的顺序来执行的,先申明的先执行,后申明的后执行。

postHandle、afterCompletion方法是以拦截器申明相反的顺序来执行,先申明的后执行。

最后,通过这一张图可以更直观的看到拦截器各个方法的调用顺序、拦截器与拦截器之间的先后执行顺序。

57ce0b5734043714a3b52b89e46fae8b.png