SpringMVC 数据转换器与格式化器:`Converter` 与 `Formatter`

SpringMVC 数据转换器与格式化器:ConverterFormatter,一场数据变形记

各位看官,大家好!今天咱不聊风花雪月,咱们来聊聊 SpringMVC 框架里那些“数据变形金刚”—— ConverterFormatter。 它们就像是默默无闻的幕后英雄,负责把前端传来的五花八门的数据,规整成后端程序看得懂的格式;又或者把后端的数据,打扮得漂漂亮亮,让前端用户赏心悦目。

说白了,它们就是数据类型转换和格式化的小能手。 但是,别看名字挺像,功能也有些重叠,它们之间还是有细微的差别和各自的适用场景。 别急,咱们这就来抽丝剥茧,把它们扒个精光!

一、数据变形的必要性:为啥需要 ConverterFormatter

想象一下,你去餐厅点了一份“宫保鸡丁”,服务员直接把鸡肉、花生米、辣椒、葱段一股脑儿地扔给你,让你自己组装。 你肯定会觉得:“这服务也太差劲了吧!”

同样,如果前端页面传过来的数据,后端程序没法直接用,那也得抓瞎。 比如,前端传来的日期是字符串 "2023-10-27",但后端需要 java.util.Date 对象才能进行业务处理。 这时候,就需要一个“服务员”来把这个字符串“组装”成 Date 对象,这个“服务员”就是 Converter 或者 Formatter

更进一步,假设后端算出了一个金额,比如12345.6789,但是前端希望展示成 "¥12,345.68"。 这时候,又需要另一个“服务员”把这个数字进行格式化,变成用户友好的字符串。

所以,ConverterFormatter 的存在,就是为了解决以下问题:

  • 类型不匹配: 前端传来的数据类型和后端需要的类型不一致。
  • 格式不统一: 前端需要展示的数据格式和后端存储的数据格式不一致。
  • 简化代码: 如果没有它们,我们需要在每个 Controller 方法里手动进行类型转换和格式化,代码会变得冗长且难以维护。

二、Converter:数据类型转换的魔术师

Converter 的主要职责是进行数据类型之间的转换。 它接收一个类型的对象作为输入,然后把它转换成另一个类型的对象作为输出。 就像一个“炼金术士”,把一种“金属”炼成另一种“金属”。

1. Converter 接口

Converter 接口非常简单,只有一个方法:

package org.springframework.core.convert.converter;

public interface Converter<S, T> {

    /**
     * Convert the source object of type {@code S} to target type {@code T}.
     * @param source the source object to convert, which must be an instance of {@code S} (never {@code null})
     * @return the converted object, which must be an instance of {@code T} (potentially {@code null})
     * @throws IllegalArgumentException if the source cannot be converted to the desired target type
     */
    T convert(S source);

}
  • S:源类型,即需要转换的类型。
  • T:目标类型,即转换后的类型。
  • convert(S source):转换方法,接收源对象 source,返回目标对象。

2. 自定义 Converter

要使用 Converter,我们需要实现这个接口,并提供自己的转换逻辑。 举个例子,假设我们需要把字符串转换成一个自定义的 User 对象:

public class User {
    private String username;
    private int age;

    // 省略构造方法、Getter 和 Setter
    public User(String username, int age) {
        this.username = username;
        this.age = age;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
import org.springframework.core.convert.converter.Converter;

public class StringToUserConverter implements Converter<String, User> {

    @Override
    public User convert(String source) {
        if (source == null || source.isEmpty()) {
            return null;
        }

        String[] parts = source.split(",");
        if (parts.length != 2) {
            throw new IllegalArgumentException("Invalid user string format. Expected 'username,age'");
        }

        String username = parts[0];
        int age = Integer.parseInt(parts[1]);

        return new User(username, age);
    }
}

在这个例子中,StringToUserConverter 实现了 Converter<String, User> 接口,它的 convert 方法把一个字符串(例如 "zhangsan,20")转换成一个 User 对象。

3. 注册 Converter

自定义了 Converter 之后,还需要把它注册到 Spring 容器中,才能让 SpringMVC 知道它的存在。 有两种常用的注册方式:

  • XML 配置:

    <mvc:annotation-driven conversion-service="conversionService"/>
    
    <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <bean class="com.example.converter.StringToUserConverter"/>
            </set>
        </property>
    </bean>
  • Java Config:

    import org.springframework.context.annotation.Configuration;
    import org.springframework.format.FormatterRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    
        @Override
        public void addFormatters(FormatterRegistry registry) {
            registry.addConverter(new StringToUserConverter());
        }
    }

4. 使用 Converter

注册了 Converter 之后,就可以在 Controller 方法中使用它了。 比如,我们可以把前端传来的字符串直接绑定到 User 对象上:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class UserController {

    @GetMapping("/user")
    @ResponseBody
    public User getUser(@RequestParam("userInfo") User user) {
        return user;
    }
}

在这个例子中,前端只需要传递一个名为 "userInfo" 的参数,其值为 "zhangsan,20",SpringMVC 就会自动使用 StringToUserConverter 把这个字符串转换成 User 对象,并注入到 getUser 方法的参数中。

5. Converter 的变体:GenericConverterConditionalConverter

除了 Converter 接口之外,Spring 还提供了两个它的变体:GenericConverterConditionalConverter

