Spring MVC学习(6)—Spring数据类型转换机制全解【一万字】

2,508 阅读45分钟

「这是我参与2022首次更文挑战的第18天,活动详情查看:2022首次更文挑战」。

基于最新Spring 5.x,详细介绍了Spring的类型转换机制,包括三种最常见的数据类型转换器PropertyEditor、Formatter、Converter、HttpMessageConverter、ConversionService等核心类。

在使用Spring以及使用Spring MVC的时候,Spring会通过一系列的类型转换机制将参数转换为我们指定的类型,这种转换对于使用者来说通常是无感的,我们只需要使用指定的类型接收即可!

下面我们来详细的了解Spring的类型转换机制,包括三种最常见的数据类型转换器PropertyEditor、Formatter、Converter,以及ConversionService等核心类。

Spring MVC学习 系列文章

Spring MVC学习(1)—MVC的介绍以及Spring MVC的入门案例

Spring MVC学习(2)—Spring MVC中容器的层次结构以及父子容器的概念

Spring MVC学习(3)—Spring MVC中的核心组件以及请求的执行流程

Spring MVC学习(4)—ViewSolvsolver视图解析器的详细介绍与使用案例

Spring MVC学习(5)—基于注解的Controller控制器的配置全解【一万字】

Spring MVC学习(6)—Spring数据类型转换机制全解【一万字】

Spring MVC学习(7)—Validation基于注解的声明式数据校验机制全解【一万字】

Spring MVC学习(8)—HandlerInterceptor处理器拦截器机制全解

Spring MVC学习(9)—项目统一异常处理机制详解与使用案例

Spring MVC学习(10)—文件上传配置、DispatcherServlet的路径配置、请求和响应内容编码

Spring MVC学习(11)—跨域的介绍以及使用CORS解决跨域问题

@[toc]

1 Spring类型转换机制概述

BeanWrapper是一个Spring的内部体系,主要用于在创建bean实例之后填充bean的依赖,我们在此前Spring IOC的源码的部分已经讲过了,BeanWrapper对于大部分开者这来说都是无感知的,被Spring内部使用,属于一个底层的对象。

Spring中的类型转换主要发生在两个地方:

  1. Spring创建bean实例时,在底层的BeanWrapper中注入Bean的属性依赖的时候,如果对于找到的依赖类型(给定的常量值或者找到依赖对象)如果不符合属性的具体类型,那么需要转换为对应的属性类型;
  2. Spring MVC中,在执行处理器方法之前,可能需要把HTTP请求的数据通过DataBinder绑定到控制器方法的给定参数上,然而HTTP参数到后端时都是String类型,而方法参数可能是各种类型,这就可能涉及到从String到给定类型的转换。

Spring提供了三种最常见的数据类型转换器PropertyEditor、Formatter、Converter,无论是Spring MVC的 DataBinder和底层的BeanWrapper都支持使用这些转换器进行数据转换:

  1. PropertyEditor是JDK自带的类型转换接口。主要用于实现String到其他类型的转换。Spring已经提供了用于常见类型转换的PropertyEditor实现。
  2. Formatter是Spring 3.0时提供的接口,只能转换String到其他类型,支持SPI机制。通常对于Spring MVC的参数绑定时的类型转换使用Formatter就可以了。Spring已经提供了用于常见类型转换的Formatter实现。
  3. Converter是Spring 3.0时提供的接口,可以提供从一个对象类型到另一个对象类型的转换,支持SPI机制,当需要实现通用类型转换逻辑时应该使用Converter。Spring已经提供了用于常见类型转换的Converter实现。

2 PropertyEditor

2.1 PropertyEditor的概述

在最开始,Spring 使用PropertyEditor(属性编辑器)的概念来实现对象和字符串之间的转换,PropertyEditor接口并非来自于Spring,而是来自Java的rt.jar核心依赖包,是JDK自带的。属性编辑器的作用我们在此前就说过了:

  1. 在Spring创建bean时将数据转换为bean的属性对应的类型。
  2. 在 Spring MVC 框架中分析、转换HTTP 请求参数。

PropertyEditor接口的常见方法如下:

public interface PropertyEditor {

    /**
     * 设置(或更改)要编辑的对象。原始类型(如"int")必须包装为相应的对象类型,如"java.lang.integer"
     *
     * @param value 要编辑的新目标对象。属性编辑器不应修改此对象,而属性编辑器应创建一个新对象来保存任何修改的值
     */
    void setValue(Object value);

    /**
     * 获取属性值。
     *
     * @return 属性的值。原始类型(如"int")将包装为相应的对象类型,如"java.lang.integer"
     */
    Object getValue();

    /**
     * 为属性提供一个表示初始值的字符串,属性编辑器以此值作为属性的默认值
     */
    String getJavaInitializationString();

    /**
     * 获取属性值的可编辑的字符串表示形式
     *
     * @return 如果值不能表示为可编辑字符串,则返回 null。如果返回非 null 值,则属性编辑器应准备好在 setAsText()中分析该字符串
     */
    String getAsText();

    /**
     * 通过分析给定字符串来设置属性值。如果字符串格式错误或此类属性不能以文本形式表示,可能会引发 java.lang.IllegalArgumentException
     *
     * @param text 要解析的字符串。
     */
    void setAsText(String text) throws java.lang.IllegalArgumentException;
}

虽然经常看见有文章说PropertyEditor仅用于支持从String到对象的转换。但是实际上在目前的Spring版本中,早已支持通过PropertyEditor实现从对象到对象的转换,典型的实现就是来自于spring-data-redis的各种PropertyEditor实现,比如ValueOperationsEditor,我们可以直接依赖ValueOperations,并且对其注入redisTemplate,Spring 在检查到类型不一致时,最终会在ValueOperationsEditor中通过注入的redisTemplate获取ValueOperations并返回。

支持从对象转换为对象的核心方法就是PropertyEditor#setValue方法。 在这里插入图片描述

2.2 内置的PropertyEditor

尽管现在如果在需要自定义转换器时,PropertyEditor被推荐使用Converter替代,但是我们仍然能够配置并且正常使用自定义的PropertyEditor,并且Spring内部就是用了很多默认PropertyEditor。

Spring 拥有许多内置的PropertyEditor实现。它们都位于 org.springframework.beans.propertyeditors包中。默认情况下,大多数(但不包括全部)由 BeanWrapperImpl来注册(位于AbstractBeanFactory#initBeanWrapper方法中,注册之后被BeanWrapper用于创建和填充Bean实例的类型转换)。很多默认属性编辑器实现也都是可配置的,比如CustomDateEditor,就可以指定日期模式

下表列出了Spring提供的常见PropertyEditor:

类型描述
ByteArrayPropertyEditor字节数组的编辑器。将String转换为相应的byte[]表示形式。默认情况下由 BeanWrapperImpl 注册。
ClassEditor支持表示类的字符串String解析与实际Class的相互转换。当找不到类时,将抛出IllegalArgumentException。默认情况下,由BeanWrapperImpl注册。
CustomBooleanEditorboolean的可自定义属性编辑器,将指定的String解析为boolean值。默认情况下,由 BeanWrapperImpl 注册,但可以通过将其自定义实例注册为自定义编辑器来覆盖。
CustomCollectionEditor集合的可自定义属性编辑器,将任何源字符串或者集合转换为给定的目标集合类型。默认情况下,由 BeanWrapperImpl 注册,但可以通过将其自定义实例注册为自定义编辑器来覆盖。
CustomDateEditorjava.util.Date 的可自定义属性编辑器,支持自定义 DateFormat。默认情况下未注册。必须由用户根据需要使用适当的格式进行手动注册。
CustomNumberEditor任何Number的子类(如Integer、 Long、 Float或 Double)的可自定义属性编辑器。默认情况下,由BeanWrapperImpl注册,但可以通过将其自定义实例注册为自定义编辑器来覆盖。
FileEditor将字符串解析为 java.io.File 对象。默认情况下,由 BeanWrapperImpl 注册。
InputStreamEditor将一个String通过中间的ResourceEditor和Resource生成一个InputStream。默认用法不会关闭inputstream。默认情况下,由BeanWrapperImpl注册。
LocaleEditor可以实现String和Locale对象的相互转换,字符串格式为[国家/地区],与Locale的 toString()方法相同)。默认情况下,由 BeanWrapperImpl 注册。
PatternEditor可以实现String和java.util.regex.Pattern对象的相互转换
PropertiesEditor可以将字符串转换为Properties对象(使用java.util.Properties类的javadoc中定义的格式格式化)。默认情况下,由BeanWrapperImpl注册。
StringTrimmerEditor修剪字符串的属性编辑器,还可以(可选)将空字符串转换为null值。默认情况下未注册,必须是用户手动注册的。
URLEditor可以将URL字符串解析为实际 URL 对象。默认情况下,由 BeanWrapperImpl 注册。

