共计 5244 个字符,预计需要花费 14 分钟才能阅读完成。
在 Web 程序中,注意到使用 Filter 的时候,Filter 由 Servlet 容器管理,它在 Spring MVC 的 Web 应用程序中作用范围如下:
│ ▲
▼ │
┌───────┐
│Filter1│
└───────┘
│ ▲
▼ │
┌───────┐
┌ ─ ─ ─│Filter2│─ ─ ─ ─ ─ ─ ─ ─ ┐
└───────┘
│ │ ▲ │
▼ │
│ ┌─────────────────┐ │
│DispatcherServlet│◀───┐
│ └─────────────────┘ │ │
│ ┌────────────┐
│ │ │ModelAndView││
│ └────────────┘
│ │ ▲ │
│ ┌───────────┐ │
│ ├───▶│Controller1│────┤ │
│ └───────────┘ │
│ │ │ │
│ ┌───────────┐ │
│ └───▶│Controller2│────┘ │
└───────────┘
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
上图虚线框就是 Filter2 的拦截范围,Filter 组件实际上并不知道后续内部处理是通过 Spring MVC 提供的 DispatcherServlet
还是其他 Servlet 组件,因为 Filter 是 Servlet 规范定义的标准组件,它可以应用在任何基于 Servlet 的程序中。
如果只基于 Spring MVC 开发应用程序,还可以使用 Spring MVC 提供的一种功能类似 Filter 的拦截器:Interceptor。和 Filter 相比,Interceptor 拦截范围不是后续整个处理流程,而是仅针对 Controller 拦截:
│ ▲
▼ │
┌───────┐
│Filter1│
└───────┘
│ ▲
▼ │
┌───────┐
│Filter2│
└───────┘
│ ▲
▼ │
┌─────────────────┐
│DispatcherServlet│◀───┐
└─────────────────┘ │
│ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ┐
│ │
│ │ ┌────────────┐ │
│ │ Render │
│ │ └────────────┘ │
│ ▲
│ │ │ │
│ ┌────────────┐
│ │ │ModelAndView│ │
│ └────────────┘
│ │ ▲ │
│ ┌───────────┐ │
├─┼─▶│Controller1│────┤ │
│ └───────────┘ │
│ │ │ │
│ ┌───────────┐ │
└─┼─▶│Controller2│────┘ │
└───────────┘
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
上图虚线框就是 Interceptor 的拦截范围,注意到 Controller 的处理方法一般都类似这样:
@Controller
public class Controller1 {@GetMapping("/path/to/hello")
ModelAndView hello() {...}
}
所以,Interceptor 的拦截范围其实就是 Controller 方法,它实际上就相当于基于 AOP 的方法拦截。因为 Interceptor 只拦截 Controller 方法,所以要注意,返回 ModelAndView
并渲染后,后续处理就脱离了 Interceptor 的拦截范围。
使用 Interceptor 的好处是 Interceptor 本身是 Spring 管理的 Bean,因此注入任意 Bean 都非常简单。此外,可以应用多个 Interceptor,并通过简单的 @Order
指定顺序。我们先写一个LoggerInterceptor
:
@Order(1)
@Component
public class LoggerInterceptor implements HandlerInterceptor {final Logger logger = LoggerFactory.getLogger(getClass());
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {logger.info("preHandle {}...", request.getRequestURI());
if (request.getParameter("debug") != null) {PrintWriter pw = response.getWriter();
pw.write("<p>DEBUG MODE</p>");
pw.flush();
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {logger.info("postHandle {}.", request.getRequestURI());
if (modelAndView != null) {modelAndView.addObject("__time__", LocalDateTime.now());
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {logger.info("afterCompletion {}: exception = {}", request.getRequestURI(), ex);
}
}
一个 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 认证的功能:
@Order(2)
@Component
public class AuthInterceptor implements HandlerInterceptor {final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {logger.info("pre authenticate {}...", request.getRequestURI());
try {authenticateByHeader(request);
} catch (RuntimeException e) {logger.warn("login by authorization header failed.", e);
}
return true;
}
private void authenticateByHeader(HttpServletRequest req) {String authHeader = req.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Basic")) {logger.info("try authenticate by authorization header...");
String up = new String(Base64.getDecoder().decode(authHeader.substring(6)), StandardCharsets.UTF_8);
int pos = up.indexOf(':');
if (pos > 0) {String email = URLDecoder.decode(up.substring(0, pos), StandardCharsets.UTF_8);
String password = URLDecoder.decode(up.substring(pos + 1), StandardCharsets.UTF_8);
User user = userService.signin(email, password);
req.getSession().setAttribute(UserController.KEY_USER, user);
logger.info("user {} login by authorization header ok.", email);
}
}
}
}
这个 AuthInterceptor
是由 Spring 容器直接管理的,因此注入 UserService
非常方便。
最后,要让拦截器生效,我们在 WebMvcConfigurer
中注册所有的 Interceptor:
@Bean
WebMvcConfigurer createWebMvcConfigurer(@Autowired HandlerInterceptor[] interceptors) {return new WebMvcConfigurer() {public void addInterceptors(InterceptorRegistry registry) {for (var interceptor : interceptors) {registry.addInterceptor(interceptor);
}
}
...
};
}
注意
如果拦截器没有生效,请检查是否忘了在 WebMvcConfigurer 中注册。
处理异常
在 Controller 中,Spring MVC 还允许定义基于 @ExceptionHandler
注解的异常处理方法。我们来看具体的示例代码:
@Controller
public class UserController {@ExceptionHandler(RuntimeException.class)
public ModelAndView handleUnknowException(Exception ex) {return new ModelAndView("500.html", Map.of("error", ex.getClass().getSimpleName(), "message", ex.getMessage()));
}
...
}
异常处理方法没有固定的方法签名,可以传入 Exception
、HttpServletRequest
等,返回值可以是 void
,也可以是ModelAndView
,上述代码通过@ExceptionHandler(RuntimeException.class)
表示当发生 RuntimeException
的时候,就自动调用此方法处理。
注意到我们返回了一个新的ModelAndView
,这样在应用程序内部如果发生了预料之外的异常,可以给用户显示一个出错页面,而不是简单的 500 Internal Server Error 或 404 Not Found。例如 B 站的错误页:
可以编写多个错误处理方法,每个方法针对特定的异常。例如,处理 LoginException
使得页面可以自动跳转到登录页。
使用 ExceptionHandler
时,要注意它仅作用于当前的 Controller,即 ControllerA 中定义的一个 ExceptionHandler
方法对 ControllerB 不起作用。如果我们有很多 Controller,每个 Controller 都需要处理一些通用的异常,例如LoginException
,思考一下应该怎么避免重复代码?
练习
使用 Interceptor。
下载练习
小结
Spring MVC 提供了 Interceptor 组件来拦截 Controller 方法,使用时要注意 Interceptor 的作用范围。