Spring框架基础能力-数据转换


Spring的数据转换器

众所周知,在书写Spring的配置文件或者前端请求后端时,我们所有配置项的值或参数值都是字符串的形式存在(上传文件的IO流也类似),根据一定的书写规则,Spring可以将这些原本为string类型的值赋值到对应的bean上或SpringMVC控制层的方法的实参上,这得益于Spring中强大的数据转换能力,下面盘点一波“Spring的数据转换器”。

Spring中的数据转换器主要分两大派系:

  • PropertyEditor(属性编辑器)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package java.beans;
    public interface PropertyEditor {
    void setValue(Object value);
    Object getValue();
    boolean isPaintable();
    void paintValue(java.awt.Graphics gfx, java.awt.Rectangle box);
    String getJavaInitializationString();
    String getAsText();
    void setAsText(String text) throws java.lang.IllegalArgumentException;
    String[] getTags();
    java.awt.Component getCustomEditor();
    boolean supportsCustomEditor();
    void addPropertyChangeListener(PropertyChangeListener listener);
    void removePropertyChangeListener(PropertyChangeListener listener);
    }

    PropertyEditor是JavaBean规范定义的接口,这是java.beans中一个接口,其设计方便对象与String之间的转换工作,而spring将其扩展,方便各种对象与String之间的转换工作。Spring所有的扩展都是通过继承PropertyEditorSupport,因为它只聚焦于转换上,所以只需复写setAsText()、getAsText()以及构造方法即可实现扩展。

    Spring 使用PropertyEditor的接口来实现对象和字符串之间的转换,比如将 2020-01-01转化为日期类型等,可以通过注册自定义编辑器来实现此功能。

    应用场景:

    • 在基于xml的配置中,我们往往通过字面值为Bean各种类型的属性提供设置值:如double、int类型,在配置文件配置字面值即可。Spring填充Bean属性时如何将这个字面值转换为对应的类型呢?我们可以隐约地感觉到一定有一个转换器在其中起作用,这个转换器就是属性编辑器。
    • 再者便是Spring MVC框架使用多种PropertyEditor分析绑定HTTP请求的各种参数
  • Converter(转换器)

    Spring的Converter可以将一种类型转换成另一种类型的一个对象

    1
    2
    3
    4
    5
    6
    package org.springframework.core.convert.converter;
    public interface Converter<S, T> {
    // 把S转成T
    @Nullable
    T convert(S source);
    }

    Spring提供了3种converter接口:

    • Converter接口 :使用最简单,最不灵活,1:1

    • ConverterFactory接口 :使用较复杂,比较灵活 1:N

      1
      2
      3
      4
      5
      package org.springframework.core.convert.converter;
      public interface ConverterFactory<S, R> {
      <T extends R> Converter<S, T> getConverter(Class<T> targetType);

      }
    • GenericConverter接口 :使用最复杂,也最灵活 N:N

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      package org.springframework.core.convert.converter;
      public interface GenericConverter {
      @Nullable
      Set<ConvertiblePair> getConvertibleTypes();
      @Nullable
      Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
      final class ConvertiblePair {
      // ...
      }
      }

既然有了PropertyEditor,那为何还需要有Converter呢?因为Java原生的PropertyEditor存在以下两点不足:

  1. 只能用于字符串和Java对象的转换,不适用于任意两个Java类型之间的转换;
  2. 对源对象及目标对象所在的上下文信息(如注解、所在宿主类的结构等)不敏感,在类型转换时不能利用这些上下文信息实施高级转换逻辑。

鉴于此,Spring 3.0在核心模型中添加了一个通用的类型转换模块。Spring希望用这个类型转换体系替换Java标准的PropertyEditor。但由于历史原因,Spring将同时支持两者。在Bean配置、Spring MVC处理方法入参绑定中使用它们。

**注:如今SpringBoot是开发首先,本文所列罗的源码均来自于SpringBoot 2.3.7.RELEASE **