BeanWrapperImpl自动注册的PropertyEditor位于PropertyEditorRegistrySupport#createDefaultEditors方法中,并且是在需要转类型但是其他自定义转换器中无法找到合适的转换器的时候才会注册的(convertIfNecessary方法内的findDefaultEditor方法),属于延迟初始化!

2.3 PropertyEditorManager

Spring使用java.beans.PropertyEditorManager来注册、搜索可能的需要的PropertyEditor。搜索路径还包括rt.jar包中的sun.bean.editors,其中包括用于Font、Color和大多数基本类型的PropertyEditor实现。

另外,如果某个类型的类型转换器与该类型的Class位于同一个包路径,并且命名为ClassName+Editor,那么当需要转换为该类型时,将会自动发现该转换器,而无需手动注册,如下实例:

com
  chank
    pop
      Something         // Something类
      SomethingEditor // 用于Something类的类型转换,将会自动发现

一个管理默认属性编辑器的管理器:PropertyEditorManager,该管理器内保存着一些常见类型的属性编辑器, 如果某个JavaBean的常见类型属性没有通过BeanInfo显式指定属性编辑器,IDE将自动使用PropertyEditorManager中注册的对应默认属性编辑器。

实际上我们前面说的spring-data-redis的各种PropertyEditor实现就是采用的这个机制取发现的,它们并没有手动注册:

在这里插入图片描述

当然,我们也可以使用标准的BeanInfo JavaBeans机制显式的指定某个类与某个属性编辑器的关系,如下实例:

com
  chank
    pop
      Something
      SomethingBeanInfo

2.4 注册自定义PropertyEditor

Spring 预注册了许多自定义属性编辑器实现(例如,将表示为字符串的类名转换为Class对象)。此外,Java 的标准 JavaBeans PropertyEditor查找机制允许对类的PropertyEditor进行适当命名,并放置在与它所支持的类相同的包中,以便可以自动找到它(上面讲过了)。

Spring提供了PropertyEditor的一个核心实现类PropertyEditorSupport,如果我们要编写自定义的属性编辑器,只需要继承这个类即可,PropertyEditorSupport实现了PropertyEditor接口的所有方法,我们继承PropertyEditorSupport之后只需要重写自己需要的方法即可,更加方便!

如果需要注册其他自定义PropertyEditors,可以使用几种机制:

  1. 最不建议、不方便是使用ConfigurableBeanFactory接口的registerCustomEditor()方法,因为这需要我们获取BeanFactory的引用。该方法将自定义的PropertyEditor直接注册到AbstractBeanFactory的customEditors缓存中,等待后续BeanWrapper的获取。
  2. 另一种(稍微方便一点)的机制是使用CustomEditorConfigurer,它是一个特殊的BeanFactoryPostProcessor,可以将自定义的PropertyEditor或者PropertyEditorRegistrar实现存入其内部的customEditors和propertyEditorRegistrars属性中,启动项目之后,它的postProcessBeanFactory方法会在所有普通bean实例化和初始化之前(创建BeanWrapper之前)调用beanFactory来将这些PropertyEditor和propertyEditorRegistrars注册到AbstractBeanFactory的customEditors和propertyEditorRegistrars缓存。

基于以上的配置,在Spring bean对应的BeanWrapper初始化时,会自动从AbstractBeanFactory的customEditors和propertyEditorRegistrars缓存中将自定义的PropertyEditor注册到自己内部(位于AbstractBeanFactory#initBeanWrapper方法中),之后被BeanWrapper用于创建和填充 Bean 实例的类型转换。

但是请注意,这种配置不适用于Spring MVC的数据绑定,因为DataBinder默认不会查找这里注册到AbstractBeanFactory中的customEditors和propertyEditorRegistrars缓存,数据绑定时需要的自定义Editor必须在org.springframework.validation.DataBinder中手动注册(通过Spring MVC的@InitBinder方法)。

如下案例,自定义了一个PropertyEditor,日期格式为“yyyy-MM-dd”:

/**
 * @author lx
 */
public class DateEditor extends PropertyEditorSupport {
    private String formatter = "yyyy-MM-dd";

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(formatter);
        try {
            Date date = simpleDateFormat.parse(text);
            System.out.println("-----DateEditor-----");
            //转换后的值设置给PropertyEditorSupport内部的value属性
            setValue(date);
        } catch (ParseException e) {
            throw new IllegalArgumentException(e);
        }
    }

    public DateEditor() {
    }

    public DateEditor(String formatter) {
        this.formatter = formatter;
    }
}

一个实体,需要将“2020-12-12”的字符串转换为Date类型的属性:

@Component
public class TestDate {
    @Value("2020-12-12")
    private Date date;

    @PostConstruct
    public void test() {
        System.out.println(date);
    }
}

将自定义的PropertyEditor注册到CustomEditorConfigurer的customEditors属性中,该属性是Map<Class<?>, Class<? extends PropertyEditor>类型,即都是Class类型:

<bean 
 class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="customEditors">
        <map>
            <entry key="java.util.Date" value="com.spring.mvc.config.DateEditor"/>
        </map>
    </property>
</bean>

启动项目,即可看到输出:

-----DateEditor-----
Sat Dec 12 00:00:00 CST 2020

通过在CustomEditorConfigurer的customEditors属性中直接添加自定义编辑器的类型确实可以注册,但是这种方法的缺点是无法为自定义的PropertyEditor指定初始化参数。

实际上在早期的Spring版本中,Map中的value类型是一个实例,因此它是支持自定义初始化参数的,但是因为PropertyEditor是有状态的,如果多个BeanWrapper共用同一个PropertyEditor实例,那么可能造成难以察觉的问题。因此,在新版本中customEditors属性的Map的Value是Class类型,并且每个BeanWrapper在设置转换器是都会创建属于自己的PropertyEditor实例。如果想要需要控制PropertyEditor的实例化过程,比如设置初始化参数,那么我们需要使用PropertyEditorRegistrar去注册它们。

还有一个缺点是,基于customEditors属性配置的PropertyEditor无法与Spring MVC的数据绑定共享同样的配置方式,即使它们都需要配置某个同样的PropertyEditor

2.4.1 使用PropertyEditorRegistrar

PropertyEditorRegistrar,顾名思义,它是一个PropertyEditor的“注册表”,Spring中还有很多Registrar结尾的类,这种类通用作用就是用于注册类名去除“Registrar”之后的数据,比如PropertyEditorRegistrar就是用于注册PropertyEditor,它还有一个可选的特性就是,可以在一次方法调用中注册多个实例并且更加灵活!

另外,PropertyEditorRegistrar实例与名为PropertyEditorRegistry的接口配合使用,而该接口又被Spring的BeanWrapper和 DataBinder都实现了,因此PropertyEditorRegistrar中的PropertyEditor配置很容易的被BeanWrapper和 DataBinder共享!

Spring为我们提供了一个PropertyEditorRegistrar的实现ResourceEditorRegistrar,如果我们要实现自己的PropertyEditorRegistrar,那么可以参数该类,特别是它的registerCustomEditors方法。实际上ResourceEditorRegistrar将会被Spring自动默认注册到容器中(位于prepareBeanFactory方法中),因此该类中的PropertyEditor通常会被所有的beanWarpper使用!

