springmvc(三)Handler、HandlerMapping和HandlerAdapter

前言

DispatcherServlet 通過 HandlerMapping 查找到 Handler,然後委託 HandlerAdapter 去執行 Handler,生成 ModelAndView。

爲什麼需要 Handler、HandlerMapping和HandlerAdapter

日常開發中的Handler、HandlerMapping和HandlerAdapter

回想下,日常的開發中,我們需要針對 HttpServeletRequest 做各種處理,我們的處理邏輯,其實就是一個 Handler。
有了Handler 之後,我們還需要配置什麼時候,DispatcherServlet 需要去使用這些 Handler 處理 HttpServeletRequest,HandlerMapping 就是提供了url 和 Handler 的映射。這樣,DispatcherServlet 可以通過 HandlerMapping 獲取到對應的 Handler ,並執行 Handler 中,我們定義的業務邏輯。
那麼 HandlerAdapter 的作用呢?其實是爲了使 DispatcherServlet 不關注具體的 Handler。考慮下面的場景,@RequestMapping 修飾的方法和實現Controller 接口的類,其實都是 Handler,這兩種 Handler 有很大不同,那麼如何使得 DispatcherServlet 可以直接調用 Handler 的邏輯,而不需要關注 Handler的形態呢?這個時候可以使用「適配器模式」,爲各種 Handler 提供不同的 HanderAdapter,DispatcherServlet 只需要調用 HandlerAdapter 的方法即可,不需要關注 HandlerAdapter 是如何通過各種形態的 Handler 去執行業務邏輯的。
日常開發中,我們一般只需要配置 Handler 和 HandlerMapping,因爲 springmvc 已經提供了常用的 HandlerAdapter,一般情況下,我們不需要指定 HandlerAdapter(除非需要自定義 Handler 的執行)。

Handler

Handler 定位是處理 HttpServletRequest。Handler 只是一個概念,springmvc 並沒有設計統一的接口,進行規範。
springmvc 內置的 Handler:

handler 作用(場景)
@RequestMapping方式 一個@RequestMapping 註解修飾的方法,可以看作一個 Handler(HandlerMethod(InvocableHandlerMethod))
實現HttpRequestHandler接口 (ResourceHttpRequestHandler) 實現 HttpRequestHandler 接口的類,可以看作是一個 Handler,常用的是 springmvc 提供的 ResourceHttpRequestHandler ,ResourceHttpRequestHandler 可以處理靜態資源請求
實現Controller 接口 實現Controller 接口的類,可以看作是 Handler。由於這種方式比較古老,不做過多糾結

HandlerMapping

HandlerMapping 定位是通過 url,匹配到合適的 Handler,並將 Handler 和 攔截器鏈捆綁在 HandlerExecutionChain 對象中,返回給調用方。
上文可以看出:一個handler可能是一個方法,也可能是一個 Controller 對象或者 HttpRequestHandler 對象。
針對這種情況, HandlerMapping分爲兩個分支來處理

  • AbstractHandlerMethodMapping:處理 url 與 Method 級別 handler 的映射
  • AbstractUrlHandlerMapping:處理 url 與 類級別 Handler 的映射(例如 Controller 接口的實現類,或者是 HttpRequestHandler 接口的實現類)

它們又統一繼承於 AbstractHandlerMapping。

springmvc 內置的 AbstractHandlerMethodMapping 實現類

HandlerMapping 作用(場景)
RequestMappingHandlerMapping 用於處理 @RequestMapping 修飾的方法 Handler

springmvc 內置的 AbstractUrlHandlerMapping 實現類

HandlerMapping 作用(場景)
SimpleUrlHandlerMapping 針對已經註冊到 SimpleUrlHandlerMapping 的 urlMap 中的所有的映射註冊 handler。(註冊時機:容器初始化之後)
BeanNameUrlHandlerMapping 針對容器中,所有以"/" 開頭的,beanName,進行 handler 註冊(註冊 handler 時機:容器初始化之後)

