Spring数据绑定之 WebDataBinderServletRequestDataBinderWebBindingInitializer...---02
Posted 大忽悠爱忽悠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring数据绑定之 WebDataBinderServletRequestDataBinderWebBindingInitializer...---02相关的知识,希望对你有一定的参考价值。
Spring数据绑定之 WebDataBinder、ServletRequestDataBinder、WebBindingInitializer...---02
WebDataBinder
上一篇我们对DataBinder的源码进行了详细的分析,下面我们对DataBinder的实现子类来做一下具体分析:
通过继承树可以看出,DataBinder的首席大弟子是WebDataBinder,以Web打头,我们大概就可以猜到,该类是用在Web请求参数到具体JavaBean属性的绑定工作中的。
单从WebDataBinder来说,它对父类进行了增强,提供的增强能力如下:
- 支持对属性名以_打头的默认值处理(自动挡,能够自动处理所有的Bool、Collection、Map等)
- 支持对属性名以!打头的默认值处理(手动档,需要手动给某个属性赋默认值,自己控制的灵活性很高)
- 提供方法,支持把MultipartFile绑定到JavaBean的属性上~
下面我们就来看看这些增强功能的具体实现吧:
WebDataBinder中提供了两个标记符合,当请求参数中存在这两个标记符合时,会进行特殊处理:
//如果请求参数中有以_开头的,那么会取请求参数对应类型的默认值
public static final String DEFAULT_FIELD_MARKER_PREFIX = "_";
//如果请求参数中有以!开头的,那么会去请求参数对应值作为默认值
public static final String DEFAULT_FIELD_DEFAULT_PREFIX = "!";
@Nullable
private String fieldMarkerPrefix = DEFAULT_FIELD_MARKER_PREFIX;
@Nullable
private String fieldDefaultPrefix = DEFAULT_FIELD_DEFAULT_PREFIX;
直接讲标记的作用,会是一头雾水,所以下面我们来看看这些标记具体的应用:
- 首先WebDataBinder对doBind方法进行了重写
@Override
protected void doBind(MutablePropertyValues mpvs)
checkFieldDefaults(mpvs);
checkFieldMarkers(mpvs);
adaptEmptyArrayIndices(mpvs);
super.doBind(mpvs);
- checkFieldDefaults检查请求参数中是否有!开头的,以!开头的请求参数会作为兜底的默认值存在
protected void checkFieldDefaults(MutablePropertyValues mpvs)
String fieldDefaultPrefix = getFieldDefaultPrefix();
if (fieldDefaultPrefix != null)
PropertyValue[] pvArray = mpvs.getPropertyValues();
for (PropertyValue pv : pvArray)
// 若你给定的PropertyValue的属性名确实是以!打头的 那就做处理如下:
// 如果JavaBean的该属性可写 && mpvs不存在去掉!后的同名属性,那就添加进来表示后续可以使用了(毕竟是默认值,没有精确匹配的高的)
// 然后把带!的给移除掉(因为默认值以已经转正了~~~)
// 其实这里就是说你可以使用!来给个默认值。比如!name表示若找不到name这个属性的时,就取它的值~~~
// 也就是说你request里若有!name保底,也就不怕出现null值啦~
if (pv.getName().startsWith(fieldDefaultPrefix))
String field = pv.getName().substring(fieldDefaultPrefix.length());
if (getPropertyAccessor().isWritableProperty(field) && !mpvs.contains(field))
mpvs.add(field, pv.getValue());
mpvs.removePropertyValue(pv);
例如: 请求参数为 name=dhy&!name=xpy,那么name最终取值为dhy, 如果只有!name=xpy,那么最终我们Controller拿到的name=xpy
- checkFieldMarkers检查请求参数中是否有_开头的,以_开头的请求参数的值,会给出相应类型的默认值
// 处理_的步骤
// 若传入的字段以_打头
// JavaBean的这个属性可写 && mpvs木有去掉_后的属性名字
// getEmptyValue(field, fieldType)就是根据Type类型给定默认值。
// 比如Boolean类型默认给false,数组给空数组[],集合给空集合,Map给空map 可以参考此类:CollectionFactory
// 当然,这一切都是建立在你传的属性值是以_打头的基础上的,Spring才会默认帮你处理这些默认值
protected void checkFieldMarkers(MutablePropertyValues mpvs)
String fieldMarkerPrefix = getFieldMarkerPrefix();
if (fieldMarkerPrefix != null)
PropertyValue[] pvArray = mpvs.getPropertyValues();
for (PropertyValue pv : pvArray)
if (pv.getName().startsWith(fieldMarkerPrefix))
String field = pv.getName().substring(fieldMarkerPrefix.length());
if (getPropertyAccessor().isWritableProperty(field) && !mpvs.contains(field))
Class<?> fieldType = getPropertyAccessor().getPropertyType(field);
mpvs.add(field, getEmptyValue(field, fieldType));
mpvs.removePropertyValue(pv);
例如: 请求参数为 name=dhy&_name=xpy,那么name最终取值为dhy, 如果只有_name=xpy,那么最终我们Controller拿到的name=null,因为String类型的默认值就是null
- bindMultipart方法用于将multipartFiles绑定到Java Bean上去
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);
);
实例演示
@Test
public void testWebDataBinder02() throws BindException
People people = new People();
WebDataBinder webDataBinder = new WebDataBinder(people);
MutablePropertyValues mpvs = new MutablePropertyValues();
mpvs.add("!name","大忽悠");
mpvs.add("_age",20);
webDataBinder.bind(mpvs);
webDataBinder.close();
System.out.println(people);
大家也可以自行去尝试一下,WebDataBinder提供的!和_标记,其实是考虑到如果对应属性不存在的情况下,我们可以给出一个默认值替代。
ServletRequestDataBinder
ServletRequestDataBinder名字中的ServletRequest已经暴露了该类的使用场景,它就是遵循了Servlet规范的request参数绑定类。
上面的WebDataBinder本质只是做了一些增强,并没有涉及到Web request请求参数的绑定,相当于并没有和Servlet规范联系到一起,而ServletRequestDataBinder就和servlet规范联系到了一块。
ServletRequestDataBinder的源码也比较简单,就不拆开讲了,直接看吧:
public class ServletRequestDataBinder extends WebDataBinder
... // 沿用父类构造
// 注意这个可不是父类的方法,是本类增强的~~~~意思就是kv都从request里来~~当然内部还是适配成了一个MutablePropertyValues
public void bind(ServletRequest request)
// 内部最核心方法是它:WebUtils.getParametersStartingWith() 把request参数转换成一个Map
// request.getParameterNames()
MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);
// 调用父类的bindMultipart方法,把MultipartFile都放进MutablePropertyValues里去~~~
if (multipartRequest != null)
bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
// 这个方法是本类流出来的一个扩展点~~~子类可以复写此方法自己往里继续添加
// 比如ExtendedServletRequestDataBinder它就复写了这个方法,进行了增强(下面会说) 支持到了uriTemplateVariables的绑定
addBindValues(mpvs, request);
doBind(mpvs);
// 这个方法和父类的close方法类似,很少直接调用
public void closeNoCatch() throws ServletRequestBindingException
if (getBindingResult().hasErrors())
throw new ServletRequestBindingException("Errors binding onto object '" + getBindingResult().getObjectName() + "'", new BindException(getBindingResult()));
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request)
ServletRequestDataBinder完成了从request对象中取出所有请求参数,然后封装为MutablePropertyValues的工作,并且还增加对文件上传请参数封装的支持和子类扩展mvps的回调接口。
实例演示
@Test
public void testServletRequestDataBinder03() throws BindException
People people = new People();
ServletRequestDataBinder srdb = new ServletRequestDataBinder(people);
//需要添加spring-test依赖支持
MockHttpServletRequest mockReq = new MockHttpServletRequest();
mockReq.addParameter("name","大忽悠");
mockReq.addParameter("age","18");
mockReq.addParameter("mother.name","1");
mockReq.addParameter("mother.age","2");
mockReq.addParameter("father.name","3");
mockReq.addParameter("father.age","4");
mockReq.addParameter("list","1","2","3","4");
mockReq.addParameter("map[1]","1");
mockReq.addParameter("map[2]","2");
srdb.bind(mockReq);
System.out.println(people);
注意: 对于map属性的赋值而言,不能写成下面这样:
mockReq.addParameter("map","'name',1");
即不能使用JSON字符串进行map的赋值,因为这和AbstractNestablePropertyAccessor底层对集合表达式解析并赋值的过程由密切关系,不清楚的可以去研究一下这部分源码。
如果想要用JSON赋值,需要添加一个前置处理,就是把JSON转换为上面的合法的map赋值格式,即可。
ExtendedServletRequestDataBinder
ExtendedServletRequestDataBinder
是对ServletRequestDataBinder
的一个增强,它用于把URI template variables参数添加进来用于绑定。它会去从request的HandlerMapping.class.getName() + ".uriTemplateVariables"
这个属性里查找到值出来用于绑定。
比如我们熟悉的@PathVariable
它就和这相关:
- 它负责把参数从url模版中解析出来,然后放在attr上,最后交给ExtendedServletRequestDataBinder进行绑定
具体是由UriTemplateVariablesHandlerInterceptor负责解析request,然后得到相关的uri模板参数,然后加入request的属性集合中去的
具体是在AbstractUrlHandlerMapping类的lookupHandler方法中,完成了对URI template variables的解析。
Spring MVC各组件近距离接触–上–02
该类的具体源码如下:
// @since 3.1
public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder
... // 沿用父类构造
//本类的唯一方法
@Override
@SuppressWarnings("unchecked")
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request)
// 它的值是:HandlerMapping.class.getName() + ".uriTemplateVariables";
String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
// 注意:此处是attr,而不是parameter
Map<String, String> uriVars = (Map<String, String>) request.getAttribute(attr);
if (uriVars != null)
uriVars.forEach((name, value) ->
// 若已经存在确切的key了,不会覆盖~~~~
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);
);
WebExchangeDataBinder
它是Spring5.0后提供的,对Reactive编程的Mono数据绑定提供支持.
MapDataBinder
它位于org.springframework.data.web是和Spring-Data相关,专门用于处理target是Map<String, Object>类型的目标对象的绑定,它并非一个public类.
WebRequestDataBinder
它是用于处理Spring自己定义的org.springframework.web.context.request.WebRequest的,旨在处理和容器无关的web请求数据绑定.
数据绑定过程中采坑的类型转换
虽然DataBinder内部提供了对类型转换的支持,但是由于某些情况下,不存在对应的自定义转换器,会导致赋值失败,那么这种情况下,应该怎么处理呢?
- 大家看一下下面这段逻辑有没有问题
@Test
public void testServletRequestDataBinder04() throws BindException
People people = new People();
ServletRequestDataBinder srdb = new ServletRequestDataBinder(people);
//需要添加spring-test依赖支持
MockHttpServletRequest mockReq = new MockHttpServletRequest();
mockReq.addParameter("name","大忽悠");
mockReq.addParameter("age","18");
mockReq.addParameter("birthday","2002-1-2");
srdb.bind(mockReq);
//检查是否在数据绑定和数据校验期间发生了异常,如果发生了则在此处抛出异常
srdb.close();
System.out.println(people);
显然错误出在了对birthday属性的赋值上,birthday属性是一个Date类型,我们传入的值是"2002-1-2",那为什么会报错呢?
- 之前讲过,数据绑定默认是通过BeanWrapper完成的
- BeanWrapper调用setPropertyValue()给属性赋值,传入的value值都会交给convertForProperty()方法根据get方法的返回值类型进行转换~(比如此处为Date类型)
- 委托给this.typeConverterDelegate.convertIfNecessary进行类型转换(比如此处为string->Date类型)
- 先this.propertyEditorRegistry.findCustomEditor(requiredType,propertyName);找到一个合适的PropertyEditor(显然此处我们没有自定义Custom处理Date的PropertyEditor,返回null)
- 回退到使用ConversionService,显然此处我们也没有设置,返回null
- 回退到使用默认的editor = findDefaultEditor(requiredType)
PropertyEditorRegistrySupport中提供的默认类型转换器中没有对Date类型、以及Jsr310提供的各种事件、日期类型的转换(当然也包括我们的自定义类型)。
- 最终的最终,回退到Spring对Array、Collection、Map的默认值处理问题,最终若是String类型,都会调用
BeanUtils.instantiateClass(strCtor, convertedValue)也就是有参构造进行初始化~~~(请注意这必须是String类型才有的权利)
所以本例中,到最后一步就相当于new Date(“2002-1-2”),因为该字符串是不符合默认的格式规范,所以会抛出异常。
Spring读源码系列番外篇08—BeanWrapper没有那么简单–上
要解决上面这个问题,那么就是需要注册一个可以转换Date的类型转换器进去,有下面几种方法:
- 可以选择Spring 3.0后推出的Formatter体系中专门用来格式化Date日期的DateFormatter
@Test
public void testServletRequestDataBinder04() throws BindException
People people = new People();
ServletRequestDataBinder srdb = new ServletRequestDataBinder(people);
//需要添加spring-test依赖支持
MockHttpServletRequest mockReq = new MockHttpServletRequest();
mockReq.addParameter("name","大忽悠");
mockReq.addParameter("age","18");
mockReq.addParameter("birthday","2002-1-2");
srdb.addCustomFormatter(new DateFormatter());
srdb.bind(mockReq);
srdb.close();
System.out.println(people);
- 或者自定义一个PropertyEditor,然后注册进去
public class MyDatePropertyEditor extends PropertyEditorSupport
private static final String PATTERN = "yyyy-MM-dd";
@Override
public String getAsText()
Date date = (Date) super.getValue();
return new SimpleDateFormat(PATTERN).format(date);
@Override
public void setAsText(String text) throws IllegalArgumentException
try
super.setValue(new SimpleDateFormat(PATTERN).parse(text));
catch (ParseException e)
System.out.println("ParseException....................");
srdb.registerCustomEditor(Date.class, new MyDatePropertyEditor());
WebBindingInitializer
WebBindingInitializer:实现此接口重写initBinder方法注册的属性编辑器是全局的属性编辑器,对所有的Controller都有效。
可以简单粗暴的理解为:WebBindingInitializer为编码方式,@InitBinder为注解方式(当然注解方式还能控制到只对当前Controller有效,实现更细粒度的控制)
public interface WebBindingInitializer
void initBinder(WebDataBinder binder);
//spring 5.0之后废弃了该方法
@Deprecated
default void initBinder(WebDataBinder binder, WebRequest request)
initBinder(binder);
此接口它的内建唯一实现类为:ConfigurableWebBindingInitializer,若你自己想要扩展,建议继承它:
public class ConfigurableWebBindingInitializer implements WebBindingInitializer
//默认支持级联属性
private boolean autoGrowNestedPaths = true;
//底层默认使用BeanWrapper
private boolean directFieldAccess = false;
@Nullable
private MessageCodesResolver messag以上是关于Spring数据绑定之 WebDataBinderServletRequestDataBinderWebBindingInitializer...---02的主要内容,如果未能解决你的问题,请参考以下文章
WebDataBinderServletRequestDataBinderWebBindingInitializer