在这里插入图片描述

下面是一个自己的PropertyEditorRegistrar:

/**
 * @author lx
 */
public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {

    private String formatter;


    /**
     * 传递一个PropertyEditorRegistry的实现,使用给定的PropertyEditorRegistry注册自定义PropertyEditor
     * BeanWrapperImpl和DataBinder都实现了PropertyEditorRegistry接口,传递的通常是 BeanWrapper 或 DataBinder。
     * <p>
     * 该方法仅仅是定义了注册的流程,只有当某个BeanWrapper 或 DataBinder实际调用时才会真正的注册
     *
     * @param registry 将要注册自定义PropertyEditor的PropertyEditorRegistry
     */
    @Override
    public void registerCustomEditors(PropertyEditorRegistry registry) {

        // 预期将创建新的属性编辑器实例,可以自己控制创建流程
        registry.registerCustomEditor(Date.class, new DateEditor(formatter));

        // 可以在此处注册尽可能多的自定义属性编辑器...
    }


    public String getFormatter() {
        return formatter;
    }

    public void setFormatter(String formatter) {
        this.formatter = formatter;
    }
}

下面是如何配置CustomEditorConfigurer并注入CustomPropertyEditorRegistrar实例:

<bean 
class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <!--propertyEditorRegistrars是一个数组,可以传递多个自定义的PropertyEditorRegistrar-->
    <property name="propertyEditorRegistrars">
        <array>
            <ref bean="customPropertyEditorRegistrar"/>
        </array>
    </property>
</bean>

<!--自定义的CustomPropertyEditorRegistrar-->
<bean id="customPropertyEditorRegistrar" class="com.spring.mvc.config.CustomPropertyEditorRegistrar">
    <property name="formatter" value="yyyy-MM-dd"/>
</bean>

启动项目,同样成功转换:

-----DateEditor-----
Sat Dec 12 00:00:00 CST 2020

注意,Spring的目的就是让每个BeanWarpper和DataBinder都初始化自己的PropertyEditor实例,这是为了防止多个实例共用一个有状态的PropertyEditor导致数据异常,如果你确定没问题的话,也可以在PropertyEditorRegistrar中配置同一个PropertyEditor实例。

2.4.1.1 共享配置

配置PropertyEditorRegistrar之后,想要将这些PropertyEditor配置应用在Spring MVC的DataBinder中非常简单,如下案例:

@Controller
public class RegistrarController {
    @Resource
    private CustomPropertyEditorRegistrar customPropertyEditorRegistrar;

    @InitBinder
    public void init(WebDataBinder binder) {
        //调用registerCustomEditors方法向当前DateBinder注册PropertyEditor
        customPropertyEditorRegistrar.registerCustomEditors(binder);
    }

    //其他控制器方法
}

只需要在控制器中引入customPropertyEditorRegistrar实例,然后在@ initBinder方法中调用registerCustomEditors方法并传入DataBinder,即可将内部配置的PropertyEditor注册到当前DataBinder中。

这种类型的PropertyEditor注册方式可以产生简洁的代码(注册多个PropertyEditor的实现只有一行代码),并允许将公共的PropertyEditor注册代码封装在一个类中,然后根据需要在多个Controllers之间共享。

3 Converter

Spring 3.0引入了core.convert包,它提供了一般类型的转换系统,作为 JavaBeans PropertyEditors属性编辑器的替代服务。

3.1 Converter SPI接口

相比于复杂的PropertyEditor接口,org.springframework.core.convert.converter.Converter是一个用于类型转换的非常简单且强大的SPI接口,翻译成中文就是“转换器”。Converter提供了核心的转换行为!

@FunctionalInterface
public interface Converter<S, T> {

    /**
     * 将 S 类型的源对象转换为目标类型 T 对象
     *
     * @param source 要转换的源对象,它必须是 S 类型的实例(从不为null)
     * @return 转换后的对象,它必须是 T 类型的实例(可能为null)
     * @throws IllegalArgumentException 如果源对象无法转换为所需的目标类型
     */
    @Nullable
    T convert(S source);

}

要想创建自己的Converter,只需要实现Converter接口,其中S表示要转换的类型,T表示要转换到的类型。

convert(S)方法的每次调用,都应该保证输入参数不为null。如果转换失败,转换器可能会引发任何非受检异常。在抛出异常时应该包裹在一个IllegalArgumentException中,同时我们必须保证Converter是线程安全的!

和PropertyEditor类似,为了方便起见,Spring在core.convert.support 包中已经提供了非常多的Converter转换器实现,其中包括从字符串到数字的转换器和其它常见类型。

下面是一个典型的Converter转换器实现:

public final class StringToInteger implements Converter<String, Integer> {

    @Override
    public Integer convert(String source) {
        return Integer.valueOf(source);
    }
}

3.2 使用ConverterFactory

Converter的类型转换是明确的,倘若对有同一父类或接口的多个子类型需要进行类型转化,为每个类型都写一个Converter显然是十分不理智的,当需要集中管理整个类层次结构的转换逻辑时,可以实现ConverterFactory接口:

/**
 * @param <S> 源类型
 * @param <R> 目标类型的超类型
 */
public interface ConverterFactory<S, R> {

    /**
     * 获取转换器从 S 转换为目标类型 T,其中 T 也是 R 的子类型。
     *
     * @param <T>        目标类型
     * @param targetType 要转换为的目标类型的Class
     * @return 从 S 到 T 的转换器
     */
    <T extends R> Converter<S, T> getConverter(Class<T> targetType);

}

参数S是需要转换的类型,R是要转换到的类的基类。然后实现getConverter(Class)方法,其中T是R的子类型。ConverterFactory用于实现一种类型到N种类型的转换。

Spring已经提供了基本的ConverterFactory的实现

在这里插入图片描述

3.3 使用GenericConverter

当需要定义复杂的Converter实现时,可以使用GenericConverter接口,GenericConverter并不是Converter的子接口,而是一个独立的顶级接口,翻译成中文就是“通用转换器”!

与Converter相比,GenericConverter更灵活并且没有强类型的签名,它支持在多个源类型和目标类型之间进行转换,用于实现N种类型到N种类型的转换。此外,GenericConverter提供了可用的源和目标字段上下文(TypeDescriptor),你可以在实现转换逻辑时使用它们。这样的上下文允许类型转换由字段注解或字段签名上声明的泛型信息驱动类型转换。

下面是GenericConverter的接口定义:

/**
 * 用于在两种或多种类型之间转换的通用转换器接口。
 * <p>
 * 这是最灵活的转换器SPI接口,也是最复杂的。它的灵活性在于GenericConverter可能支持在多个源/目标类型对之间转换
 * 此外,GenericConverter的实现在类型转换过程中可以访问源/目标字段上下文。
 * 这允许解析源和目标字段元数据,如注解和泛型信息,这些元数据可用于影响转换逻辑。
 */
public interface GenericConverter {

    /**
     * 返回所有此转换器可以转换的源类型和目标类型的ConvertiblePair
     * 每个ConvertiblePair都表示一组可转换的源类型以及目标类型。
     * <p>
     * 对于ConditionalConverter,此方法可能会返回 null 以指示应考虑所有ConvertiblePair
     */
    @Nullable
    Set<ConvertiblePair> getConvertibleTypes();