PropertyEditor属性编辑器

PropertyEditor在Bean配置上的使用

以字符串转换为自定义对象为需求;

定义一个Student实体类

1
2
3
4
5
6
7
8
9
10
@NoArgsConstructor
@AllArgsConstructor
@Data
@ToString
public class Student {

@Max(100)
private Integer id;
private String name;
}

定义一个Student的属性编辑器,继承PropertyEditorSupport以实现PropertyEditor接口

1
2
3
4
5
6
7
8
9
10
11
12
/** 把“(1001,张三)” 转换成 {@link Student}*/
public class StudentEditor extends PropertyEditorSupport {
/**
* @param text 这里进来的字符串不会是空的
* @throws IllegalArgumentException
*/
@Override
public void setAsText(String text) throws IllegalArgumentException {
// 解析字符串过程...
this.setValue(new Student(1001,"张三")); // 关键的setValue方法
}
}

注入一个属性编辑器的配置

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
@Configuration
public class PropertyEditorConfig {

private static Map<Class<?>, Class<? extends PropertyEditor>> customEditors;

/**
* <p>
* 注册属性编辑器,使IOC创建bean时拥有string类型转为目标类型的能力
* </p>
* <p>
* 注意SpringMVC进行参数绑定时是无法利用此能力的,这是{@link org.springframework.beans.factory.BeanFactory}的能力,SpringMVC上使用需要额外注册
* </p>
*
* {@link CustomEditorConfigurer}实现了{@link org.springframework.beans.factory.config.BeanFactoryPostProcessor}接口,设置为静态方法以提高优先级
* @return {@link CustomEditorConfigurer}
* @see ws.spring.convert.controller.CustomControllerAdvice#initBinder(WebDataBinder) SpringMVC在参数绑定期间注册属性编辑器实例
*/
@Bean
public static CustomEditorConfigurer customEditorConfigurer() {

CustomEditorConfigurer configurer = new CustomEditorConfigurer();
configurer.setCustomEditors(getPropertyEditors());
return configurer;
}

/**
* 缓存全部的属性编辑器
* @return
*/
public static Map<Class<?>, Class<? extends PropertyEditor>> getPropertyEditors() {

if (customEditors == null) {

synchronized (PropertyEditorConfig.class) {
if (customEditors == null) {

customEditors = new HashMap<>(4/3 + 1);
customEditors.put(Student.class, StudentEditor.class);
}
}
}
return customEditors;
}
}

自定义bean组件需要注入Student类的属性,支持校验

1
2
3
4
5
6
7
8
9
10
@ConfigurationProperties("custom.bean")
@Component
@Slf4j
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class CustomBean {
private Student student;
}

配置文件

1
2
3
custom:
bean:
student: (81,张三)

疑点:Spring如何使用到了我们注册的PropertyEditor?

因为CustomEditorConfigurer实现了BeanFactoryPostProcessor接口,往beanFactory注册了我们的PropertyEditor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.springframework.beans.factory.config;
public class CustomEditorConfigurer implements BeanFactoryPostProcessor, Ordered {
// ...
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
if (this.propertyEditorRegistrars != null) {
for (PropertyEditorRegistrar propertyEditorRegistrar : this.propertyEditorRegistrars) {
beanFactory.addPropertyEditorRegistrar(propertyEditorRegistrar);
}
}
if (this.customEditors != null) {
this.customEditors.forEach(beanFactory::registerCustomEditor); // 注册 PropertyEditor
}
}
}

PropertyEditor在MVC参数绑定上的使用

首先要清楚一个概念,MVC的参数绑定看起来很像bean配置过程,基本也是从字符串到java对象的转换,但是前者是MVC模块的功能,后者是beanFactory的能力,MVC只是Spring体系中的一员,IOC中beanFactory才是整个Spring体系的核心。所以数据转换这样的基础功能,MVC的参数绑定是不能使用beanFactory的转换能力的,因为参数绑定过程不是bean的创建过程,创建的对象不是SpringBean。所以数据转换的功能在MVC模块是需要注册进去才有的,即一次编写,多处注册。

