Spring Framework:DataBinder&TypeConvert

示例代码

参考文档

涉及问题

  1. 什么是数据绑定(Data Binding)?
  2. DataBinderBeanWrapper 之间的关系?
  3. BeanWrapper 都具备哪些功能 ?
  4. 数据绑定过程中的类型转换(TypeConvert)是怎么实现的?

DataBinder

Data binding is useful for letting user input be dynamically bound to the domain model of an application (or whatever objects you use to process user input). Spring provides the aptly named DataBinder to do exactly that. The Validator and the DataBinder make up the validation package, which is primarily used in but not limited to the web layer.

由此可知,数据绑定就是将用户输入绑定到领域对象模型的过程。通常是指将用户提供的一系列属性与领域对象的 Fields 进行绑定并赋值的过程。

对于用户提供的这些属性,spring 提供了一个 PropertyValue 类来进行封装。其中 name 表示属性名称,value 则表示该属性对应的值,convertedValue 则用来存储经过转化后的值(如果有必要的话)。

public class PropertyValue extends BeanMetadataAttributeAccessor 
    implements Serializable {

    private final String name;

    @Nullable
    private final Object value;

    @Nullable
    private Object convertedValue;
  
  // ...省略部分代码
}
  • DataBinder

对此,spring 提供了一个 DataBinder 类来提供数据绑定功能。它除了提供了将 PropertyValue 绑定到目标对象 target 的功能外,还提供了Validator相关的数据校验功能。不过此篇我们暂且忽略数据校验部分,而是先把重心放在数据绑定部分。

我们先通过一段简单的代码来对 DataBinder 类有一个直观的了解:

public static void main(String[] args) {
  User user = new User();
  DataBinder dataBinder = new DataBinder(user, "user");

  MutablePropertyValues propertyValues = new MutablePropertyValues();
  propertyValues.addPropertyValue("id", 10);
  propertyValues.addPropertyValue("name", " jerry");
  propertyValues.addPropertyValue("age", 18);

  dataBinder.bind(propertyValues);
  System.out.println(user);
}

这段代码的输出结果为:

User{id=10, name='jerry', age=18}

MutablePropertyValues 内置了一个 PropertyValue 的集合。我们可以通过 DataBinderbind(propertyValues) 方法来将一个或多个属性值绑定到目标对象上。
通过对 bind 方法进行 debug 分析,我们发现实际上 DataBinder 调用的是一个更底层的类 BeanWrapperImpl 来实现对 Bean 的操作的。

BeanWrapper:一个底层的 Bean 操作类

Spring 官方文档:

The BeanWrapper is a fundamental concept in the Spring Framework and is used in a lot of places. However, you probably do not need to use the BeanWrapper directly.

如官方文档所言,BeanWrapper 作为一个 spring 框架的一个底层基础工具(通常并不需要直接去使用它),运用于多个地方。例如:DataBinder 以及 BeanFactory

接下来,我们便对 BeanWrapper 进行一个详细的分析:

首先,我们先看一下 BeanWrapper 的默认实现类 BeanWrapperImpl 的类继承结构图:

image-20200911232330745

我们重点关注这三个接口,先对它们有一个直观的了解,然后再逐个分析:

  • BeanWrapper

提供了包装 Bean 的能力,以及对所封装 Bean自省能力。(自省是指自我观察获取自身内部结构,自我描述的能力)

  • PropertyAccessor

提供了直观的操作属性的能力。

  • TypeConverter

提供了类型转换的能力。

那么,总结一下,BeanWrapperImpl 是一个可以包装一个目标对象,并可以对目标对象进行自省以及属性赋值的工具类。并且在属性赋值的过程中支持属性值的类型转换。那么 BeanWrapperImpl 便具备以下能力:

  • 自省能力
  • 属性存取能力
  • 类型转化能力

下面呢,将会对这些能力的实现逐一进行更详细的分析。

Bean Introspection : 自省能力

Bean Introspection 是指 Bean 的自省。

Spring 官方文档:

The org.springframework.beans package adheres to the JavaBeans standard. A JavaBean is a class with a default no-argument constructor and that follows a naming convention where (for example) a property named bingoMadness would have a setter method setBingoMadness(..) and a getter method getBingoMadness(). For more information about JavaBeans and the specification, see javabeans.

