Spring之WebMvcConfigurationSupport
注: WebMvcConfigurationSupport是spring5.x的配置方式。如果是6.x,则是通过实现WebMvcConfigurer的方式实现(建议再加上@EnableWebMvc)
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。