类型转换

Spring 提供了两套类型转换体系:旧的 PropertyEditor(Spring 2.x)和新的 ConversionService(Spring 3+,推荐)。Spring MVC 请求参数绑定、@Value 注入、Spring Data 查询参数均依赖这套机制。

核心接口

ConversionService(门面)
    │
    ├── Converter<S, T>           简单的 S → T 一对一转换
    ├── ConverterFactory<S, R>    S → R 的子类型(枚举/数字族)
    └── GenericConverter          多类型对,最灵活

Formatter<T>Converter 的 Locale 感知版本(String ↔ T),适合日期、货币等需要本地化格式的场景。


自定义 Converter

场景:字符串 → 枚举(按 code 字段)

// 枚举定义(有 code 字段)
public enum OrderStatus {
    PENDING(0), PAID(1), SHIPPED(2), COMPLETED(3), CANCELLED(4);
 
    private final int code;
 
    OrderStatus(int code) { this.code = code; }
 
    public int getCode() { return code; }
 
    public static OrderStatus fromCode(int code) {
        for (OrderStatus s : values()) {
            if (s.code == code) return s;
        }
        throw new IllegalArgumentException("未知状态码: " + code);
    }
}
 
// Converter 实现
@Component
public class IntegerToOrderStatusConverter
        implements Converter<Integer, OrderStatus> {
 
    @Override
    public OrderStatus convert(Integer source) {
        return OrderStatus.fromCode(source);
    }
}

注册到 ConversionService(@Component 配合 @EnableWebMvc 时 Spring Boot 会自动扫描):

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
 
    @Autowired
    private IntegerToOrderStatusConverter orderStatusConverter;
 
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(orderStatusConverter);
    }
}

使用:

// GET /orders?status=1  自动转换为 OrderStatus.PAID
@GetMapping("/orders")
public List<Order> list(@RequestParam OrderStatus status) { ... }

场景:ConverterFactory 批量处理枚举

为所有实现 CodeEnum 接口的枚举统一注册转换器,无需每个枚举单独写:

// 统一接口
public interface CodeEnum {
    int getCode();
}
 
// 枚举实现
public enum PayType implements CodeEnum {
    ALIPAY(1), WECHAT(2), BANK(3);
    private final int code;
    PayType(int code) { this.code = code; }
    public int getCode() { return code; }
}
 
// ConverterFactory
@Component
public class IntegerToCodeEnumConverterFactory
        implements ConverterFactory<Integer, CodeEnum> {
 
    @Override
    public <T extends CodeEnum> Converter<Integer, T> getConverter(Class<T> targetType) {
        return source -> {
            for (T constant : targetType.getEnumConstants()) {
                if (constant.getCode() == source) return constant;
            }
            throw new IllegalArgumentException(
                "枚举 " + targetType.getSimpleName() + " 不存在 code=" + source);
        };
    }
}
 
// 注册
@Override
public void addFormatters(FormatterRegistry registry) {
    registry.addConverterFactory(new IntegerToCodeEnumConverterFactory());
}

自定义 Formatter

适合 String ↔ 对象的双向转换,且需要感知 Locale(如日期格式):

@Component
public class MoneyFormatter implements Formatter<BigDecimal> {
 
    @Override
    public BigDecimal parse(String text, Locale locale) throws ParseException {
        // 去掉货币符号和千分位分隔符
        String cleaned = text.replaceAll("[¥,$,\\s,]", "").replace(",", "");
        return new BigDecimal(cleaned);
    }
 
    @Override
    public String print(BigDecimal object, Locale locale) {
        NumberFormat fmt = NumberFormat.getCurrencyInstance(locale);
        return fmt.format(object);
    }
}
// GET /products?maxPrice=¥1,000.00  → BigDecimal(1000)
@GetMapping("/products")
public List<Product> search(@RequestParam BigDecimal maxPrice) { ... }

日期时间转换

application.yml 全局配置(推荐)

spring:
  mvc:
    format:
      date: yyyy-MM-dd
      date-time: yyyy-MM-dd HH:mm:ss
      time: HH:mm:ss
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Shanghai

@DateTimeFormat(字段/参数级别)

// URL 参数
@GetMapping("/orders")
public List<Order> list(
    @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate,
    @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant createdAt
) { ... }
 
// 表单/请求体(非 JSON)
public class OrderQuery {
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime startTime;
}

Jackson JSON 日期(@JsonFormat

public class Order {
    // JSON 序列化/反序列化
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
    private LocalDateTime createdAt;
 
    // 返回时间戳(毫秒)
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    private LocalDateTime updatedAt;
}

@DateTimeFormat 负责 URL 参数/表单绑定,@JsonFormat 负责 JSON body,两者互不干扰,需要时可同时标注。


Spring MVC 的完整转换链

HTTP 请求参数(字符串)
        │
        ▼ HandlerMethodArgumentResolver
   ConversionService.convert(String, TargetType)
        │
        ├─ 内置 Converter(String→Integer, String→Boolean 等)
        ├─ 自定义 Converter / ConverterFactory / Formatter
        └─ @DateTimeFormat 驱动的日期 Formatter
        │
        ▼
   Controller 方法参数(目标类型)

@RequestBody 的 JSON 反序列化走 Jackson,不经过 ConversionService,通过 HttpMessageConverter 处理,详见 消息转换


@InitBinder(遗留方案)

Spring MVC 早期使用 WebDataBinder + PropertyEditor,现代项目推荐用 ConversionService,仅在维护旧代码时了解即可:

@RestControllerAdvice
public class GlobalBinderConfig {
 
    // 全局注册,适用所有 Controller
    @InitBinder
    public void initBinder(WebDataBinder binder) {
        // 日期格式(PropertyEditor 方式)
        binder.registerCustomEditor(Date.class,
            new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), true));
 
        // 字符串去除首尾空格
        binder.registerCustomEditor(String.class,
            new StringTrimmerEditor(true));
    }
}

Jackson 自定义序列化/反序列化

针对 JSON body 的类型处理,使用 Jackson 的 JsonSerializer / JsonDeserializer

// Long 转 String(防止前端 JS 精度丢失)
public class LongToStringSerializer extends JsonSerializer<Long> {
    @Override
    public void serialize(Long value, JsonGenerator gen,
                          SerializerProvider serializers) throws IOException {
        gen.writeString(value.toString());
    }
}
 
public class Order {
    @JsonSerialize(using = LongToStringSerializer.class)
    private Long id;
}
 
// 全局配置(所有 Long 字段)
@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
    return builder -> builder.serializerByType(
        Long.class, new LongToStringSerializer());
}

Jackson 序列化详见 消息转换


常见问题

问题原因解决方案
@RequestParam 枚举转换失败Spring 默认按 name() 匹配注册自定义 Converter
日期参数 400格式不匹配@DateTimeFormat 或配置 spring.mvc.format.date
JSON 日期格式错误Jackson 默认序列化为时间戳@JsonFormat 或全局 date-format
Long 精度丢失JS Number 最大 53 位@JsonSerialize(using = LongToStringSerializer.class)
转换器未生效未注册到 FormatterRegistry实现 WebMvcConfigurer.addFormatters 或加 @Component

相关链接

  • 消息转换HttpMessageConverter,JSON / XML body 的读写
  • 参数校验@Valid 与类型转换的执行顺序
  • MVCHandlerMethodArgumentResolver 与参数绑定流程
  • 属性绑定@ConfigurationProperties 中的类型转换
  • 国际化Formatter 的 Locale 参数与本地化格式