Springorg.springframework.beans 包遵守了 JavaBeans 标准。一个 JavaBean 呢,是指一个具备默认构造函数,并且遵守一定命名规约的类。比如,我们平日里熟知的 gettersetter 方法,则分别对应了相关属性的读方法和写方法。

java.beans 包下,提供了一个 Introspector 工具类,提供了对 JavaBean 的自省能力,通过Introspector.getBeanInfo(Class clazz) 方法,可以返回一个自省结果 BeanInfo 对象。通过该对象可以获取属性的描述 PropertyDescriptor,方法的描述 MethodDescriptor 以及Bean 的描述 BeanDescriptor

通过 PropertyDescriptor 则可以获取对应属性的类型,以及相应的读写方法。

BeanWrapperImpl 提供的对所包装 Bean 的自省能力,实际上也是来源于 java.beans.Introspector

BeanWrapperImpl 类中,存在这样一个成员变量:CachedIntrospectionResults

通过观察 CachedIntrospectionResults 可以看出,它也是调用 Introspector 来对类进行自省。然后基于此并做了相应的缓存处理。

    private static BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
        for (BeanInfoFactory beanInfoFactory : beanInfoFactories) {
            BeanInfo beanInfo = beanInfoFactory.getBeanInfo(beanClass);
            if (beanInfo != null) {
                return beanInfo;
            }
        }
        return (shouldIntrospectorIgnoreBeaninfoClasses ?
                Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) :
                Introspector.getBeanInfo(beanClass));
    }

至此,我们的 BeanWrapperImpl 已经具备了一定的自省能力。

private static void main(String[] args) {
  BeanWrapper beanWrapper = new BeanWrapperImpl(User.class);
  PropertyDescriptor[] propertyDescriptors = beanWrapper.getPropertyDescriptors();
  for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
    System.out.printf("%s : %s, %s\n", propertyDescriptor.getName(),
                      propertyDescriptor.getReadMethod(),
                      propertyDescriptor.getWriteMethod());
  }
}

输出结果:

age : public java.lang.Integer com.grasswort.beans.model.User.getAge(), public void com.grasswort.beans.model.User.setAge(java.lang.Integer)
class : public final native java.lang.Class java.lang.Object.getClass(), null
id : public java.lang.Long com.grasswort.beans.model.User.getId(), public void com.grasswort.beans.model.User.setId(java.lang.Long)
name : public java.lang.String com.grasswort.beans.model.User.getName(), public void com.grasswort.beans.model.User.setName(java.lang.String)

PropertyAccessor:属性存取能力

Java Doc

Common interface for classes that can access named properties (such as bean properties of an object or fields in an object) . Serves as base interface for {@link BeanWrapper}.

PropertyAccessor 作为 BeanWrapper 的一个基本接口,定义了一系列对命名属性的访问和存储方法。

  • 判断属性是否可读,可写
    • isReadableProperty
    • isWritableProperty
  • 获取属性的类型
    • getPropertyType
    • getPropertyTypeDescriptor
  • 属性值的获取
    • getPropertyValue
  • 设置属性值
    • setPropertyValue
    • setPropertyValues

通过这些方法,我们可以直接地去操作 Bean 的属性。

private static void main(String[] args) {
  BeanWrapper beanWrapper = new BeanWrapperImpl(User.class);
  beanWrapper.setPropertyValue("id", "1");
  beanWrapper.setPropertyValue("name", "jerry");
  System.out.println("id : " + beanWrapper.getPropertyValue("id"));
  System.out.println("name : " + beanWrapper.getPropertyValue("name"));
  System.out.println(beanWrapper.getWrappedInstance());
}

输出结果:

id : 1
name : jerry
User{id=1, name='jerry', age=null}

TypeConverter:类型转换能力

以上,我们已经了解到,我们可以通过 BeanWrappersetPropertyValue 方法来设置属性的值。但是上文示例代码中使用的是:

beanWrapper.setPropertyValue("id", "1");

而在 User.java 中,id 声明的却是 Long 类型:

private Long id;

而之所以能够设置属性值成功,则说明在赋值的过程中,一定存在一个类型转换的过程。