先定义一个控制层的增强

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
@Slf4j
@RestControllerAdvice(assignableTypes = {PropertyEditorController.class})
public class CustomControllerAdvice {
@InitBinder
public void initBinder(WebDataBinder binder) {
// 注册属性编辑器
registerCustomEditor(binder);
}

private void registerCustomEditor(WebDataBinder binder) {

/*
此种绑定方式看似很好,先找到对应的属性编辑器,再进行注册,但是此时 target 为 null,不知道 target的类型,也就无法“对症下药”

Object target = binder.getTarget();
if (target == null) {
return;
}
// 注册对应的属性编辑器到当前的绑定器,如果存在的话
Class<?> entityClass = target.getClass();
Class<? extends PropertyEditor> entityPropertyEditorClass = PropertyEditorConfig.getPropertyEditors().get(entityClass);
if (entityPropertyEditorClass != null) {

registerCustomEditor(binder,entityClass,entityPropertyEditorClass);
}
*/
// 注册全部的属性编辑器到当前的绑定器(比较退而求其次的做法)
PropertyEditorConfig.getPropertyEditors()
.forEach((entityClass,entityPropertyEditorClass) -> registerCustomEditor(binder,entityClass,entityPropertyEditorClass));
}

private void registerCustomEditor(WebDataBinder binder,Class<?> entityClass, Class<? extends PropertyEditor> entityPropertyEditorClass) {

try {
Constructor<? extends PropertyEditor> constructor = entityPropertyEditorClass.getConstructor();
PropertyEditor editor = constructor.newInstance();
binder.registerCustomEditor(entityClass,editor);
} catch (ReflectiveOperationException e) {
log.error("属性编辑器<{}>没有无参构造方法",entityPropertyEditorClass.getTypeName(),e);
throw new UnsupportedOperationException(e);
}
}
}

定义控制层

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
@Slf4j
@RestController
public class PropertyEditorController {

/**
* get请求string参数转{@link Student},使用{@link RequestParam}指定参数名
* <p>但是无法进行数据校验
* @param student Get请求的String参数
* @return String
*/
@GetMapping("/property-editor-assign")
public String stringToStudentAssign(@Validated @RequestParam("student") Student student) {

log.info("student: {}",student);
return String.valueOf(student);
}

/**
* get请求string参数转{@link Student},不指定参数名
* @param student Get请求的String参数
* @return String
* @deprecated 无法从Get请求的String参数映射
*/
@GetMapping("/property-editor-non-assign")
@Deprecated
public String stringToStudentNonAssign(@Validated Student student) {

log.info("student: {}",student);
return String.valueOf(student);
}
}

总结

PropertyEditor是线程不安全的,一个实例对应一次String转换操作,而且在IOC启动时的bean配置和MVC参数绑定功能上需要各自注册,且MVC参数绑定增强时不能获取参数类型进行按需注册,退而求其次的做法是全部注册可能的PropertyEditor。

Converter转换器

Converter在Bean配置上的使用

在此之前我们需要了解一个新的接口ConversionService

1
2
3
4
5
6
7
8
9
package org.springframework.core.convert;
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);
}

顾名思义就是Converter的服务,这个接口通过管理ConverterConverterFactoryGenericConverter统一对外提供转换服务,所以Spring的的Bean转换操作使用的是ConversionService,贴上ConfigurableBeanFactory的接口声明

1
2
3
4
5
6
7
8
9
10
package org.springframework.beans.factory.config;
public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {
/**
* Specify a Spring 3.0 ConversionService to use for converting
* property values, as an alternative to JavaBeans PropertyEditors.
* @since 3.0
*/
void setConversionService(@Nullable ConversionService conversionService);
// ...
}

