类型转换与格式化: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 统一注册,接口参数想怎么收就怎么收。

笔记加载中…