BeanWrapperImpl 之所以具备类型转换能力,是因为它继承自 TypeConverterSupportTypeConverterSupport 组合了一个 TypeConverterDelegate 委派对象,类型转换逻辑将交由 TypeConvertDelegate 去执行操作。
为了使头脑中对此处结构更加清晰,特意对 BeanWrapperImpl 类图进行了一次重新整理:

在图中心偏上位置的是一个 PropertyEditorRegistrySupport 类。它提供了 PropertyEditorRegistry 的具体实现,支持对基于 JavaBeansjdk1.1 提供的类型转换接口 PropertyEditor 进行注册和扩展。除此之外,它还包含了一个可选的(@NullableConversionService 的成员变量,提供了对 spring 3 的一系列spring 自己提供的类型转换接口的支持与扩展。

  • 基础的 PropertyEditor

  • 可选的 ConversionService

而具体的转换逻辑,则在 TypeConvertDelegate 类中进行了体现。

@Nullable
    public <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue,
            @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException {

    // 先尝试寻找自定义的 PropertyEditor
        PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);

    // 如果没有找到对应的 PropertyEditor,并且 ConversionService 不为空。则尝试使用 ConversionService 去转换。
        ConversionService conversionService = this.propertyEditorRegistry.getConversionService();
        if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {
            // ...
        }

        Object convertedValue = newValue;

        // 如果找到了相应的 PropertyEditor
    // 或者没找到,但是传入类型与要求类型不符,则寻找默认 PropertyEditor 进行转换
        if (editor != null 
        || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) {
            // ...
        }

        // 如果还没转换成功,则尝试应用一些标准类型转换规则。
    // 包含数字、枚举、数组、集合等的处理。
  }

OK,那么我们已经了解到类型转换可以有两种扩展方式:

  • 基于 JavaBeansPropertyEditor
  • Spring 3 提供的 core.convert 包下的新接口,包含 Converter<S, T>GenericConverterConverterFactory<S, T>

PropertyEditor:基于 JavaBeans 的类型转换接口

PropertyEditor 位于 java.beans 包下,支持将 String 类型的对象转换成其他任意类型的对象。

通常我们是通过 setAsText(String text) 方法来传入一个字符串,然后经过自定义的处理逻辑后调用 setValue(Object obj) 方法,将转换后的值存储起来,客户端便可以通过 getValue() 方法获取转换后的值。

另外调用setValue(Object obj)方法的时候会触发一个PropertyChangeEvent事件,通知所有监听的 PropertyChangeListener

通常,我们创建一个自定义的 PropertyEditor 时,并不需要直接实现 PropertyEditor 接口。而是通过继承 PropertyEditorSupport 类,然后选择性地重写 setAsText(String text)getAsText() 方法即可。

例如:

public class StringToUserPropertyEditor extends PropertyEditorSupport {
    // 例如: 1-jerry-8

    @Override
    public String getAsText() {
        User user = (User) getValue();
        return user.getId() + "-" + user.getName() + "-" + user.getAge();
    }

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        String[] strArray = text.split("-");
        User user = new User();
        user.setId(Long.valueOf(strArray[0]));
        user.setName(strArray[1]);
        user.setAge(Integer.valueOf(strArray[2]));
        setValue(user);
    }

    public static void main(String[] args) {
        String text = "1-jerry-8";
        StringToUserPropertyEditor editor = new StringToUserPropertyEditor();
        editor.setAsText(text);
        User user = (User) editor.getValue();
        System.out.println(user);
    }
}

输出结果为:

User{id=1, name='jerry', age=8}

PropertyEditor 虽然具备了类型转换的能力,却也存在着一定的局限性。

  1. 它的来源类型只能是 String 类型。
  2. 它的实现缺少类型安全,实现类无法感知目标转换类型。
  3. 它的实现使用了成员变量 value 来存储转换结果,线程不安全。
  4. 除了类型转换为,它的实现了还包含了 JavaBeans 时间以及 JavaGUI 交互(在以上的 UML 类图中省略了这部分方法)。违反了职责单一原则。

因此, Spring 3 又提供了一些新的类型转换接口支持。

Spring 3 core.convert 通用类型转换

  • Convert<S, T>