再看看使用Converter如何实现以字符串转换为自定义对象的需求;

定义实体类Town

1
2
3
4
5
6
7
8
9
10
@NoArgsConstructor
@AllArgsConstructor
@Data
@ToString
public class Town {

@Max(100)
private Integer code;
private String name;
}

定义转换器且加入IOC

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class TownConverter implements Converter<String, Town> {

/**
* 将 "100-南京" 转换成{@link Town}对象
*/
@Override
public Town convert(String source) throws IllegalArgumentException {
// 解析字符串过程...
return new Town(100,"南京");
}
}

往IOC中注入配置类

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

/**
* bean名称必须叫 "conversionService"
*/
@Bean
public ConversionServiceFactoryBean conversionService(@Autowired TownConverter townConverter) {

ConversionServiceFactoryBean conversionService = new ConversionServiceFactoryBean();
Set<Converter> converters = new HashSet<>();
converters.add(townConverter);
// add other
conversionService.setConverters(converters);
return conversionService;
}
}

ConversionServiceFactoryBean类是一个工厂bean

1
2
3
4
package org.springframework.context.support;
public class ConversionServiceFactoryBean implements FactoryBean<ConversionService>, InitializingBean {
// ...
}

自定义bean组件需要注入Town类的属性,支持校验

1
2
3
4
5
6
7
8
9
10
11
12
13
@Validated
@ConfigurationProperties("custom.bean")
@Component
@Slf4j
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class CustomBean {

@Valid
private Town town;
}

配置文件

1
2
3
custom:
bean:
town: 100-南京

疑点:为什么我们注册一个到名为”conversionService”类型为ConversionService的bean,Spring IOC容器在bean配置时就可以使用这个conversionService来完成属性的数据转换呢?

答案在ConfigurableApplicationContextAbstractApplicationContext的源码里

1
2
3
4
5
6
7
8
9
10
11
package org.springframework.context;
public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {
/**
* Name of the ConversionService bean in the factory.
* If none is supplied, default conversion rules apply.
* @since 3.0
* @see org.springframework.core.convert.ConversionService
*/
String CONVERSION_SERVICE_BEAN_NAME = "conversionService";
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package org.springframework.context.support;
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {
// ...
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));
}
// ...
}
}

AbstractApplicationContext在完成beanFactory的初始化工作时,会从beanFactory中获取名为”conversionService”类型为ConversionService的bean,将其作为后续beanFactory的转换服务。

Converter在MVC参数绑定上的使用

前面我们已经把TownConverter加入IOC中了,在Controller上我们可以直接享受其转换能力。

定义控制层

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
@Slf4j
@RestController
public class ConvertController {

/**
* get请求string参数转{@link Town},使用{@link RequestParam}指定参数名
* <p>/converter/assign?town=1001-风雷镇
* <p>但是无法进行数据校验,见{@link CustomControllerAdvice#initBinder(WebDataBinder)}
* @param town Get请求的String参数
* @return String
* @see ws.spring.convert.converter.TownConverter 加入IOC之后控制层直接可以进行String到{@link Town}的转换
*/
@GetMapping("/converter/assign")
public String stringToTown(@RequestParam("town") @Validated Town town) {

log.info("town: {}",town);
return String.valueOf(town);
}

/**
* get请求string参数转{@link Town},不指定参数名
* <p>/converter/non-assign?town=1001-风雷镇
* <p>可以进行数据校验
*
* @param town Get请求的String参数
* @return String
*/
@GetMapping("/converter/non-assign")
public String stringToTown2(@Validated Town town) {

log.info("town: {}",town);
return String.valueOf(town);
}
}

疑点:为什么MVC的参数绑定可以直接使用我们加入到IOC的Converter bean的能力而不需要像PropertyEditor那样额外注册呢?

解答:

首先我们知道WebMvcConfigurer接口是MVC模块的配置接口,其中有一个addFormatters方法,我们可以通过FormatterRegistry注册器注册我们的Formatter(见下文)、Converter等。

1
2
3
4
5
6
package org.springframework.web.servlet.config.annotation;
public interface WebMvcConfigurer {
// ..
default void addFormatters(FormatterRegistry registry) {
}
}

FormatterRegistry继承了ConverterRegistry接口

1
2
3
4
package org.springframework.format;
public interface FormatterRegistry extends ConverterRegistry {
// ..
}

ConverterRegistry接口可以注册Converter等

1
2
3
4
5
6
7
8
9
package org.springframework.core.convert.converter;
public interface ConverterRegistry {

void addConverter(Converter<?, ?> converter);
<S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter);
void addConverter(GenericConverter converter);
void addConverterFactory(ConverterFactory<?, ?> factory);
void removeConvertible(Class<?> sourceType, Class<?> targetType);
}

切入点:

在MVC的自动装配类WebMvcAutoConfiguration中,可以看到这样一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.springframework.boot.autoconfigure.web.servlet;
// ...
public class WebMvcAutoConfiguration {
//...
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
// ...
@Override
public void addFormatters(FormatterRegistry registry) {
ApplicationConversionService.addBeans(registry, this.beanFactory);
}
}
]

WebMvcAutoConfigurationAdapter实现WebMvcConfigurer接口注册到IOC中,并调用ApplicationConversionService.addBeans(registry, this.beanFactory);方法注册了一些bean

继续跟源码 ApplicationConversionService.addBeans(registry, this.beanFactory);可以看到beanFactory中的Converter bean被注册到FormatterRegistry中。

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
package org.springframework.boot.convert;
public class ApplicationConversionService extends FormattingConversionService {
/**
* Add {@link GenericConverter}, {@link Converter}, {@link Printer}, {@link Parser}
* and {@link Formatter} beans from the specified context.
* @param registry the service to register beans with
* @param beanFactory the bean factory to get the beans from
* @since 2.2.0
*/
public static void addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory) {
Set<Object> beans = new LinkedHashSet<>();
beans.addAll(beanFactory.getBeansOfType(GenericConverter.class).values());
beans.addAll(beanFactory.getBeansOfType(Converter.class).values());
beans.addAll(beanFactory.getBeansOfType(Printer.class).values());
beans.addAll(beanFactory.getBeansOfType(Parser.class).values());
for (Object bean : beans) {
if (bean instanceof GenericConverter) {
registry.addConverter((GenericConverter) bean);
}
else if (bean instanceof Converter) {
registry.addConverter((Converter<?, ?>) bean);
}
else if (bean instanceof Formatter) {
registry.addFormatter((Formatter<?>) bean);
}
else if (bean instanceof Printer) {
registry.addPrinter((Printer<?>) bean);
}
else if (bean instanceof Parser) {
registry.addParser((Parser<?>) bean);
}
}
}
// ...
}

同时WebMvcAutoConfigurationAdapter还引入了EnableWebMvcConfiguration配置类

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
package org.springframework.boot.autoconfigure.web.servlet;
// ...
public class WebMvcAutoConfiguration {
//...
@Configuration(proxyBeanMethods = false)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
//...

/// 注入 mvcConversionService,完成 RequestMappingHandlerMapping 的配置
@Bean
@Primary
@Override
public RequestMappingHandlerMapping requestMappingHandlerMapping(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
// Must be @Primary for MvcUriComponentsBuilder to work
return super.requestMappingHandlerMapping(contentNegotiationManager, conversionService,
resourceUrlProvider);
}
// ...

/// 声明bean mvcConversionService
@Bean
@Override
public FormattingConversionService mvcConversionService() {
Format format = this.mvcProperties.getFormat();
WebConversionService conversionService = new WebConversionService(new DateTimeFormatters()
.dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
addFormatters(conversionService);
return conversionService;
}
// ...
}
]

