【小家Spring】聊聊Spring中的数据绑定 --- DataBinder本尊(源码分析)
每篇一句
唯有热爱和坚持,才能让你在程序人生中屹立不倒,切忌跟风什么语言或就学什么去~
相关阅读
【小家Spring】聊聊Spring中的数据绑定 --- 属性访问器PropertyAccessor和实现类DirectFieldAccessor的使用
【小家Spring】聊聊Spring中的数据绑定 --- BeanWrapper以及Java内省Introspector和PropertyDescriptor
对Spring感兴趣可扫码加入wx群:`Java高工、架构师3群`(文末有二维码)
前言
数据绑定 这个概念在任何一个成型的框架中都是特别重要的(尤其是web框架),它能让框架更多的自动化,更好容错性以及更高的编码效率。它提供的能力是:把字符串形式的参数转换成服务端真正需要的类型的转换(当然可能还包含校验)。
对Spring
中的数据绑定场景,小伙伴们就再熟悉不过了。比如我们Controller
中只需要使用Model对象就能完成request
到Model对象的自动数据自动绑定,使用起来着实非常方便~(完全屏蔽了Servlet的API)
既然数据绑定这么重要,但是为何鲜有人提起呢?我也上网搜了搜关于DataBinder
的相关资料,相对来说还是寥寥无几的~
我们不提起并不代表它不重要,这些都是Spring它帮我们默默的干了。这也印证了那句名言嘛:我们的安好是因为有人替我们负重前行
查到网上的资料,大都停留在如何使用WebDataBinder
的说明上,并且几乎没有文章是专门分析核心部件DataBinder
的,本文作为此方面的一股清流,在此把我结合官方文档、源码的所获分享给大家~
DataBinder
注意,此类所在的包是org.springframework.validation
,所以可想而知,它不仅仅完成数据的绑定,还会和数据校验有关~
注意:我看到有的文章说
DataBinder
在绑定的时候还会进行数据校验Validation,其实这个是不准确的,容易误导人(校验动作不发生在DataBinder
本类)
还有说
DataBinder
数据绑定最终依赖的是BeanWrapper
,其实这个也是不准确的,实际上依赖的是PropertyAccessor
。
DataBinder使用Demo
先看一个简单Demo
,体验一把直接使用DataBinder
进行数据绑定吧:
public static void main(String[] args) throws BindException {
Person person = new Person();
DataBinder binder = new DataBinder(person, "person");
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("name", "fsx");
pvs.add("age", 18);
binder.bind(pvs);
Map<?, ?> close = binder.close();
System.out.println(person);
System.out.println(close);
}
输出:
Person{name='fsx', age=18}
{person=Person{name='fsx', age=18}, org.springframework.validation.BindingResult.person=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
其实Spring
一直是在弱化数据绑定对使用者的接触(这就是为何鲜有人提起的原因),所以之前博文也说到Spring
并不推荐直接使用BeanWrapper
去自己绑定数据(而是都让框架自己来完成吧~)。
BeanWrapper
不推荐直接使用,但是DataBinder
是一个更为成熟、完整
些的数据绑定器,若实在有需求使用它是比使用BeanWrapper
是个更好的选择~
其实直接使用顶层的
DataBinder
也是一般不会的,而是使用它的子类。比如web包下大名鼎鼎的WebDataBinder
~
源码分析
DataBinder
的源码相对来说还是颇为复杂的,它提供的能力非常强大,也注定了它的方法非常多、属性也非常多。
首先看看类声明:
public class DataBinder implements PropertyEditorRegistry, TypeConverter {}
它是个实现类,直接实现了PropertyEditorRegistry
和TypeConverter
这两个接口,因此它可以注册java.beans.PropertyEditor
,并且能完成类型转换(TypeConverter
)。
关于数据转换这块内容,有兴趣的可参见:【小家Spring】聊聊Spring中的数据转换:Converter、ConversionService、TypeConverter、PropertyEditor
接下里分析具体源码(需要解释说明都写在源码处了):
public class DataBinder implements PropertyEditorRegistry, TypeConverter {
/** Default object name used for binding: "target". */
public static final String DEFAULT_OBJECT_NAME = "target";
/** Default limit for array and collection growing: 256. */
public static final int DEFAULT_AUTO_GROW_COLLECTION_LIMIT = 256;
@Nullable
private final Object target;
private final String objectName; // 默认值是target
// BindingResult:绑定错误、失败的时候会放进这里来~
@Nullable
private AbstractPropertyBindingResult bindingResult;
//类型转换器,会注册最为常用的那么多类型转换Map<Class<?>, PropertyEditor> defaultEditors
@Nullable
private SimpleTypeConverter typeConverter;
// 默认忽略不能识别的字段~
private boolean ignoreUnknownFields = true;
// 不能忽略非法的字段(比如我要Integer,你给传aaa,那肯定就不让绑定了,抛错)
private boolean ignoreInvalidFields = false;
// 默认是支持级联的~~~
private boolean autoGrowNestedPaths = true;
private int autoGrowCollectionLimit = DEFAULT_AUTO_GROW_COLLECTION_LIMIT;
// 这三个参数 都可以自己指定~~ 允许的字段、不允许的、必须的
@Nullable
private String[] allowedFields;
@Nullable
private String[] disallowedFields;
@Nullable
private String[] requiredFields;
// 转换器ConversionService
@Nullable
private ConversionService conversionService;
// 状态码处理器~
@Nullable
private MessageCodesResolver messageCodesResolver;
// 绑定出现错误的处理器~
private BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor();
// 校验器(这个非常重要)
private final List<Validator> validators = new ArrayList<>();
// objectName没有指定,就用默认的
public DataBinder(@Nullable Object target) {
this(target, DEFAULT_OBJECT_NAME);
}
public DataBinder(@Nullable Object target, String objectName) {
this.target = ObjectUtils.unwrapOptional(target);
this.objectName = objectName;
}
... // 省略所有属性的get/set方法
// 提供一些列的初始化方法,供给子类使用 或者外部使用 下面两个初始化方法是互斥的
public void initBeanPropertyAccess() {
Assert.state(this.bindingResult == null, "DataBinder is already initialized - call initBeanPropertyAccess before other configuration methods");
this.bindingResult = createBeanPropertyBindingResult();
}
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;
}
// 你会发现,初始化DirectFieldAccess的时候,校验的也是bindingResult ~~~~
public void initDirectFieldAccess() {
Assert.state(this.bindingResult == null, "DataBinder is already initialized - call initDirectFieldAccess before other configuration methods");
this.bindingResult = createDirectFieldBindingResult();
}
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;
}
...
// 把属性访问器返回,PropertyAccessor(默认直接从结果里拿),子类MapDataBinder有复写
protected ConfigurablePropertyAccessor getPropertyAccessor() {
return getInternalBindingResult().getPropertyAccessor();
}
// 可以看到简单的转换器也是使用到了conversionService的,可见conversionService它的效用
protected SimpleTypeConverter getSimpleTypeConverter() {
if (this.typeConverter == null) {
this.typeConverter = new SimpleTypeConverter();
if (this.conversionService != null) {
this.typeConverter.setConversionService(this.conversionService);
}
}
return this.typeConverter;
}
... // 省略众多get方法
// 设置指定的可以绑定的字段,默认是所有字段~~~
// 例如,在绑定HTTP请求参数时,限制这一点以避免恶意用户进行不必要的修改。
// 简单的说:我可以控制只有指定的一些属性才允许你修改~~~~
// 注意:它支持xxx*,*xxx,*xxx*这样的通配符 支持[]这样子来写~
public void setAllowedFields(@Nullable String... allowedFields) {
this.allowedFields = PropertyAccessorUtils.canonicalPropertyNames(allowedFields);
}
public void setDisallowedFields(@Nullable String... disallowedFields) {
this.disallowedFields = PropertyAccessorUtils.canonicalPropertyNames(disallowedFields);
}
// 注册每个绑定进程所必须的字段。
public void setRequiredFields(@Nullable String... requiredFields) {
this.requiredFields = PropertyAccessorUtils.canonicalPropertyNames(requiredFields);
if (logger.isDebugEnabled()) {
logger.debug("DataBinder requires binding of required fields [" + StringUtils.arrayToCommaDelimitedString(requiredFields) + "]");
}
}
...
// 注意:这个是set方法,后面是有add方法的~
// 注意:虽然是set,但是引用是木有变的~~~~
public void setValidator(@Nullable Validator validator) {
// 判断逻辑在下面:你的validator至少得支持这种类型呀 哈哈
assertValidators(validator);
// 因为自己手动设置了,所以先清空 再加进来~~~
// 这步你会发现,即使validator是null,也是会clear的哦~ 符合语意
this.validators.clear();
if (validator != null) {
this.validators.add(validator);
}
}
private void assertValidators(Validator... validators) {
Object target = getTarget();
for (Validator validator : validators) {
if (validator != null && (target != null && !validator.supports(target.getClass()))) {
throw new IllegalStateException("Invalid target for Validator [" + validator + "]: " + target);
}
}
}
public void addValidators(Validator... validators) {
assertValidators(validators);
this.validators.addAll(Arrays.asList(validators));
}
// 效果同set
public void replaceValidators(Validator... validators) {
assertValidators(validators);
this.validators.clear();
this.validators.addAll(Arrays.asList(validators));
}
// 返回一个,也就是primary默认的校验器
@Nullable
public Validator getValidator() {
return (!this.validators.isEmpty() ? this.validators.get(0) : null);
}
// 只读视图
public List<Validator> getValidators() {
return Collections.unmodifiableList(this.validators);
}
// since Spring 3.0
public void setConversionService(@Nullable ConversionService conversionService) {
Assert.state(this.conversionService == null, "DataBinder is already initialized with ConversionService");
this.conversionService = conversionService;
if (this.bindingResult != null && conversionService != null) {
this.bindingResult.initConversion(conversionService);
}
}
// =============下面它提供了非常多的addCustomFormatter()方法 注册进PropertyEditorRegistry里=====================
public void addCustomFormatter(Formatter<?> formatter);
public void addCustomFormatter(Formatter<?> formatter, String... fields);
public void addCustomFormatter(Formatter<?> formatter, Class<?>... fieldTypes);
// 实现接口方法
public void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor);
public void registerCustomEditor(@Nullable Class<?> requiredType, @Nullable String field, PropertyEditor propertyEditor);
...
// 实现接口方法
// 统一委托给持有的TypeConverter~~或者是getInternalBindingResult().getPropertyAccessor();这里面的
@Override
@Nullable
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
@Nullable MethodParameter methodParam) throws TypeMismatchException {
return getTypeConverter().convertIfNecessary(value, requiredType, methodParam);
}
// ===========上面的方法都是开胃小菜,下面才是本类最重要的方法==============
// 该方法就是把提供的属性值们,绑定到目标对象target里去~~~
public void bind(PropertyValues pvs) {
MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues ? (MutablePropertyValues) pvs : new MutablePropertyValues(pvs));
doBind(mpvs);
}
// 此方法是protected的,子类WebDataBinder有复写~~~加强了一下
protected void doBind(MutablePropertyValues mpvs) {
// 前面两个check就不解释了,重点看看applyPropertyValues(mpvs)这个方法~
checkAllowedFields(mpvs);
checkRequiredFields(mpvs);
applyPropertyValues(mpvs);
}
// allowe允许的 并且还是没有在disallowed里面的 这个字段就是被允许的
protected boolean isAllowed(String field) {
String[] allowed = getAllowedFields();
String[] disallowed = getDisallowedFields();
return ((ObjectUtils.isEmpty(allowed) || PatternMatchUtils.simpleMatch(allowed, field)) &&
(ObjectUtils.isEmpty(disallowed) || !PatternMatchUtils.simpleMatch(disallowed, field)));
}
...
// protected 方法,给target赋值~~~~
protected void applyPropertyValues(MutablePropertyValues mpvs) {
try {
// 可以看到最终赋值 是委托给PropertyAccessor去完成的
getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
// 抛出异常,交给BindingErrorProcessor一个个处理~~~
} catch (PropertyBatchUpdateException ex) {
for (PropertyAccessException pae : ex.getPropertyAccessExceptions()) {
getBindingErrorProcessor().processPropertyAccessException(pae, getInternalBindingResult());
}
}
}
// 执行校验,此处就和BindingResult 关联上了,校验失败的消息都会放进去(不是直接抛出异常哦~ )
public void validate() {
Object target = getTarget();
Assert.state(target != null, "No target to validate");
BindingResult bindingResult = getBindingResult();
// 每个Validator都会执行~~~~
for (Validator validator : getValidators()) {
validator.validate(target, bindingResult);
}
}
// 带有校验提示的校验器。SmartValidator
// @since 3.1
public void validate(Object... validationHints) { ... }
// 这一步也挺有意思:实际上就是若有错误,就抛出异常
// 若没错误 就把绑定的Model返回~~~(可以看到BindingResult里也能拿到最终值哦~~~)
// 此方法可以调用,但一般较少使用~
public Map<?, ?> close() throws BindException {
if (getBindingResult().hasErrors()) {
throw new BindException(getBindingResult());
}
return getBindingResult().getModel();
}
}
从源源码的分析中,大概能总结到DataBinder
它提供了如下能力:
- 把属性值
PropertyValues
绑定到target上(bind()
方法,依赖于PropertyAccessor
实现~) - 提供校验的能力:提供了public方法
validate()
对各个属性使用Validator
执行校验~ - 提供了注册属性编辑器(
PropertyEditor
)和对类型进行转换的能力(TypeConverter
)
总结
本文介绍了Spring
用于数据绑定的最直接类DataBinder
,它位于spring-context
这个工程的org.springframework.validation
包内,所以需要再次明确的是:它是Spring提供的能力而非web提供的~
虽然我们DataBinder
是Spring提供,但其实把它发扬光大是发生在Web环境,也就是大名鼎鼎的WebDataBinder
,毕竟我们知道一般只有进行web交互的时候,才会涉及到字符串 -> Java类型/对象的转换,这就是下个章节讲述的重中之重~
知识交流
若文章格式混乱,可点击
:原文链接-原文链接-原文链接-原文链接-原文链接
The last:如果觉得本文对你有帮助,不妨点个赞呗。当然分享到你的朋友圈让更多小伙伴看到也是被作者本人许可的~
若对技术内容感兴趣可以加入wx群交流:Java高工、架构师3群
。
若群二维码失效,请加wx号:fsx641385712
(或者扫描下方wx二维码)。并且备注:"java入群"
字样,会手动邀请入群