    /**
     * 将源对象转换为TypeDescriptor(类型描述符)描述的目标类型。
     *
     * @param source     要转换的源对象(可能是null)
     * @param sourceType 正在转换的字段的类型描述符
     * @param targetType 要转换为的字段的类型描述符
     * @return 转换的对象
     */
    @Nullable
    Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType);


    /**
     * 源类型到目标类型对的持有者
     */
    final class ConvertiblePair {

        private final Class<?> sourceType;

        private final Class<?> targetType;

        /**
         * 创建一个新的ConvertiblePair
         *
         * @param sourceType 源类型
         * @param targetType 目标类型
         */
        public ConvertiblePair(Class<?> sourceType, Class<?> targetType) {
            Assert.notNull(sourceType, "Source type must not be null");
            Assert.notNull(targetType, "Target type must not be null");
            this.sourceType = sourceType;
            this.targetType = targetType;
        }

        public Class<?> getSourceType() {
            return this.sourceType;
        }

        public Class<?> getTargetType() {
            return this.targetType;
        }


        /*用于判断是否已存在某个源类型到目标类型的组*/

        @Override
        public boolean equals(@Nullable Object other) {
            if (this == other) {
                return true;
            }
            if (other == null || other.getClass() != ConvertiblePair.class) {
                return false;
            }
            ConvertiblePair otherPair = (ConvertiblePair) other;
            return (this.sourceType == otherPair.sourceType && this.targetType == otherPair.targetType);
        }

        @Override
        public int hashCode() {
            return (this.sourceType.hashCode() * 31 + this.targetType.hashCode());
        }

        @Override
        public String toString() {
            return (this.sourceType.getName() + " -> " + this.targetType.getName());
        }
    }

}

GenericConverter中拥有一个内部类ConvertiblePair,这个内部类用于封装一种可转换的源类型与目标类型对,一个GenericConverter可以拥有多个ConvertiblePair。

要实现GenericConverter,需要重写getConvertibleTypes()方法以返回受转换支持的源类型到目标类型对,也就是ConvertiblePair。然后实现convert(Object, TypeDescriptor, TypeDescriptor)方法,该方法包含转换的逻辑。源 TypeDescriptor(字段描述符)提供对保存了要转换值的源字段的访问。目标 TypeDescriptor提供对要设置转换值的目标字段的访问。

TypeDescriptor作为类型描述符,保存了对应参数、字段的元数据,可以从其中获取对应参数、字段的名字、类型、注解、泛型的信息!

Spring已经提供了基本的GenericConverter的实现,一个很好的例子就是在Java数组和集合之间转换的转换器,比如ArrayToCollectionConverter,首先线它会创建对应的集合类型,然后在将数组元素存入集合中时,如有必要,会尝试将数组元素类型转换为集合元素的泛型类型!

在这里插入图片描述

3.3.1 使用ConditionalGenericConverter

如果觉得只通过源类型和目标类型是否匹配来判断能够支持转换的方式太过简单了,还需要在特定的条件成立时才支持转换,比如可能希望在目标字段上存在指定的注解时才表示可以转换,或者可能希望仅在目标字段的类型上定义了特定方法(如静态的valueOf方法)时才表示可以转换,此时我们可以使用ConditionalGenericConverter接口。

ConditionalGenericConverter是GenericConverter 和ConditionalConverter的结合,允许自定义匹配条件来确定是否能执行转换!

/**
 * 条件转换器,允许有条件的执行转换
 * <p>
 * 通常用于根据字段或类级别的特征(如注解或方法)的存在有选择地匹配自定义转换逻辑。
 * 例如,从 String 字段转换为 Date 字段时,如果目标字段已使用@DateTimeFormat注解,则matches可能会返回true
 */
public interface ConditionalConverter {

    /**
     * 是否可以应用从源类型到当前正在考虑的目标类型的转换?
     *
     * @param sourceType 正在转换的字段的类型描述符
     * @param targetType 要转换为的字段的类型描述符
     * @return 如果应执行转换,则为 true,否则为 false
     */
    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

/**
 * ConditionalGenericConverter同时继承了GenericConverter和ConditionalConverter接口,支持更复杂的判断
 */
public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

Spring已经提供了基本的ConditionalGenericConverter的实现,大部分的实现都是用来处理有集合或数组参与的转换,一个很好的例子同样是ArrayToCollectionConverter,它会在getConvertibleTypes方法判断源类型和目标类型满足条件之后,继续调用matches方法判断源数组元素类型到底能否转换为目标集合的元素类型,如果可以转换,那么才会真正的执行转换的逻辑!

在这里插入图片描述

3.4 ConversionService API接口

由于整个Conversion机制的复杂性,Spring提供了ConversionService 接口,该接口定义了一系列统一的API方法供外部调用,用于在运行时执行类型转换,屏蔽了具体的内部调用逻辑。这是基于门面(facade)设计模式

/**
 * 用于类型转换的服务接口。
 * 这是进入转换系统的入口,通过调用convert(Object, Class) 来使用这个系统执行线程安全的类型转换。
 */
public interface ConversionService {

    /**
     * 如果源类型的对象可以转换为目标类型,则返回 true
     * <p>
     * 如果此方法返回 true,则意味着convert(Object, Class) 方法能够将源型的实例转换为目标类型。
     * <p>
     * 对于集合、数组和map类型之间的转换,此方法将返回 true,即使转换调用仍可能抛出ConversionException(如果基础元素不可转换)。
     * 调用者在使用集合和map时应处理此特殊情况。
     *
     * @param sourceType 要转换的源类型(如果源对象为 null,则可能为 null)
     * @param targetType 要转换为的目标类型(必需存在)
     * @return 如果可以执行转换,则为 true,如果不执行,则为 false
     * @throws IllegalArgumentException 如果targetType目标类型为null
     */
    boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);

    /**
     * 如果源类型的对象可以转换为目标类型,则返回 true
     * <p>
     * 如果此方法返回 true,则意味着convert(Object, TypeDescriptor, TypeDescriptor)方法能够将源类型实例转换为目标类型。
     * <p>
     * 对于集合、数组和map类型之间的转换,此方法将返回 true,即使转换调用仍可能抛出ConversionException(如果基础元素不可转换)。
     * 调用者在使用集合和map时应处理此特殊情况。
     *
     * @param sourceType 有关要转换的源类型的上下文,也就是TypeDescriptor(如果源对象为 null,则可能为 null)
     * @param targetType 要转换为的目标类型的上下文,也就是TypeDescriptor(必需存在)
     * @return 如果可以在源类型和目标类型之间执行转换,则为 true,如果不执行,则为 false
     * @throws IllegalArgumentException 如果targetType目标类型为null
     */
    boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);

    /**
     * 将给定的源对象转换为指定的目标类型。
     *
     * @param source     要转换的源对象(可能是 null )
     * @param targetType 要转换为的目标类型(必需存在)
     * @return 转换的对象,目标类型的实例
     * @throws ConversionException      如果发生转换异常
     * @throws IllegalArgumentException 如果targetType目标类型为null
     */
    @Nullable
    <T> T convert(@Nullable Object source, Class<T> targetType);

    /**
     * 将给定的源对象转换为指定的目标类型。
     * TypeDescriptor提供有关发生转换的源和目标位置(通常是对象字段或属性位置)的其他上下文,从中可以获取到字段名、类型、注解、泛型等信息
     *
     * @param source     要转换的源对象(可能是 null )
     * @param sourceType 有关要转换的源类型的上下文,也就是TypeDescriptor(如果源对象为 null,则可能为 null)
     * @param targetType 要转换为的目标类型的上下文,也就是TypeDescriptor(必需存在)
     * @return 转换的对象,目标类型的实例
     * @throws ConversionException      如果发生转换异常
     * @throws IllegalArgumentException 如果目标类型为null,或源类型为null,但源对象不是null
     */
    @Nullable
    Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);

}

ConversionService仅仅是一个用于类型转换的服务接口,大多数ConversionService的实现还实现了ConverterRegistry接口(一个提供了注册Converter方法的接口),它为注册Converter转换器提供了SPI机制。因此,ConversionService的实现通常具有支持注册多个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);

    /**
     * 删除对应 源/目标类型对 的所有Converter
     */
    void removeConvertible(Class<?> sourceType, Class<?> targetType);

}

通常,需要进行类型转换的时候,我们直接调用ConversionService的方法即可,而实际上的具体的转换逻辑将会被委托给其内部注册的转换器实例! Spring在core.convert.support包中提供了一系列健壮的conversionService实现和相关配置类。比如GenericConversionService是适用于大多数环境的通用实现,提供了配置Converter的功能。比如ConversionServiceFactory则是一个提供了为ConversionService注册Converter的服务的通用工厂。