  • GenericConverter 提供了更灵活的类型转换能力。 它允许你根据源类型和目标类型的上下文信息来决定是否进行转换。 比如,你可以根据注解或者其他条件来选择不同的转换逻辑。

    package org.springframework.core.convert.converter;
    
    import org.springframework.core.convert.TypeDescriptor;
    
    import java.util.Set;
    
    public interface GenericConverter {
    
        /**
         * Return the set of source-target type pairs that this converter can convert between.
         * <p>Each entry is a {@link ConvertiblePair} describing a source type and a target
         * that this converter can convert between.
         * <p>For example: <code>{String.class, Integer.class}</code> indicates this converter
         * can convert from {@link String} to {@link Integer}.
         * <p>Implementations may return {@code null} to indicate all source-target pairings are possible.
         * @return the source-target type pairs that this converter can convert between
         */
        Set<ConvertiblePair> getConvertibleTypes();
    
        /**
         * Convert the source object to the target type.
         * @param source the source object to convert (may be {@code null})
         * @param sourceType the type descriptor of the source object
         * @param targetType the type descriptor of the target type
         * @return the converted object (may be {@code null})
         */
        Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
    
        /**
         * Holder for a source-target type pair.
         */
        final class ConvertiblePair {
    
            private final Class<?> sourceType;
    
            private final Class<?> targetType;
    
            public ConvertiblePair(Class<?> sourceType, Class<?> targetType) {
                this.sourceType = sourceType;
                this.targetType = targetType;
            }
    
            public Class<?> getSourceType() {
                return this.sourceType;
            }
    
            public Class<?> getTargetType() {
                return this.targetType;
            }
    
            @Override
            public boolean equals(Object other) {
                if (this == other) {
                    return true;
                }
                if (!(other instanceof ConvertiblePair)) {
                    return false;
                }
                ConvertiblePair otherPair = (ConvertiblePair) other;
                return (this.sourceType.equals(otherPair.sourceType) && this.targetType.equals(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());
            }
        }
    
    }
  • ConditionalConverter 也是一个接口,它允许你根据特定的条件来决定是否使用某个 Converter。 比如,你可以根据 HTTP 请求头或者其他上下文信息来选择不同的转换逻辑。ConditionalConverter 接口通常与 GenericConverter 一起使用。

    package org.springframework.core.convert.converter;
    
    /**
     * A converter that executes only when a specific condition is {@linkplain #matches met}.
     *
     * @author Phillip Webb
     * @since 3.2
     */
    public interface ConditionalConverter {
    
        /**
         * Should the conversion from {@code sourceType} to {@code targetType} occur?
         * @param sourceType the type descriptor of the value to convert.
         * @param targetType the type descriptor of the type to convert to.
         * @return {@code true} if conversion should occur.
         */
        boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
    }

三、Formatter:数据格式化的艺术家

Formatter 的主要职责是对数据进行格式化,将其转换成用户友好的字符串。 它就像一个“化妆师”,把数据打扮得漂漂亮亮,让用户赏心悦目。

1. Formatter 接口

Formatter 接口继承自 PrinterParser 接口:

package org.springframework.format;

import java.text.ParseException;
import java.util.Locale;

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

}
  • Printer<T> 负责把对象 T 格式化成字符串。

    package org.springframework.format;
    
    import java.util.Locale;
    
    public interface Printer<T> {
    
        /**
         * Print the object of type T for display using the specified {@link Locale}.
         * @param object the object to print
         * @param locale the current locale
         * @return the formatted text string
         */
        String print(T object, Locale locale);
    }
  • Parser<T> 负责把字符串解析成对象 T

    package org.springframework.format;
    
    import java.text.ParseException;
    import java.util.Locale;
    
    public interface Parser<T> {
    
