SpringBoot自定义类型转换

前言

ConversionService 是 Spring 中用于类型转换的服务接口,也是转换系统的入口点。

转换器

Converter<S,T>

  • 缺少 SourceType 和 TargetType 前置判断
  • 仅能转换单一的 SourceType 和 TargetType

ConverterFactory<S, R>

  • 一对多的转换,将S类型转换为R的子类

GenericConverter

  • 复合类型或通用类型的实现方式
  • 多对多的转换

ConditionalConverter

  • 在 GenericConverter 基础上加上条件

ConditionalGenericConverter

  • 看代码就能猜到是结合了 GenericConverter 和 ConditionalGenericConverter 的功能
1
public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {}

统一类型转换服务

GenericConversionService

  • 通用 ConversionService 模板实现
  • 不内置转换器实现

DefaultConversionService

  • 基础 ConversionService 实现
  • 内置常用转换器实现

FormattingConversionService

  • 通用 Formatter + GenericConversionService 实现
  • 不内置转换器和 Formatter 实现

DefaultFormattingConversionService

  • DefaultConversionService + 格式化实现

基本用法

1
2
3
4
@Autowired
ConversionService conversionService;

Integer[] convert = conversionService.convert(Arrays.asList(1, 2, 3), Integer[].class);

List<Integer> 转换 Integer数组,底层使用的是 org.springframework.core.convert.support.CollectionToArrayConverter

自定义类型转换

Spring 中 conversionService 注册时机

在应用启动的时候会去寻找 conversionService 这个 Bean。

Spring 会在 refresh() 方法的 finishBeanFactoryInitialization(beanFactory) 中进行 getBean 然后 set 到 beanFactory 中。

org.springframework.context.support.AbstractApplicationContext#finishBeanFactoryInitialization

1
2
3
4
5
6
7
8
9
10
11
12
String CONVERSION_SERVICE_BEAN_NAME = "conversionService";

protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
// Initialize conversion service for this context.
if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
beanFactory.setConversionService(
beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
}
// 以下省略
...
}

所以在整个 Spring 上下文中 conversionService 是唯一的。

Converter 实现

一对一转换 String 转 LocalDateTime

1
2
3
4
5
6
public class LocalDateTimeConverter implements Converter<String, LocalDateTime> {
@Override
public LocalDateTime convert(@NonNull String source) {
return LocalDateTime.parse(source, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
}

ConverterFactory<S, R> 实现

一对多转换,将S类型转换为R的子类

  1. 先定义三个 POJO 类

    Foo 和 Bar 都继承 Person

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Data
    public class Person {
    private String profession;
    }

    @Data
    @EqualsAndHashCode(callSuper = true)
    public class Foo extends Person {
    private String name;
    private Integer age;
    }

    @Data
    @EqualsAndHashCode(callSuper = true)
    public class Bar extends Person {
    private String name;
    private Integer age;
    }
  2. 转换实现

    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
    public class PersonConverter implements ConverterFactory<String, Person> {

    @NonNull
    @Override
    public <T extends Person> Converter<String, T> getConverter(@NonNull Class<T> targetType) {
    return new StringToPerson<>(targetType);
    }

    private static final class StringToPerson<T extends Person> implements Converter<String, T> {

    private final Class<T> targetType;

    public StringToPerson(Class<T> targetType) {
    this.targetType = targetType;
    }

    @SuppressWarnings("unchecked")
    @Override
    public T convert(String source) {
    String[] split = source.split("-");
    if (Foo.class == targetType) {
    return (T) new Foo(split[0], Integer.valueOf(split[1]));
    } else if (Bar.class == targetType) {
    return (T) new Bar(split[0], Integer.valueOf(split[1]));
    }
    return null;
    }
    }
    }
  3. 注册到 Spring 中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration
    public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(new LocalDateTimeConverter());
    registry.addConverterFactory(new PersonConverter());
    }
    }

    FormatterRegistry 实际也是继承了 ConverterRegistry,ConversionService 是使用 ConverterRegistry 来注册转换器的。
    Converter 是通用元件,可以在应用程序的任意层使用。
    Formatter 是专用元件,专门为Web层设计。

Spring 中 conversionService 调用时机

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException {

// Instantiate the bean.
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
// 以下省略
...
}