3.5 配置ConversionService

ConversionService是一个无状态对象,被设计为在应用程序启动时实例化,然后可以在多个线程之间共享。在Spring应用程序中,通常为每个Spring容器(或ApplicationContext)配置一个ConversionService实例。Spring接收转换服务,并在框架需要执行类型转换时使用它。我们还可以将此ConversionService注入到任何bean中,并直接调用它的转换方法。

3.5.1 配置BeanWarpper的ConversionService

如果想要手动注册全局生效的默认ConversionService,那么需要将id命名为“conversionService”。在容器的finishBeanFactoryInitialization方法初始化全部普通bean实例之前,Spring容器会首先初始化这个名为“conversionService”的ConversionService,并且设置到AbstractBeanFactory的conversionService属性中。

在这里插入图片描述

在后续的BeanWarpper的初始化方法中(AbstractBeanFactory#initBeanWrapper方法中),会获取这个注册的conversionService并保存在自己的内部,用于后续的属性填充的类型转换服务:

在这里插入图片描述

对于的普通spring项目来说,不会注册默认的ConversionService,因此底层BeanWarpper的ConversionService为null,对于boot项目则会默认注册一个ApplicationConversionService服务。所以说,如果未在Spring中注册ConversionService,则BeanWarpper会使用基于PropertyEditor属性编辑器的原始转换系统。

下面是注册一个默认ConversionService的示例:

<bean id="conversionService" class="org.springframework.context.support.Con
versionServiceFactoryBean"/>

这里我们并没有直接配置ConversionService,而是配置的是一个ConversionServiceFactoryBean对象,它是一个FactoryBean类型的对象, 通过工厂模式来生产ConversionService,这也符合Spring的一贯作风,比较重且复杂的类构造起来一般采用工厂模式,它生产的对象实际类型就是DefaultConversionService

在这里插入图片描述

在创建DefaultConversionService时,其默认会注册一些常用的Converter,如果要用自己的自定义转换器补充或重写默认转换器,那么可以设置ConversionServiceFactoryBean的converters属性,该属性值可以配置为任何Converter、ConverterFactory或GenericConverter的实现

如下是一个自定义的Converter实现:

/**
 * 把字符串转换Date
 *
 * @author lx
 */
@Component
public class StringToDateConverter implements Converter<String, Date> {

    /**
     * String source    传入进来字符串
     *
     * @param source 传入的要被转换的字符串
     * @return 转换后的格式类型
     */
    @Override
    public Date convert(String source) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        Date parse;
        try {
            parse = dateFormat.parse(source);
            System.out.println("--------StringToDateConverter---------");
        } catch (ParseException e) {
            throw new IllegalArgumentException(e);
        }
        return parse;
    }
}

然后将其配置到ConversionServiceFactoryBean中即可:

<!--配置类型转换服务工厂,它会默认创建DefaultConversionService,并且支持注入自定义
类型转换器之外-->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <!--注入自定义转换器实例-->
            <ref bean="stringToDateConverter"/>
        </set>
    </property>
</bean>

3.5.1.1 直接使用ConversionService

我们可以直接在程序中以编程式的使用DefaultConversionService并且不需要创建DefaultConversionService的实例,因为DefaultConversionService实现了懒加载的单例模式,我们可以通过getSharedInstance()方法直接获取共享实例。

public class DefaultConversionService extends GenericConversionService {

    @Nullable
    private static volatile DefaultConversionService sharedInstance;

    public static ConversionService getSharedInstance() {
        DefaultConversionService cs = sharedInstance;
        if (cs == null) {
            synchronized (DefaultConversionService.class) {
                cs = sharedInstance;
                if (cs == null) {
                    cs = new DefaultConversionService();
                    sharedInstance = cs;
                }
            }
        }
        return cs;
    }

    //…………
}

3.5.2 配置DataBinder的ConversionService

上面注册的默认全局ConversionService仅适用于BeanWarpper,对于Spring MVC的DataBinder无效,因为DataBinder初始化并在绑定ConversionService时不会使用AbstractBeanFactory的conversionService属性,DataBinder的ConversionService有另外的配置方法:

  1. 对于XML配置来说,配置<mvc:annotation-driven>标签即表示会默认使用一个DefaultFormattingConversionService实例,可以通过conversion-service属性指定某个ConversionService实例。
  2. 对于JavaConfig配置来说,通过加入@EnableWebMvc注解也能注册一个默认的DefaultFormattingConversionService,而注册一个id为mvcConversionService的ConversionService即表示替代这个默认的DefaultFormattingConversionService
  3. 如果没有这两种配置,那么DataBinder同样没有ConversionService。

如下配置,使得BeanWarpper和DataBinder都是用同一个conversionService:

<!--conversion-service属性指定在字段绑定期间用于类型转换的转换服务的bean名称。 -->
<!--如果不指定,则表示注册默认DefaultFormattingConversionService-->
<mvc:annotation-driven conversion-service="conversionService"/>

<!--配置类型转换服务工厂,它会默认创建DefaultConversionService,并且支持注入自定义类型转换器之外-->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <!--注入自定义转换器实例-->
            <ref bean="stringToDateConverter"/>
        </set>
    </property>
</bean>

关于DefaultFormattingConversionService,我们下面会介绍!

4 Formarter

如上节所述,core.convert 是一种通用类型转换系统。它提供了一个统一的ConversionService API 以及一个强类型的Converter SPI,用于实现从一种类型到另一种类型的转换逻辑,甚至提供了更多扩展功能的GenericConverter以及ConditionalGenericConverter,Spring通常建议使用此系统为beanWarpper绑定bean属性值。此外,Spring表达式语言(SPEL)和DataBinder都可以使用此系统绑定字段值。例如,当SPEL需要将Short强制转换为Long以完成expression.setValue(Object bean, Object value)操作时,core.convert系统将执行强制转换。

Spring MVC中,HTTP中的源数据都是String类型,数据绑定需要将String转换为其他类型,同时也可能需要将数据转换为具有本地格式的字符串样式进行展示,core.convert中更通用的Converter SPI不能直接满足这种格式要求。为了解决这些问题,Spring 3 引入了一个方便的 Formatter SPI,用于在web环境下替代PropertyEditor。

通常,当需要实现通用类型转换逻辑时,可以使用Converter SPI,例如,在java.util.Date和Long之间转换。当在客户端环境(如Web应用程序)中工作并且需要解析和输出本地化字段值时,可以使用Formatter SPI。ConversionService为两个SPI提供统一的类型转换API。

4.1 Formatter SPI

org.springframework.format.Formatter是一个用于实现字段格式化逻辑的非常简单并且是强类型的SPI接口。

public interface Formatter<T> extends Printer<T>, Parser<T> {}

Formatter继承了Printer和Parser接口,下面是这两个接口的定义:

@FunctionalInterface
public interface Printer<T> {

    /**
     * 打印类型 T 的对象以进行显示
     *
     * @param object 要打印的实例
     * @param locale 当前用户的区域(本地化)设置
     * @return 打印的文本字符串
     */
    String print(T object, Locale locale);

}

@FunctionalInterface
public interface Parser<T> {

    /**
     * 分析文本字符串以生成 T
     *
     * @param text   文本字符串
     * @param locale 当前用户区域设置
     * @return T 的实例
     * @throws ParseException           当 java.text 解析库中发生解析异常时
     * @throws IllegalArgumentException 当发生解析异常时
     */
    T parse(String text, Locale locale) throws ParseException;

}

如果要创建自己的Formatter, 需要实现上面提到的Formatter接口以完成T类型对象的格式化和解析功能。

实现print()方法根据客户端的区域设置以打印T实例,实现 parse()方法根据客户端的区域设置以及文本字符串中分析 T 的实例。如果解析尝试失败,则 Formatter 应引发ParseException或IllegalArgumentException异常。注意确保 Formatter 实现是线程安全的。

org.springframework.format.support子包提供了常见方便使用的Formatter实现。