這裏需要注意下,SimpleUrlHandlerMapping 只會註冊其 urlMap 屬性中包含的 handler,而 BeanNameUrlHandlerMapping 則是針對整個容器中 beanName 以"/"開頭的 bean,進行 Handler 註冊。

HandlerAdapter

HandlerAdapter 的定位是,調用 Handler 處理請求。
Springmvc 提供的 HandlerAdapter 實現

HandlerAdapter 作用(場景)
HttpRequestHandlerAdapter 用來調用實現了 HttpRequestHandler 接口的 handler 類
SimpleControllerHandlerAdapter 用來調用實現了 Controller 接口的 handler 類
RequestMappingHandlerAdapter 用來調用 @RequestMapping 修飾的 handler 方法

使用場景:利用 ResourceHttpRequestHandler 進行靜態資源處理

場景:如果我們需要將 html 文件以及 css img 等靜態文件從 servlet 服務器中返回,其實我們不需要自定義 handler,直接使用 Spring 提供的 ResourceHttpRequestHandler 即可做到

方式一:將 ResourceHttpRequestHandler 配置爲一個 Spring 容器中的 bean

將 ResourceHttpRequestHandler 配置爲一個 Spring 容器中的 bean,不過需要注意,bean 的名字,必須以"/" 開頭,這是因爲,BeanNameUrlHandlerMapping 只會處理註冊bean 名稱以“/” 開頭的 ResourceHttpRequestHandler。

