使用Interceptor

在Web程序中,注意到使用Filter的时候,Filter由Servlet容器管理,它在Spring MVC的Web应用程序中作用范围如下:

  1. ┌───────┐
  2. Filter1
  3. └───────┘
  4. ┌───────┐
  5. ─│Filter2│─
  6. └───────┘
  7. ┌─────────────────┐
  8. DispatcherServlet│<───┐
  9. └─────────────────┘
  10. ┌────────────┐
  11. ModelAndView││
  12. └────────────┘
  13. ┌───────────┐
  14. ├───>│Controller1│────┤
  15. └───────────┘
  16. ┌───────────┐
  17. └───>│Controller2│────┘
  18. └───────────┘

上图虚线框就是Filter2的拦截范围,Filter组件实际上并不知道后续内部处理是通过Spring MVC提供的DispatcherServlet还是其他Servlet组件,因为Filter是Servlet规范定义的标准组件,它可以应用在任何基于Servlet的程序中。

如果只基于Spring MVC开发应用程序,还可以使用Spring MVC提供的一种功能类似Filter的拦截器:Interceptor。和Filter相比,Interceptor拦截范围不是后续整个处理流程,而是仅针对Controller拦截:

  1. ┌───────┐
  2. Filter1
  3. └───────┘
  4. ┌───────┐
  5. Filter2
  6. └───────┘
  7. ┌─────────────────┐
  8. DispatcherServlet│<───┐
  9. └─────────────────┘
  10. ┌────────────┐
  11. ModelAndView
  12. └────────────┘
  13. ┌───────────┐
  14. ├─┼─>│Controller1│──┼─┤
  15. └───────────┘
  16. ┌───────────┐
  17. └─┼─>│Controller2│──┼─┘
  18. └───────────┘

上图虚线框就是Interceptor的拦截范围,注意到Controller的处理方法一般都类似这样:

  1. @Controller
  2. public class Controller1 {
  3. @GetMapping("/path/to/hello")
  4. ModelAndView hello() {
  5. ...
  6. }
  7. }

所以,Interceptor的拦截范围其实就是Controller方法,它实际上就相当于基于AOP的方法拦截。因为Interceptor只拦截Controller方法,所以要注意,返回ModelAndView后,后续对View的渲染就脱离了Interceptor的拦截范围。

使用Interceptor的好处是Interceptor本身是Spring管理的Bean,因此注入任意Bean都非常简单。此外,可以应用多个Interceptor,并通过简单的@Order指定顺序。我们先写一个LoggerInterceptor

  1. @Order(1)
  2. @Component
  3. public class LoggerInterceptor implements HandlerInterceptor {
  4. final Logger logger = LoggerFactory.getLogger(getClass());
  5. @Override
  6. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  7. logger.info("preHandle {}...", request.getRequestURI());
  8. if (request.getParameter("debug") != null) {
  9. PrintWriter pw = response.getWriter();
  10. pw.write("<p>DEBUG MODE</p>");
  11. pw.flush();
  12. return false;
  13. }
  14. return true;
  15. }
  16. @Override
  17. public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
  18. logger.info("postHandle {}.", request.getRequestURI());
  19. if (modelAndView != null) {
  20. modelAndView.addObject("__time__", LocalDateTime.now());
  21. }
  22. }
  23. @Override
  24. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
  25. logger.info("afterCompletion {}: exception = {}", request.getRequestURI(), ex);
  26. }
  27. }

一个Interceptor必须实现HandlerInterceptor接口,可以选择实现preHandle()postHandle()afterCompletion()方法。preHandle()是Controller方法调用前执行,postHandle()是Controller方法正常返回后执行,而afterCompletion()无论Controller方法是否抛异常都会执行,参数ex就是Controller方法抛出的异常(未抛出异常是null)。

preHandle()中,也可以直接处理响应,然后返回false表示无需调用Controller方法继续处理了,通常在认证或者安全检查失败时直接返回错误响应。在postHandle()中,因为捕获了Controller方法返回的ModelAndView,所以可以继续往ModelAndView里添加一些通用数据,很多页面需要的全局数据如Copyright信息等都可以放到这里,无需在每个Controller方法中重复添加。