org.springframework.format.number子包提供了NumberStyleFormatter、 CurrencyStyleFormatter和PercentStyleFormatter来格式化Number对象,其内部使用java.text.NumberFormat。

org.springframework.format.number.money提供了与JSR-354集成的针对货币的格式化器,比如CurrencyUnitFormatter、MonetaryAmountFormatter。

org.springframework.format.datetime子包提供了DateFormatter,内部使用java.text.DateFormat来格式化java.util.Date对象。org.springframework.format.datetime.joda子包基于joda时间库提供全面的datetime格式支持。

4.2 注解驱动格式化

字段格式化可以按字段类型或注解进行配置,要绑定一个注解到Formatter,可以实现AnnotationFormatterFactory接口。

BeanWarpper和DataBinder都支持通过自定义注解驱动类型转换,但是请注意Spring MVC请求数据绑定时只能对某个完整变量(URI路径变量、请求参数、请求体数据)进行注解驱动类型转换,如果是@RequestBody之类的请求或者使用一个变量表示一个实体时,变量内部的数据不支持注解驱动类型转换。

/**
 * 用于创建formatter以格式化使用特定注解的字段值的工厂。
 * <p>
 * 例如,DateTimeFormatAnnotationForMatterFactory 可能会创建一个formatter,该formatter对使用@DateTimeForma注解的字段格式化为Date类型
 *
 * @param <A> 应触发格式化的注解的类型
 */
public interface AnnotationFormatterFactory<A extends Annotation> {

    /**
     * 可使用A类型注解的字段类型。
     */
    Set<Class<?>> getFieldTypes();

    /**
     * 获取 Printer 以 print 具有指定注解的fieldType类型的字段值
     *
     * @param annotation 注解实例
     * @param fieldType  注解的字段类型
     * @return the printer
     */
    Printer<?> getPrinter(A annotation, Class<?> fieldType);

    /**
     * 获取 Parser 以 parse 有指定注解的fieldType类型的字段值
     *
     * @param annotation 注解实例
     * @param fieldType  注解的字段类型
     * @return the parser
     */
    Parser<?> getParser(A annotation, Class<?> fieldType);

}

泛型A表示与格式化逻辑关联的注解,例如org.springframework.format.annotation.DateTimeFormatgetFieldTypes()方法返回可在其上使用注解的字段类型。getprinter()返回Printer以打印注解字段的值。getParser()返回一个Parser来解析注解字段的clientValue。

org.springframework.format.annotation包中提供了AnnotationFormatterFactory关联的注解的实现:

  1. @NumberFormat用格式化Number类型的字段(比如Double、Long),对应NumberFormatAnnotationFormatterFactory、Jsr354NumberFormatAnnotationFormatterFactory。
  2. @DateTimeFormat 用于格式化java.util.Date、java.util.Calendar、Long(时间戳毫秒)以及JSR-310 java.time 和 Joda-Time 值类型。对应DateTimeFormatAnnotationFormatterFactory、JodaDateTimeFormatAnnotationFormatterFactory、Jsr310DateTimeFormatAnnotationFormatterFactory。

使用时也很简单,开启Spring mvc配置之后,Spring MVC默认会注册这些AnnotationFormatterFactory,我们可直接使用上面的注解。

下面的示例使用@DateTimeFormat将前端传递的yyyy-MM-dd类型的日期字符串参数格式化为Date!

public class MyDate {
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date date;

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }
}

一个控制器方法:

@RequestMapping("/dateTimeFormat/{date}")
@ResponseBody
public MyDate annotationFormatterFactory(MyDate date) {
    System.out.println(DateFormat.getDateTimeInstance().format(date.getDate()));
    return date;
}

访问/dateTimeFormat/2021-01-29,可以看到如下输出:

2021-1-29 0:00:00

说明格式化成功,此时页面展示的JSON字符串样式如下:

在这里插入图片描述

4.3 FormatterRegistry SPI

org.springframework.format.FormatterRegistry是可用于注册formatters的SPI接口,它还继承了ConverterRegistry,因此同样可以注册converters

public interface FormatterRegistry extends ConverterRegistry {

    void addPrinter(Printer<?> printer);

    void addParser(Parser<?> parser);

    void addFormatter(Formatter<?> formatter);

    void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);

    void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);

    void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);
}

实际上,上面看到的FormatterRegistry提供的注册Formatter、Parser、Printer、AnnotationFormatterFactory的方法在内部会被转换为注册对应的Converter,因为它们的功能的本质都是相同的,并且Converter的功能涵盖了Formatter所有功能!因此Formatter和Converter可以看作是同一个底层类的不同表层展示:

在这里插入图片描述

FormatterRegistry SPI 允许我们集中配置格式规则,而不是在控制器之间复制这样的配置。例如,可能希望强制所有日期字段以某种方式格式化,或者具有特定注解的字段以某种方式格式化。使用共享的FormatterRegistry,我们只需定义一次这些规则,这些规则在需要格式化时会自动使用。

FormattingConversionService是一个适用于大多数环境的FormatTerregistry的实现。并且因为FormattingConversionService还继承了GenericConversionService,因此可以直接使用FormattingConversionService将Converter和Formatter都配置在一起。

Spring MVC默认使用的DefaultFormattingConversionService就实现了FormattingConversionService。

4.4 FormatterRegistrar SPI

org.springframework.format.FormatterRegistrar是一个用于为FormatterRegistry注册formatters和converters的通用SPI接口,它类似于前面学习的PropertyEditorRegistrar,可一次性注册多个formatter和converter,在为给定格式(如日期格式)注册多个相关converters和formatters时,FormatterRegistrar非常有用。

public interface FormatterRegistrar {

    /**
     * 为FormatterRegistry注册formatters和converters
     *
     * @param registry 要使用的 FormatterRegistry 实例
     */
    void registerFormatters(FormatterRegistry registry);

}

4.5 配置全局转换器

我们在“配置DataBinder的ConversionService”的部分就说过了,在开启MVC配置之后,DataBinder会默认使用一个DefaultFormattingConversionService作为conversionService,当然我们也可以配置自定义的conversionService。

在学习Fromatter之后,我们可以使用FormattingConversionServiceFactoryBean工厂而不是ConversionServiceFactoryBean来作为一个真正的BeanWrapper和DataBinder 都共用的conversionService,因为它支持更多特性,比如同时支持注册formatters和converters!

下面我们提供一个简单的自定义的全局conversionService配置,其内部通过FormatterRegistrar注册两个AnnotationFormatterFactory实例,以期待实现基于注解的格式化转换!

自定义两个注解:

/**
 * @author lx
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
public @interface PersonFormat {

}
/**
 * @author lx
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
public @interface SexFormat {

    String value() default "";

}

Person实体,其内部的sex字段标注了@SexFormat注解,该类用来测试Spring MVC的DataBinder的数据绑定:

/**
 * @author lx
 */
public class Person {
    private Long id;
    private String tel;
    private Integer age;
    @SexFormat
    private String sex;

    public Person(Long id, String tel, Integer age, String sex) {
        this.id = id;
        this.tel = tel;
        this.age = age;
        this.sex = sex;
    }

    public Person() {
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTel() {
        return tel;
    }

    public void setTel(String tel) {
        this.tel = tel;
    }

    public Integer getAge() {
        return age;
    }

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

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
}

PersonFormatter的实现,用于将前端传递的person字符串转换为Person,我们约定前段传递的person字符串使用“|”分隔:

/**
 * @author lx
 */
public class PersonFormatter implements Formatter<Person> {

    @Override
    public Person parse(String text, Locale locale) throws ParseException {
        String[] split = text.split("\\|");
        if (split.length == 4) {
            return new Person(Long.valueOf(split[0]), split[1], Integer.valueOf(split[2]), split[3]);
        }
        throw new ParseException("参数格式不正确:" + text, 0);
    }

    @Override
    public String print(Person object, Locale locale) {
        return object.toString();
    }
}

SexFormatter的实现,用于将性别的数字转换为性别字符串:

public class SexFormatter implements Formatter<String> {
    private static final String MAN = "男";
    private static final String WOMAN = "女";
    private static final String OTHER = "未知";


