BeanWrapper

BeanWrapper是什么

Spring底层操作Java Bean的核心接口。

通常不直接使用该接口,而是通过BeanFactoryDataBinder

提供分析和操作标准Java Bean的操作: 获取和设置属性值(单个或批量)、获取属性描述以及查询属性的可读性/可写性的能力。

此接口还支持嵌套属性,允许将子属性上的属性设置为无限深度。

BeanWrapperextractOldValueForEditor默认值是false,可以避免调用 getter方法。将此选项设置为true,可以向自定义编辑器暴露当前属性值。

可以看出BeanWrapper是操作Java Bean 的强大利器。

类结构

BeanWrapper类结构

BeanWrapper 继承自TypeConverterPropertyEditorRegistryPropertyAccessor, ConfigurablePropertyAccessor接口。从名称可以看出具备了类型转换,属性编辑器注册,属性访问及配置的功能。

使用方式

接下来看看如何使用BeanWrapper来操作我们的Java Bean。

Spring给我们提供了一个实现类BeanWrapperImpl,我们就用这个类来展示。

获取属性

Bean对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Student {
private String name;

private String age;

private ClassRoom classRoom;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getAge() {
return age;
}

public void setAge(String age) {
this.age = age;
}

public ClassRoom getClassRoom() {
return classRoom;
}

public void setClassRoom(ClassRoom classRoom) {
this.classRoom = classRoom;
}
}

定义了3个属性,看一下使用方法:

1
2
3
4
Student student = new Student();
BeanWrapper wrapper = new BeanWrapperImpl(student);
System.out.println("展示bean 的属性");
Arrays.stream(wrapper.getPropertyDescriptors()).forEach(System.out::println);

结果如下:

1
2
3
4
5
展示bean 的属性
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=age]
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=class]
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=classRoom]
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=name]

可以看出将有get方法的属性已经打印出来了。同时可以看到多打印了一个class属性,但是我们的类里面没有定义这个属性,Object类中有getClass的方法。我们大胆猜测Spring会遵循Java Bean的设计原则,通过get方法来获取属性。

现在将age改成age1getAge方法不变,看一下结果。

1
2
3
4
5
展示bean 的属性
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=age]
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=class]
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=classRoom]
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=name]

打印出来的属性名一样。现在交换一下,将getAge改成getAge1,属性age1改成age

1
2
3
4
5
展示bean 的属性
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=age1]
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=class]
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=classRoom]
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=name]

可以看到获取到的属性已经变成了age1。这充分验证了我们的猜想。

我们可以看一下Spring的代码,里面使用了java.beans包下Introspector类来获取Bean的信息。

嵌套属性

上面的结果中,我们并没有获取到ClassRoom的属性。BeanWrapper并不支持这种操作,我们可以扩展一下,比如判断属性,如果是自定义的类型,那么就再调用一次BeanWrapper的方法。这有个前提是这个属性不为null

1
2
3
4
5
6
7
8
9
10
Student student = new Student();
student.setClassRoom(new ClassRoom());
BeanWrapper wrapper = new BeanWrapperImpl(student);

System.out.println("展示bean 的属性");
Arrays.stream(wrapper.getPropertyDescriptors()).forEach(System.out::println);

System.out.println("展示bean 的嵌套属性");
wrapper = new PowerfulBeanWrapper(student);
Arrays.stream(wrapper.getPropertyDescriptors()).forEach(System.out::println);

先上结果:

1
2
3
4
5
6
7
8
9
10
11
12
展示bean 的属性
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=age]
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=class]
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=classRoom]
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=name]
展示bean 的嵌套属性
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=age]
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=class]
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=classRoom]
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=name]
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=class]
org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=name]

ClassRoom类只有一个name 属性,这里看到也打印出来了。证明思路是对的,只是这个结构还需要组织一下,现在是扁平的。

下面看一下PowerfulBeanWrapper的实现方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class PowerfulBeanWrapper extends BeanWrapperImpl {
public PowerfulBeanWrapper(Object o) {
super(o);
}

@Override
public PropertyDescriptor[] getPropertyDescriptors() {
PropertyDescriptor[] propertyDescriptors = super.getPropertyDescriptors();
List<PropertyDescriptor> propertyDescriptorList = new ArrayList<>(Arrays.asList(propertyDescriptors));
Arrays.stream(propertyDescriptors).forEach(propertyDescriptor -> {
Object value = getPropertyValue(propertyDescriptor.getName());
if (value != null && !(value instanceof Class) && !value.getClass().isPrimitive()) {
propertyDescriptorList.addAll(Arrays.asList(new BeanWrapperImpl(value).getPropertyDescriptors()));
}
});
return propertyDescriptorList.toArray(new PropertyDescriptor[0]);
}
}

直接继承自BeanWrapperImpl类,覆盖了getPropertyDescriptors方法。遍历属性值,如果不为空并且不是Class,则再获取一次这个属性的属性。这里只获取了2层属性,可以在获取嵌套属性时换成我们的PowerfulBeanWrapper类既可支持无限层。

获取属性值

可以使用BeanWrapper的getPropertyValue方法来获取属性值。上面的代码中已经展示过了。

支持获取嵌套属性:

1
2
3
4
5
6
7
Student student = new Student();
ClassRoom classRoom = new ClassRoom();
classRoom.setName("room1");
student.setClassRoom(classRoom);
BeanWrapper wrapper = new BeanWrapperImpl(student);
System.out.println(wrapper.getPropertyValue("name"));
System.out.println(wrapper.getPropertyValue("classRoom.name"));

结果:

1
2
null
room1

可以看出来还是很方便的。

注: 当嵌套对象为空时,默认获取嵌套对象的属性会抛出异常。 这时可以加一个设置:

1
2
wrapper.setAutoGrowNestedPaths(true);
System.out.println("嵌套对象为空时:" + wrapper.getPropertyValue("classRoom.name"));

该属性的意义是自动扩展嵌套属性,按照默认值来初始化属性。此处就会将classRoom初始化,并且里面的属性为空。

1
嵌套对象为空时:null

设置属性值

可以通过setPropertyValue方法来设置属性值。同上,当嵌套对象为空时,不能设置嵌套对象的属性,设置wrapper.setAutoGrowNestedPaths(true)即可。

注意以下代码:

1
2
3
private String age;

wrapper.setPropertyValue("age",1);

在这里设置属性值的时候是整数型,但是age声明的时候是String。BeanWrapper是如何正确的赋值的呢?

BeanWrapperImpl内部会委托给TypeConverterDelegate类,先查找自定义PropertyEditor, 如果没有找到的话,则查找ConversionService,没有的话查找默认的PropertyEditor,再没有的话使用内部定义好的转换策略(按类型去判断,然后去转换)。

PropertyEditor

PropertyEditor属于Java Bean规范里面的类,可以给GUI程序设置对象属性值提供方便,所以接口里有一些和GUI相关的方法,显然目前已经过时了。同时,官方文档上解释,它是线程不安全的。必须得有一个默认构造函数。可以想象一下,在界面上填入一个值,这个值一般来说都是String类型的,填入之后这个值能自动设置到对应的对象中( 这里纯粹是我意淫的,对awt并不是很熟,不知道是不是这样)。了解安卓编程的朋友可能知道,我们要取界面上填的值,通常要拿到界面元素,然后再拿到值,然后再设置到对象中去。当界面上有很多个输入控件时,这样繁琐的操作,简直要人命。所以安卓后来出了数据绑定。这里有一篇文章讲得很好。

BeanWrapperImpl内置了一些 PropertyEditor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
private void createDefaultEditors() {
this.defaultEditors = new HashMap<>(64);

// Simple editors, without parameterization capabilities.
// The JDK does not contain a default editor for any of these target types.
this.defaultEditors.put(Charset.class, new CharsetEditor());
this.defaultEditors.put(Class.class, new ClassEditor());
this.defaultEditors.put(Class[].class, new ClassArrayEditor());
this.defaultEditors.put(Currency.class, new CurrencyEditor());
this.defaultEditors.put(File.class, new FileEditor());
this.defaultEditors.put(InputStream.class, new InputStreamEditor());
this.defaultEditors.put(InputSource.class, new InputSourceEditor());
this.defaultEditors.put(Locale.class, new LocaleEditor());
this.defaultEditors.put(Path.class, new PathEditor());
this.defaultEditors.put(Pattern.class, new PatternEditor());
this.defaultEditors.put(Properties.class, new PropertiesEditor());
this.defaultEditors.put(Reader.class, new ReaderEditor());
this.defaultEditors.put(Resource[].class, new ResourceArrayPropertyEditor());
this.defaultEditors.put(TimeZone.class, new TimeZoneEditor());
this.defaultEditors.put(URI.class, new URIEditor());
this.defaultEditors.put(URL.class, new URLEditor());
this.defaultEditors.put(UUID.class, new UUIDEditor());
this.defaultEditors.put(ZoneId.class, new ZoneIdEditor());

// Default instances of collection editors.
// Can be overridden by registering custom instances of those as custom editors.
this.defaultEditors.put(Collection.class, new CustomCollectionEditor(Collection.class));
this.defaultEditors.put(Set.class, new CustomCollectionEditor(Set.class));
this.defaultEditors.put(SortedSet.class, new CustomCollectionEditor(SortedSet.class));
this.defaultEditors.put(List.class, new CustomCollectionEditor(List.class));
this.defaultEditors.put(SortedMap.class, new CustomMapEditor(SortedMap.class));

// Default editors for primitive arrays.
this.defaultEditors.put(byte[].class, new ByteArrayPropertyEditor());
this.defaultEditors.put(char[].class, new CharArrayPropertyEditor());

// The JDK does not contain a default editor for char!
this.defaultEditors.put(char.class, new CharacterEditor(false));
this.defaultEditors.put(Character.class, new CharacterEditor(true));

// Spring's CustomBooleanEditor accepts more flag values than the JDK's default editor.
this.defaultEditors.put(boolean.class, new CustomBooleanEditor(false));
this.defaultEditors.put(Boolean.class, new CustomBooleanEditor(true));

// The JDK does not contain default editors for number wrapper types!
// Override JDK primitive number editors with our own CustomNumberEditor.
this.defaultEditors.put(byte.class, new CustomNumberEditor(Byte.class, false));
this.defaultEditors.put(Byte.class, new CustomNumberEditor(Byte.class, true));
this.defaultEditors.put(short.class, new CustomNumberEditor(Short.class, false));
this.defaultEditors.put(Short.class, new CustomNumberEditor(Short.class, true));
this.defaultEditors.put(int.class, new CustomNumberEditor(Integer.class, false));
this.defaultEditors.put(Integer.class, new CustomNumberEditor(Integer.class, true));
this.defaultEditors.put(long.class, new CustomNumberEditor(Long.class, false));
this.defaultEditors.put(Long.class, new CustomNumberEditor(Long.class, true));
this.defaultEditors.put(float.class, new CustomNumberEditor(Float.class, false));
this.defaultEditors.put(Float.class, new CustomNumberEditor(Float.class, true));
this.defaultEditors.put(double.class, new CustomNumberEditor(Double.class, false));
this.defaultEditors.put(Double.class, new CustomNumberEditor(Double.class, true));
this.defaultEditors.put(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, true));
this.defaultEditors.put(BigInteger.class, new CustomNumberEditor(BigInteger.class, true));

