很多人对spring mvc的请求处理流程都不陌生,今天我们要展开讲解的是请求体数据body的绑定、验证、格式化、类型转换,它是怎样实现的呢?其实就是大家熟悉的HandlerAdapter干的事情。

为何要讲这个呢?其实和我最近的工作内容是分不开的,刚好在设计开发一个数据聚合组件(它主要是解决微服务化后vo 拆分之疼),当然后续会开源出来的。

目录

spring mvc知识回顾

BeanWrapper

ConversionService

Formatter

DataBinder

四者关系图

Validator

非Spring MVC的使用

Spring MVC的使用

HttpMessageConverter和ConversionService是什么关系?


spring mvc知识回顾

1 用户向服务器发送请求,请求被Spring的DispatcherServlet(简称DS)捕获
2~5 DS 首先对URL进行解析,得到请求资源标识符(URI),然后根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain对象的形式返回
6~7 DS 根据获得的Handler,选择一个合适的HandlerAdapter(如果成功获得HandlerAdapter后,此时将开始执行拦截器的preHandler(...)方法)
8

提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)。 在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:验证、格式化、类型转换

HttpMessageConveter:将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息。

9 Handler执行完成后,向DS返回一个ModelAndView对象
10 此时DS将开始执行拦截器的postHandler(...)方法
11 DS根据返回的ModelAndView,选择一个适合的ViewResolver(必须是已经注册到Spring容器中的ViewResolver)。并调用 该ViewResolver 结合Model和View,来渲染视图。将渲染结果返回给客户端。

在8中是有关spring mvc request vo的处理,其实Spring官方文档有专门有些章节阐述“Validation, Data Binding, and Type Conversion”,其实用法很简单,但它内部是如何实现的却鲜有人知晓。

BeanWrapper

BeanWrapper是一个方便开发人员使用字符串来对Java Bean的属性执行get、set操作的工具类。它为那些UI类app提供了极大的便利,是以字符串和用户交互的。

Foo foo = new Foo();
BeanWrapperImpl fooWrapper = new BeanWrapperImpl(foo);
fooWrapper.setPropertyValue("intProperty", "1");
Object intProperty = fooWrapper.getPropertyValue("intProperty");

另外,BeanWrapper内部使用了两种机制:

1. PropertyEditor

BeanWrapper和java bean的内省模式密切关联,之前的文章分享过,而PropertyEditor(只提供了String >> Object的转换)隶属于Java Bean规范。

2. ConversionService

Spring自3.0之后提供的替代PropertyEditor的机制

注:按照Spring官方文档的说法,当容器内没有注册ConversionService的时候,会退回使用PropertyEditor机制。

ConversionService

ConversionService及其相关一套类型转换机制是一套通用的类型转换SPI,相比PropertyEditor只提供String >> Object的转换,ConversionService能够提供任意Object >> Object的转换。

由此我们可以看出,Spring为何要使用ConversionService替代PropertyEditor有三个原因:

  1. ConversionService功能更强大,支持的类型转换范围更广
  2. ConverterFactory支持一整个class hierarchy的转换(也就是多态),PropertyEditor则不行
  3. Java Bean这个规范最初是和Java GUI(Swing)一起诞生的,PropertyEditor接口里有大量和GUI相关的方法,显然已经过时了。顺便提一句,Java Bean和POJO不是一个概念,Java Bean不仅有setter、getter,还有一系列和Java GUI配套的东西。

Formatter

Formatter SPI是另外一套和PropertyEditor类似的,String<->Object的转换机制,但是有两个优点:

  1. 接口更干净,没有关于GUI的部分,只有 Printer.print() 和 Parser.parse() 两个方法
  2. 基于注解,支持同一类型的属性根据不同的格式来做String<->Object的转换。比如日期类型,一个字段的格式是yyyy-MM-dd,另一个格式是yyyyMMdd,如果利用PropertyEditor是比较麻烦,但是在这里就可以利用@DateTimeFormat来达到这个效果。

Spring提供了DefaultFormattingConversionService来支持Formatter SPI,也就是说如果要使用Formatter SPI,依然可以利用ConversionService接口。

注:Formatter SPI必须基于注解才可以使用,这点和ConversionService基于类型不同。

DataBinder

DataBinder主要提供了两个功能:

  1. 利用BeanWrapper,给对象的属性设值
  2. 在设值的同时做Validation