    @Override
    public String parse(String text, Locale locale) {
        if ("0".equals(text)) {
            return MAN;
        }
        if ("1".equals(text)) {
            return WOMAN;
        }
        return OTHER;
    }

    @Override
    public String print(String object, Locale locale) {
        return object;
    }


    public static class WomanFormatter extends SexFormatter {

        @Override
        public String parse(String text, Locale locale) {
            return WOMAN;
        }
    }

    public static class ManFormatter extends SexFormatter {

        @Override
        public String parse(String text, Locale locale) {
            return MAN;
        }
    }
}

PersonAnnotationFormatterFactory的实现:

/**
 * @author lx
 */
@Component
public class PersonAnnotationFormatterFactory implements AnnotationFormatterFactory<PersonFormat> {

    Set<Class<?>> classSet = Collections.singleton(Person.class);

    @Override
    public Set<Class<?>> getFieldTypes() {
        return classSet;
    }

    @Override
    public Parser<?> getParser(PersonFormat annotation, Class<?> fieldType) {
        return configureFormatterFrom(annotation);
    }

    @Override
    public Printer<?> getPrinter(PersonFormat annotation, Class<?> fieldType) {
        return configureFormatterFrom(annotation);
    }

    private Formatter<Person> configureFormatterFrom(PersonFormat annotation) {
        return new PersonFormatter();
    }
}

SexAnnotationFormatterFactory的实现:

/**
 * @author lx
 */
@Component
public class SexAnnotationFormatterFactory implements AnnotationFormatterFactory<SexFormat> {

    Set<Class<?>> classSet = Collections.singleton(String.class);

    @Override
    public Set<Class<?>> getFieldTypes() {
        return classSet;
    }

    @Override
    public Parser<?> getParser(SexFormat annotation, Class<?> fieldType) {
        return configureFormatterFrom(annotation);
    }

    @Override
    public Printer<?> getPrinter(SexFormat annotation, Class<?> fieldType) {
        return configureFormatterFrom(annotation);
    }

    private Formatter<String> configureFormatterFrom(SexFormat annotation) {
        String value = annotation.value();
        if ("0".equals(value)) {
            return new SexFormatter.ManFormatter();
        }
        if ("1".equals(value)) {
            return new SexFormatter.WomanFormatter();
        }
        return new SexFormatter();
    }
    
}

一个Controller控制器,用于测试Spring MVC的DataBinder,同时其内部有一个sex属性标注了@SexFormat注解,该类用来测试Spring 的BeanWrapper:

/**
 * @author lx
 */
@RestController
public class AnnotationFormatterFactoryController {

    /*用于测试DataBinder的数据转换*/


    @RequestMapping("/annotationFormatterFactory/{person}")
    @ResponseBody
    public Person annotationFormatterFactory(@PersonFormat Person person, @SexFormat String sex) {
        System.out.println(sex);
        return person;
    }

    /*用于测试BeanWrapper的数据转换*/

    @SexFormat("2")
    @Value("1")
    private String sex;

    @PostConstruct
    public void test() {
        System.out.println(sex);
    }
}

一个自定义的FormatterRegistrar,将两个自定义的AnnotationFormatterFactory注册到FormatterRegistry中:

/**
 * @author lx
 */
@Component
public class CustomFormatterRegistrar implements FormatterRegistrar {

    @Resource
    private PersonAnnotationFormatterFactory personAnnotationFormatterFactory;
    @Resource
    private SexAnnotationFormatterFactory sexAnnotationFormatterFactory;
    
    @Override
    public void registerFormatters(FormatterRegistry registry) {
        registry.addFormatterForFieldAnnotation(personAnnotationFormatterFactory);
        registry.addFormatterForFieldAnnotation(sexAnnotationFormatterFactory);
    }
}

下面是配置文件,Spring的BeanWrapper和Spring MVC的DataBinder都支持该conversionService:

<!--conversion-service属性指定在字段绑定期间用于类型转换的转换服务的bean名称。 -->
<!--如果不指定,则表示注册默认DefaultFormattingConversionService-->
<mvc:annotation-driven conversion-service="conversionService"/>

<!--配置工厂,它会默认创建DefaultFormattingConversionService,并且支持注入自定义converters和formatters-->
<!--如果将它命名为conversionService,那么BeanWrapper和DataBinder 都共用此conversionService-->
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <!--注入自定义formatters,支持Formatter 和 AnnotationFormatterFactory的实例-->
    <property name="formatters">
        <set/>
    </property>
    <!--注入自定义converters,支持Converter、ConverterFactory、GenericConverter的实例-->
    <property name="converters">
        <set/>
    </property>
    <!--注入自定义的formatterRegistrars-->
    <property name="formatterRegistrars">
        <set>
            <ref bean="customFormatterRegistrar"/>
        </set>
    </property>
</bean>

启动项目,我们即发现了输出了“女”字符,说我们配置的conversionService对于Spring BeanWrapper的属性数据转换成功生效!

访问/annotationFormatterFactory/1234134|123456|11|1,结果如下:

在这里插入图片描述

成功的从指定格式的字符串转换Person参数实例,Spring MVC的DataBinder测试成功,但是我们发现其内部的sex属性却还是1,并没有转换为性别字符串,为什么呢?因为Spring MVC请求数据绑定时只能对某个完整变量(URI路径变量、请求参数、请求体数据)进行整体的注解驱动类型转换,比如上面的person路径变量就可以整体转换为Person对象成功,但是变量内部的部分数据不支持注解驱动类型转换,比如上面的性别数据标识符“1”位于person变量字符串内部,这样就不能应用注解驱动类型转换!

如果想要处理,那么可以添加一个单独的请求参数“sex”。访问/annotationFormatterFactory/1234134|123456|11|1?sex=1,结果如下:

在这里插入图片描述

成功的进行了内部属性的转换!

5 HttpMessageConverter

HttpMessageConverter同样Spring 3.0加入的一个Converter,但是它不属于org.springframework.core.convert体系,而是位于org.springframework.http.converter包,它不会参与BeanWrapper、DataBinder、SPEL中的类型转换操作,它常被用在HTTP客户端(比如RestTemplate)和服务端(比如Spring MVC REST风格的controllers)的数据转换中,详细描述如下:

  1. 在Spring MVC的控制器方法中,如果使用@RequestBody、HttpEntity<B>、@RequestPart等设置请求参数时,将会采用HttpMessageConverter完成请求正文(请求体)到方法参数的类型转换,并且这里的转换操作。
  2. 在Spring MVC的控制器方法中,如果使用@ResponseBody、HttpEntity<B>, ResponseBodyEmitter、SseEmitterResponseEntity<B>等设置响应时,将会采用HttpMessageConverter对返回的实体对象执行转换操作并写入响应正文(响应体)。
  3. 在通过RestTemplate进行远程HTTP调用的时候,将会通过HttpMessageConverter将调用返回的响应正文(响应体)转换为指定类型的对象,开发者可以直接获取转换后的对象。

对于请求体的转换操作发生在DataBinder创建之前。某个请求体或者响应体使用什么哪种HttpMessageConverter来进行转换,与请求或者响应中的media type(MIME,媒体类型)有关,Spring MVC已经提供了主要的媒体类型的HttpMessageConverter实现,默认情况下,HttpMessageConverter在客户端的 RestTemplate和服务器端的 RequestMappingHandlerAdapter中注册。

所有转换器会支持各自默认的媒体类型,但可以通过设置supportedMediaTypes属性来覆盖它。

下面是HttpMessageConverter的常见实现以及简介:

MessageConverter描述
StringHttpMessageConverter可以从 HTTP 请求和响应读取和写入字符串数据。默认情况下,此转换器支持所有媒体类型(/),并使用Content-Type为text/plain的内容类型进行写入。
FormHttpMessageConverter可以从 HTTP 请求和响应读取和写入表单数据。默认情况下,此转换器支持读取和写入application/x-www-form-urlencoded媒体类型MultiValueMap<String, String>。默认还支持写“multipart/form-data”和"multipart/mixed"媒体类型的数据MultiValueMap<String, Object>,但是无法读取这两个请求的数据,也就是说无法支持文件上传。如果想要支持multipart/form-data媒体类型的请求,请配置MultipartResolver组件
ByteArrayHttpMessageConverter可以从 HTTP 请求和响应读取和写入byte[]字节数组。默认情况下,此转换器支持所有媒体类型 (/),并使用Content-Type为application/octet-stream的内容类型进行写入。这可以通过设置受支持媒体类型属性来覆盖。
MarshallingHttpMessageConverter可以从 HTTP 请求和响应通过org.springframework.oxm包中的Marshaller和Unmarshaller来读取和写入XML数据。默认情况下,此转换器支持text/xml 和application/xml。
MappingJackson2HttpMessageConverter最常见的Converter。可以从 HTTP 请求和响应通过Jackson 2.x 的ObjectMapper读取和写入Json数据。可以使用Jackson提供的注解根据需要自定义 JSON 映射规则(比如@JsonView)。当需要进一步控制JSON序列化和反序列化规则时,可以自定义ObjectMapper的实现并通过该Converter的objectMapper属性注入。默认情况下,此转换器支持application/json。
MappingJackson2XmlHttpMessageConverter可以从 HTTP 请求和响应通过通过Jackson XML 扩展的 XmlMapper 读取和写入 XML数据。可以使用JAXB或Jackson提供的注解根据需要自定义 XML 映射规则。当需要进一步控制XML序列化和反序列化规则时,可以自定义XmlMapper的实现并通过该Converter的objectMapper属性注入。默认情况下,此转换器支持application/xml。
SourceHttpMessageConverter可以从 HTTP 请求和响应读取和写入javax.xml.transform.Source数据,仅支持 DOMSource、SAXSource 和 StreamSource类型。默认情况下,此转换器支持text/xml和application/xml。
BufferedImageHttpMessageConverter可以从 HTTP 请求和响应读取和写入java.awt.image.BufferedImage数据。默认情况下,此转换器可以读取ImageIO#getReaderMIMETypes()方法返回的所有媒体类型,并且使用ImageIO#getWriterMIMETypes()方法返回的第一个可用的媒体类型进行写入。

5.1 配置MessageConverter

在通过注解和JavaConfig开启MVC配置之后,我们可以通过重写configureMessageConverters方法来替换Spring MVC创建的默认转换器,比如配置FastJson转换器,或者重写extendMessageConverters方法来扩展或者修改转换器!

在这里插入图片描述

图中的addDefaultHttpMessageConverters方法用于注册默认的转换器,从该方法的源码中能够得知,如果存在jackson的依赖,那么会自动注册MappingJackson2HttpMessageConverter

在这里插入图片描述

如下JavaConfig配置:

/**
 * @author lx
 */
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
                .indentOutput(true)
                .dateFormat(new SimpleDateFormat("yyyy-MM-dd"));
        converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
    }
}

这表示我们自定义配置的MappingJackson2HttpMessageConverter在序列化和反序列化Date时仅支持“yyyy-MM-dd”格式。

以下XML配置,可达到和JavaConfg配置同样的效果:

<mvc:annotation-driven>
    <!--配置自定义的消息转换器-->
    <mvc:message-converters>
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
            <!--配置objectMapper-->
            <property name="objectMapper">
                <bean class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"
                      p:indentOutput="true"
                      p:simpleDateFormat="yyyy-MM-dd"/>
            </property>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

推荐使用JavaConfg的配置!

6 Spring MVC的DataBinder

和此前介绍的@ModelAttribute方法类似,Spring MVC的@InitBinder方法同样在控制器方法之前执行,并且同样支持与@RequestMapping控制器方法相同的许多参数。但是,它们通常使用WebDataBinder参数(用于注册类型转换器)和并且返回值为void。

和此前介绍的@ModelAttribute方法类似,@Controller类和@ControllerAdvice类中都可以定义@InitBinder方法,@Controller类中的@InitBinder方法默认支持范围是当前类的请求,@ControllerAdvice类中方法默认支持范围是所有的请求!

@InitBinder方法有如下作用:

  1. 将请求参数(即表单或查询数据)绑定到model对象,这和@ModelAttribute差不多,但不是它的主要功能。
  2. 通过注册类型转换器,用于将基于字符串的请求值(例如请求参数,路径变量,请求头,Cookie等)转换为控制器方法参数的目标类型。

每次的请求,在HandlerAdapter的handler方法内都会创建一个WebDataBinder对象:

在这里插入图片描述

创建WebDataBinder之后,会尝试添加预配置的conversionService,如果开启了Spring mvc的配置(通过@EnableWebMvc注解或者<mvc:annotation-driven/>标签,这个conversionService的配置我们在上面就讲过了)并且没有更改,那么默认就是一个DefaultFormattingConversionService,其内部封装了常见的全局可用的Converter和Formarter(Formatter会被转换为Converter),这个conversionService中的转换器是全局可用的

随后回调所有符合规则的@InitBinder方法,然后我们可以在该方法中对WebDataBinder对象中注册适用于当前请求的自定义的PropertyEditor和Formatter转换器(Formatter会被转换为PropertyEditor),但是这里注册的转换器将值绑定到该Binder对象本身,也就是说每一次请求中通过WebDataBinder的@InitBinder方法注册的转换器是不可复用的。

在解析时,首先使用注册的局部PropertyEditor来解析,然后在使用全局的conversionService来解析!

如下案例,有一个Controller:

@RestController
public class InitBinderController {

    @GetMapping("/initBinder")
    public void handle(Date date) {
        System.out.println(date);
    }
}

如果我们访问/initBinder?date=2012/12/12,结果如下:

Wed Dec 12 00:00:00 CST 2012

可以发现,可以实现字符串到时间的转换,这是因为默认的conversionService提供了这种格式的字符串到Date类型的Converter,如果我们尝试换一种格式呢?

如果我们访问/initBinder?date=2012-12-12,结果如下:

在这里插入图片描述

此时直接抛出异常了!因为字符串格式不匹配,此时我们可以自定义类型转换器,如上面所讲,我们可以定义三种转换器,如果在@InitBinder方法中注册,那么我们可以注册PropertyEditor和Formatter这两种类型的转换器!

对于这种时间字符串转换为Date的转换器,PropertyEditor和Formatter都已经提供了各自的实现,我们只需要传入给定的字符串模式即可,当然也可以自己实现!

如果我们注册PropertyEditor,那么如下声明:

@RestController
public class InitBinderController {

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        System.out.println("----initBinder------");
        //注册一个自定义的PropertyEditor
        //第一个参数表示转换后属性的类型,第二个参数是自定义的PropertyEditor的实例
        //这个CustomDateEditor是Spring内置的专门用于格式化时间的PropertyEditor,我们只需要设置时间字符串的模式即可
        binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));
    }

    @GetMapping("/initBinder")
    public void handle(Date date) {
        System.out.println(date);
    }
}

再次访问/initBinder?date=2012-12-12,结果如下:

----initBinder------
Wed Dec 12 00:00:00 CST 2012

成功的进行了转换!

如果我们注册Formatter,那么如下声明:

@InitBinder
public void initBinder(WebDataBinder binder) {
    System.out.println("----initBinder------");
    //注册一个自定义的Formatter
    //这个DateFormatter是Spring内置的专门用于格式化时间的Formatter
    //只需要在构造器参数中设置时间字符串的模式即可,这里并没有设置,因为DateFormatter内置了本地模式支持解析yyyy-MM-dd
    binder.addCustomFormatter(new DateFormatter());
}

再次访问/initBinder?date=2012-12-12,结果如下:

----initBinder------
Wed Dec 12 00:00:00 CST 2012

同样成功的进行了转换!

相关文章:

  1. spring.io/
  2. Spring Framework 5.x 学习
  3. Spring Framework 5.x 源码

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!