WebMvcConfigurationSupport是mvc的核心配置。開發spring,了解和掌握這個是必須的。
為了簡約篇幅,本文把"WebMvcConfigurationSupport"縮寫為wms。
本文主要關注圍繞DispatcherServlet(分發者服務程序)發生的和wms有關的一些內容,不關注http請求在容器部分的機制和代碼。
通過本文能夠:
- 了解wms是做什么的
- 了解大體如何使用wms
- 作為了解和使用wms的一個簡單參考
一、wms的介紹
1.原文注釋
This is the main class providing the configuration behind the MVC Java config.
It is typically imported by adding @EnableWebMvc to anapplication @Configuration class.
An alternative more advanced option is to extend directly from this class and override methods as necessary,
remembering to add @Configuration to the subclass and @Bean to overridden @Bean methods.
For more details see the javadoc of @EnableWebMvc.
This class registers the following HandlerMappings:
•RequestMappingHandlerMapping ordered at 0 for mapping requests to annotated controller methods.
•HandlerMapping ordered at 1 to map URL paths directly to view names.
•BeanNameUrlHandlerMapping ordered at 2 to map URL paths to controller bean names.
•RouterFunctionMapping ordered at 3 to map router functions.
•HandlerMapping ordered at Integer.MAX_VALUE-1 to serve static resource requests.
•HandlerMapping ordered at Integer.MAX_VALUE to forward requests to the default servlet.
Registers these HandlerAdapters:
•RequestMappingHandlerAdapter for processing requests with annotated controller methods.
•HttpRequestHandlerAdapter for processing requests with HttpRequestHandlers.
•SimpleControllerHandlerAdapter for processing requests with interface-based Controllers.
•HandlerFunctionAdapter for processing requests with router functions.
Registers a HandlerExceptionResolverComposite with this chain ofexception resolvers:
•ExceptionHandlerExceptionResolver for handling exceptions through org.springframework.web.bind.annotation.ExceptionHandler methods.
•ResponseStatusExceptionResolver for exceptions annotated with org.springframework.web.bind.annotation.ResponseStatus.
•DefaultHandlerExceptionResolver for resolving known Springexception types
Registers an AntPathMatcher and a UrlPathHelperto be used by:
•the RequestMappingHandlerMapping,
•the HandlerMapping for ViewControllers
•and the HandlerMapping for serving resources
Note that those beans can be configured with a PathMatchConfigurer.
Both the RequestMappingHandlerAdapter and the ExceptionHandlerExceptionResolver are configured with default instances of the following by default:
•a ContentNegotiationManager
•a Default FormattingConversionService
•an org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean if a JSR-303 implementation is available on the classpath
•a range of HttpMessageConverters depending on the third-partylibraries available on the classpath.
我們翻譯下:
wms是webMvc通過java代碼方式配置的主類。
典型地,可以通過給一個應用的配置類(帶@Configuration)添加@EnableWebMvc注解,即可導入Mvc java 配置。
另外一個更加高級的替代選項是繼承本類,並根據需要重寫一些方法,並記得為這個子類添加@Configuration注解,同時為那些重寫@Bean方法添加@Bean注解。
更多的內容,請參考@EnableWebMvc的java文檔
本類注冊以下處理器映射:
.RequestMappingHandlerMapping(RequestMapping處理器映射) ,順序為0,用於映射帶注解的控制器方法
.HandlerMapping (處理器映射),順序1,用於映射url路徑和視圖名稱
.BeanNameUrlHandlerMapping(bean名稱處理器映射),順序2,用於映射url路徑到控制器bean名稱
.RouterFunctionMapping(路由器功能映射),順序3,用於映射路由函數
.HandlerMapping (處理器映射),順序Integer.MAX_VALUE-1,用於處理靜態資源請求
.HandlerMapping (處理器映射),順序Integer.MAX_VALUE,用於服務器內部重定向到一個默認的servlet
注冊這些處理器適配器:
•RequestMappingHandlerAdapter(RequestMapping處理器適配器),用於處理到帶注解的控制器方法請求
•HttpRequestHandlerAdapter(http請求處理器適配器),用於處理 帶HttpRequestHanders的請求
•SimpleControllerHandlerAdapter(簡單控制器處理器適配器),用於處理基於接口的控制的請求
•HandlerFunctionAdapter(處理器功能適配器),用於處理帶路由器功能的請求
注冊處理器異常解析復合器,該復合器帶有一些異常處理器:
•ExceptionHandlerExceptionResolver(異常處理器異常解析器),用於處理org.springframework.web.bind.annotation.ExceptionHandler的異常
•ResponseStatusExceptionResolver(響應狀態異常解析器),用於處理org.springframework.web.bind.annotation.ResponseStatus異常
•DefaultHandlerExceptionResolver(默認處理器異常解析器),用於處理已知的的spring異常類
注冊一個antPathMatcher和一個urlPathHelper,以便能夠被以下對象使用:
•RequestMappingHandlerMapping(RequestMapping處理器映射)
•HandlerMapping for ViewControllers ,用於視圖控制器的處理器映射
•HandlerMapping for serving resources ,用於處理資源的處理器映射
注意,以上這些bean可以通過PathMatchCOnfigurer配置。
RequestMappingHandlerAdapter和ExceptionHandlerExceptionResolver都由以下對象的默認實例配置了:
.ContentNegotiationManager(媒體類型管理器,用於偵測請求的媒體類型)
.FormattingConversionService(格式轉換服務)
.org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean(bean驗證器,如果類路徑具有JSR-303的實現)
.HttpMessageConverters(http消息轉換器)。具體有幾個轉換器,需要看類路徑的三方實現庫的多寡
2.功能
如它的名稱,即執行各種webMvc的配置,通過在其中定義大量的有關Bean(大約有25個左右),方便MVC的流程的特定環節從Bean工廠中獲取這些bean,並利用這些Bean完成webMvc的大部分工作。
概括下,wms就是這些功能:
- 注冊處理器映射,以便把請求導向具體的控制器、控制器方法、視圖、資源。注意,這些映射器的目的是做一個關系匹配
- 注冊處理器適配器,為具體的功能選擇具體的處理程序。例如參數解析,消息轉換等
- 注冊異常處理器
- 注冊路徑映射解析有關類
總而言之,言而總之,wms就是告訴spring用於處理請求的有關工具,這些工具用於處理映射關系,解析參數,返回消息等等。
一個完整的web請求和響應過程中需要執行的動作所使用的配置(程序、配置)基本都可以在wms中進行管理,記住這一點基本上算是對wms有所了解了。
二、名詞解析
需要記住和理解的名詞非常多,只能挑本人關注的。
1.解釋器/解決器
英文:Resolver
中文含義:問題解決者。 針對不同的場景,可以有很多的解釋。在本文中,按照習慣,我們從用途進行翻譯,大體翻譯為解釋器,轉換器都可以。
2.攔截器
英文:interceptor
中文含義:進行攔截的人/物。在spring中,專門指攔截http請求的一段程序。和過濾器基本一樣,沒有什么本質上的區別。不過攔截器可以打斷請求的過程,而過濾器主要做重定向(如果有必要)
3.控制器、參數、方法
控制器:controller,對應注解@Controller
參數:parameter,這里主要闡述的是控制器中方法的入參
方法:method,這里指控制器中的方法(有Request的方法)
我們日常spring編程主要針對這三者。
某種程度上,只要會復制粘貼+mvc你就可以說自己是一個java開發工程師了,是不是很容易啊?
4.返回值、消息
返回值:return value。控制器方法大部分情況下都有返回值,如果沒有,那么spring也會給一個默認的響應
消息:message.這里指從客戶端發送的請求消息或者是服務端返回給客戶端的消息。這里我們主要關心ResponseBody注解。
5.Cors
英文:CORS(Cross-origin resource sharing)
中文:跨源資源共享
一般情況下,web服務器不允許跨源資源共享,但很多時候又有這個需求。所以需要指定什么資源可以被什么其它非同源請求訪問。
6.資源和視圖
資源:resource,spring通常指靜態的數據(包括圖片,腳本,文本,多媒體信息等等)
視圖:view,spring指展示信息的頁面
7.驗證器
英文:validator,spring通常指用於校驗特定參數的簡單業務邏輯
8.格式化器和轉換器
格式化器:formatter
轉換器:converter
二者有相通之處,但基本一致,格式和轉換基本即使你中有我,我中有你,密不可分。其作用,顧名思義,就是進行轉化/格式化。 例如消息轉換、屬性轉換等。
9.異常
具體略。
10.應用上下文和sevlet上下文
這兩個太重要,因為WebMvcConfigurationSupport實現的很大一部分和應用上下文、服務器上下文有關。
- ApplicationContext:應用上下文,屬於spring自有的一個應用上下文,主要管理spring的bean。能夠管理bean,絕對是spring的最核心能力之一。
包路徑:org.springframework.context.ApplicationContext
- ServletContext:服務器上下文,准確地說,通常地說,它指的是web服務器/容器的根上下文,容器通常為每個應用創建一個服務器上下文,保存應用和web相關的許多基本信息。
包路徑:javax.servlet.ServletContext
我們開發的系統,某個方便面來說就是和兩個東西關聯:web+bean。web和bean有關的上下文就是 ServletContext,ApplicationContext。
11.媒體/媒介類型
英文:MediaType
包路徑:org.springframework.http.MediaType
根據介紹,媒體/媒介類型實為MimeType的子類,spring僅僅對它做了一些包裝,方便使用。
所以,根本上要理解MiME在http協議中的地位。
Mime的非常友好的介紹見這里:MIME 類型 - HTTP | MDN (mozilla.org)
或者看這里 MIME(多用途互聯網郵件擴展類型)_百度百科 (baidu.com)
兩個結合起來,就能夠明白mime是什么東西。
如果有什么值得記的,就是為什么叫” mail extensions"(郵件擴展)。
郵件大概是互聯網最早傳遞的數據信息,而且基本上是最重要的,所以開始命名的時候就這么叫了。但是現在iana(互聯網號碼分配機構)又慢慢要拋棄mime這個概念,改為mediaType(媒體類型)。
很明顯這么稱呼是更加合理的,如果沒有什么特別說明,“媒體類型”就是表示MINE或者上下文類型(context-type)
12.請求映射處理器映射器(RequestMappingHandlerMapping)
注意這個前綴 RequestMapping(請求映射)本身是注解的名稱(@RequestMapping)。
所以這個東西可以理解為RequestMapping注解的處理器映射器,大體作用是在注解和資源之間建立映射關系。
我們看下源碼(默認的):
@Bean @SuppressWarnings("deprecation") public RequestMappingHandlerMapping requestMappingHandlerMapping( @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager, @Qualifier("mvcConversionService") FormattingConversionService conversionService, @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) { RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping(); mapping.setOrder(0); mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider)); mapping.setContentNegotiationManager(contentNegotiationManager); mapping.setCorsConfigurations(getCorsConfigurations()); PathMatchConfigurer pathConfig = getPathMatchConfigurer(); if (pathConfig.getPatternParser() != null) { mapping.setPatternParser(pathConfig.getPatternParser()); } else { mapping.setUrlPathHelper(pathConfig.getUrlPathHelperOrDefault()); mapping.setPathMatcher(pathConfig.getPathMatcherOrDefault()); Boolean useSuffixPatternMatch = pathConfig.isUseSuffixPatternMatch(); if (useSuffixPatternMatch != null) { mapping.setUseSuffixPatternMatch(useSuffixPatternMatch); } Boolean useRegisteredSuffixPatternMatch = pathConfig.isUseRegisteredSuffixPatternMatch(); if (useRegisteredSuffixPatternMatch != null) { mapping.setUseRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch); } } Boolean useTrailingSlashMatch = pathConfig.isUseTrailingSlashMatch(); if (useTrailingSlashMatch != null) { mapping.setUseTrailingSlashMatch(useTrailingSlashMatch); } if (pathConfig.getPathPrefixes() != null) { mapping.setPathPrefixes(pathConfig.getPathPrefixes()); } return mapping; }
這個代碼核心是創建一個RequestMapping的處理器映射器,這個映射器又包含了什么東西了?
- 添加默認的轉換器和資源url管理器的攔截器
- 添加請求媒體類型管理器
- 添加了跨域請求配置
- 添加了路徑匹配器,以及路徑有關的一些其它程序
13.請求映射處理器適配器(RequestMappingHandlerAdapter)
RequestMapping注解處理器適配器。
看源碼(默認):
@Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter( @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager, @Qualifier("mvcConversionService") FormattingConversionService conversionService, @Qualifier("mvcValidator") Validator validator) {
RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
adapter.setContentNegotiationManager(contentNegotiationManager);
adapter.setMessageConverters(getMessageConverters());
adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(conversionService, validator));
adapter.setCustomArgumentResolvers(getArgumentResolvers());
adapter.setCustomReturnValueHandlers(getReturnValueHandlers());
if (jackson2Present) {
adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
}
AsyncSupportConfigurer configurer = getAsyncSupportConfigurer();
if (configurer.getTaskExecutor() != null) {
adapter.setTaskExecutor(configurer.getTaskExecutor());
}
if (configurer.getTimeout() != null) {
adapter.setAsyncRequestTimeout(configurer.getTimeout());
}
adapter.setCallableInterceptors(configurer.getCallableInterceptors());
adapter.setDeferredResultInterceptors(configurer.getDeferredResultInterceptors());
return adapter; }
- 設置媒體類型管理器(和映射器一樣,都必須了解請求的媒體類型)
- 設置消息轉換器
- web綁定初始化器(和轉換服務、驗證器有關)
- 設置自定義參數解釋器
- 設置自定義返回值處理器
- 添加jackson請求體RequestBody顧問(如果有jackson類)
- 添加jackson響應體ResponseBody顧問(如果有jackson類)
- 配置異步任務執行器,設置異步請求超時。如果有配置異步
- 配置異步的可調用攔截器
- 配置異步延時結果攔截器
把這個內容和RequestMappingHanlderMapping的內容對照下,我們就更能夠理解二者的區別。
--------------------------------------------------------------------------------------------------------------------
- 映射器:主管資源和路徑有關的內容
- 適配器:主管具體請求消息處理和返回消息
注:spring已經提供了消息處理器的默認實現JackSon.
RequestMappingHandlerAdapter和RequestMappingHandleMapping是DispatcherServlet(分發器)的主要處理內容。
以下是分發器屬性的主要內容:
/** MultipartResolver used by this servlet. */ @Nullable private MultipartResolver multipartResolver; /** LocaleResolver used by this servlet. */ @Nullable private LocaleResolver localeResolver; /** ThemeResolver used by this servlet. */ @Nullable private ThemeResolver themeResolver; /** List of HandlerMappings used by this servlet. */ @Nullable private List<HandlerMapping> handlerMappings; /** List of HandlerAdapters used by this servlet. */ @Nullable private List<HandlerAdapter> handlerAdapters; /** List of HandlerExceptionResolvers used by this servlet. */ @Nullable private List<HandlerExceptionResolver> handlerExceptionResolvers; /** RequestToViewNameTranslator used by this servlet. */ @Nullable private RequestToViewNameTranslator viewNameTranslator; /** FlashMapManager used by this servlet. */ @Nullable private FlashMapManager flashMapManager; /** List of ViewResolvers used by this servlet. */ @Nullable private List<ViewResolver> viewResolvers;
三、spring-mvc種http請求大概執行過程
這么多名詞,其實本質上就是圍繞http展開的,具體來說就是圍繞spring的http實現來展開。
我們使用spring的主要目的就是為了web(http)應用。可以說,沒有web,spring的存在的意義基本上就沒有了。
利用ide提供的調試工具,最容易能夠直觀地了解一個http請求的大概執行過程。如果願意,還可以詳細到每一行代碼。
下圖是調試的時候,顯示的調用鏈:
四、常用-配置攔截器
如何配置攔截器,這個沒有什么太多可說。
記住一點,現在不但spring自身實現功能的時候使用了很多的攔截器(這個在springCloud中很明顯),就是一般的開發人員也喜歡(濫)用大量的攔截器。
我們只能感激一點:現在電腦越來愉快了。
配置例子。
@Override protected void addInterceptors(InterceptorRegistry registry) { // 以下是啟用 token認證的. 本例使用cookie認證的時候,請注釋掉,避免無法完成測試 // 反過來,如果啟用了token認證,那么過濾器驗證就可以取消掉 ,或者取消 上文的 registrationBean() registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**").excludePathPatterns("*.js") .excludePathPatterns("*.html").excludePathPatterns("*.css").excludePathPatterns("/login") .excludePathPatterns("/logout").excludePathPatterns("/index").excludePathPatterns("/"); super.addInterceptors(registry); }
五、常用-方法參數(輸入)解析(請求)
spring已經有提供默認的以下幾個注解的處理:@RequstBody,@RequestParam
但是當我們開發api的時候,常常有這樣的需求:
- 在過濾器或者攔截器攔截請求並獲取有關參數
- 在具體的控制方法中想獲取請求的一些固定信息,例如登錄用戶信息,授權信息等等 。
5.1 例子-自定義參數注解
、 如果每個地方都寫,有點麻煩,所以spring允許自定義的參數解析。
例子:
a.定義一個注解 @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface SessionAtrrAnnotation { }
b.繼承並實現HandlerMethodArgumentResolver public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(SessionAtrrAnnotation.class); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { return webRequest.getNativeRequest(HttpServletRequest.class).getSession().getAttribute("userInfo"); } }
c.在wms中注冊 @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(new LoginUserArgumentResolver()); } d.應用 @RequestMapping(value = "/deleteById") @ResponseBody public PublicReturn deleteById(@SessionAtrrAnnotation LoginUserInfo loginInfo) { //TODO:刪除用戶 return null; }
5.2 自定義特定數據類型解析
這種自定義類型,有兩種情況:
a.解析非JSON媒體類型的參數
b.解析媒體類型為JSON的參數中某個屬性
這個網絡上有很多解決方案,例如:
SpringMVC自定義處理多種日期格式的格式轉換器_二木成林的博客-CSDN博客_springmvc日期轉換 -- 這個是解析非json媒體類型參數的
不過作者是比較傳統的xml配置,如果是springboot,直接在wms中覆蓋addFormatters即可。
fastjson序列化時間自定義格式_biangabiang的博客-CSDN博客_fastjson自定義序列化格式 --使用阿里巴巴的fastjson,不過記得先配置Http消息轉換器為FastJson(覆蓋wms中的configureMessageConverters)
這是因為fastjson允許針對不同類型使用不同的序列化程序。
5.3媒體類型為JSON,且使用JackSon處理特定類型
無論是JackSon還是FastJson的,它們都是實現了HttpMessageConverter接口。 標准一樣,細節有所區別而已。
所以,如果使用默認的JackSon的時候,可以和網上常常提到的FASTjson一樣的思路來解決問題。
有關內容可以看這個:Jackson Tutorial | Baeldung
然而比較簡單的方式還是覆蓋下configureMessageConverters,在其中定制化MappingJackson2HttpMessageConverter的各種屬性。
例如網上有這樣的例子:
@Bean public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() { Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder().serializers(LOCAL_DATETIME_SERIALIZER) .serializationInclusion(JsonInclude.Include.NON_NULL); return new MappingJackson2HttpMessageConverter(builder.build()); }
如果僅僅是想自定義序列化或者反序列化器,那么使用注解即可:
@JsonSerialize(using = FamilyJSONSerializer.class) public class Family { private Integer id; private String name; private Date addTime; private Date lastOptime; private String batchNo; }
FamilyJSONSerializer的代碼如下:
package study.config.message; import java.io.IOException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import study.model.family.Family; public class FamilyJSONSerializer extends JsonSerializer<Family>{ @Override public void serialize(Family value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (value==null) { gen.writeString("{}"); return; } gen.writeString(value.toString()); } @Override public Class<Family> handledType(){ return Family.class; } }
自定義消息的解析,通常不是那么迫切,只要知道大概即可。
沒有必要耗費太多的時間研究,並重新做一個輪子。
六、常用-響應消息(http消息轉換)配置(響應)
spring的設計是允許有多個http消息轉換器,每個轉換器對應不同的媒體類型。這些轉換器只要實現HttpMessageConverter接口即可。
當然如果你自己自定義,也可以讓一個轉換器對應n種媒體類型,或者n個轉換器對應一個媒體類型。
如果是這種情況,spring會采用一定的選擇缺略,保證每個媒體類型都可以進行適當的轉換。
網上太多了,不再詳細說明了。常見的即使用fastJson--覆蓋wms中的configureMessageConverters。
下面是jackson的例子:
@Override protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new ByteArrayHttpMessageConverter()); converters.add(new StringHttpMessageConverter()); converters.add(new ResourceHttpMessageConverter()); converters.add(new ResourceRegionHttpMessageConverter()); MappingJackson2HttpMessageConverter jconverter = new MappingJackson2HttpMessageConverter(); ObjectMapper objectMapper=jconverter.getObjectMapper(); objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() { @Override public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { jsonGenerator.writeString(""); } }); DateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); objectMapper.setDateFormat(dateFormat); converters.add(jconverter); }
主要兩點:設置日期格式、null輸出為"",節約前端編碼工作量。
七、常用-配置視圖和資源解釋
例如:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**") .addResourceLocations("classpath:/static/") .addResourceLocations("classpath:/public/") .addResourceLocations("classpath:/resources/") .addResourceLocations("file:" + uploadPath); } @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("main/index"); registry.setOrder(Ordered.HIGHEST_PRECEDENCE); super.addViewControllers(registry); }
八、小結
對於大部分的javaEE開發的工程師而言,都有必要理解webMvc的機制,以及wms的配置。
wms既簡單又復雜,涉及到webMvc的方法面面,相關的源碼非常之多,如要透徹了解其原理和機制只有一個方法:仔細閱讀並做好筆記。
此外,建議在閱讀本代碼前,先掌握http請求的基本原理和流程。另外為了提高閱讀和理解的效率,建議開啟調試模式,逐步調試,就能夠較快了解這里所涉及的有關知識。
我個人一直喜歡用這個方法。現在ide對於這個的支持非常好,無論是eclipse,idea還是netbean,似乎除了跟蹤不到機器指令,任何東西都可以跟蹤和窺探,強大到難以置信。
在調試過程中,可以發現代碼是從web容器(或者服務器)開始執行的,並最好從org.springframework.web.servlet.DispatcherServlet開始,因為在此類之前的大部分屬於容器的api。