【小家Spring】聊聊Spring中的数据绑定 --- DataBinder本尊(源码分析)的更多相关文章
- Spring PropertyResolver 占位符解析(二)源码分析
Spring PropertyResolver 占位符解析(二)源码分析 Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) ...
- Spring Boot启动命令参数详解及源码分析
使用过Spring Boot,我们都知道通过java -jar可以快速启动Spring Boot项目.同时,也可以通过在执行jar -jar时传递参数来进行配置.本文带大家系统的了解一下Spring ...
- RocketMQ中Broker的HA策略源码分析
Broker的HA策略分为两部分①同步元数据②同步消息数据 同步元数据 在Slave启动时,会启动一个定时任务用来从master同步元数据 if (role == BrokerRole.SLAVE) ...
- spring cloud EurekaClient 多网卡 ip 配置 和 源码分析(转)
https://blog.csdn.net/qq_30062125/article/details/83856655 1.前言对于spring cloud,各个服务实例需要注册到Eureka注册中心. ...
- Spring笔记(5) - 声明式事务@EnableTransactionManagement注解源码分析
一.背景 前面详解了实现Spring事务的两种方式的不同实现:编程式事务和声明式事务,对于配置都使用到了xml配置,今天介绍Spring事务的注解开发,例如下面例子: 配置类:注册数据源.JDBC模板 ...
- Fabric2.2中的Raft共识模块源码分析
引言 Hyperledger Fabric是当前比较流行的一种联盟链系统,它隶属于Linux基金会在2015年创建的超级账本项目且是这个项目最重要的一个子项目.目前,与Hyperledger的另外几个 ...
- 【Java】NIO中Selector的select方法源码分析
该篇博客的有些内容和在之前介绍过了,在这里再次涉及到的就不详细说了,如果有不理解请看[Java]NIO中Channel的注册源码分析, [Java]NIO中Selector的创建源码分析 Select ...
- RocketMQ中Broker的刷盘源码分析
上一篇博客的最后简单提了下CommitLog的刷盘 [RocketMQ中Broker的消息存储源码分析] (这篇博客和上一篇有很大的联系) Broker的CommitLog刷盘会启动一个线程,不停地 ...
- Flink中Idle停滞流机制(源码分析)
前几天在社区群上,有人问了一个问题 既然上游最小水印会决定窗口触发,那如果我上游其中一条流突然没有了数据,我的窗口还会继续触发吗? 看到这个问题,我蒙了???? 对哈,因为我是选择上游所有流中水印最小 ...
随机推荐
- SQL Server Update 所有表的某一列(列名相同,类型相同)数值
); WITH T AS (SELECT SchemaName = c.TABLE_SCHEMA, TableName = c.TABLE_NAME, ColumnName = c.COLUMN_NA ...
- Android零基础入门第45节:GridView简单使用
原文:Android零基础入门第45节:GridView简单使用 前面一共用了8期来学习ListView列表的相关操作,其实学习的ListView的知识完全适用于AdapterView的其他子类,如G ...
- 判断jQuery选择器结果为空 - CSDN博客
原文:判断jQuery选择器结果为空 - CSDN博客 jQuery选择器获取到的是一个对象,所以无论页面上存在或者不存在元素,这个对象都不为空.因此,如果要使用jQuery检查元素再给某个页面上是否 ...
- Unity 3d新手上路
作为一位unity新手,初学遇到了不少坑,而且不知道怎么找,发觉网上关于unity的文档好少,还是我暂时没找到. 现在说说void OnTriggerEnter(Collider e),这个函数是我加 ...
- 新兴技术袭来,Web开发如何抉择?
土豆网同步更新:http://www.tudou.com/plcover/VHNh6ZopQ4E/ 使用HTML 创建Mac OS App 视频教程. 官方QQ群: (1)App实践出真知 434 ...
- OpenSSL的命令行用法,以及参数大全
c:\openssl\bin>opensslOpenSSL> versionOpenSSL 1.0.2j 26 Sep 2016OpenSSL> https://wiki.opens ...
- 星星的模块封装类 IDSStarsScoreView
1 IDSStarsScoreView 的实现效果 2 类的封装方法: <声明文件> // // IDSStarsScoreView.h // Near // // ...
- ASP.NET MVC3在Visual Studio 2010中的变化
在VS2010中新建一个MVC3项目可以看出与以往的MVC2发生了很明显的变化 1.ASP.NET MVC3必要的运行环境为.NET 4.0 (想在3.5用MVC3,没门!) 2.默认MVC3模板项目 ...
- [Err] 1146 - Table 'performance_schema.session_status' doesn't exist已解决
刚刚接触MySQL,就往数据库添加数据,就遇到这个问题 解决方案就是找到你安装MySQL的bin目录 然后在cmd输入 mysql_upgrade -u root -p --force 回车,然后输入 ...
- 利用GitLab自动同步软件仓库
利用GitLab自动同步GitHub.Gitee.Bitbucket软件仓库 我在码云的账号:userName密码:password项目地址:https://gitee.com/Bytom/bytom ...