美文网首页Spring
Spring Framework:DataBinder&Type

Spring Framework:DataBinder&Type

作者: 烟波一棹知何许 | 来源:发表于2020-09-13 14:25 被阅读0次

    示例代码

    参考文档

    涉及问题

    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>
    

    相关文章

      网友评论

        本文标题:Spring Framework:DataBinder&Type

        本文链接:https://www.haomeiwen.com/subject/aubnektx.html