// Only register config value editors if explicitly requested.
if (this.configValueEditorsActive) {
StringArrayPropertyEditor sae = new StringArrayPropertyEditor();
this.defaultEditors.put(String[].class, sae);
this.defaultEditors.put(short[].class, sae);
this.defaultEditors.put(int[].class, sae);
this.defaultEditors.put(long[].class, sae);
}
}

这里没有注册String, 所以走的是内置方案,直接调用toString方法

1
2
3
4
if (String.class == requiredType && ClassUtils.isPrimitiveOrWrapper(convertedValue.getClass())) {
// We can stringify any primitive value...
return (T) convertedValue.toString();
}

自定义PropertyEditor

当Spring提供的PropertyEditor无法满足我们的需求时,我们可以自定义PropertyEditor

一般不直接实现接口,而是继承PropertyEditorSupport类。Spring中大多数场景都是将传入的字符串转换成对应的属性值,需要重写setAsText方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 转换String -> ClassRoom;
*/
public class ClassRoomPropertyEditor extends PropertyEditorSupport {
@Override
public void setAsText(String text) throws IllegalArgumentException {
//将逗号分隔的值转换成对象的属性值:room3,3
String[] strings = Optional.ofNullable(text).orElseGet(String::new).split(",");
ClassRoom classRoom = new ClassRoom();
classRoom.setName(strings[0]);
classRoom.setSize(Integer.parseInt(strings[1]));
setValue(classRoom);
}
}

上面的代码中,将字符串进行分隔,第一个值作为ClassRoomname值,第二个值作为size。如何使用这个PropertyEditor?

先注册这个类,再设置StudentclassRoom属性:

1
2
3
4
wrapper = new BeanWrapperImpl(student);
//注解自定义PropertyEditor
wrapper.registerCustomEditor(ClassRoom.class, new ClassRoomPropertyEditor());
wrapper.setPropertyValue("classRoom", "room3,3");

这样就给Student类的classRoom属性进行了初始化。

ConversionService

ConversionService是Spring提供的一套通用的类型转换机制的入口,相比PropertyEditor来说:

  1. 支持的类型转换范围更广。
  2. 支持从父类型转换为子类型,即多态。
  3. 省去了Java GUI相关的概念。
  4. 线程安全。

方法

  • boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType):如果可以将sourceType对象转换为targetType,则返回true

    如果此方法返回true,则意味着convert(Object, Class)方法能够将sourceType实例转换为targetType

    关于集合、数组和Map类型需要特别注意:对于集合、数组和Map类型之间的转换,此方法将返回true,即使在底层元素不可转换的情况下,转换过程仍然可能生成一个ConversionException。在处理集合和映射时,调用者需要处理这种特殊情况。

  • boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType):如果可以将sourceType对象转换为targetType,则返回trueTypeDescriptor 提供关于将要发生转换的源和目标位置的附加上下文信息,通常是对象字段或属性的位置。

    如果此方法返回true,则意味着convert(Object、TypeDescriptor、TypeDescriptor)能够将sourceType实例转换为targetType

    关于集合、数组和Map类型需要特别注意:对于集合、数组和Map类型之间的转换,此方法将返回true,即使在底层元素不可转换的情况下,转换过程仍然可能生成一个ConversionException。在处理集合和映射时,调用者需要处理这种特殊情况。

  • <T> T convert(@Nullable Object source, Class<T> targetType):将给定的对象转换为指定的targetType类型对象。

  • Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType):将给定的对象转换为指定的targetType类型对象。TypeDescriptor 提供关于将要发生转换的源和目标位置的附加上下文信息,通常是对象字段或属性位置。

Converter

Converter是具体的某类转换器接口,负责将某个类型的对象转成另外一个类型的对象。并且是一个函数式接口。

就提供了一个转换方法:

  • T convert(S source):转换对象类型。

ConverterFactory

生产一种Converter,这种Converter可以将对象从S转换为R的子类型。也就是说支持多态功能。
实现类还可以实现ConditionalConverter接口。

  • <T extends R> Converter<S, T> getConverter(Class<T> targetType):根据目标类型T获取Converter,该Converter将源类型S转换成R的子类型T

ConditionalConverter

该接口可以根据源和目标TypeDescriptor的属性选择性地执行ConverterGenericConverterConverterFactory