        /**
         * Parse a text String to produce an instance of T.
         * @param text the text to parse
         * @param locale the current locale
         * @return an instance of T
         * @throws ParseException if the text cannot be parsed
         */
        T parse(String text, Locale locale) throws ParseException;
    }

2. 自定义 Formatter

要使用 Formatter,我们需要实现 Formatter 接口,并提供自己的格式化和解析逻辑。 举个例子,假设我们需要格式化一个金额:

import org.springframework.format.Formatter;

import java.math.BigDecimal;
import java.text.ParseException;
import java.util.Locale;

public class MoneyFormatter implements Formatter<BigDecimal> {

    @Override
    public String print(BigDecimal object, Locale locale) {
        return "¥" + object.setScale(2, BigDecimal.ROUND_HALF_UP).toString();
    }

    @Override
    public BigDecimal parse(String text, Locale locale) throws ParseException {
        try {
            return new BigDecimal(text.substring(1)); // 移除 "¥" 符号
        } catch (NumberFormatException e) {
            throw new ParseException("Invalid money format: " + text, 0);
        }
    }
}

在这个例子中,MoneyFormatter 实现了 Formatter<BigDecimal> 接口,它的 print 方法把 BigDecimal 对象格式化成 "¥123.45" 这样的字符串,parse 方法把 "¥123.45" 这样的字符串解析成 BigDecimal 对象。

3. 注册 Formatter

注册 Formatter 的方式和注册 Converter 类似,也有两种常用的方式:

  • XML 配置:

    <mvc:annotation-driven conversion-service="conversionService"/>
    
    <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
        <property name="formatters">
            <set>
                <bean class="com.example.formatter.MoneyFormatter"/>
            </set>
        </property>
    </bean>
  • Java Config:

    import org.springframework.context.annotation.Configuration;
    import org.springframework.format.FormatterRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    
        @Override
        public void addFormatters(FormatterRegistry registry) {
            registry.addFormatter(new MoneyFormatter());
        }
    }

4. 使用 Formatter

注册了 Formatter 之后,就可以在 Controller 方法中使用它了。 有两种常用的方式:

  • @NumberFormat@DateTimeFormat 注解: Spring 提供了 @NumberFormat@DateTimeFormat 注解,可以方便地对数字和日期进行格式化。

    import org.springframework.format.annotation.NumberFormat;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import java.math.BigDecimal;
    
    @Controller
    public class MoneyController {
    
        @GetMapping("/money")
        @ResponseBody
        public String getMoney(@NumberFormat(pattern = "#,###.00") BigDecimal money) {
            return "Money: " + money;
        }
    }

    在这个例子中,@NumberFormat(pattern = "#,###.00") 注解会把 BigDecimal 对象格式化成 "1,234.56" 这样的字符串。

  • FormattingConversionServiceFactoryBean 如果你需要更灵活的格式化配置,可以使用 FormattingConversionServiceFactoryBean。 它可以让你自定义格式化规则,并把它们应用到整个应用程序中。

5. AnnotationFormatterFactory:简化 Formatter 的创建

如果你的 Formatter 需要根据注解的属性来动态地生成格式化规则,可以使用 AnnotationFormatterFactory。 它可以让你把注解和 Formatter 关联起来,从而简化 Formatter 的创建过程。

四、Converter vs Formatter:傻傻分不清楚?

看到这里,你可能会觉得 ConverterFormatter 的功能很相似,都是用来进行数据转换的。 那么,它们之间到底有什么区别呢?

特性 Converter Formatter
主要职责 数据类型转换 数据格式化
接口 ConverterGenericConverterConditionalConverter FormatterPrinterParser
应用场景 类型不匹配的转换 需要特定格式的展示和解析
是否需要 Locale 不需要 需要,因为格式化通常和地区有关
侧重点 类型转换 格式化和解析,强调用户体验
常用注解 @NumberFormat@DateTimeFormat

简单来说,Converter 关注的是数据类型的转换,而 Formatter 关注的是数据的格式化。 Converter 就像一个“翻译”,把一种语言翻译成另一种语言;而 Formatter 就像一个“美妆师”,把人打扮得漂漂亮亮。

五、总结:数据变形的艺术

ConverterFormatter 是 SpringMVC 框架中非常重要的两个组件,它们负责把前端传来的数据转换成后端程序需要的格式,又把后端的数据格式化成前端用户友好的格式。 掌握了它们,你就可以轻松地处理各种数据变形的需求,让你的应用程序更加健壮和易用。

希望这篇文章能帮助你更好地理解 ConverterFormatter 的原理和使用方法。 记住,它们就像是数据变形的魔术师,让你的数据在不同的场景下都能焕发出新的光彩!

最后,祝各位看官编程愉快,早日成为数据变形的大师!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注