舉例: /*.html 的靜態資源請求直接返回對應的靜態文件(由於我這邊使用的是 springboot,所以我的靜態資源文件都在 classpath:static/ 下)

    @Bean("/*.html")
    public ResourceHttpRequestHandler mm(){
        ResourceHttpRequestHandler resourceHttpRequestHandler = new ResourceHttpRequestHandler();
        List<String> locations =  new ArrayList<>();
        locations.add("classpath:static/");
        resourceHttpRequestHandler.setLocationValues(locations);
        return resourceHttpRequestHandler;
    }

這樣,如果請求的 url 與 /*.html 匹配,則直接響應 classpath:static/ 目錄下的資源文件

方式二:直接生成 SimpleUrlHandlerMapping,並向其註冊 ResourceHttpRequestHandler

    @Bean
    public SimpleUrlHandlerMapping ss(ApplicationContext applicationContext) throws Exception {

        ResourceHttpRequestHandler resourceHttpRequestHandler = new ResourceHttpRequestHandler();
        List<String> locations = new ArrayList<>();
        locations.add("classpath:static/");
        locations.add("classpath:static/css/");
        locations.add("classpath:static/img/");
        resourceHttpRequestHandler.setLocationValues(locations);
        List<ResourceResolver> resourceResolvers = new ArrayList<>();
        resourceResolvers.add(new PathResourceResolver());
        resourceHttpRequestHandler.setApplicationContext(applicationContext);
        resourceHttpRequestHandler.afterPropertiesSet();

        Map<String, HttpRequestHandler> urlMap = new LinkedHashMap<>();
        urlMap.put("/*.html", resourceHttpRequestHandler);
        urlMap.put("/*.css", resourceHttpRequestHandler);
        urlMap.put("/*.img", resourceHttpRequestHandler);

        SimpleUrlHandlerMapping simpleUrlHandlerMapping = new SimpleUrlHandlerMapping();
        simpleUrlHandlerMapping.setUrlMap(urlMap);

        return simpleUrlHandlerMapping;
    }

【推薦】方式三:直接利用 springmvc 提供的配置 api,註冊 ResourcehttpRequestHandler

方式一的缺點:

  • 每個 ResourcehttpRequestHandler 都需要作爲一個 bean,而且跟請求 url 映射是通過 bean 名稱來確定的
    方式二的缺點:
  • 代碼複雜度稍高,必須非常熟悉ResourceHttpRequestHandler 和 SimpleUrlHandlerMapping

方式三是通過 Springmvc 提供的配置 API—WebMvcConfigurer 的 addResourceHandlers api,來註冊 ResourcehttpRequestHandler

@Configuration
@EnableWebMvc
public class MyConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("*.html").addResourceLocations("classpath:static/");
        registry.addResourceHandler("css/**").addResourceLocations("classpath:/META-INF/resources/css/");
        registry.addResourceHandler("img/**").addResourceLocations("classpath:/META-INF/resources/img/");
        registry.addResourceHandler("js/**").addResourceLocations("classpath:/META-INF/resources/js/");
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
}

這種方式,其實跟第二種沒有太大的區別,只是 springmvc 在WebMvcConfigurationSupport 的基礎上提供了更加便捷的 API。使用的時候,需要在 @Configuration 類上,使用 @EnableWebMvc 修飾(@EnableWebMvc 的主要作用是註冊一個 DelegatingWebMvcConfiguration 收集 WebMvcConfigurer 實現類中的各種配置)

小結1

到這裏, Handler、HandlerMapping、HandlerAdapter 各自的定位已經十分明顯了。需要記住的是,Springmvc 提供的兩類 handler

  • 基於 @RequestMapping 修飾的 handler 方法
  • 實現 Controller / HttpRequestHandler 接口的 handler 類
    因爲有了這兩類 handler,纔有了後面對應的的 HandlerMapping 和 HandlerAdapter

HandlerMethod、RequestMappingHandlerMapping 和 RequestMappingHandlerAdapter 工作流程

流程一:RequestMappingHandlerMapping 根據 url 映射 HandlerMethod

  • step 1 註冊 HandlerMethod:RequestMappingHandlerMapping 的 afterPropertiesSet 方法中,會掃描所有被 @Controller 修飾的類的 bean,並找到被 @RequestMapping 修飾的方法,並將方法註冊到 mappingRegistry 中
  • step 2 根據 url 匹配:RequestMappingHandlerMapping 對請求的 url 在 mappingRegistry 中進行 HandlerMethod 匹配

流程二:RequestMappingHandlerAdapter 執行 HandlerMethod

  • step 1 參數解析:爲HandlerMethod 中的各個參數,匹配合適的參數解析器,並 通過 WebDataBinder 或者 HttpMessageConverter 解析參數
  • step 2 方法執行:通過反射,執行方法,傳入 step 1 中解析好的參數
  • step 3 處理返回值

參數解析大致分爲以下幾種類型:

  • 繼承 AbstractNamedValueMethodArgumentResolver 的參數解析類
    Spring Web使用AbstractNamedValueMethodArgumentResolver抽象命名值(named value)控制器方法參數解析的邏輯。像是一般意義上我們通過POST FORM方式傳遞的參數,GET URL中通過QueryString傳遞的參數,請求的頭部信息,URL路徑變量這種命名了的參數,都可以稱作"命名值"(named value)。一般我們獲取到的 named value 爲 String 類型,這個時候,還需要使用 WebDataBinder 轉換爲所需的 java bean
  • 繼承 AbstractMessageConverterMethodArgumentResolver 的參數解析類
    使用 HttpMessageConverter 解析報文,並反序列化爲參數類型

使用場景:自定義@ContentType 註解,並通過自定義參數解析器解析@ContentType 註解修飾的方法參數

定義註解:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ContentType {
}

定義解析器

public class ContentTypeResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(ContentType.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        return webRequest.getHeader("content-type");
    }
}

註冊解析器到配置中:

@Configuration
@EnableWebMvc
public class MyConfig implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new ContentTypeResolver());
    }


}

測試:

@Controller
public class MyController3 {

    @RequestMapping("/test999")
    public void test(@ContentType String contentType){
        System.out.println(contentType);
    }
}

結果:
在這裏插入圖片描述

推薦閱讀

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