类型转换与格式化:ConversionService 与 Formatter 自定义
HTTP 参数全是字符串,而方法形参可能是 Integer、LocalDate 甚至自定义类型,中间靠类型转换系统完成。Spring 的核心是 ConversionService,它把老旧的 PropertyEditor 和新式 Converter/Formatter 统一管理起来。
转换体系三个角色
| 角色 | 用途 | 典型实现 |
|---|---|---|
| ConversionService | 统一入口,负责全部类型转换 | DefaultFormattingConversionService |
| Converter<S,T> | 无状态双向通用转换(任何场景) | String → 枚举、DTO → DTO |
| Formatter<T> | 面向展示的「字符串 ↔ 对象」(含本地化) | 日期、数字格式化 |
@EnableWebMvc 默认给 MVC 装配的是 DefaultFormattingConversionService(本身是 FormattingConversionService 子类),表单绑定、@RequestParam、@PathVariable 的转换都走它。
注册自定义 Converter
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new PhoneConverter());
}
}
实现 Converter,把外部传入的 138xxxx 归一化成一个值对象:
public class PhoneConverter implements Converter<String, PhoneNumber> {
@Override
public PhoneNumber convert(String source) {
return new PhoneNumber(source.replaceAll("\\s+", "")); // 去掉空格
}
}
// GET /api/call?phone=138 0013 8000 → PhoneNumber("13800138000")
Formatter:带本地化的字符串互转
Formatter 有两个方向:parse(字符串 → 对象,进参时)与 print(对象 → 字符串,渲染/展示时):
public class MoneyFormatter implements Formatter<Money> {
@Override
public Money parse(String text, Locale locale) {
return new Money(new BigDecimal(text)); // "19.90" → Money
}
@Override
public String print(Money money, Locale locale) {
return money.getValue().toPlainString(); // Money → "19.90"
}
}
把它注册进 FormatterRegistry 后,@RequestParam Money price 就能直接绑定。
注解式格式化:@DateTimeFormat / @NumberFormat
约束属性上标注即可,由格式化系统读取注解执行:
public class EventDTO {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm")
private LocalDateTime startTime;
@NumberFormat(style = NumberFormat.Style.CURRENCY)
private BigDecimal budget;
}
类型转换失败的表现
转换抛异常时,MVC 把它包装成绑定/类型不匹配错误:@RequestParam/@PathVariable 场景表现为 MethodArgumentTypeMismatchException(400);表单对象场景进入 BindingResult 的 fieldErrors,不会直接抛 500。
老机制与新机制的关系
- PropertyEditor 是 JDK 老方案(单线程状态不安全),Spring 里用 WebDataBinder 的 registerCustomEditor 注册,正在被 Converter/Formatter 取代。
- @InitBinder 可以在单个控制器内补充「该控制器专用」的编辑器与校验器:
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new MoneyFormatter()); // 仅本控制器生效
}
选择建议
- 字符串与业务值对象的通用转换 → Converter。
- 展示层字符串格式化(含 Locale)→ Formatter,注册一次全局生效。
- 仅某个控制器用 → @InitBinder。
- 日期时间:表单/参数绑定用 @DateTimeFormat;JSON 序列化请用 Jackson 的 JavaTimeModule 与 @JsonFormat,两者是两套独立机制,别混用。
转换与格式化都是「字符串进出」的适配层。设计上让 Converter 只管数据、Formatter 管展示,配合 FormatterRegistry 统一注册,接口参数想怎么收就怎么收。