不难发现,MVC的数据转换服务是由名为”mvcConversionService”的ConversionService完成的,看看mvcConversionService这个bean的创建做了哪些事。

WebConversionService源码

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.springframework.boot.autoconfigure.web.format;
public class WebConversionService extends DefaultFormattingConversionService {

public WebConversionService(DateTimeFormatters dateTimeFormatters) {/// 构造器
super(false); /// 父类构造
if (dateTimeFormatters.isCustomized()) {
addFormatters(dateTimeFormatters);
}
else {
addDefaultFormatters(this);
}
}
}

DefaultFormattingConversionService构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.springframework.format.support;
public class DefaultFormattingConversionService extends FormattingConversionService {

public DefaultFormattingConversionService() {
this(null, true);
}
public DefaultFormattingConversionService(boolean registerDefaultFormatters) {
this(null, registerDefaultFormatters);
}
public DefaultFormattingConversionService(
@Nullable StringValueResolver embeddedValueResolver, boolean registerDefaultFormatters) {

if (embeddedValueResolver != null) {
setEmbeddedValueResolver(embeddedValueResolver);
}
DefaultConversionService.addDefaultConverters(this); /// 添加默认的 Converter
if (registerDefaultFormatters) {
addDefaultFormatters(this);
}
}
// ...
}

小结,EnableWebMvcConfiguration配置类提供了MVC的默认配置,并添加注册了框架默认的Converter 到mvcConversionService,利用mvcConversionService完成 RequestMappingHandlerMapping 的配置

如图:

总结

Converter加入IOC可以被MVC管理,从而在参数绑定上使用其能力,但是要注意controller方法写法的区别。在Bean配置上使用Converter则需要手动注册到名为”conversionService”类型为ConversionService的配置bean中,IOC在bean配置时才能享受到其能力。

Formatter

数据转换中还有一个特殊的接口,Formatter

1
2
3
package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
1
2
3
4
package org.springframework.format;
public interface Printer<T> {
String print(T object, Locale locale);
}
1
2
3
4
package org.springframework.format;
public interface Parser<T> {
T parse(String text, Locale locale) throws ParseException;
}

容易想到格式化器是java对象和String之间的转换功能在不同地区语言上的加强,所以它是在MVC控制层使用的,根据不同地区信息进行数据转换。

Formatter数据转换

定义实体类User

1
2
3
4
5
6
7
8
9
10
11
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {

private Long id;
private String name;
@Email
private String email;
}

定义格式化器,加入IOC

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
/** 实现 id-name-email 到 User的互转,此处为了演示忽略 locale */
@Slf4j
@Component
public class UserFormatter implements Formatter<User> {

/** like: "id-name-email" */
private static final Pattern USER_STRING_PATTERN = Pattern.compile("[0-9]{1,}-[a-zA-Z\\u4e00-\\u9fa5]{1,}-\\S*");

@Override
public User parse(String text, Locale locale) throws ParseException {

log.info("parse: {}", text);
if (!USER_STRING_PATTERN.matcher(text).matches()) {
throw new ParseException("The value [" + text + "] is not matcher format <id-name-email>",0);
}
String[] fields = text.split("-");
return new User(Long.valueOf(fields[0]),fields[1],fields[2]);
}

@Override
public String print(User user, Locale locale) {

log.info("print: {}", user);
return user.getId() + "-" + user.getName() + "-" + user.getEmail();
}
}