//引自org.springframework.validation;
public class DataBinder implements PropertyEditorRegistry, TypeConverter {public void bind(PropertyValues pvs) {MutablePropertyValues mpvs = pvs instanceof MutablePropertyValues ? (MutablePropertyValues)pvs : new MutablePropertyValues(pvs);this.doBind(mpvs);}protected void doBind(MutablePropertyValues mpvs) {this.checkAllowedFields(mpvs);this.checkRequiredFields(mpvs);this.applyPropertyValues(mpvs);}protected void applyPropertyValues(MutablePropertyValues mpvs) {try {this.getPropertyAccessor().setPropertyValues(mpvs, this.isIgnoreUnknownFields(), this.isIgnoreInvalidFields());} catch (PropertyBatchUpdateException var7) {PropertyAccessException[] var3 = var7.getPropertyAccessExceptions();int var4 = var3.length;for(int var5 = 0; var5 < var4; ++var5) {PropertyAccessException pae = var3[var5];this.getBindingErrorProcessor().processPropertyAccessException(pae, this.getInternalBindingResult());}}}
}

前四者关系图

ConversionService有两种实现,也就是说,如果要支持Formatter SPI,只需要让BeanWrapper切换使用不同的ConversionService即可。

  • DefaultConversionService,不支持Formatter SPI
  • DefaultFormattingConversionService,支持Formatter SPI

Validator

Validator较简单,如果类路径上存在 Bean Validation(例如,Hibernate Validator),则将LocalValidatorFactoryBean 注册为全局Validator,以便与@Valid一起使用在controller 方法参数上。

非Spring MVC的使用

