类型转换
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 |