定义controller

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
@Slf4j
@Controller
@RequestMapping("/formatter")
public class FormatterController {

//-------------------------------
// Formatter 的转换器效果
//-------------------------------
/**
* Get参数(String类型)直接解析为{@link User},指定参数名
* <p>/formatter/user-request-query-assign?user=100-tom-123qq.com
* <p>但无法进行{@link Validated}校验
* @param user user
* @return String
* @see ws.spring.convert.formatter.UserFormatter#parse(String, Locale)
*/
@GetMapping("/user-request-query-assign")
@ResponseBody
public String formatUserWhenQueryParamAssign(@Validated @RequestParam("user") User user) {

log.info("user: {}",user);
return String.valueOf(user);
}

/**
* Get参数(String类型)直接解析为{@link User},不指定参数名
* <p>/formatter/user-request-query-non-assign?user=100-tom-123qq.com
* <p>可以进行{@link Validated}校验
* @param user user
* @return String
* @see ws.spring.convert.formatter.UserFormatter#parse(String, Locale)
*/
@GetMapping("/user-request-query-non-assign")
@ResponseBody
public String formatUserWhenQueryParamNonAssign(@Validated User user) {

log.info("user: {}",user);
return String.valueOf(user);
}
}

Formatter view视图格式化

在MVC后端视图渲染时,我们可以通过格式化来指定数据在视图中的呈现内容,比如有一个pojo

1
2
3
4
5
6
7
8
9
10
11
12
@NoArgsConstructor
@AllArgsConstructor
@Data
@ToString
public class NumberWrapper {

@NumberSeparate
private Integer code;
@NumberSeparate('=')
@Max(100)
private Long number;
}

我们希望,视图中使用NumberWrapper的code属性时,将code数值进行单个拆分,如”123” -> “1-2-3”,将number也进行拆分,并指定分隔符为”=”,如如”123” -> “1=2=3”,分隔符通过NumberSeparate注解指定。

在controller中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
@Controller
@RequestMapping("/formatter")
public class FormatterController {
/**
* 在jsp视图中格式化
* @param model
* @return
*/
@RequestMapping("/number-print")
public String numberWrapperPrint(Model model) {

NumberWrapper wrapper = new NumberWrapper(123,456L);
log.info("wrapper: {}",wrapper);
model.addAttribute("wrapper",wrapper);
return "show";
}
}

show.jsp内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ taglib prefix="from" uri="http://www.springframework.org/tags/form" %>
<html>
<head>
<title>welcome</title>
<meta charset="UTF-8">
</head>
<body>
<form:form modelAttribute="wrapper">
<%-- 1-2-3 --%>
<h6><from:input path="code"/></h6>
<%-- 4=5=6 --%>
<h6><from:input path="number"/></h6>
</form:form>
<H1>JSP</H1>
</body>
</html>

效果

代码实现

定义注解

1
2
3
4
5
6
7
8
9
10
11
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NumberSeparate {

@AliasFor("value")
char separator() default '-';

@AliasFor("separator")
char value() default '-';
}

实现一个注解格式化工厂AnnotationFormatterFactory,且加入IOC容器

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
71
72
73
@Component
public class NumberSeparateAnnotationFormatterFactory implements AnnotationFormatterFactory<NumberSeparate> {

/**
* @return 支持的类型
*/
@Override
public Set<Class<?>> getFieldTypes() {

return new HashSet<>(Arrays.asList(Integer.class,Long.class));
}

@Override
public Printer<?> getPrinter(NumberSeparate annotation, Class<?> fieldType) {

char separator = annotation.separator();
return getFormatter(separator,fieldType);
}

@Override
public Parser<?> getParser(NumberSeparate annotation, Class<?> fieldType) {

char separator = annotation.separator();
return getFormatter(separator,fieldType);
}

private static Formatter<?> getFormatter(char separator, Class<?> clazz) {

if (Integer.class.equals(clazz)) {

return new NumberSeparateFormatter<>(separator, Integer::valueOf);
} else if (Long.class.equals(clazz)) {

return new NumberSeparateFormatter<>(separator, Long::valueOf);
} else {

throw new IllegalArgumentException("不支持的类型");
}
}
/** 实现格式化器完成 "123" -> “1-2-3” 的互转,继承Formatter以便同时实现 Printer 和 Parser */
private static class NumberSeparateFormatter<T extends Number> implements Formatter<T> {

private final char separator;
private final Function<String,T> converter;

private NumberSeparateFormatter(char separator, Function<String,T> converter) {
this.separator = separator;
this.converter = converter;
}

@Override
public String print(T object, Locale locale) {

char[] chars = object.toString().toCharArray();
StringBuilder sb = new StringBuilder();
for (char aChar : chars) {
sb.append(aChar).append(separator);
}
return sb.substring(0,sb.length() - 1);
}

@Override
public T parse(String text, Locale locale) throws ParseException {

String replace = text.replace(String.valueOf(separator), "");
try {
return converter.apply(replace);
} catch (NumberFormatException e) {
throw new ParseException("格式错误",0);
}
}
}
}