通常用于根据字段或类特征(如注解或方法)的存在选择性地匹配自定义转换逻辑。例如,当从String字段转换为Date字段时,如果目标字段还有@DateTimeFormat注解,则实现类matches方法可能返回true,也就是说如果目标字段上没有@DateTimeFormat注解,那么可能不会应用该转换,该接口可以控制需不需要转换。

另外一个例子,当从字符串字段转换为Account字段时,如果目标Account类定义了公共静态findAccount(String)方法,则实现类matches方法可能返回true

  • boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType):是否需要转换。

GenericConverter

这是最灵活的转换器SPI接口,也是最复杂的。它的灵活性在于GenericConverter可以支持多个源/目标类型对之间的转换(参见getConvertibleTypes()方法)。此外,GenericConverter实现类在类型转换过程中可以访问源/目标字段上下文,允许解析源和目标字段元数据,如注解和泛型信息,这些信息可用于复杂转换逻辑。

当比较简单的ConverterConverterFactory接口够用时,通常不应使用此接口。
实现类还可以实现ConditionalConverter接口。

  • Set getConvertibleTypes():返回此Converter可以在源类型和目标类型之间转换的类型。
    Set中每条都是一个可转换的源到目标类型对。ConvertiblePair保存源类型与目标类型的映射关系。
    对于ConditionalConverter,此方法可能返回null,意味着该GenericConverter适用于所有源与目标类型对。未实现ConditionalConverter接口的实现类,此方法不能返回null

ConverterRegistry

用来注册Converter

ConversionService组件结构

ConversionService结构

ConversionService提供转换功能的统一入口,ConverterRegistry提供Converter注册功能,将Converter集中起来,转换时从中查出对应的ConverterConverter负责具体的转换过程。ConfigurableConversionService继承自ConversionServiceConverterRegistry,集成转换和注册功能。

下面看一下具体的实现类。

GenericConversionService

基础转换服务实现,适用于大部分情况。直接实现ConfigurableConversionService接口,实现了注册与转换功能。在注册ConverterConverterFactory时,会将其转换成GenericConverter

DefaultConversionService

继承自GenericConversionService,配置了适合大多数环境的Converter
该类使用时可以直接实例化,暴露出静态方法addDefaultConverters(ConverterRegistry),用于对某个ConverterRegistry实例进行特殊处理,也就是说当某个ConverterRegistry需要增加一个默认的Converter时,可以调用这个方法。

这里我们可以直接使用DefaultConversionService类,Spring已经配置了一些Converter

1
2
3
4
5
6
7
8
student = new Student();
wrapper = new BeanWrapperImpl(student);
wrapper.setAutoGrowNestedPaths(true);
//注册ConversionService
wrapper.setConversionService(new DefaultConversionService());
wrapper.setPropertyValue("classRoom.size", "3");
System.out.println("ConversionService, 设置嵌套对象的属性 size:" + wrapper.getPropertyValue("classRoom.size"));
//这里将字符串转换成数字

Spring 提供的Converter

在Spring 的org.springframework.core.convert.support包中内置了一些转换器,提供数组、集合、字符串、数字、枚举、对象、Map、Boolean等之间的转换功能。

Array Collection Stream ByteBuffer String(Character) Number(Integer) Object Enum Map Boolean Charset Currency Locale Properties TimeZone UUID Calendar
Array ArrayToArrayConverter ArrayToCollectionConverter StreamConverter ByteBufferConverter ArrayToStringConverter ArrayToObjectConverter
Collection CollectionToArrayConverter CollectionToCollectionConverter StreamConverter CollectionToStringConverter CollectionToObjectConverter
Stream StreamConverter StreamConverter
String(Character) StringToArrayConverter StringToCollectionConverter StringToCharacterConverter StringToNumberConverterFactory StringToEnumConverterFactory StringToBooleanConverter StringToCharsetConverter StringToCurrencyConverter StringToLocaleConverter StringToPropertiesConverter StringToTimeZoneConverter StringToUUIDConverter
ByteBuffer ByteBufferConverter ByteBufferConverter
Number(Integer) NumberToCharacterConverter NumberToNumberConverterFactory IntegerToEnumConverterFactory
Object ObjectToArrayConverter ObjectToCollectionConverter ByteBufferConverter ObjectToStringConverter ObjectToObjectConverter,IdToEntityConverter
Enum EnumToStringConverter EnumToIntegerConverter
Map MapToMapConverter
Boolean
Properties PropertiesToStringConverter
ZoneId ZoneIdToTimeZoneConverter
ZonedDateTime ZonedDateTimeToCalendarConverter

StringToBooleanConverter