Spring 3 之后,提供了这样一个接口 Convert<S, T>

S 代表了来源数据类型, T 则代表了目标转换类型。通过 Java 泛型限制了方法的参数类型和返回类型。解决了 PropertyEditor 的类型不安全问题,同时 T convert(S source) 的方法设计,相比于 PropertyEditor需要先存储后取值的接口方法设计,更有助于其实现类实现一个线程安全的类型转换器。

该接口实现非常的简单,且容易理解,便不再举例说明。

但它仍然存在一些小的问题,由于泛型擦除的原因,当我们注册了许多 Converter 的时候,我们不可能去一一进行转换尝试。

所以 Spring 又提供了一个 ConditionalConvert 接口。

public interface ConditionalConverter {
   boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

我们的实现类可以同时实现这两个接口:ConverterConditionalConverter。在转化过程中会先去调用 matches 方法,来判断是否匹配,匹配的话才会调用 convert 方法进行转换。

public class PropertiesToUserConverter implements Converter<Properties, User>, ConditionalConverter {

    @Override
    public User convert(Properties source) {
        User user = new User();
        user.setId(Long.valueOf(source.getProperty("id", "0L")));
        user.setName(String.valueOf(source.getOrDefault("name", "")));
        user.setAge(Integer.valueOf(source.getProperty("age", "0")));
        return user;
    }

    @Override
    public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
        return sourceType.getObjectType().isAssignableFrom(Properties.class)
                && targetType.getObjectType().isAssignableFrom(User.class);
    }

    public static void main(String[] args) {
        Properties properties = new Properties();
        properties.setProperty("id", "1");
        properties.setProperty("name", "jerry");
        properties.setProperty("age", "8");
        PropertiesToUserConverter converter = new PropertiesToUserConverter();
        boolean matched = converter.matches(TypeDescriptor.forObject(properties), TypeDescriptor.valueOf(User.class));
        if (matched) {
            User user = converter.convert(properties);
            System.out.println(user);
        }
    }
}
  • GenericConverter

Spring 还提供了一个支持复杂类型转换的接口:GenericConverter。它相比于 Converter<S, T> 更加灵活。

可以通过 getConvertibleTypes() 返回一个 ConvertiblePair 集合。每一个 ConvertiblePair 都包含了一个 soureTypetargetType ,表示可转换类型对。

GenericConverter 可以与 Converter 相配合,对复杂的数据类型进行转换。通常可以用于对集合类型的对象进行转换 。

具体可以查看相应的实现类来查看。


Spring 官方文档:

Because GenericConverter is a more complex SPI interface, you should use it only when you need it. Favor Converter or ConverterFactory for basic type conversion needs.

不过,GenericConverter 作为通用类型转换器虽然功能强大,但只有需要的时候才去使用它。如果可以,还是要优先考虑 Converter 或者 ConverterFactory

ConversionService

core.convert 包下,还存在这样一个类 ConversionService

public interface ConversionService {

    boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);

    boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);

    @Nullable
    <T> T convert(@Nullable Object source, Class<T> targetType);

    @Nullable
    Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);

}

Spring 官方文档:

A ConversionService is a stateless object designed to be instantiated at application startup and then shared between multiple threads. In a Spring application, you typically configure a ConversionService instance for each Spring container (or ApplicationContext). Spring picks up that ConversionService and uses it whenever a type conversion needs to be performed by the framework. You can also inject this ConversionService into any of your beans and invoke it directly.

JavaDoc :

A service interface for type conversion. This is the entry point into the convert system.

Call {@link #convert(Object, Class)} to perform a thread-safe type conversion using this system.

JavaDoc 所言,这是一个类型转换服务入口类(Spring 3 core.convert)。通常,我们使用这个类来做类型转换即可。

它的实现类实现了 ConverterRegistry 接口,支持 ConverterConverterFactoryGenericConverter 的注册与扩展。

Spring 应用程序中,我们可以通过 ConversionServiceFactoryBean 来将 ConversionService 注册到容器中。但是需要注意的是 id 必须为 conversionService

<bean id="conversionService"
        class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="example.MyCustomConverter"/>
        </set>
    </property>
</bean>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352