这样就可以在view视图上使用Formatter 的格式化能力

同时在参数绑定上也可以完成字符串到java对象转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j
@Controller
@RequestMapping("/formatter")
public class FormatterController {
/**
* 在参数绑定时解析参数到注解上的属性
* <p> /formatter/number-parse?code=1-2-3&number=4=5=6 即 /formatter/number-parse?code=1-2-3&number=4%3D5%3D6
* <p> 但是不经过参数校验,{@link NumberWrapper#number}上的注解不会得到校验
* @param wrapper wrapper
* @return String
*/
@GetMapping("/number-parse")
@ResponseBody
public String numberWrapperParse(NumberWrapper wrapper) {

log.info("wrapper: {}",wrapper);
return String.valueOf(wrapper);
}
}

疑点:为什么Formatter在MVC参数绑定上拥有和Converter一样的效果?

还记得MVC的自动装配中,名为”mvcConversionService”类型为 WebConversionService 的bean吗,其继承关系如图,看的出其继承FormattingConversionService实现了ConversionService 接口

FormattingConversionService在注册Formatterr时,通过内部类PrinterConverterParserConverter进行封装,实际注册的是它俩,所以MVC才拥有了数据转换的能力,故表面上看Formatter拥有和Converter一样的效果,源码如下

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
package org.springframework.format.support;
public class FormattingConversionService extends GenericConversionService implements FormatterRegistry, EmbeddedValueResolverAware {
/** 添加 Printer */
@Override
public void addPrinter(Printer<?> printer) {
Class<?> fieldType = getFieldType(printer, Printer.class);
addConverter(new PrinterConverter(fieldType, printer, this));
}
/** 添加 Parser */
@Override
public void addParser(Parser<?> parser) {
Class<?> fieldType = getFieldType(parser, Parser.class);
addConverter(new ParserConverter(fieldType, parser, this));
}
/** 添加 Formatter */
@Override
public void addFormatter(Formatter<?> formatter) {
addFormatterForFieldType(getFieldType(formatter), formatter);
}

@Override
public void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter) {
addConverter(new PrinterConverter(fieldType, formatter, this));
addConverter(new ParserConverter(fieldType, formatter, this));
}

@Override
public void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser) {
addConverter(new PrinterConverter(fieldType, printer, this));
addConverter(new ParserConverter(fieldType, parser, this));
}

// ...

/** 封装 Printer 实现 GenericConverter */
private static class PrinterConverter implements GenericConverter {
private final Class<?> fieldType;
private final TypeDescriptor printerObjectType;
@SuppressWarnings("rawtypes")
private final Printer printer;
private final ConversionService conversionService;
// ...
}

/** 封装 Parser 实现 GenericConverter */
private static class ParserConverter implements GenericConverter {
private final Class<?> fieldType;
private final Parser<?> parser;
private final ConversionService conversionService;
// ...
}
}

总结

Formatter用于数据对不同地区的语言信息进行格式化,如MVC视图的数据呈现格式化,同时因为FormattingConversionService内部对Formatter进行了增强,所以Formatter有了数据转换的能力,而且可以根据语言信息进行不同的转换。