String转换成Booleantrueon、yes、1 转换成Boolean.TRUEfalse、off 、no0 转换成Boolean.FALSE`。

1
2
3
4
5
6
wrapper.setPropertyValue("good", "1");
System.out.println("ConversionService, 设置bool值。 good:" + wrapper.getPropertyValue("good"));

//-------
//输出:
//ConversionService, 设置bool值。 good:true

自定义Converter

当Spring提供的转换器无法满足我们需要时,我们可以自定义转换逻辑。

上面提到的ConverterGenericConverterConverterFactory三个接口都可以用来实现转换逻辑。该如何选择?

  • Converter:单值。从S->T。一对一转换。

  • ConverterFactory:从S -> T extends R。一对多转换。

  • GenericConverter :功能最复杂。实现多个类型对的转换。多对多转换。

在自定义PropertyEditor示例中,我们实现了从String转换到ClassRoom的功能。在这里通过Converter来实现此功能。

  1. StringToClassRoomConverter实现String转换到ClassRoom`的功能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class StringToClassRoomConverter implements Converter<String, ClassRoom> {
    @Override
    public ClassRoom convert(String source) {
    String[] strings = Optional.ofNullable(source).orElseGet(String::new).split(",");
    ClassRoom classRoom = new ClassRoom();
    classRoom.setName(strings[0]);
    classRoom.setSize(Integer.parseInt(strings[1]));
    return classRoom;
    }
    }
  2. 将此StringToClassRoomConverter注册到我们使用的ConversionService中。

    1
    2
    3
    DefaultConversionService conversionService = new DefaultConversionService();
    conversionService.addConverter(new StringToClassRoomConverter());
    wrapper.setConversionService(conversionService);
  3. BeanWrapper使用此ConversionService设置Bean属性。这里不一定用BeanWrapper 来操作,可以直接调用ConversionService来转换。

    1
    2
    3
    4
    5
    6
    7
    wrapper.setPropertyValue("classRoom", "room4,4");
    System.out.println("自定义Converter, 设置嵌套对象的属性 name:" + wrapper.getPropertyValue("classRoom.name"));
    System.out.println("自定义Converter, 设置嵌套对象的属性 size:" + wrapper.getPropertyValue("classRoom.size"));

    //---------
    //自定义Converter, 设置嵌套对象的属性 name:room4
    //自定义Converter, 设置嵌套对象的属性 size:4

    从输出结果来看,已经成功转换成功。

当我们想从一个类型转换成某些类型时,可以实现ConverterFactory接口,因为我们也不知道总共有哪些类型,不可能每个类型都写一个ConverterFactory。比如说从String转换成枚举类型,前端传枚举类型的字面值,转换成具体的枚举类型。Spring 内置了StringToEnumConverterFactory来实现此功能。直接调用Enum的静态方法Enum.valueOf(this.enumType, source.trim())来实现转换。同理还有IntegerToEnumConverterFactory通过枚举的序号来转换。

当我们遇到容器型的转换需求时,因为容器内部保存的类型可能是多种多样的,比如说List里面既有String,也有int,我们要统一转成Long型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
DefaultConversionService conversionService = new DefaultConversionService();
List source = new ArrayList();
source.add("1");
source.add(2);
//这里要注意初始化成内部类,才能正确获取泛型,不然不会转换。因为这个例子中,如果不写成内部类,源类型和目标类型其实是一致的,CollectionToCollectionConverter(这个转换器支持集合之间的转换) 内部不会去做转换。
List<Long> longList = new ArrayList<>(){};
for (Object s : source) {
System.out.println(s.getClass() + " , " + s);
}
List convert = conversionService.convert(source, longList.getClass());
for (Object s : convert) {
System.out.println(s.getClass() + " , " + s);
}

//-----------------
//不是内部类,不转换
//class java.lang.String , 1
//class java.lang.Integer , 2
//class java.lang.String , 1
//class java.lang.Integer , 2

//-------------
//正确转换
//class java.lang.String , 1
//class java.lang.Integer , 2
//class java.lang.Long , 1
//class java.lang.Long , 2

如果有多个Converter可以处理同一个转换需求,那么则看注意的先后顺序了,会取第一个符合条件的转换器。这里可以优化一下。

Formatter

在前后端交互时,通常会遇到日期格式这样的问题,Converter虽然说也能解决这个问题,在转换时获取到正确的格式然后进行转换。但是这样无法灵活控制我们的格式,我们得把所有格式都写在我们的Converter里,换一种格式,又得改一次这个类。这时候Formatter接口出现了。

Formatter接口位于context包中。

先看一下Formatter类,从ParserPrinter接口继承而来,实现了String <-> Object转换的功能。

Formatter结构

FormatterRegistry

继承自ConverterRegistry接口,用来注册Formatter

  • void addFormatter(Formatter<?> formatter):向特定类型的字段添加Formatter。字段类型由Formatter的泛型提供。

  • void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter):向给定类型的字段添加Formatter

    在打印时,如果声明了Formatter的类型T,并且不能将fieldType赋值给T,则在打印字段值的任务委托给Formatter之前,将尝试强制转换为T。在解析时,如果Formatter返回的已解析对象不能分配给运行时字段类型,则在返回已解析字段值之前,将尝试强制转换为fieldType。例如,DateFormatter声明的泛型为Date,如果这里的fieldTypeDateTime(假设存在),如果DateTime可以用Date变量接收,则意味着可以分配给Date,也就不需要转换。否则则需要转换为Date类型。具体能否分配,可以查看该方法:org.springframework.core.convert.TypeDescriptor#isAssignableTo

  • void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser):添加Printer/Parser对来格式化特定类型的字段,formatter 将委托给指定的Printer进行打印,并委托指定的Parser进行解析。

    在打印时,如果声明了Printer的类型T,并且fieldType不能赋值给T,则在委托Printer打印字段值之前,将尝试转换化类型T。在解析时,如果Parser返回的对象不能分配给fieldType,则在返回解析后的字段值之前,将尝试转换为fieldType。这个方法与上一个方法的区别就是将Formatter拆开。

  • void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory):为格式化注解标注的字段添加Formatter。此方法与上述方法的区别是不再以类型作为转换的依据了,而是根据注解来转换。比如某个字段上使用了DateTimeFormat注解,那会调用对应的Formatter

