前言
最近在springboot开发的一个项目中,发现了一个奇怪的问题。
发现随便访问一个不存在的接口,总是能找到一个对应的hander,并没有进入预期中的404处理流程。
为了解决统一异常处理的问题,仔细梳理了一次spring mvc的 DispatcherServlet 的处理流程。让我觉得好奇和感兴趣的是,在debug的过程中,spring boot在默认情况下,为我们准备了7个HandlerMapping,而单独使用springmvc XML配置注解驱动下的spring mvc提供的默认HandlerMapping只有三个。
XML配置中的注解驱动
熟悉spring容器的初始化流程的同学们可能知道,spring 容器在解析XML配置文件的阶段会处理自定义的名字空间,例如 mvc: ,mvc这个名字空间就是在该阶段处理。
在该阶段,spring读取每个包 META-INF 下的 spring.schemas 和 spring.handlers 文件,这些文件映射了名字空间和处理该名字空间的处理器。spring实例化这些处理器,并找到处理对应名字空间的处理器,运行init方法。
现在回到本文的配角:mvc。 该名字空间的处理器是 MvcNamespaceHandler 。MvcNamespaceHandler 的 init方法中,向spring 容器中注册了各种各样的 BeanDefinitionParser实例,这些实例将解析各自的mvc名字空间下的元素。其中,注解驱动 mvc:annotation-driven 节点的annotation-driven 元素由 AnnotationDrivenBeanDefinitionParser 解析器解析 。
AnnotationDrivenBeanDefinitionParser 解析器在解析 annotation-driven 元素时被调用。在解析阶段,会向spring容器中注册一些默认的bean。例如大家熟悉的HandlerMapping 、HandlerAdapter等。在这里,将会向spring 容器注册这些bean的beanDefinition,容器在接下来的流程中会实例化bean并注册到容器中。下面是默认的3个handlerMapping。
![7362a4e8e1754023c88a4810d11f8396.png](https://i-blog.csdnimg.cn/blog_migrate/ebb4d04ba87a2d8ca90847d2d4be0ef3.jpeg)
spring boot下的handlerMapping
spring boot为了减少开发者的配置工作量,提供了各种各样的自动配置类,官方的自动配置类都在spring-boot-autoconfigure 包下。
spring boot是在什么时候、怎么做到自动配置的呢?
我们注意到 EnableAutoConfiguration 注解上有个@Import注解,该注解相当于XML配置中的 引入其它配置文件。
@Import注解中指定了 选择器 AutoConfigurationImportSelector ,该选择器的作用是在 selectImports() 方法中从spring.factories 文件中获取 key 为 org.springframework.boot.autoconfigure.EnableAutoConfiguration 指定的所有类。然后把这些类都加入到了容器中。
另外,这些类有个共同特点:它们都有个 @Configuration 注解。Spring中对Configuration类的解析是通过 ConfigurationClassPostProcessor 进行的。具体的过程大家可以自行查看代码。总之,@Configuration 注解标识的类中所有的@Bean的类又这样实例化并放到了容器中。
我们花了一些篇幅介绍 EnableAutoConfiguration 的原理,那么有什么用呢?
回到 spring.factories 文件中,我们在 org.springframework.boot.autoconfigure.EnableAutoConfiguration 中发现了关键类:WebMvcAutoConfiguration 。
说到这里,我们看下spring boot启动下,默认情况下,DispatcherServlet中的HandlerMapping有哪些。通过debug,可以看到spring boot默认给我们准备了7个HandlerMapping。
![bb3ea1931f0c8990c7b0686752ac98e0.png](https://i-blog.csdnimg.cn/blog_migrate/91373a25bfa8321644d5674818ce33ee.jpeg)
下面简单说明下前5个HandlerMapping的作用和匹配的URL。
(1)第一个SimpleUrlHandlerMapping
该bean在 WebMvcAutoConfiguration 的内部配置类 FaviconConfiguration 中声明。它匹配的URL路径是**/favicon.ico,作用是响应浏览器获取收藏夹中的icon文件。
(2)RequestMappingHandlerMapping
该bean在 WebMvcAutoConfiguration 的内部配置类 EnableWebMvcConfiguration 中声明。它和XML的注解驱动一样,匹配在Controller中@RequestMapping注解指定的URL。
(3)WelcomePageHandlerMapping
该bean在 WebMvcAutoConfiguration 的内部配置类 WebMvcAutoConfigurationAdapter 中声明。给URL路径是/或者/index映射到欢迎页面。
(4)BeanNameUrlHandlerMapping
bean的名称(也就是beanID)作为URL,由匹配该URL的Controller处理业务。Controller必须继承AbstractController。基本不会在工作中使用这个玩意。
(5)第二个SimpleUrlHandlerMapping
该bean在WebMvcAutoConfiguration 的内部配置类 EnableWebMvcConfiguration 的父类 WebMvcConfigurationSupport 中声明。
@Bean
public HandlerMapping resourceHandlerMapping() {}
从该bean的名称,可以知道它是专门用于映射资源文件的handlerMapping。看下它的URL与handler的映射关系
![637e717a2d3e2a801f95cf29c90de4f3.png](https://i-blog.csdnimg.cn/blog_migrate/13aa9dcdf7d49232c2c302a00e87deef.jpeg)
可以看到,其中有一个/**,所有匹配不到的接口请求将由ResourceHttpRequestHandler处理、获取并返回。而ResourceHttpRequestHandler内部将会查找对应的静态资源,如果查不到将直接response.sendError()最终重定向到/error视图,最终交给springboot默认的/error控制器处理。
![a10df04cf34c2cd49e348bb57a746388.png](https://i-blog.csdnimg.cn/blog_migrate/7bcecd8dfbc3c911970ab8cf695bb24a.jpeg)
默认的/error控制器对应rest请求会返回一个json格式的数据,而普通的请求会基于HTTP错误状态码去error/下查找对应状态码的视图。
![0eda88811d6cc9a1d3119231c98db783.png](https://i-blog.csdnimg.cn/blog_migrate/0c916e3c2b73537b42b2d378115a2226.jpeg)
如何让404请求统一走自定义异常处理流程
通过上述分析,可以知道,对于默认的springboot配置,不存在的接口会走入/error流程,最根本的原因是配置了处理/**的
@Bean
public HandlerMapping resourceHandlerMapping() {}
基于此,我们首先先去除这个配置。
在org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#addResourceHandlers中加入了/**的配置。
我们通过在spring的配置文件中加入
spring.resources.addMappings=false
表示不加入默认的静态资源路径处理。
![2dc9631a00267b099012c3e92efa7cd6.png](https://i-blog.csdnimg.cn/blog_migrate/ab1a5c01e23577b1ca1f7d148aeebec9.jpeg)
这样再次访问一个不存在的接口,我们会看到DispatchServlet中会返回null了,进入noHandlerFound()。
![bc32cb37cf52f1801fa48a0ea813f553.png](https://i-blog.csdnimg.cn/blog_migrate/9ac67c8737903e3d8edbf391a114a969.jpeg)
默认配置下,找不到hander,直接sendError()。我们期望让它抛出异常,已便在后面的异常处理器中处理。
在spring的配置文件中加入
spring.mvc.throw-exception-if-no-handler-found=true
让其抛出异常,这样我们就能处理这个异常了。
小结
springboot确实简单方便,你完全不需要任何配置就能让springmvc运转起来。但也正是因为简单而让你失去了内部的细节,因此你在使用前强烈建议必须要十分清楚底层的原理,否则可能会发生很多超乎你预期的结果。