Spring Core Context(要解析spring xml的)其实也使用ConversionService,但是是非强制的。让Spring Core Context使用conversionService的方式很简单,配置一个名字叫做conversionService的Bean即可。需要注意的是,因为这个Bean是在非常早的时候就被使用的(AbstractApplicationContext#L834),因此它最好不要依赖过多的其他的Bean,避免造成启动失败。

Spring在读取xml配置文件的时候,因为xml文件实际上是一个文本文件,所有值的设置都是String,这个时候如果给bean的复杂类型属性设置值,它会用到PropertyEditor或ConversionService。

<bean id="someBean" class="a.b.c.SomeBean">
    <property name="color" value="red"/>
</bean>  

例子中的color属性是Color类型,这时就会利用到PropertyEditor和ConversionService。

Spring MVC的使用

Spring MVC对于conversionService的使用比较特殊,它自己会注册一个名字叫做mvcConversionService类型为DefaultFormattingConversionService的Bean。因此会存在以下陷阱:

  • 如果Core Context中也定义了一个ConversionService,那么在MVC环境下,会有两个ConversionService的Bean。
  • 针对Core Context的ConversionService做的Customize如FormatterRegistrar、ConverterRegistryFormatterRegistryConversionServiceFactoryBeanFormattingConversionServiceFactoryBean是不会应用到MVC的那个ConversionService上。

上图在后面源码中细讲,对于mvcConversionService的配置途径见“DataBinder”或“MVC Config API”。

HttpMessageConverter和ConversionService是什么关系?

个人理解,可以他们是不同的两种东西,二者各司其职,前者转换请求body信息和响应body信息,后者用于请求参数的转换。都可以接受文本信息,最终解析成对象。根本的区别:

HttpMessageConvert

官方文档中的说明:We can use the @RequestBody annotation on the argument of a Controller method to indicate that the body of the HTTP Request is deserialized to that particular Java entity. To determine the appropriate converter, Spring will use the “Content-Type” header from the client request.

对,@RequestBody决定了要使用HttpMessageConverter,而Content-Type则是选择具体某一个配置器。HttpMessageConverter<T>,默认有很多配置器:StringHttpMessageConverter,ByteArrayHttpMessageConverter,SourceHttpMessageConverter,FormHttpMessageConverter 。

ConversionService

它使用的是 WebDataBinder (extends DataBinder),处理url或@RequestParam等非@RequestBody参数,它数据绑定是这样的流程:

  1. 将ServletRequest对象及处理方法入参对象实例传给DataBinder
  2. DataBinder 调用转配在Spring Web上下文中的ConversionService进行数据类型转换、数据格式化等工作,将ServletRequest中的消息填充到入参对象中
  3. 调用 Validator 对已经绑定的请求信息数据的入参对象进行数据合法性校验,生成数据绑定结果 BindingResult。BindingResult 包含完成绑定的入参对象和相应的校验错误对象。而后将 BindingResult 中的入参对象及校验错误对象赋给处理方法的入参。
// 引自org.springframework.web.bind.annotation.support.HandlerMethodInvoker
// 进行数据类型转换、填充并验证
private Object[] resolveHandlerArguments(Method handlerMethod, Object handler,NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception {Class<?>[] paramTypes = handlerMethod.getParameterTypes();Object[] args = new Object[paramTypes.length];// controller方法的每个参数for (int i = 0; i < args.length; i++) {MethodParameter methodParam = new SynthesizingMethodParameter(handlerMethod, i);methodParam.initParameterNameDiscovery(this.parameterNameDiscoverer);GenericTypeResolver.resolveParameterType(methodParam, handler.getClass());String paramName = null;String headerName = null;boolean requestBodyFound = false;String cookieName = null;String pathVarName = null;String attrName = null;boolean required = false;String defaultValue = null;boolean validate = false;Object[] validationHints = null;int annotationsFound = 0;Annotation[] paramAnns = methodParam.getParameterAnnotations();for (Annotation paramAnn : paramAnns) {if (RequestParam.class.isInstance(paramAnn)) {RequestParam requestParam = (RequestParam) paramAnn;paramName = requestParam.name();required = requestParam.required();defaultValue = parseDefaultValueAttribute(requestParam.defaultValue());annotationsFound++;}else if (RequestHeader.class.isInstance(paramAnn)) {RequestHeader requestHeader = (RequestHeader) paramAnn;headerName = requestHeader.name();required = requestHeader.required();defaultValue = parseDefaultValueAttribute(requestHeader.defaultValue());annotationsFound++;}else if (RequestBody.class.isInstance(paramAnn)) {requestBodyFound = true;annotationsFound++;}else if (CookieValue.class.isInstance(paramAnn)) {CookieValue cookieValue = (CookieValue) paramAnn;cookieName = cookieValue.name();required = cookieValue.required();defaultValue = parseDefaultValueAttribute(cookieValue.defaultValue());annotationsFound++;}else if (PathVariable.class.isInstance(paramAnn)) {PathVariable pathVar = (PathVariable) paramAnn;pathVarName = pathVar.value();annotationsFound++;}else if (ModelAttribute.class.isInstance(paramAnn)) {ModelAttribute attr = (ModelAttribute) paramAnn;attrName = attr.value();annotationsFound++;}else if (Value.class.isInstance(paramAnn)) {defaultValue = ((Value) paramAnn).value();}else {Validated validatedAnn = AnnotationUtils.getAnnotation(paramAnn, Validated.class);if (validatedAnn != null || paramAnn.annotationType().getSimpleName().startsWith("Valid")) {validate = true;Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(paramAnn));validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[]{hints});}}}if (annotationsFound > 1) {throw new IllegalStateException("Handler parameter annotations are exclusive choices - " +"do not specify more than one such annotation on the same parameter: " + handlerMethod);}if (annotationsFound == 0) {Object argValue = resolveCommonArgument(methodParam, webRequest);if (argValue != WebArgumentResolver.UNRESOLVED) {args[i] = argValue;}else if (defaultValue != null) {args[i] = resolveDefaultValue(defaultValue);}else {Class<?> paramType = methodParam.getParameterType();if (Model.class.isAssignableFrom(paramType) || Map.class.isAssignableFrom(paramType)) {if (!paramType.isAssignableFrom(implicitModel.getClass())) {throw new IllegalStateException("Argument [" + paramType.getSimpleName() + "] is of type " +"Model or Map but is not assignable from the actual model. You may need to switch " +"newer MVC infrastructure classes to use this argument.");}args[i] = implicitModel;}else if (SessionStatus.class.isAssignableFrom(paramType)) {args[i] = this.sessionStatus;}else if (HttpEntity.class.isAssignableFrom(paramType)) {// 调用HttpMessageConvert服务args[i] = resolveHttpEntityRequest(methodParam, webRequest);}else if (Errors.class.isAssignableFrom(paramType)) {throw new IllegalStateException("Errors/BindingResult argument declared " +"without preceding model attribute. Check your handler method signature!");}else if (BeanUtils.isSimpleProperty(paramType)) {paramName = "";}else {attrName = "";}}}if (paramName != null) {// 调用ConversionService服务args[i] = resolveRequestParam(paramName, required, defaultValue, methodParam, webRequest, handler);}else if (headerName != null) {// 调用ConversionService服务args[i] = resolveRequestHeader(headerName, required, defaultValue, methodParam, webRequest, handler);}else if (requestBodyFound) {// 调用HttpMessageConvert服务args[i] = resolveRequestBody(methodParam, webRequest, handler);}else if (cookieName != null) {// 调用HttpMessageConvert服务args[i] = resolveCookieValue(cookieName, required, defaultValue, methodParam, webRequest, handler);}else if (pathVarName != null) {// 调用HttpMessageConvert服务args[i] = resolvePathVariable(pathVarName, methodParam, webRequest, handler);}else if (attrName != null) {// 调用ConversionService服务WebDataBinder binder =resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler);boolean assignBindingResult = (args.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1]));if (binder.getTarget() != null) {doBind(binder, webRequest, validate, validationHints, !assignBindingResult);}args[i] = binder.getTarget();if (assignBindingResult) {args[i + 1] = binder.getBindingResult();i++;}implicitModel.putAll(binder.getBindingResult().getModel());}}return args;
}

最后,HttpMessageConverter和ConversionService是没有关系的,这点很多人讲的都是错的!

spring mvc的DataBinder、Validator、BeanWrapper、ConversionService、Formatter相关推荐

  1. 11月30在spring mvc中使用Validator框架和文件上传

    首先回顾了spring mvc中的表单验证和业务逻辑校验失败后,回到表单页面中显示错误信息的整个内部运行流程. 表单校验出错后回到表单注册页面是由默认的SimpleFormController的pro ...

  2. spring mvc后端校验validator

    springmvc中我们使用hibernate的校验框架validation: pom: <validator.version>5.2.4.Final</validator.vers ...

  3. 11月21日spring mvc的表单校验培训日记

    看到HtmlFilter中的一段代码: StringBuffer result = new StringBuffer(content.length + 50); 顺便问问学员们其中的一些思想和原因,结 ...

  4. Spring MVC 学习总结(五)——校验与文件上传

    目录 一.Spring MVC验证器Validator 1.1.定义验证器 1.2.执行校验 1.3.在UI中添加错误标签 1.4.测试运行 二.JSR303验证器 2.1.添加hibernate-v ...

  5. Spring MVC 数据类型转换器

    Spring MVC默认数据类型转换器: ConversionService converters =java.lang.Boolean -> java.lang.String : org.sp ...

  6. Spring MVC 学习总结(五)——校验与文件上传 转自 张果 博客;已经编程校验;正确无误;...

    Spring MVC 学习总结(五)--校验与文件上传 目录 一.Spring MVC验证器Validator 1.1.定义验证器 1.2.执行校验 1.3.在UI中添加错误标签 1.4.测试运行 二 ...

  7. Spring MVC Formatter(数据格式化)详解

    Spring MVC 框架的 Formatter<T> 与 Converter<S,T> 一样,也是一个可以将一种数据类型转换成另一种数据类型的接口.不同的是,Formatte ...

  8. Spring MVC DataBinder

    我们都知道Spring MVC在处理HTTP请求的时候的数据都是来自于HTTP 请求.这不是废话吗,:)! 那么Spring MVC是如何把HTTP中的请求中的数据纳入到其中呢?我们都知道Spring ...

  9. Spring验证示例 - Spring MVC Form Validator

    Spring验证示例 - Spring MVC Form Validator 当我们在任何Web应用程序中接受用户输入时,就有必要对它们进行验证.我们可以使用JavaScript验证客户端的用户输入, ...

最新文章

  1. 【网络流】学习笔记:一次理解网络流!
  2. JS改变input的value值不触发onchange事件解决方案 (转)
  3. jsp页面遍历后台传递的对象
  4. jvm gc停顿_在JVM中记录世界停顿
  5. 创建css的时候选择器有哪几类,CSS3-CSS的选择器共有几类?
  6. ES和Kibana在docker中的安装
  7. CentOS网络配置与重启方法
  8. 如何看待没有学历的程序员?
  9. matlab2012安装过程中退出,Matlab 2012安装图解
  10. 数字ic验证工程师面试题|ic验证面试常问88道
  11. 人工雨量计_自动站与人工站遥测雨量计降水量对比分析
  12. freebsd上运行hpool
  13. 快速使用git、svn
  14. MPLS(Multi-Protocol Label Switching)——多协议标签交换
  15. C语言中puts跟printf的区别
  16. grpc系列1-K8S集群,VIP,grpc._channel._InactiveRpcError connection reset by peer解决方案 Paddleserving服务化部署
  17. 判断点P是否在三角形ABC内
  18. Undertow文件上传10M以上抛异常UT000020: Connection terminated as request was larger than 10485760
  19. 电子电路学习笔记(14)——LDO(低压差线性稳压器)
  20. iOS最全的三方库、插件、博客汇总

热门文章

  1. 润生集团2022年度业绩内部汇报 | 相信 · 热爱 · 创造
  2. 在机器学习领域,怎样写好一篇学术论文
  3. 泛型、Collection和List
  4. yii2 ajax访问控制器,yii2控制器Controller Ajax操作示例
  5. 一文搞明白整型提升,截断等相关概念【C语言】
  6. SOLO环境的安装配置
  7. OPPO互联网java后端二面题目
  8. 序列标注-命名实体识别
  9. Gitee提交代码到远程仓库的分支中
  10. Luzj's Zte 中兴认证客户端Linux版