文章目录
- 控制器方法执行流程
- @ControllerAdvice 之 @ModelAttribute
- 返回值处理器
- MessageConverter
- @ControllerAdvice 之 @ResponseBodyAdvice
- BeanNameUrlHandlerMapping 与 SimpleControllerHandlerAdapter
- RouterFunctionMapping 与 HandlerFunctionAdapter
- SimpleUrlHandlerMapping 与 HttpRequestHandlerAdapter
- WelcomePageHandlerMapping
- 映射器与适配器小结
- MVC异常处理
- @ControllerAdvice 之 @ExceptionHandler
- Tomcat 异常处理
- 错误页处理
- BasicErrorController
- ViewResolver
- MVC处理流程
控制器方法执行流程
我们知道RequestMappingHandlerAdapter一个重要的作用就是去执行控制器方法,而这个控制器方法就是下面的HandlerMethod
HandlerMethod 需要
- bean 即是哪个 Controller
- method 即是 Controller 中的哪个方法
Controller有了,Controller中的方法有了,那么我们就可以使用反射进行调用了。
当然这远远不够,我们的方法上有参数,我们要进行一系列的处理,所以RequestMappingHandlerAdapter其实最后执行的是ServletInvocableHandlerMethod,它具有以下四个组件:
- WebDataBinderFactory 负责对象绑定、类型转换
- ParameterNameDiscoverer 负责参数名解析
- HandlerMethodArgumentResolverComposite 负责解析参数
- HandlerMethodReturnValueHandlerComposite 负责处理返回值
准备阶段
根据这幅图我们可以看到:
- RequestMappingHandlerAdapter首先会做一些准备工作
- 创建WebDataBinderFactory,为以后的参数绑定和类型转换做铺垫
- 这里涉及第一个拓展点:通过解析@InitBinder添加一些自定义的类型转换器等。
- 创建ModelFactory工厂:用来产生模型数据
- 这里也涉及一个拓展点:通过解析@ModelAttribute,将方法的返回值作为模型数据进行添加。添加的位置就是ModelAndViewContainer(后面会有详细说明)
- 创建WebDataBinderFactory,为以后的参数绑定和类型转换做铺垫
调用阶段
这个时候万事俱备,就可以开始调用我们前面说的ServletInvocableHandlerMethod了,调用的时候会做三件事:
- 准备参数
- 通过HandlerMethodArgumentResolverComposite (组合参数解析器)获取参数
- 在这个过程中有一个拓展点:有的解析器会根据RequestBodyAdvice对请求体进行拓展(后面也会详细介绍)
- 某些参数解析器工作的过程中会涉及到数据绑定生成模型数据(也就是使用了@ModelAttribute的参数),它们也会被存储到ModelAndViewContainer中
- 最后得到参数
- 通过HandlerMethodArgumentResolverComposite (组合参数解析器)获取参数
- 反射调用控制器方法:bean有了,方法有了,参数也有了,开始进行反射调用。最后得到返回结果。
- 处理返回值
- HandlerMethodReturnValueHandlerComposite处理返回值
- 这里涉及拓展:有的返回值处理器会根据ResponseBodyAdvice对请求体进行拓展
- 有的返回值代表了模型数据,他们会被添加到ModelAndViewContainer中去。
- HandlerMethodReturnValueHandlerComposite处理返回值
- 返回值处理结束后ServletInvocableHandlerMethod会找ModelAndViewContainer获得ModelAndView对象(因为最终控制器的返回结果要统一成ModelAndViewu)。然后把ModelAndView返回给上层的调用者也就是DispatcherServlet。
接下来我们帮上述过程使用代码模拟一下:
public class A26 {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(WebConfig.class);
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setApplicationContext(context);
adapter.afterPropertiesSet();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("name", "张三");
/*
现在可以通过 ServletInvocableHandlerMethod 把这些整合在一起, 并完成控制器方法的调用, 如下
*/
ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(
new Controller1(), Controller1.class.getMethod("foo", User.class));
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);
handlerMethod.setDataBinderFactory(factory);
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));
ModelAndViewContainer container = new ModelAndViewContainer();
// 获取模型工厂方法
Method getModelFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getModelFactory", HandlerMethod.class, WebDataBinderFactory.class);
getModelFactory.setAccessible(true);
ModelFactory modelFactory = (ModelFactory) getModelFactory.invoke(adapter, handlerMethod, factory);
// 初始化模型数据
modelFactory.initModel(new ServletWebRequest(request), container, handlerMethod);
handlerMethod.invokeAndHandle(new ServletWebRequest(request), container);
System.out.println(container.getModel());
context.close();
}
@ControllerAdvice 之 @ModelAttribute
@ModelAttribute有两种用法:
- @ModelAttribute注解可以在控制器方法的入参上使用,用于将请求参数绑定到该入参对应的对象中。并将该值添加到ModelAndViewContainer 中
- @ModelAttribute注解也可以在控制器方法的返回值上使用,用于将返回值作为模型对象添加到ModelAndViewContainer 中
这个地方要注意:
- 如果@ModelAttribute没有指定名称,那么会将类型的首字母小写当作名称存入ModelAndViewContainer 。
- 如果@ModelAttribute加在了方法上那么它的解析器就不是ServletModelAttributeMethodProcessor,而是RequestMappingHandlerAdapter。解析完了之后在我们去执行控制器方法之前,会有一个ModelFactory来调用标注了@ModelAttribute的方法,并把它们的返回结果作为模型数据存储在ModelAndViewContainer 中。
什么是ModelFactory?
ModelFactory
是Spring MVC框架中的一个接口,用于创建和管理模型对象(模型对象
是指用于在控制器方法和视图之间传递数据的Java对象,它可以让我们方便地将数据从控制器方法传递到视图中,从而实现动态生成页面的效果)。它的主要作用是创建模型对象,以及在控制器方法中共享模型对象。
具体来说,ModelFactory定义了两个方法:
- createModel(): 用于创建一个新的模型对象。该方法会在每次请求到达控制器方法时被调用,用于创建一个新的模型对象。
- mergeAttributes(): 用于将模型对象中的属性添加到ModelAndView对象中。该方法会在控制器方法执行完毕后被调用,用于将模型对象中的属性添加到ModelAndView对象中,以便在视图中使用。
在Spring MVC框架中,默认使用的是ExtendedModelMap类来实现ModelFactory接口。ExtendedModelMap类继承了LinkedHashMap类,因此它可以存储键值对,并且可以保持键值对的顺序。
还要注意:
- RequestMappingHandlerAdapter 初始化时会解析 @ControllerAdvice 中的 @ModelAttribute 方法
- RequestMappingHandlerAdapter 会以类为单位,在该类首次使用时,解析此类的 @ModelAttribute 方法
- 以上两种 @ModelAttribute 的解析结果都会缓存来避免重复解析
- 控制器方法调用时,会综合利用本类的 @ModelAttribute 方法和 @ControllerAdvice 中的 @ModelAttribute 方法创建模型工厂
返回值处理器
常见的返回值处理器:
- 返回值类型为ModelAndView时,分别获取其模型和视图名,放入 ModelAndViewContainer
- 返回值类型为 String 时,把它当做视图名,放入 ModelAndViewContainer
- 返回值添加了 @ModelAttribute 注解时,将返回值作为模型,放入 ModelAndViewContainer
- 我们这个时候缺少视图,默认的处理是会把@RequestMapping中的路径名当作视图的名字
- 返回值省略 @ModelAttribute 注解且返回非简单类型时,将返回值作为模型,放入 ModelAndViewContainer
- 我们这个时候缺少视图,默认的处理是会把@RequestMapping中的路径名当作视图的名字
- 返回值类型为 ResponseEntity 时
- 此时走 MessageConverter,并设置 ModelAndViewContainer.requestHandled 为 true
- 返回值类型为 HttpHeaders 时
- 会设置 ModelAndViewContainer.requestHandled 为 true
- 返回值添加了 @ResponseBody 注解时
- 此时走 MessageConverter,并设置 ModelAndViewContainer.requestHandled 为 true
- 注意:
- @ResponseBody 是返回值处理器解析的
- 但具体转换工作是 MessageConverter 做的
前4种方法都要走视图渲染流程:
而后三种直接响应就行,不用走视图的解析、渲染流程,ModelAndViewContainer中是空的。这是因为他们的处理器中把ModelAndViewContainer.requestHandled 设为 true
MessageConverter
MessageConverter是一个接口,它的主要作用是:将请求信息(如JSON、XML等)转换为对象,或将对象转换为响应信息。
当客户端发送HTTP请求时,请求中的数据可能以不同的格式进行传输,比如JSON、XML、文本等。而服务器端需要将这些数据转换成Java对象,以便后续的处理。此时,MessageConverter就可以派上用场了。它可以根据请求中的Content-Type头信息来自动选择相应的转换器,将请求中的数据转换成Java对象。
当服务器端需要将Java对象转换成HTTP响应时,同样可以使用MessageConverter来实现。比如,我们可以将Java对象转换成JSON格式的数据,然后将其作为HTTP响应返回给客户端。在这个过程中,MessageConverter会根据响应中的Content-Type头信息来自动选择相应的转换器,将Java对象转换成对应的格式。
Spring MVC中内置了许多实现,例如:
- MappingJackson2HttpMessageConverter: 将JSON转换为对象或将对象转换为JSON
- Jaxb2RootElementHttpMessageConverter: 将XML转换为对象或将对象转换为XML
- StringHttpMessageConverter: 将String类型的请求/响应体转换为对象或将对象转换为String
- 等等
我们也可以自定义MessageConverter实现,以满足特定的需求。
接下来我们使用代码模拟一下消息转换器的工作过程:
//写入json
public static void test1() throws IOException {
MockHttpOutputMessage message = new MockHttpOutputMessage();
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
if (converter.canWrite(User.class, MediaType.APPLICATION_JSON)) {
converter.write(new User("张三", 18), MediaType.APPLICATION_JSON, message);
System.out.println(message.getBodyAsString());
}
}
//写入xml
private static void test2() throws IOException {
MockHttpOutputMessage message = new MockHttpOutputMessage();
MappingJackson2XmlHttpMessageConverter converter = new MappingJackson2XmlHttpMessageConverter();
if (converter.canWrite(User.class, MediaType.APPLICATION_XML)) {
converter.write(new User("李四", 20), MediaType.APPLICATION_XML, message);
System.out.println(message.getBodyAsString());
}
}
//读入json
private static void test3() throws IOException {
MockHttpInputMessage message = new MockHttpInputMessage("""
{
"name":"李四",
"age":20
}
""".getBytes(StandardCharsets.UTF_8));
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
if (converter.canRead(User.class, MediaType.APPLICATION_JSON)) {
Object read = converter.read(User.class, message);
System.out.println(read);
}
}
结果:
那么这么多MessageConverter是如何决定由谁来解析的呢?
-
首先看 @RequestMapping 上有没有指定
相当于response.setContentType(“application/json”);
-
其次看 request 的 Accept 头有没有指定
-
最后按 MessageConverter 的顺序, 谁能谁先转换
@ControllerAdvice 之 @ResponseBodyAdvice
@ControllerAdvice与@ResponseBodyAdvice、@RequestBodyAdvice可以配合,来实现对请求体/响应体的增强拓展,当然其本质上是对消息转换的拓展。
这里我们就以@ResponseBodyAdvice为例,ResponseBodyAdvice 增强 在整个 HandlerAdapter 调用过程中所处的位置如下:
它的主要作用是:对@ResponseBody或ResponseEntity方法返回的内容进行后处理。
@ResponseBodyAdvice支持的后处理操作包括:
- 对Object类型的body进行包装或解包装
- 基于内容类型改变body对象
- 添加或修改响应头
- 等等
@ResponseBodyAdvice最常用的功能就是返回响应体前进行自定义的包装。其目的就是保持响应体的统一。
我们来看一个例子:
将{"name":"王五","age":18}
响应结果转换成 {"code":xx, "msg":xx, data: {"name":"王五","age":18} }
@Configuration
public class WebConfig {
@ControllerAdvice
static class MyControllerAdvice implements ResponseBodyAdvice<Object> {
// 满足条件才转换
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
if (returnType.getMethodAnnotation(ResponseBody.class) != null ||
AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null) {
// returnType.getContainingClass().isAnnotationPresent(ResponseBody.class)) {
return true;
}
return false;
}
// 将 User 或其它类型统一为 Result 类型
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Result) {
return body;
}
return Result.ok(body);
}
}
// @Controller
// @ResponseBody
@RestController
public static class MyController {
public User user() {
return new User("王五", 18);
}
}
}
注意:
- 这里我们使用了AnnotationUtils.findAnnotation方法,对注解进行一个深度寻找,例如@RestController中包含@ResponseBody的情况。
BeanNameUrlHandlerMapping 与 SimpleControllerHandlerAdapter
这两者配套使用,就像RequestMappingHandlerMapping和RequestMappingHandlerAdapter,一个做路径映射,另一个做控制器方法调用。不过这一组更加的简单,他们属于SpringMVC更为早期的实现。
我们来看一下使用代码:
@Configuration
public class WebConfig {
@Bean // ⬅️内嵌 web 容器工厂
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory(8080);
}
@Bean // ⬅️创建 DispatcherServlet
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean // ⬅️注册 DispatcherServlet, Spring MVC 的入口
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}
// /c1 --> /c1
// /c2 --> /c2
@Bean
public BeanNameUrlHandlerMapping beanNameUrlHandlerMapping() {
return new BeanNameUrlHandlerMapping();
}
@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
return new SimpleControllerHandlerAdapter();
}
@Component("/c1")
public static class Controller1 implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().print("this is c1");
return null;
}
}
@Component("/c2")
public static class Controller2 implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().print("this is c2");
return null;
}
}
@Bean("/c3")
public Controller controller3() {
return (request, response) -> {
response.getWriter().print("this is c3");
return null;
};
}
}
代码解读:
- BeanNameUrlHandlerMapping是通过bean的名字进行路径映射。比如说请求是
/c1
,那么它也会去容器里面寻找一个名叫/c1
的bean。当然前提是这个bean的名字必须以/
开头,才会被当作处理请求的bean。 - SimpleControllerHandlerAdapter也是用来做控制器方法调用。只不过调用规则不同:作为控制器的类必须实现同一个
接口Controller
(不是注解Controller)。
接下来我们自己通过代码来实现一个:
@Configuration
public class WebConfig_1 {
@Bean // ⬅️内嵌 web 容器工厂
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory(8080);
}
@Bean // ⬅️创建 DispatcherServlet
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean // ⬅️注册 DispatcherServlet, Spring MVC 的入口
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}
// /c1 --> /c1
// /c2 --> /c2
@Component
static class MyHandlerMapping implements HandlerMapping {
@Override
public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
String key = request.getRequestURI();
Controller controller = collect.get(key);
if (controller == null) {
return null;
}
return new HandlerExecutionChain(controller);
}
@Autowired
private ApplicationContext context;
private Map<String, Controller> collect;
@PostConstruct
public void init() {
collect = context.getBeansOfType(Controller.class).entrySet()
.stream().filter(e -> e.getKey().startsWith("/"))
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
System.out.println(collect);
}
}
@Component
static class MyHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return handler instanceof Controller;
}
@Override
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof Controller controller) {
controller.handleRequest(request, response);
}
return null;
}
@Override
public long getLastModified(HttpServletRequest request, Object handler) {
return -1;
}
}
@Component("/c1")
public static class Controller1 implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().print("this is c1");
return null;
}
}
@Component("c2")
public static class Controller2 implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().print("this is c2");
return null;
}
}
@Bean("/c3")
public Controller controller3() {
return (request, response) -> {
response.getWriter().print("this is c3");
return null;
};
}
}
RouterFunctionMapping 与 HandlerFunctionAdapter
这一组较新,在Spring5.2才引入
使用代码:
@Configuration
public class WebConfig {
@Bean // ⬅️内嵌 web 容器工厂
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory(8080);
}
@Bean // ⬅️创建 DispatcherServlet
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean // ⬅️注册 DispatcherServlet, Spring MVC 的入口
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}
@Bean
public RouterFunctionMapping routerFunctionMapping() {
return new RouterFunctionMapping();
}
@Bean
public HandlerFunctionAdapter handlerFunctionAdapter() {
return new HandlerFunctionAdapter();
}
@Bean
public RouterFunction<ServerResponse> r1() {
return route(GET("/r1"), request -> ok().body("this is r1"));
}
@Bean
public RouterFunction<ServerResponse> r2() {
return route(GET("/r2"), request -> ok().body("this is r2"));
}
}
r1简化之前的代码:
总结:
函数式控制器
a. RouterFunctionMapping, 通过 RequestPredicate 映射
b. handler 要实现 HandlerFunction 接口
c. HandlerFunctionAdapter, 调用 handler
对比
a. RequestMappingHandlerMapping, 以 @RequestMapping 作为映射路径
b. 控制器的具体方法会被当作 handler
c. RequestMappingHandlerAdapter, 调用 handler
函数式控制器就是在匹配方式上,调用方式上做了些改动。新是新但是功能并没有RequestMapping那套体系强大,毕竟对参数解析,返回值处理并没有那么多丰富的逻辑。好处就是十分的简洁,适合一些业务逻辑不复杂的场景。
SimpleUrlHandlerMapping 与 HttpRequestHandlerAdapter
又是一组,不过这一组一般是用来处理静态资源的。
使用代码:
@Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping(ApplicationContext context) {
SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
Map<String, ResourceHttpRequestHandler> map
= context.getBeansOfType(ResourceHttpRequestHandler.class);
handlerMapping.setUrlMap(map);
return handlerMapping;
}
@Bean
public HttpRequestHandlerAdapter httpRequestHandlerAdapter() {
return new HttpRequestHandlerAdapter();
}
@Bean("/**")
public ResourceHttpRequestHandler handler1() {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setLocations(List.of(new ClassPathResource("static/")));
return handler;
}
@Bean("/img/**")
public ResourceHttpRequestHandler handler2() {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setLocations(List.of(new ClassPathResource("images/")));
return handler;
}
目录结构:
- SimpleUrlHandlerMapping 不会在初始化时收集映射信息,需要手动收集
- SimpleUrlHandlerMapping 映射路径,同样也是bean的名字作为匹配的路径
- ResourceHttpRequestHandler 作为静态资源 handler
- HttpRequestHandlerAdapter, 调用此 handler
这个handler其实还可以进行优化。我们来看看ResourceHttpRequestHandler 的初始化方法afterPropertiesSet(一位它实现了InitializingBean接口),在这个方法中我们可以看到他设置了一个资源解析器:
也就是说本质上他寻找那些静态资源是依靠的这个PathResourceResolver这个资源解析器。Spring内部还有其他几种资源解析器,可以增强它的功能。
- PathResourceResolver:就是在磁盘上都对应的资源
- EncodedResourceResolver:可以读该资源所对应的压缩资源
- CachingResourceResolver:在读取资源的时候加入缓存功能
增强代码:
@Bean("/**")
public ResourceHttpRequestHandler handler1() {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setLocations(List.of(new ClassPathResource("static/")));
handler.setResourceResolvers(List.of(
// ⬇️缓存优化
new CachingResourceResolver(new ConcurrentMapCache("cache1")),
// ⬇️压缩优化
new EncodedResourceResolver(),
// ⬇️原始资源解析
new PathResourceResolver()
));
return handler;
}
注意:
- 责任链模式体现
- 压缩文件需要手动生成
- 注意此处资源解析器的顺序,优先走缓存
WelcomePageHandlerMapping
又叫欢迎页映射器,其作用是把访问根目录的请求映射到一个欢迎页上去。另外这个欢迎页映射器是SpringBoot提供的,传统的SpringMVC没有:
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext context) {
Resource resource = context.getResource("classpath:static/index.html");
return new WelcomePageHandlerMapping(null, context, resource, "/**");
}
@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
return new SimpleControllerHandlerAdapter();
}
注意💡:
- 欢迎页支持静态欢迎页与动态欢迎页
- WelcomePageHandlerMapping 映射欢迎页(即只映射 ‘/’)
- 其内部会生成一个实现了Controller接口的处理器ParameterizableViewController (所以后来使用SimpleControllerHandlerAdapter进行handler的调用)
- ParameterizableViewController 作用是不执行逻辑,仅根据视图名找视图
- 视图名固定为 forward:index.html
- SimpleControllerHandlerAdapter, 调用 handler
- 转发至 /index.html
- 处理 /index.html 又会走上面的静态资源处理流程
映射器与适配器小结
- HandlerMapping 负责建立请求与控制器之间的映射关系
- RequestMappingHandlerMapping (与 @RequestMapping 匹配)
- WelcomePageHandlerMapping (/)
- BeanNameUrlHandlerMapping (与 bean 的名字匹配 以 / 开头)
- RouterFunctionMapping (函数式 RequestPredicate, HandlerFunction)
- SimpleUrlHandlerMapping (静态资源 通配符 /** /img/**)
- 之间也会有顺序问题, boot 中默认顺序如上
- HandlerAdapter 负责实现对各种各样的 handler 的适配调用
- RequestMappingHandlerAdapter 处理:@RequestMapping 方法
- 参数解析器、返回值处理器体现了组合模式
- SimpleControllerHandlerAdapter 处理:Controller 接口
- HandlerFunctionAdapter 处理:HandlerFunction 函数式接口
- HttpRequestHandlerAdapter 处理:HttpRequestHandler 接口 (静态资源处理)
- 这也是典型适配器模式体现
- RequestMappingHandlerAdapter 处理:@RequestMapping 方法
MVC异常处理
我们进入DispatcherServlet中遇到异常只是把异常捉住,赋值给一个Exception变量并没有马上处理,而是在接下来的processDispatchResult中去处理异常:
我们进入这个方法发现里面的逻辑大致为:
- 如果Exception非空走异常处理逻辑
- 如果Exception为空则走视图渲染的逻辑
我们可以看到异常处理的核心方法是processHandlerException,我们继续进入这个方法,看看里面干了什么:
- 在DispatcherServlet初始化的时候收集一些异常处理器
- 然后遍历异常处理器对异常进行处理
所有的异常处理器都继承了HandlerExceptionResolver接口,这里我们说一个其中比较重要的异常处理器ExceptionHandlerExceptionResolver。
- ExceptionHandlerExceptionResolver 初始化时会解析 @ControllerAdvice 中的 @ExceptionHandler 方法
- ExceptionHandlerExceptionResolver 会以类为单位,在该类首次处理异常时,解析此类的 @ExceptionHandler 方法
- 以上两种 @ExceptionHandler 的解析结果都会缓存来避免重复解析
我们来通过代码看看过程:
public class A30 {
public static void main(String[] args) throws NoSuchMethodException {
ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
resolver.afterPropertiesSet();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
// 1.测试 json
HandlerMethod handlerMethod = new HandlerMethod(new Controller1(), Controller1.class.getMethod("foo"));
Exception e = new ArithmeticException("被零除"); resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}
}
控制器:
结果:
- 处理异常的本质就是执行异常处理方法,而这个方法也有参数、返回值,所以也需要参数解析器、返回值处理器。这里我们使用了@ResponseBody注解,所以需要一个消息转换器。
- ExceptionHandlerExceptionResolver也实现了InitializingBean接口,所以也有afterPropertiesSet方法,在这个方法中为其添加了几个参数解析器以及返回值处理器
- 调用resolveException处理异常的时候:根据HandlerMethod就会知道将来这个异常是在哪个类中被触发的,然后进入这个类中检查,看是否有标注为@ExceptionHandler的方法,找到之后它会把方法的参数异常类型与实际发生的异常类型进行一个异常匹配,如果能匹配上则说明这个方法就是进行异常处理的,然后反射调用这个方法,调用的时候参数该解析的解析,返回值该处理的处理。
接下来我们测试一下嵌套异常:
public class A30 {
public static void main(String[] args) throws NoSuchMethodException {
ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
resolver.afterPropertiesSet();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
// 3.测试嵌套异常
HandlerMethod handlerMethod = new HandlerMethod(new Controller3(), Controller3.class.getMethod("foo"));
Exception e = new Exception("e1", new RuntimeException("e2", new IOException("e3")));
resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}
}
控制器:
结果:
之所以可以可以进行嵌套循环是因为:
之后会拿这个异常的数组去进行匹配。
最后我们小总结一下:
关于ExceptionHandlerExceptionResolver:
- 它能够重用参数解析器、返回值处理器,实现组件重用
- 它能够支持嵌套异常
@ControllerAdvice 之 @ExceptionHandler
@ControllerAdvice + @ExceptionHandler是一个全局异常处理方案
我们来看一段示例代码:
public class A31 {
public static void main(String[] args) throws NoSuchMethodException {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
// ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
// resolver.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
// resolver.afterPropertiesSet();
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
ExceptionHandlerExceptionResolver resolver = context.getBean(ExceptionHandlerExceptionResolver.class);
HandlerMethod handlerMethod = new HandlerMethod(new Controller5(), Controller5.class.getMethod("foo"));
Exception e = new Exception("e1");
resolver.resolveException(request, response, handlerMethod, e);
System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}
static class Controller5 {
public void foo() {
}
}
}
结果:
那么我们的异常处理器是在什么时机找到的全局异常处理方法呢?
还是在内置初始化的时候:
我们进入这个方法看看:
总结一下:
- ExceptionHandlerExceptionResolver 初始化时会解析 @ControllerAdvice 中的 @ExceptionHandler 方法
- ExceptionHandlerExceptionResolver 会以类为单位,在该类首次处理异常时,解析此类的 @ExceptionHandler 方法
- 以上两种 @ExceptionHandler 的解析结果都会缓存来避免重复解析
Tomcat 异常处理
错误页处理
我们上面所涉及的异常处理其实能力是有限的,我们知道 @ExceptionHandler 只能处理发生在 mvc 流程中的异常,例如控制器内、拦截器内,那么如果是 Filter 出现了异常,如何进行处理呢?,这个时候我们需要一个更上层的异常处理者,也就是Tomcat异常处理。
我们可以看到Tomcat的错误页面是这样的:
那么我们能对其进行定制吗?
我们只需要加两个bean就可以完成这个需求:
@Bean // ⬅️修改了 Tomcat 服务器默认错误地址, 出错时使用请求转发方式跳转
public ErrorPageRegistrar errorPageRegistrar() {
return webServerFactory -> webServerFactory.addErrorPages(new ErrorPage("/error"));
}
@Bean // ⬅️TomcatServletWebServerFactory 初始化前用它增强, 注册所有 ErrorPageRegistrar
public ErrorPageRegistrarBeanPostProcessor errorPageRegistrarBeanPostProcessor() {
return new ErrorPageRegistrarBeanPostProcessor();
}
- ErrorPageRegistrar :错误的处理路径,也就是定义Tomcat默认错误页面的地址
- ErrorPageRegistrarBeanPostProcessor :简单来说就是让你定义的错误页面生效
- 如果出现了错误会使用请求转发forward的方法跳转到错误地址
- Tomcat只要出现异常就会把它存在request作用域中
BasicErrorController
- 在 Spring Boot 中 Tomcat的异常处理,是这么实现的:
- 因为内嵌了 Tomcat 容器,因此可以配置 Tomcat 的错误页面,Filter 与 错误页面之间是通过请求转发跳转的,可以在这里做手脚
- 先通过 ErrorPageRegistrarBeanPostProcessor 这个后处理器配置错误页面地址,默认为
/error
也可以通过${server.error.path}
进行配置 - 当 Filter 发生异常时,不会走 Spring 流程,但会走 Tomcat 的错误处理,于是就希望转发至
/error
这个地址- 当然,如果没有 @ExceptionHandler,那么最终也会走到 Tomcat 的错误处理
- Spring Boot 又提供了一个 BasicErrorController,它就是一个标准 @Controller,@RequestMapping 配置为
/error
,所以处理异常的职责就又回到了 Spring - 异常信息由于会被 Tomcat 放入 request 作用域,因此 BasicErrorController 里也能获取到
- 具体异常信息会由 DefaultErrorAttributes 封装好
- BasicErrorController 通过 Accept 头判断需要生成哪种 MediaType 的响应
- 如果要的不是 text/html,走 MessageConverter 流程
- 如果需要 text/html,走 mvc 流程,此时又分两种情况
- 配置了 ErrorViewResolver,根据状态码去找 View
- 没配置或没找到,用 BeanNameViewResolver 根据一个固定为 error 的名字找到 View,即所谓的 WhitelabelErrorView
ViewResolver
ViewResolver 是一个接口,它的主要作用是:将逻辑视图名解析为实际的 View 对象。
在 Spring MVC 中,我们通常使用逻辑视图名来指定视图,例如:
return "user/list";
然后,ViewResolver 会根据这个逻辑视图名,解析出对应的 View 对象:
View view = viewResolver.resolveViewName("user/list", locale);
常见的 View 对象有:
- InternalResourceView:渲染 JSP 视图
- FreeMarkerView:渲染 FreeMarker 模板
- RedirectView:重定向到另一个 URL
- 等等
Spring MVC 中默认配置了若干 ViewResolver,例如:
- InternalResourceViewResolver:将逻辑视图名解析为 JSP 视图
- FreeMarkerViewResolver:将逻辑视图名解析为 FreeMarker 模板
- RedirectViewResolver:用于重定向视图
当我们返回一个逻辑视图名时,Spring MVC 会根据注册的 ViewResolver 解析成实际的 View 对象,然后由视图渲染工具(ViewRenderer)来渲染视图,生成客户端响应。
所以,ViewResolver 的主要特征是:
- 将逻辑视图名解析为 View 对象实例
- 不同的 ViewResolver 支持不同类型的 View(JSP、FreeMarker 等)
- 我们可以注册多个 ViewResolver,他们之间并不冲突
- 当渲染视图时,会根据注册的 ViewResolver 解析出对应的 View
- 默认的 ViewResolver 有 InternalResourceViewResolver、FreeMarkerViewResolver 等
- 自定义 ViewResolver 可以选择支持任意类型的 View
除此之外,ViewResolver 还有如下优点:
- 解耦视图技术与 Spring MVC,我们可以自由选择视图技术
- 可扩展性强,自定义 ViewResolver 可以支持任意视图类型
- 通过在配置中调整 ViewResolver 顺序,可以灵活选择优先视图技术
- 对视图技术的支持可以动态添加或删除
- 使用逻辑视图名,屏蔽了视图技术的差异,提高可移植性
MVC处理流程
当浏览器发送一个请求 http://localhost:8080/hello
后,请求到达服务器,其处理流程是:
-
服务器提供了 DispatcherServlet,它使用的是标准 Servlet 技术
- 路径:默认映射路径为
/
,即会匹配到所有请求 URL,可作为请求的统一入口,也被称之为前控制器- jsp 不会匹配到 DispatcherServlet
- 其它有路径的 Servlet 匹配优先级也高于 DispatcherServlet
- 创建:在 Boot 中,由 DispatcherServletAutoConfiguration 这个自动配置类提供 DispatcherServlet 的 bean
- 初始化:DispatcherServlet 初始化时会优先到容器里寻找各种组件,作为它的成员变量
- HandlerMapping,初始化时记录映射关系
- HandlerAdapter,初始化时准备参数解析器、返回值处理器、消息转换器
- HandlerExceptionResolver,初始化时准备参数解析器、返回值处理器、消息转换器
- ViewResolver:视图解析器
- 路径:默认映射路径为
-
DispatcherServlet 会利用 RequestMappingHandlerMapping 查找控制器方法
-
例如根据 /hello 路径找到 @RequestMapping(“/hello”) 对应的控制器方法
-
控制器方法会被封装为 HandlerMethod 对象,并结合匹配到的拦截器一起返回给 DispatcherServlet
-
HandlerMethod 和拦截器合在一起称为 HandlerExecutionChain(调用链)对象
-
-
DispatcherServlet 接下来会:
- 调用拦截器的 preHandle 方法
- RequestMappingHandlerAdapter 调用 handle 方法,准备数据绑定工厂、模型工厂、ModelAndViewContainer、将 HandlerMethod 完善为 ServletInvocableHandlerMethod
- @ControllerAdvice 全局增强点1️⃣:补充模型数据
- @ControllerAdvice 全局增强点2️⃣:补充自定义类型转换器
- 使用 HandlerMethodArgumentResolver 准备参数
- @ControllerAdvice 全局增强点3️⃣:RequestBody 增强
- 调用 ServletInvocableHandlerMethod
- 使用 HandlerMethodReturnValueHandler 处理返回值
- @ControllerAdvice 全局增强点4️⃣:ResponseBody 增强
- 根据 ModelAndViewContainer 获取 ModelAndView
- 如果返回的 ModelAndView 为 null,不走第 4 步视图解析及渲染流程
- 例如,有的返回值处理器调用了 HttpMessageConverter 来将结果转换为 JSON,这时 ModelAndView 就为 null
- 如果返回的 ModelAndView 不为 null,会在第 4 步走视图解析及渲染流程
- 如果返回的 ModelAndView 为 null,不走第 4 步视图解析及渲染流程
- 调用拦截器的 postHandle 方法
- 处理异常或视图渲染
- 如果 1~3 出现异常,走 ExceptionHandlerExceptionResolver 处理异常流程
- @ControllerAdvice 全局增强点5️⃣:@ExceptionHandler 异常处理
- 正常,走视图解析及渲染流程
- 如果 1~3 出现异常,走 ExceptionHandlerExceptionResolver 处理异常流程
- 调用拦截器的 afterCompletion 方法