我们知道Spring通常以bean的形式来组织功能模块,Spring MVC也不列外。Spring MVC以一系列特定类型的bean来构建整个框架。
相关Bean类型
Bean类型 | 说明 |
---|---|
HandlerMapping | 实现了url到处理器的映射关系,包括与之关联的拦截器(interceptors);有两个主要实现类,RequestMappingHandlerMapping支持@RequestMapping注解标注的处理器,SimpleUrlHandlerMapping支持手动添加映射。 |
HandlerAdapter | 封装了调用处理器的过程;这个类的存在的主要目的是将DispatcherServlet与处理器调用细节隔离开;比如RequestMappingHandlerAdapter与RequestMappingHandlerMapping对应 |
HandlerExceptionResolver | 处理http请求过程中发生的异常 |
ViewResolver | 将处理器返回的view名字,解析为最终的View对象来渲染对请求的响应 |
LocaleResolver, LocaleContextResolver | 解析用户的Locale,http请求的后续处理环节可使用 |
ThemeResolver | 用户主题解析,支持用户个性化的页面布局、css等 |
MultipartResolver | 如果http请求时一个multi-part类型,解析各个part的内容,处理器方法中可以直接使用 |
FlashMapManager | 管理Flash属性,可以在request之间传递,通常用于url重定向 |
这些bean工作在DispatcherServlet背后,构建起http请求处理的完整流程。我们可以在Spring容器中定义对应类型的bean实例,DispatcherServlet会自动查找它们;如果没有找到,就会使用DispatcherServlet.properties里配置的类型来创建。
http请求处理流程
简要介绍一下DispatcherServlet使用这些bean处理http请求的大体流程:
- 将WebApplicationContext绑定为request上的一个attribute,key为DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE;
- LocaleResolver绑定到request,使后续处理过程可以随时访问用户的Locale;
- ThemeResolver绑定到request;
- MultipartResolver检查request的请求体是不是multipart,如果是,将request包装为MultipartHttpServletRequest;
- 轮询所有的HandlerMapping,查找对应的handler;
- HandlerAdapter执行该handler,包括拦截器,handler方法等;
- 依据handler返回结果,生成Http响应;
- 如果发生异常,由HandlerExceptionResolver来处理;
HandlerMapping
这个系列的文章会陆续介绍上面大部分bean(不过我们没有必要了解上面所有bean的工作细节,只要知道他们的存在,在需要时再研究即可)。这一章单独介绍一下HandlerMapping,虽然我们几乎不太可能直接使用它,但它对我们理解Spring MVC的工作方式十分重要。
下面介绍HandlerMapping以及与之直接关联的概念和Spring MVC类型。
handler
handler这个概念在Spring MVC里面指http请求对应的处理逻辑,它负责处理请求并生成http响应。Spring MVC默认支持三种handler:
- 一是@Controller里用@RequestMapping标注的方法
- 二是HttpRequestHandler接口的实现
- 三是
org.springframework.web.servlet.mvc.Controller
接口的实现
HandlerAdapter
上面之所以说有三种类型的handler,实际上是因为Spring MVC内置三种类型的HandlerAdapter,分别是RequestMappingHandlerAdapter,HttpRequestHandlerAdapter,和SimpleControllerHandlerAdapter。他们封装了如何执行对应类型handler的细节。
我们可以支持新类型的handler,只要同时添加配套的HandlerAdapter就行。
HandlerExecutionChain
HandlerExecutionChain包裹了http请求对应handler和相关的拦截器(HandlerInterceptor)。在一次http请求的处理流程中,DispatchServlet先从HandlerMapping中映射出一个HandlerExecutionChain实例,再通过HandlerAdapter来执行它。
HandlerMapping
现在可以看看HandlerMapping这个接口的定义了:
public interface HandlerMapping {
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}
只要HandlerMapping能返回一个有效HandlerExecutionChain,就表明映射成功。DispatchServlet内部有一个HandlerMapping列表,对每个http请求,轮询这个列表,得到有效的HandlerExecutionChain为止。
因为拦截器是可选的,所以只要有handler,就有HandlerExecutionChain。如果查找不到任何handler,默认返回一个HTTP 404给客户端。
探查HandlerMapping实例
由于HandlerMapping是Spring bean,我们可以扫描容器来探查一下应用中HandlerMapping的状态,示例代码如下:
@RestController
public class HelloController implements Controller {
@Autowired
private ApplicationContext applicationContext;
@RequestMapping("/test")
public String test(String name) {
return "test/"+name;
}
@RequestMapping("/handlerMapping")
public String handlerMapping() {
StringBuilder sb = new StringBuilder();
Map<String, HandlerMapping> handlerMapping = applicationContext.getBeansOfType(HandlerMapping.class);
List<HandlerMapping> mappingList = new ArrayList<>(handlerMapping.values());
AnnotationAwareOrderComparator.sort(mappingList);
mappingList.forEach(mapping -> {
if (mapping instanceof SimpleUrlHandlerMapping) {
sb.append("SimpleUrlHandlerMapping:<br/>");
((SimpleUrlHandlerMapping) mapping).getUrlMap().forEach((k, v) -> {
sb.append(" ").append(k).append("====>").append(v).append("<br/>");
});
} else if (mapping instanceof RequestMappingHandlerMapping) {
sb.append("RequestMappingHandlerMapping:<br/>");
((RequestMappingHandlerMapping) mapping).getHandlerMethods().forEach((k, v) -> {
sb.append(" ").append(k).append("====>").append(v).append("<br/>");
});
} else {
sb.append("UnknownMapping:<br/>").append(" ").append(mapping).append("<br/>");
}
});
return sb.toString();
}
这段代码将HandlerMapping这个类型的所有bean找出来,通过AnnotationAwareOrderComparator.sort
按优先级进行排序,然后依次查看:对于SimpleUrlHandlerMapping,打印它的url和handler;对于RequestMappingHandlerMapping,打印url和对应的方法信息;对于其他类型,标志为UnknownMapping。
我用的Spring Boot 2.1.3来运行这个示例,打印的结果大致如下:
SimpleUrlHandlerMapping:
**/favicon.ico====>ResourceHttpRequestHandler [class path resource [META-INF/resources/], class path resource [resources/], class path resource [static/], class path resource [public/], ServletContext resource [/], class path resource []]
RequestMappingHandlerMapping:
{ /test}====>public java.lang.String controller.HelloController.test()
{ /handlerMapping}====>public java.lang.String controller.HelloController.handlerMapping()
{ /error}====>public org.springframework.http.ResponseEntity> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
{ /error, produces [text/html]}====>public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
UnknownMapping:
org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping@d53029f
UnknownMapping:
org.springframework.boot.autoconfigure.web.servlet.WelcomePageHandlerMapping@11ba16b0
SimpleUrlHandlerMapping:
/webjars/**====>ResourceHttpRequestHandler ["classpath:/META-INF/resources/webjars/"]
/**====>ResourceHttpRequestHandler ["classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/", "/"]
打印结果里面包含的信息还是挺多的,我们来分析一番:
- 第一个SimpleUrlHandlerMapping,将
**/favicon.ico
这个url映射给了一个叫做ResourceHttpRequestHandler的处理器,从名字看,这是一个读取静态资源的handler,后面打印的是资源位置; - 第二个是RequestMappingHandlerMapping,不出意外,我们在Controller里面通过注解@RequestMapping定义的两个映射
/test
和/handlerMapping
都出现在这里;Spring Boot默认还定义了/error
的映射; - 第三个是BeanNameUrlHandlerMapping类型的映射,它直接定义url到bean name的映射,待会我们再研究它;
- 接着又是一个静态资源的SimpleUrlHandlerMapping,可以让我们直接访问Spring Boot应用classpath
/resource,/static, /public,/META-INF/resources/
下的静态资源
我们至少可以的得出两个结论:
- 所有通过@RequestMapping声明的映射关系,都会被一个RequestMappingHandlerMapping类型的bean所封装;
- 基于Spring Boot的web应用之所以能直接通过url访问某些特定路径下的静态资源,是因为它预置了一个SimpleUrlHandlerMapping,映射了URI
/**
。
注意:上面这些HandlerMapping是Spring Boot的自动配置机制创建的,如果你在工程中做了自定义配置,那么结果可能完全不一样。
SimpleUrlHandlerMapping
SimpleUrlHandlerMapping可以让我们手动添加url到handler的映射关系:
@Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setUrlMap(Collections.singletonMap("/simple","helloController"));
mapping.setOrder(Integer.MIN_VALUE);
return mapping;
}
上面引用的helloController这个bean必须是Spring MVC支持的handler类型。
如果没有这句代码mapping.setOrder(Integer.MIN_VALUE);
,访问http://localhost:8080/simple
会报404错误,这是因为HandlerMapping是按优先级排列的。我们自定义的SimpleUrlHandlerMapping默认Order是0,请求被系统内置的那个处理静态资源的SimpleUrlHandlerMapping拦截了。
BeanNameUrlHandlerMapping
这个mapping动态扫描ApplicationContext下面,有没有和URL路径同名的bean:
@Component("/bean")
public class BeanHandler implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.getWriter().write("BeanHandler response");
return null;
}
}
本地运行这个应用,在浏览器输入http://localhost:8080/bean
即可访问它。
本章完整示例代码见示例工程的handlerMapping模块。
总结
在实现层面,Spring MVC由一系列特定的bean类型来构成;DispatchServlet调用这些bean来完成http请求的处理。我们可以通过定义这些类型的bean来配置Spring MVC,不过更好的方式是使用WebMvcConfigurer接口。
在这些bean类型中,个人认为HandlerMapping是最重要的,尽管实际项目中我们几乎不直接使用它。HandlerMapping封装了URL到处理器之间的映射关系,通过对一个Sping Boot Web工程的HandlerMapping实例进行分析,我们彻底搞明白了为什么放在static、public这些路径下的静态资源能直接通过url访问到。
多个HandlerMapping之间可能会发生url映射的竞争,从而导致预料之外的404错误;有了本章的知识,对此类问题,我们应该有了明确的定位思路。