什么是数据绑定?简单一句话就是把请求中参数信息绑定到目标方法的参数上。数据绑定是参数解析过程中的一部分。SpringMVC通过反射机制对目标处理方法进行解析,将请求消息绑定到处理方法的入参中。如下图所示:
那么这里我们就要研究一下数据绑定相关的那些组件。
【1】绑定工厂WebDataBinderFactory
工厂嘛,使用了工厂方法设计模式,只有一个抽象方法用来让子类实现以创建一个WebDataBinder实例。
public interface WebDataBinderFactory { WebDataBinder createBinder(NativeWebRequest webRequest, @Nullable Object target, String objectName) throws Exception; }
- NativeWebRequest:当前请求
- target:数据绑定器的目标对象,如果是为简单类型创建,则target为null;
- objectName:target的名字
WebDataBinderFactory家族图示(典型的工厂方法设计模式)
① DefaultDataBinderFactory
这里我们先看一下其createBinder方法,也是核心入口方法。
@Override @SuppressWarnings("deprecation") public final WebDataBinder createBinder( NativeWebRequest webRequest, @Nullable Object target, String objectName) throws Exception { // 提供了默认实现,也可以让子类覆盖 WebDataBinder dataBinder = createBinderInstance(target, objectName, webRequest); if (this.initializer != null) { this.initializer.initBinder(dataBinder, webRequest); } // 空方法,让子类实现 initBinder(dataBinder, webRequest); return dataBinder; }
如下所示,其创建了一个WebRequestDataBinder
实例。WebRequestDataBinder主要是用来将web request parameters
绑定到JavaBeans,支持 multipart files。
protected WebDataBinder createBinderInstance( @Nullable Object target, String objectName, NativeWebRequest webRequest) throws Exception { return new WebRequestDataBinder(target, objectName); }
其initBinder
方法是个空方法,让子类InitBinderDataBinderFactory
实现。
这里我们关注一下this.initializer.initBinder(dataBinder, webRequest);
。也就是初始化器首先对数据绑定器进行初始化,这里最终会走到ConfigurableWebBindingInitializer#initBinder
,如下所示,在后面使用databinder进行类型转换、格式校验时用的就是这里填充进去的“功能属性对象”
。
@Override public void initBinder(WebDataBinder binder) { binder.setAutoGrowNestedPaths(this.autoGrowNestedPaths); if (this.directFieldAccess) { // 设置directFieldAccess=true binder.initDirectFieldAccess(); } // 设置messageCodesResolver ,默认是DefaultMessageCodesResolver if (this.messageCodesResolver != null) { binder.setMessageCodesResolver(this.messageCodesResolver); } // 设置BindingErrorProcessor,默认是DefaultBindingErrorProcessor if (this.bindingErrorProcessor != null) { binder.setBindingErrorProcessor(this.bindingErrorProcessor); } // 设置校验器 if (this.validator != null && binder.getTarget() != null && this.validator.supports(binder.getTarget().getClass())) { binder.setValidator(this.validator); } // 设置类型转换服务 if (this.conversionService != null) { binder.setConversionService(this.conversionService); } // 设置属性编辑注册器,能够向binder注册一系列编辑器 if (this.propertyEditorRegistrars != null) { for (PropertyEditorRegistrar propertyEditorRegistrar : this.propertyEditorRegistrars) { propertyEditorRegistrar.registerCustomEditors(binder); } } }
② InitBinderDataBinderFactory
InitBinderDataBinderFactory
是通过@InitBinder
方法向WebDataBinder 添加初始化行为。其并没有重写父类的创建DataBinder方法,但是提供了初始化DataBinder方法。
public void initBinder(WebDataBinder dataBinder, NativeWebRequest request) throws Exception { for (InvocableHandlerMethod binderMethod : this.binderMethods) { if (isBinderMethodApplicable(binderMethod, dataBinder)) { Object returnValue = binderMethod.invokeForRequest(request, null, dataBinder); if (returnValue != null) { throw new IllegalStateException( "@InitBinder methods must not return a value (should be void): " + binderMethod); } } } }
这里我们需要多说一点,在进行参数解析的时候,如果binderFactory不为null且是DefaultDataBinderFactory或子类InitBinderDataBinderFactory。那么其createBinder会进行初始化initBinder(dataBinder, webRequest);。而InitBinderDataBinderFactory的initBinder方法如上所示,会首先判断当前binderMethods是否有符合当前WebDataBinder的@InitBinder方法binderMethod。如果有合适的binderMethod,那么将会在初始化绑定器的过程中反射调用该方法。
如下图所示,能够看到InvocableHandlerMethod#invokeForRequest方法被调用了两次,第二次的时候providedArgs不再为空。
③ ServletRequestDataBinderFactory
其创建了一个ServletRequestDataBinder(子类ExtendedServletRequestDataBinder),ServletRequestDataBinder可以将请求的参数绑定到目标对象,如Query String Parameters、form-data的参数,并支持multipart files的绑定。
具体子类是ExtendedServletRequestDataBinder,ExtendedServletRequestDataBinder将 URI template variables(如路径变量/getUser/{id}中的id)解析提供给数据绑定使用。
@Override protected ServletRequestDataBinder createBinderInstance( @Nullable Object target, String objectName, NativeWebRequest request) throws Exception { return new ExtendedServletRequestDataBinder(target, objectName); }
【2】数据绑定器DataBinder
数据绑定器是为目标对象绑定属性值,同时支持属性校验与绑定结果分析。可以通过BindingResult拿到绑定结果。
① 校验器处理
DataBinder提供了一些校验器处理方法如addValidators(添加校验器)、replaceValidators(替换校验器)、getValidators(获取校验器)、getValidato(获取第一个校验器)r以及核心的validate方法。
public void validate(Object... validationHints) { Object target = getTarget(); Assert.state(target != null, "No target to validate"); BindingResult bindingResult = getBindingResult(); // Call each validator with the same binding result for (Validator validator : getValidators()) { if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) { ((SmartValidator) validator).validate(target, bindingResult, validationHints); } else if (validator != null) { validator.validate(target, bindingResult); } } }
代码主要含义就是首先获取taget(要绑定的目标对象)、bindingResult
与Validators
,然后遍历Validators并进行校验,将校验结果放到bingdingResult中。
一个校验通过的bingdingResult如下所示
如果校验不通过,那么其errors属性将会保存错误信息,如下所示
② 转换器处理
数据绑定过程中,获取到请求中的数据后向目标对象进行绑定,那么这个阶段可能涉及到类型转换,如String转换为Integer。DataBinder实现了TypeConverter
接口,提供了系列重载convertIfNecessary
方法。
@Override @Nullable public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType) throws TypeMismatchException { return getTypeConverter().convertIfNecessary(value, requiredType); } @Override @Nullable public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType, @Nullable MethodParameter methodParam) throws TypeMismatchException { return getTypeConverter().convertIfNecessary(value, requiredType, methodParam); } @Override @Nullable public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType, @Nullable Field field) throws TypeMismatchException { return getTypeConverter().convertIfNecessary(value, requiredType, field); } @Nullable @Override public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException { return getTypeConverter().convertIfNecessary(value, requiredType, typeDescriptor); }
这里面我们可以看一下其getTypeConverter方法:
protected TypeConverter getTypeConverter() { if (getTarget() != null) { return getInternalBindingResult().getPropertyAccessor(); } else { return getSimpleTypeConverter(); } }
也就是说如果target不为null(复杂类型比如SysUser),那么就获取一个BeanWrapperImpl实例。如果是一个简单类型如Integer age,那么就获取一个SimpleTypeConverter。
额外说明的是,在类型转换的过程中,都离不开一个conversionService实例(默认是DefaultFormattingConversionService)。该实例内部拥有系统的converters,正是这些converts(有100多个)实现了类型的转换。从下图可以看到,二者都是TypeConverterSupport的子类,实现了TypeConverter、PropertyEditorRegistry接口。PropertyEditorRegistry接口给自定义属性编辑器注册提供了入口。
WebDataBinder 使用registerCustomEditor应用
@InitBinder public void initBinderTest(WebDataBinder binder){ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); dateFormat.setLenient(false); binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true)); }
③ 绑定结果BindingResult
数据绑定过程中很可能因为数据类型转换异常(或其他错误)绑定失败,这些错误信息就保存在了BindingResult中。我们可以通过该接口获取到错误信息进行对应处理。这里我们可以简单看一下其家族树,不展开分析。详情参考博文SpringMVC中使用JSR303进行数据校验实践详解
其创建BindingResult的两个方法
protected AbstractPropertyBindingResult createBeanPropertyBindingResult() { BeanPropertyBindingResult result = new BeanPropertyBindingResult(getTarget(), getObjectName(), isAutoGrowNestedPaths(), getAutoGrowCollectionLimit()); if (this.conversionService != null) { result.initConversion(this.conversionService); } if (this.messageCodesResolver != null) { result.setMessageCodesResolver(this.messageCodesResolver); } return result; } protected AbstractPropertyBindingResult createDirectFieldBindingResult() { DirectFieldBindingResult result = new DirectFieldBindingResult(getTarget(), getObjectName(), isAutoGrowNestedPaths()); if (this.conversionService != null) { result.initConversion(this.conversionService); } if (this.messageCodesResolver != null) { result.setMessageCodesResolver(this.messageCodesResolver); } return result; }
获取BindingResult的核心方法
// 根据directFieldAccess 获取DirectFieldBindingResult或者BeanPropertyBindingResult protected AbstractPropertyBindingResult getInternalBindingResult() { if (this.bindingResult == null) { this.bindingResult = (this.directFieldAccess ? createDirectFieldBindingResult(): createBeanPropertyBindingResult()); } return this.bindingResult; }
如下实例方法三个参数分别为pojo、Errors以及map
这里我们解析得到的参数实例为:
④ 核心绑定方法
也就是把给定的属性-值绑定到目标对象上,这个过程可能会抛出“required”或者类型转换异常等错误。
public void bind(PropertyValues pvs) { MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues ? (MutablePropertyValues) pvs : new MutablePropertyValues(pvs)); doBind(mpvs); } protected void doBind(MutablePropertyValues mpvs) { checkAllowedFields(mpvs); checkRequiredFields(mpvs); applyPropertyValues(mpvs); }
可以看到doBind是一个模板方法:
checkAllowedFields
方法是检测被允许的field,如果发现有不被允许的field,则移除掉。*checkRequiredFields
方法是检测是那些声明了“required”
的filed是否都存在,如果有不存在的将会把错误信息放到BindingResult中。
- applyPropertyValues则是核心的属性-值绑定到目标对象的方法。
【3】WebDataBinder
WebDataBinder是DataBinder的子类,其覆盖了父类的doBind方法。
@Override protected void doBind(MutablePropertyValues mpvs) { checkFieldDefaults(mpvs); checkFieldMarkers(mpvs); adaptEmptyArrayIndices(mpvs); super.doBind(mpvs); }
其同样是个模板方法,在处理属性默认值(字段以“!”开头,提供默认值代替空值)、属性标记(字段以"_"开头,如果表单提交没有该字段对应的值,则重置该字段)、字段名称去掉"[]"
(如果字段名称以[]
结尾)后,调用了父类的doBind方法。
如下演示!
的使用:
public static void main(String[] args){ SysUser sysUser = new SysUser(); WebDataBinder binder = new WebDataBinder(sysUser, "sysUser"); // 设置属性(此处演示一下默认值) MutablePropertyValues pvs = new MutablePropertyValues(); // 使用!来模拟各个字段手动指定默认值 pvs.add("!name", "jane"); pvs.add("age", 18); pvs.add("!age", 10); // 上面有确切的值了,默认值不会再生效 binder.bind(pvs); System.out.println(sysUser); }
打印结果:SysUser{name='jane', sex='null', age=18}
值得一提的是,其提供了方法bindMultipart
供子类调用,该方法用来处理multipart files
:
protected void bindMultipart(Map<String, List<MultipartFile>> multipartFiles, MutablePropertyValues mpvs) { multipartFiles.forEach((key, values) -> { if (values.size() == 1) { MultipartFile value = values.get(0); if (isBindEmptyMultipartFiles() || !value.isEmpty()) { mpvs.add(key, value); } } else { mpvs.add(key, values); } }); }
【4】ServletRequestDataBinder
ServletRequestDataBinder
继承自WebDataBinder
,有唯一子类ExtendedServletRequestDataBinder
。此类把web请求限定为了Servlet Request,和Servlet规范强绑定。
Special {@link org.springframework.validation.DataBinder} to perform data binding from servlet request parameters to JavaBeans, including support for multipart files.
ServletRequestDataBinder可以将请求的参数绑定到目标对象,如Query String Parameters、form-data的参数,并支持multipart files的绑定。
子类ExtendedServletRequestDataBinder将 URI template variables(如路径变量/getUser/{id}中的id)解析提供给数据绑定使用。
① ServletRequestDataBinder的bind方法
ServletRequestDataBinder
提供了方法bind,其对MultipartRequest
请求做了支持可以绑定文件域(通过调用父类WebDataBinder
的bindMultipart
方法)。
public void bind(ServletRequest request) { // 从request中获取属性-值 MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request); // 判断当前请求是否为MultipartRequest MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class); if (multipartRequest != null) { // 调用父类webDataBinder方法 bindMultipart(multipartRequest.getMultiFileMap(), mpvs); } else if (StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/")) { HttpServletRequest httpServletRequest = WebUtils.getNativeRequest(request, HttpServletRequest.class); if (httpServletRequest != null) { StandardServletPartUtils.bindParts(httpServletRequest, mpvs, isBindEmptyMultipartFiles()); } } addBindValues(mpvs, request); // 调用父类方法 doBind(mpvs); }
① 这里尝试获取mpvs,然后判断是否为MultipartRequest或者
StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/")&&httpServletRequest != null之后调用对应的bindMultipart或者bindParts方法。如http://localhost:8081/testUser/1?name=jane得到的mpvs如下图所示:
- ② addBindValues是一步兼容,从request中获取属性
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE
对应的值,然后放入mpvs。这时候的mpvs可能如下(可以看到多了一个URI Template Variable id=1):
- ③ 调用父类核心的doBind方法,将mpvs中的属性-值绑定到目标target上
这里比较有意思的是addBindValues方法是一个空方法,且用protected修饰。很明显其是想让子类实现(ExtendedServletRequestDataBinder子类进行了实现)。
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) { }
如下所示,执行doBind(mpvs);
方法前的DataBinder对象。
如下所示,执行doBind(mpvs);
方法后的DataBinder对象。可以看到从mpvs中找到SysUser拥有的属性给sysUser实例对象赋值(通常是反射调用set方法,如setName)。
当然如果BindingResult
不是BeanPropertyBindingResult
而是DirectFieldBindingResult
则是另一种方式。
② ExtendedServletRequestDataBinder
ExtendedServletRequestDataBinder实现/覆盖了父类的addBindValues方法,主要是从request中获取URL路径变量集合,然后找到符合当前请求的属性-值放入MutablePropertyValues mpvs
中。
@Override protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) { // HandlerMapping.class.getName() + ".uriTemplateVariables" String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; @SuppressWarnings("unchecked") Map<String, String> uriVars = (Map<String, String>) request.getAttribute(attr); if (uriVars != null) { uriVars.forEach((name, value) -> { if (mpvs.contains(name)) { if (logger.isWarnEnabled()) { logger.warn("Skipping URI variable '" + name + "' because request contains bind value with same name."); } } else { mpvs.addPropertyValue(name, value); } }); } }
此属性放入的第一个地方是:AbstractUrlHandlerMapping.lookupHandler() --> chain.addInterceptor(new UriTemplateVariablesHandlerInterceptor(uriTemplateVariables)); --> preHandle()方法 -> exposeUriTemplateVariables(this.uriTemplateVariables, request); -> request.setAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables);
第二个地方是:AbstractHandlerMethodMapping.lookupHandlerMethod()->RequestMappingInfoHandlerMapping.handleMatch()–>RequestMappingInfoHandlerMapping.extractMatchDetails()–>request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern);
request.setAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriVariables);
如下所示,一个请求对象的attributes中包含了一个org.springframework.web.servlet.HandlerMapping.uriTemplateVariables
属性:
我们也顺便多看一眼当一个请求发起到找到对应的HandlerMethod时,请求属性里面放入了什么?
③ 表单提交绑定过程
这里描述的背景为填写表单对象,传递到后台(参数为SysUser sysUser,也就是常见的实体对象)。
① SpringMVC框架将ServletRequest对象以及目标方法的入参实例(objectName,attribute) 传递给WebDataBinderFactory
实例,以创建DataBinder实例对象。
- objectName为
@ModelAttribute
注解的value值或者根据参数类型自动创建的key - 其中attribute会根据目标方法参数类型从ModelAndViewContainer 获取或者创建-这是Value
② DataBinder调用装配在SpringMVC上下文中的ConversionService组件进行数据类型转换、数据格式化工作并将请求信息填充到创建的入参对象中。
③ 调用Validator 组件对已经绑定了请求消息的入参对象(创建的)进行数据合法性校验,并最终生成数据结果(BindingResult
)
④ SpringMVC 抽取 BindingResult 中的入参对象(getTarget)和校验错误对象(BindingResult),将他们赋给处理方法的相应入参
【5】WebRequestDataBinder
这个与不同的是其是把web request parameters 绑定到 JavaBeans,同样支持multipart files。也就是说其跳出了Servlet规范,额外做了支持。其主要对org.springframework.web.context.request.WebRequest做处理。
如下代码所示,WebRequestDataBinder首先从request中获取参数值封装为MutablePropertyValues 。然后分别判断是否为MultipartRequest、或者请求头Content-Type以multipart/开头两种情况进行处理,最后调用父类的doBind方法。
public void bind(WebRequest request) { MutablePropertyValues mpvs = new MutablePropertyValues(request.getParameterMap()); if (request instanceof NativeWebRequest) { MultipartRequest multipartRequest = ((NativeWebRequest) request).getNativeRequest(MultipartRequest.class); if (multipartRequest != null) { bindMultipart(multipartRequest.getMultiFileMap(), mpvs); } else if (StringUtils.startsWithIgnoreCase(request.getHeader("Content-Type"), "multipart/")) { HttpServletRequest servletRequest = ((NativeWebRequest) request).getNativeRequest(HttpServletRequest.class); if (servletRequest != null) { StandardServletPartUtils.bindParts(servletRequest, mpvs, isBindEmptyMultipartFiles()); } } } doBind(mpvs); }