我們知道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錯誤;有了本章的知識,對此類問題,我們應該有了明確的定位思路。