前言
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);
}
}
結果: