SpringMVC之三:HandlerMapping

我們知道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請求的大體流程:

  1. 將WebApplicationContext綁定爲request上的一個attribute,key爲DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE;
  2. LocaleResolver綁定到request,使後續處理過程可以隨時訪問用戶的Locale;
  3. ThemeResolver綁定到request;
  4. MultipartResolver檢查request的請求體是不是multipart,如果是,將request包裝爲MultipartHttpServletRequest;
  5. 輪詢所有的HandlerMapping,查找對應的handler;
  6. HandlerAdapter執行該handler,包括攔截器,handler方法等;
  7. 依據handler返回結果,生成Http響應;
  8. 如果發生異常,由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("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;").append(k).append("====>").append(v).append("<br/>");
                });
            } else if (mapping instanceof RequestMappingHandlerMapping) {
                sb.append("RequestMappingHandlerMapping:<br/>");
                ((RequestMappingHandlerMapping) mapping).getHandlerMethods().forEach((k, v) -> {
                    sb.append("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;").append(k).append("====>").append(v).append("<br/>");
                });
            } else {
                sb.append("UnknownMapping:<br/>").append("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;").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/", "/"]

打印結果裏面包含的信息還是挺多的,我們來分析一番:

  1. 第一個SimpleUrlHandlerMapping,將**/favicon.ico這個url映射給了一個叫做ResourceHttpRequestHandler的處理器,從名字看,這是一個讀取靜態資源的handler,後面打印的是資源位置;
  2. 第二個是RequestMappingHandlerMapping,不出意外,我們在Controller裏面通過註解@RequestMapping定義的兩個映射/test/handlerMapping都出現在這裏;Spring Boot默認還定義了/error的映射;
  3. 第三個是BeanNameUrlHandlerMapping類型的映射,它直接定義url到bean name的映射,待會我們再研究它;
  4. 接着又是一個靜態資源的SimpleUrlHandlerMapping,可以讓我們直接訪問Spring Boot應用classpath/resource,/static, /public,/META-INF/resources/下的靜態資源

我們至少可以的得出兩個結論:

  1. 所有通過@RequestMapping聲明的映射關係,都會被一個RequestMappingHandlerMapping類型的bean所封裝;
  2. 基於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錯誤;有了本章的知識,對此類問題,我們應該有了明確的定位思路。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章