SpringMVC 数据转换器与格式化器:Converter
与 Formatter
,一场数据变形记
各位看官,大家好!今天咱不聊风花雪月,咱们来聊聊 SpringMVC 框架里那些“数据变形金刚”—— Converter
和 Formatter
。 它们就像是默默无闻的幕后英雄,负责把前端传来的五花八门的数据,规整成后端程序看得懂的格式;又或者把后端的数据,打扮得漂漂亮亮,让前端用户赏心悦目。
说白了,它们就是数据类型转换和格式化的小能手。 但是,别看名字挺像,功能也有些重叠,它们之间还是有细微的差别和各自的适用场景。 别急,咱们这就来抽丝剥茧,把它们扒个精光!
一、数据变形的必要性:为啥需要 Converter
和 Formatter
?
想象一下,你去餐厅点了一份“宫保鸡丁”,服务员直接把鸡肉、花生米、辣椒、葱段一股脑儿地扔给你,让你自己组装。 你肯定会觉得:“这服务也太差劲了吧!”
同样,如果前端页面传过来的数据,后端程序没法直接用,那也得抓瞎。 比如,前端传来的日期是字符串 "2023-10-27",但后端需要 java.util.Date
对象才能进行业务处理。 这时候,就需要一个“服务员”来把这个字符串“组装”成 Date
对象,这个“服务员”就是 Converter
或者 Formatter
。
更进一步,假设后端算出了一个金额,比如12345.6789,但是前端希望展示成 "¥12,345.68"。 这时候,又需要另一个“服务员”把这个数字进行格式化,变成用户友好的字符串。
所以,Converter
和 Formatter
的存在,就是为了解决以下问题:
- 类型不匹配: 前端传来的数据类型和后端需要的类型不一致。
- 格式不统一: 前端需要展示的数据格式和后端存储的数据格式不一致。
- 简化代码: 如果没有它们,我们需要在每个 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
的变体:GenericConverter
和 ConditionalConverter
除了 Converter
接口之外,Spring 还提供了两个它的变体:GenericConverter
和 ConditionalConverter
。
-
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
接口继承自 Printer
和 Parser
接口:
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
:傻傻分不清楚?
看到这里,你可能会觉得 Converter
和 Formatter
的功能很相似,都是用来进行数据转换的。 那么,它们之间到底有什么区别呢?
特性 | Converter |
Formatter |
---|---|---|
主要职责 | 数据类型转换 | 数据格式化 |
接口 | Converter ,GenericConverter ,ConditionalConverter |
Formatter ,Printer ,Parser |
应用场景 | 类型不匹配的转换 | 需要特定格式的展示和解析 |
是否需要 Locale | 不需要 | 需要,因为格式化通常和地区有关 |
侧重点 | 类型转换 | 格式化和解析,强调用户体验 |
常用注解 | 无 | @NumberFormat ,@DateTimeFormat |
简单来说,Converter
关注的是数据类型的转换,而 Formatter
关注的是数据的格式化。 Converter
就像一个“翻译”,把一种语言翻译成另一种语言;而 Formatter
就像一个“美妆师”,把人打扮得漂漂亮亮。
五、总结:数据变形的艺术
Converter
和 Formatter
是 SpringMVC 框架中非常重要的两个组件,它们负责把前端传来的数据转换成后端程序需要的格式,又把后端的数据格式化成前端用户友好的格式。 掌握了它们,你就可以轻松地处理各种数据变形的需求,让你的应用程序更加健壮和易用。
希望这篇文章能帮助你更好地理解 Converter
和 Formatter
的原理和使用方法。 记住,它们就像是数据变形的魔术师,让你的数据在不同的场景下都能焕发出新的光彩!
最后,祝各位看官编程愉快,早日成为数据变形的大师!