FormattingConversionService

FormattingConversionService类是Spring提供的支持Formatter接口的实现类,继承自GenericConversionService类,实现了FormatterRegistryEmbeddedValueResolverAware接口,在GenericConversionService类的基础上增加了注册Formatter的功能。EmbeddedValueResolverAware接口用来解决字符串占位符、国际化等问题。注册Formatter时,会将Formatter转换成GenericConverter,调用此GenericConverterconvert方法时,将会调用Formatterprintparse方法,由此可以看出我们需要格式化时,还是调用FormattingConversionServiceconvert方法即可。

DefaultFormattingConversionService

Spring 内部提供了一些Formatter,会通过DefaultFormattingConversionService注册,我们无不特殊要求,可以直接使用此类,再在此基础上注册我们自定义的Formatter

Spring提供的Formatter

FormatterFormatterRegistrar DefaultFormattingConversionService是否注册 说明
NumberFormatAnnotationFormatterFactory Y 用于支持@NumberFormat注解
CurrencyUnitFormatter JSR-354相关的jar包出现在classpath时注册 javax.money.CurrencyUnit
MonetaryAmountFormatter JSR-354相关的jar包出现在classpath时注册 javax.money.MonetaryAmount
Jsr354NumberFormatAnnotationFormatterFactory JSR-354相关的jar包出现在classpath时注册 @NumberFormat注解
DateTimeFormatterRegistrar 用来注册JSR-310新版日期和时间相关
JodaTimeFormatterRegistrar 如果使用了Joda包,则会注册相关的``
DateFormatterRegistrar 注册@DateTimeFormat注解的AnnotationFormatterFactory用于DateCalendarLong之间格式化以及DateCalendarLong之间的转换的Converter。默认不会注册用于直接转换的DateFormatter,不需要@DateTimeFormat注解,我们可以手动注册。

使用示例

  1. 以日期为例,如果不用注解的话,我们需要手动注册一下Formatter
1
2
3
4
5
6
7
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
conversionService.addFormatter(new DateFormatter());
Date date = new Date();
System.out.println(conversionService.convert(date, String.class));

//----------------
// 2019年9月3日

DateFormatter还支持指定格式。可以通过构造函数传入。

1
2
3
4
conversionService.addFormatter(new DateFormatter("yyyy-MM-dd"));

//-------
// 2019-09-03
  1. 还是以日期为例,使用Spring提供的@DateTimeFormat注解。

    先创建一个类,里面有个日期字段使用@DateTimeFormat注解。

    1
    2
    3
    class Question {
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date createTime;

    指定格式为yyyy-MM-dd,这里我们借助BeanWrapper来触发格式化的动作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();

    Question question = new Question();
    BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(question);
    beanWrapper.setConversionService(conversionService);
    beanWrapper.setPropertyValue("createTime", "2019-09-03");
    System.out.println(question.getCreateTime());

    //-----------
    // Tue Sep 03 00:00:00 CST 2019

    // 将时间格式化 字符串
    System.out.println("注解格式化日期:" + conversionService.convert(question.getCreateTime(), new TypeDescriptor(question.getClass().getDeclaredField("createTime")), TypeDescriptor.valueOf(String.class)));

    //---------
    //注解格式化日期:2019-09-03

    通过打印的信息,可以看到已经成功将字符串parseDate,并将日期format为字符串。由于注解在字段上,我们只提供了Date的值,所以还需要通过TypeDescriptor将字段的附加信息传递进去,这样才能正确识别到字段上的注解。

自定义Formatter

除了Spring 给我们提供的这些Formatter之外,我们还可以自定义来实现特殊功能。

比如前台传过来一段字符串,我们根据正则表达式截取部分字符。

定义StringFormat注解

此注解用来标注该字段需要用我们的自定义逻辑。可以指定具体的正则表达式。

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
public @interface StringFormat {
String pattern();
}

定义StringFormatAnnotationFormatterFactory

此类实现AnnotationFormatterFactory接口,规定StringFormat注解支持的字段类型。我们使用正则分隔字符串,那么可能得到多个目标串,所以getFieldTypes返回List来接收目标类型。getParser方法返回我们的自定义StringFormatFormatter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* The types of fields that may be annotated with the &lt;A&gt; annotation.
*/
@Override
public Set<Class<?>> getFieldTypes() {
return Set.of(List.class, String.class);
}

/**
* Get the Printer to print the value of a field of {@code fieldType} annotated with
* {@code annotation}.
* <p>If the type T the printer accepts is not assignable to {@code fieldType}, a
* coercion from {@code fieldType} to T will be attempted before the Printer is invoked.
*
* @param annotation the annotation instance
* @param fieldType the type of field that was annotated
* @return the printer
*/
@Override
public Printer<?> getPrinter(StringFormat annotation, Class<?> fieldType) {
return new StringFormatFormatter(annotation.pattern());
}

/**
* Get the Parser to parse a submitted value for a field of {@code fieldType}
* annotated with {@code annotation}.
* <p>If the object the parser returns is not assignable to {@code fieldType},
* a coercion to {@code fieldType} will be attempted before the field is set.
*
* @param annotation the annotation instance
* @param fieldType the type of field that was annotated
* @return the parser
*/
@Override
public Parser<?> getParser(StringFormat annotation, Class<?> fieldType) {
return new StringFormatFormatter(annotation.pattern());
}

自定义 StringFormatFormatter

该类实现Formatter。用来负责具体的解析逻辑。该类需要使用到注解中定义的正则表达式,这样我们就可以灵活控制每个字段的转换规则了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private static class StringFormatFormatter implements Formatter<Collection> {
private Pattern pattern;
StringFormatFormatter(String pattern) {
this.pattern = Pattern.compile(pattern);
}

/**
* Parse a text String to produce a T.
*
* @param text the text string
* @param locale the current user locale
* @return an instance of T
* @throws ParseException when a parse exception occurs in a java.text parsing library
* @throws IllegalArgumentException when a parse exception occurs
*/
@Override
public Collection parse(String text, Locale locale) throws ParseException {
List<String> list = new ArrayList<>();
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
list.add(matcher.group());
}
return list;
}

}