doCreateBean 中有个 createBeanInstance 方法,点击查看

1
2
3
4
5
protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
// 以上省略
...
return instantiateBean(beanName, mbd);
}

重点在 instantiateBean 中在点进去查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected BeanWrapper instantiateBean(String beanName, RootBeanDefinition mbd) {
try {
Object beanInstance;
if (System.getSecurityManager() != null) {
beanInstance = AccessController.doPrivileged(
(PrivilegedAction<Object>) () -> getInstantiationStrategy().instantiate(mbd, beanName, this),
getAccessControlContext());
}
else {
beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, this);
}
BeanWrapper bw = new BeanWrapperImpl(beanInstance);
initBeanWrapper(bw);
return bw;
}
catch (Throwable ex) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex);
}
}

进入第 13 行的 initBeanWrapper(bw); 方法查看

1
2
3
4
protected void initBeanWrapper(BeanWrapper bw) {
bw.setConversionService(getConversionService());
registerCustomEditors(bw);
}

再次看到了熟悉的 getConversionService() 设置到 BeanWrapper 中

进入 registerCustomEditors(bw);

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
protected void registerCustomEditors(PropertyEditorRegistry registry) {
if (registry instanceof PropertyEditorRegistrySupport) {
((PropertyEditorRegistrySupport) registry).useConfigValueEditors();
}
if (!this.propertyEditorRegistrars.isEmpty()) {
for (PropertyEditorRegistrar registrar : this.propertyEditorRegistrars) {
try {
registrar.registerCustomEditors(registry);
}
catch (BeanCreationException ex) {
Throwable rootCause = ex.getMostSpecificCause();
if (rootCause instanceof BeanCurrentlyInCreationException) {
BeanCreationException bce = (BeanCreationException) rootCause;
String bceBeanName = bce.getBeanName();
if (bceBeanName != null && isCurrentlyInCreation(bceBeanName)) {
if (logger.isDebugEnabled()) {
logger.debug("PropertyEditorRegistrar [" + registrar.getClass().getName() +
"] failed because it tried to obtain currently created bean '" +
ex.getBeanName() + "': " + ex.getMessage());
}
onSuppressedException(ex);
continue;
}
}
throw ex;
}
}
}
if (!this.customEditors.isEmpty()) {
this.customEditors.forEach((requiredType, editorClass) ->
registry.registerCustomEditor(requiredType, BeanUtils.instantiateClass(editorClass)));
}
}

第 8 行 会把我们自定义的转换器注册进去。

测试

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
@RestController
@RequestMapping("/converter")
public class ConverterController {

@Autowired
ConversionService conversionService;

@GetMapping("/person/{foo}")
public Foo getFoo(@PathVariable Foo foo) {
System.out.println(foo);
return foo;
}

@GetMapping("/time")
public LocalDateTime getTime(@RequestParam LocalDateTime localDateTime) {
return localDateTime;
}

@GetMapping("")
public String getDemo() {
LocalDateTime time = conversionService.convert("2020-12-01 12:23:23", LocalDateTime.class);
Bar bar = conversionService.convert("bar-998", Bar.class);
System.out.println(time);
System.out.println(bar);
return "OK";
}
}

如果使用的是 Post 方式提交 还需要 jackson 的配置

spring 默认使用的序列化器是 jackson

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class MyConfig {

//项目全局的时间出参格式化
private static final String dateTimeFormat = "yyyy-MM-dd HH:mm:ss";

@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
return builder -> {
builder.simpleDateFormat(dateTimeFormat);
builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(dateTimeFormat)));
builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(dateTimeFormat)));
};
}
}

只配置了 String 转 LocalDateTime

这样就相当于是一个全局配置 不用在每个属性上加 @JsonFormat 注解了。

总结

本篇讲述了 Spring 中 ConversinService 作为集中类型转换器的使用与配置。并列举了如何自定义转换器(给出了2个 Demo),更多写法可以参考 DefaultConversionService 中的默认实现。

在结尾的时候讲述了 jackson 转LocalDateTime 的全局化配置。

看多过太多项目中在时间属性上加 @JsonFormat 注解,和还在使用 Date 作为时间类型了(JDK8 开始支持 LocalDateTime 和 LocalDate)

欢迎纠错与讨论,希望本篇对您有所帮助。