我们再继续添加一个AuthInterceptor,用于替代上一节使用AuthFilter进行Basic认证的功能:

  1. @Order(2)
  2. @Component
  3. public class AuthInterceptor implements HandlerInterceptor {
  4. final Logger logger = LoggerFactory.getLogger(getClass());
  5. @Autowired
  6. UserService userService;
  7. @Override
  8. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
  9. throws Exception {
  10. logger.info("pre authenticate {}...", request.getRequestURI());
  11. try {
  12. authenticateByHeader(request);
  13. } catch (RuntimeException e) {
  14. logger.warn("login by authorization header failed.", e);
  15. }
  16. return true;
  17. }
  18. private void authenticateByHeader(HttpServletRequest req) {
  19. String authHeader = req.getHeader("Authorization");
  20. if (authHeader != null && authHeader.startsWith("Basic ")) {
  21. logger.info("try authenticate by authorization header...");
  22. String up = new String(Base64.getDecoder().decode(authHeader.substring(6)), StandardCharsets.UTF_8);
  23. int pos = up.indexOf(':');
  24. if (pos > 0) {
  25. String email = URLDecoder.decode(up.substring(0, pos), StandardCharsets.UTF_8);
  26. String password = URLDecoder.decode(up.substring(pos + 1), StandardCharsets.UTF_8);
  27. User user = userService.signin(email, password);
  28. req.getSession().setAttribute(UserController.KEY_USER, user);
  29. logger.info("user {} login by authorization header ok.", email);
  30. }
  31. }
  32. }
  33. }

这个AuthInterceptor是由Spring容器直接管理的,因此注入UserService非常方便。

最后,要让拦截器生效,我们在WebMvcConfigurer中注册所有的Interceptor:

  1. @Bean
  2. WebMvcConfigurer createWebMvcConfigurer(@Autowired HandlerInterceptor[] interceptors) {
  3. return new WebMvcConfigurer() {
  4. public void addInterceptors(InterceptorRegistry registry) {
  5. for (var interceptor : interceptors) {
  6. registry.addInterceptor(interceptor);
  7. }
  8. }
  9. ...
  10. };
  11. }

如果拦截器没有生效,请检查是否忘了在WebMvcConfigurer中注册。

处理异常

在Controller中,Spring MVC还允许定义基于@ExceptionHandler注解的异常处理方法。我们来看具体的示例代码:

  1. @Controller
  2. public class UserController {
  3. @ExceptionHandler(RuntimeException.class)
  4. public ModelAndView handleUnknowException(Exception ex) {
  5. return new ModelAndView("500.html", Map.of("error", ex.getClass().getSimpleName(), "message", ex.getMessage()));
  6. }
  7. ...
  8. }

异常处理方法没有固定的方法签名,可以传入ExceptionHttpServletRequest等,返回值可以是void,也可以是ModelAndView,上述代码通过@ExceptionHandler(RuntimeException.class)表示当发生RuntimeException的时候,就自动调用此方法处理。

注意到我们返回了一个新的ModelAndView,这样在应用程序内部如果发生了预料之外的异常,可以给用户显示一个出错页面,而不是简单的500 Internal Server Error或404 Not Found。例如B站的错误页:

500

可以编写多个错误处理方法,每个方法针对特定的异常。例如,处理LoginException使得页面可以自动跳转到登录页。

使用ExceptionHandler时,要注意它仅作用于当前的Controller,即ControllerA中定义的一个ExceptionHandler方法对ControllerB不起作用。如果我们有很多Controller,每个Controller都需要处理一些通用的异常,例如LoginException,思考一下应该怎么避免重复代码?

练习

使用Interceptor - 图2下载练习:使用Interceptor (推荐使用IDE练习插件快速下载)

小结

Spring MVC提供了Interceptor组件来拦截Controller方法,使用时要注意Interceptor的作用范围。

读后有收获可以支付宝请作者喝咖啡,读后有疑问请加微信群讨论:

使用Interceptor - 图3 使用Interceptor - 图4