使用

将我们的Formatter注册到FormattingConversionService,遇到对应的转换,则会调用我们的Formatter。下面的例子中StringFormatEntity类使用到了自定义的StringFormat注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@StringFormat(pattern = "\\d+")
private List<String> formats;


public class CustomFormatterDemo {
public static void main(String[] args) throws NoSuchFieldException {
//注册 StringFormatAnnotationFormatterFactory
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
conversionService.addFormatterForFieldAnnotation(new StringFormatAnnotationFormatterFactory());
StringFormatEntity formatEntity = new StringFormatEntity();
System.out.println("自定义注解格式化:" + conversionService.convert("fff43ffd344", TypeDescriptor.valueOf(String.class) , new TypeDescriptor(formatEntity.getClass().getDeclaredField("formats"))));
}
}


// 自定义注解格式化:[43, 344]

从打印出来的结果可以看出来已经正确将字符串转换成了List。不过List中都是字符串,我们还可以将字符串转换成数字类型。需要我们来主动转换吗?其实是不需要的,Spring 已经帮我们考虑到了此种情景,可以自动将List中的元素也转换成对应的类型。还记得CollectionToCollectionConverter这个转换器吗?不过有个细节要注意:**我们的Formatter返回的结果不能是ArrayList, 这样会丢失泛型,不能正确转换,所以我们可以返回ArrayList的子类List<String> list = new ArrayList<>(){};, 这样会保留泛型,会调用后续的Converter**。

Converter 的注册与获取

主要通过ConverterRegistryFormatterRegistry来注册以及移除Converter

ConverterRegistry

方法

  1. void addConverter(Converter<?, ?> converter):注册简单的Converter,转换类型从Converter的泛型中获取。

  2. <S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter): 注册普通转换器,并且明确指定可转换类型。

    可以针对多个不同转换类型的情况重用Converter,而不必为每个对创建Converter类。指定的源类型是Converter定义的类型的子类型,目标类型是Converter定义的类型的父类型。为什么要如此定义?拿Spring提供的ObjectToStringConverter为例,该Converter定义的转换类型为Object -> String,调用Object.toString()方法,只要是Object的子类,都可以调用此方法转换成String,因为toString()是共有的方法。同理,目标类型指定的类型需要是我定义的父类型,这样转换出来的一定是需要的类型。

  3. void addConverter(GenericConverter converter):注册GenericConverter

  4. void addConverterFactory(ConverterFactory<?, ?> factory):注册ConverterFactory

  5. void removeConvertible(Class<?> sourceType, Class<?> targetType):移除sourceTypetargetType的转换功能。

GenericConversionService

GenericConversionService类实现了ConverterRegistry接口。现在看一下具体的注册过程。

  1. void addConverter(Converter<?, ?> converter)

    因为没有指定转换类型,所以只能从Converter的泛型中获取转换类型,如果获取不到,则会抛出异常。获取到之后,则会创建ConverterAdapter实例,通过void addConverter(GenericConverter converter)方法进行注册。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ResolvableType[] typeInfo = getRequiredTypeInfo(converter.getClass(), Converter.class);
    //如果是代理对象,还需要从代理类中获取
    if (typeInfo == null && converter instanceof DecoratingProxy) {
    typeInfo = getRequiredTypeInfo(((DecoratingProxy) converter).getDecoratedClass(), Converter.class);
    }
    if (typeInfo == null) {
    throw new IllegalArgumentException("Unable to determine source type <S> and target type <T> for your " + "Converter [" + converter.getClass().getName() + "]; does the class parameterize those types?");
    }
    addConverter(new ConverterAdapter(converter, typeInfo[0], typeInfo[1]));
  2. <S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter)

    由于指定了转换类型,直接注册就完事了。

    1
    addConverter(new ConverterAdapter(converter, ResolvableType.forClass(sourceType), ResolvableType.forClass(targetType)));
  3. void addConverterFactory(ConverterFactory<?, ?> factory)

    也是先从泛型中推断出转换的类型,然后创建ConverterFactoryAdapter实例进行注册。

    1
    2
    3
    4
    5
    6
    7
    8
    ResolvableType[] typeInfo = getRequiredTypeInfo(factory.getClass(), ConverterFactory.class);
    if (typeInfo == null && factory instanceof DecoratingProxy) {
    typeInfo = getRequiredTypeInfo(((DecoratingProxy) factory).getDecoratedClass(), ConverterFactory.class);
    }
    if (typeInfo == null) {
    throw new IllegalArgumentException("Unable to determine source type <S> and target type <T> for your " + "ConverterFactory [" + factory.getClass().getName() + "]; does the class parameterize those types?");
    }
    addConverter(new ConverterFactoryAdapter(factory, new ConvertiblePair(typeInfo[0].toClass(), typeInfo[1].toClass())));
  4. void addConverter(GenericConverter converter)

    上面几种注册方式最终都会调用此方法,也就是说会将ConverterConverterFactory转换成GenericConverter 。这里使用到了适配器模式

    1
    2
    3
    4
    //添加到内部容器中去
    this.converters.add(converter);
    //使缓存失效
    invalidateCache();

ConverterAdapter适配器

实现了ConditionalGenericConverter接口,将Converter转换成GenericConverter。在matches方法去判断转换类型是否匹配。转换时直接调用内部转换器的转换方法。

ConverterFactoryAdapter适配器

实现了ConditionalGenericConverter接口,将ConverterFactoryAdapter转换成GenericConverter。在matches方法去判断转换类型是否匹配。转换时直接调用内部ConverterFactory获取的转换器的转换方法。

ConvertiblePair

该类保存了转换的源类型和目标类型,并重写了equalshashCode方法用于比较。GenericConverter返回ConvertiblePair集合表示所支持的转换类型。

Converters

此类用来管理所有注册的Converter。提供添加和删除的功能。添加时获取到此Converter支持的类型,如果为空并且是ConditionalConverter,则代表它支持所有类型。得到支持的类型后,遍历每个类型,获取到已经注册的ConvertersForPair,该类维护转换类型到Converter之间的关系,而且是一对多的关系,也就是说同一种转换类型,会存在多个Converter。拿到ConvertersForPair后,将该Converter添加进去,后添加的会在前面,获取时符合条件时会优先返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void add(GenericConverter converter) {
Set<ConvertiblePair> convertibleTypes = converter.getConvertibleTypes();
if (convertibleTypes == null) {
Assert.state(converter instanceof ConditionalConverter,
"Only conditional converters may return null convertible types");
this.globalConverters.add(converter);
}
else {
for (ConvertiblePair convertiblePair : convertibleTypes) {
ConvertersForPair convertersForPair = getMatchableConverters(convertiblePair);
//后添加的在前面
convertersForPair.add(converter);
}
}
}

FormatterRegistry注册 Formatter

继承自ConverterRegistry接口,增加了注册Formmater的方法。

FormattingConversionService

该类继承自GenericConversionService类,并实现了FormatterRegistry接口。在添加Formatter时会将其转换为PrinterConverterParserConverter。在添加AnnotationFormatterFactory转换为AnnotationPrinterConverterAnnotationParserConverter。可以想象到这四个类也是实现了Converter,最终通过convert方法来调用parseprint方法。

获取Converter

GenericConversionService的转换过程中,来了一个转换类型,需要获取到对应的Converter。在Convertersfind方法中先拿到源类型和目标类型继承的所有类型(包括接口),比如说源类型是String,那么获取到的就是StringSerializableComparableCharSequenceObject,如果是枚举还将获取到Enum。找到之后一一进行组合去获取Converter,比如目标类型是Integer,则第一次组合就是String->Integer,如果找到了支持String->IntegerConverter,则会返回这个。这么做的目的是支持一个Converter可以转换多个类型,比如String-> Enum,通过字面量转换成枚举,如果没有这个机制,那么我们就得为每个枚举都定义一个Converter,但是有了这个机制,我们就可以支持所有的枚举类型。其实就是通过这个机制来支持ConverterFactory。这个机制可以保证子类可以通过父类转换器进行转换(这种转换方式需要注意父类无法感知子类的特殊属性),但不能保证父类可以通过子类转换器,如果可以保证Converter能正确转换,则可以通过<S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter)方法显式进行注册。比如我们只有String->IntegerConverter,但是我们需要将String转换为Number,则可以通过这个方法注册addConverter(String.class, Number.class, StringToIntegerConverter)

1
2
3
4
5
6
7
8
9
10
11
12
DefaultConversionService conversionService = new DefaultConversionService();
//先去掉内置的转换器
conversionService.removeConvertible(String.class, Number.class);
//再注册上我们自己定义的 String -> Integer
conversionService.addConverter(new StringToIntegerConverter());
System.out.println(conversionService.convert("1", Number.class));

//这种情况下是无法正确转换的。

//但是通过这个方法显式注册之后可以正确转换
conversionService.addConverter(String.class, Number.class, new StringToIntegerConverter());
// 1

下面是查找的大致过程:

查找converter的图示

DirectFieldAccessor

通过反射直接访问Bean实例的字段。可以直接绑定到字段,而不需要通过JavaBean set方法。

从Spring 4.2开始,绝大多数BeanWrapper特性已经被合并到AbstractPropertyAccessor中,这意味着这个类也支持属性遍历以及集合和Map 访问。

DirectFieldAccessorextractOldValueForEditor属性默认为true,因为在读取字段的时候是直接通过反射去拿到的字段值,不需要调用getter方法。

PropertyAccessorFactory

可以通过此类来获取BeanWrapperDirectFieldAccessor