类型转换与数据绑定 — Converter / DataBinder
一句话定位:Spring 的类型转换与数据绑定是 Web 请求的"翻译官"——把用户提交的字符串参数翻译成 Java 对象能理解的类型,把对象属性绑定到表单字段,让前后端数据流通无障碍。
为什么需要类型转换与数据绑定?
HTTP 请求中的一切都是字符串:URL 参数、表单字段、Header 值。但 Java 方法参数可能是 Long、LocalDate、BigDecimal、枚举类型,甚至是复杂对象。Spring 的 DataBinder(数据绑定器) 和 Converter(转换器) 就是负责这个"翻译"工作的。
Converter 体系
核心接口
Spring 提供了两套转换接口:
| 接口 | 用途 | 示例 |
|---|---|---|
Converter<S, T> | 单向转换 | StringToIntegerConverter |
ConverterFactory<S, R> | 批量转换(一族类型) | StringToNumberConverterFactory |
GenericConverter | 复杂条件转换 | StringToEnumConverter |
Formatter<T> | 格式化(考虑 Locale) | DateFormatter、NumberFormatter |
自定义 Converter
乐途公司的商品 ID 有特殊格式:FX-10086,但数据库里存的是纯数字 10086。小崔写了一个 Converter:
@Component
public class FeixiangProductIdConverter implements Converter<String, Long> {
private static final String PREFIX = "FX-";
@Override
public Long convert(String source) {
if (source == null || source.isBlank()) {
return null;
}
if (source.startsWith(PREFIX)) {
return Long.valueOf(source.substring(PREFIX.length()));
}
// 也支持纯数字
return Long.valueOf(source);
}
}
注册到 Spring:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new FeixiangProductIdConverter());
}
}
Controller 中直接使用:
@GetMapping("/api/products/{productId}")
public Product getProduct(@PathVariable Long productId) {
// 请求 /api/products/FX-10086 时,自动转换为 10086L
return productService.findById(productId);
}
自定义 Formatter
乐途公司的价格显示需要保留两位小数,且支持千分位:
@Component
public class CurrencyFormatter implements Formatter<BigDecimal> {
@Override
public BigDecimal parse(String text, Locale locale) throws ParseException {
// 解析:去掉 ¥ 和逗号
String clean = text.replace("¥", "").replace(",", "").trim();
return new BigDecimal(clean);
}
@Override
public String print(BigDecimal object, Locale locale) {
// 格式化:¥1,888.88
return String.format(locale, "¥%,.2f", object);
}
}
DataBinder 详解
基本绑定流程
DataBinder 是 Spring 数据绑定的核心类,负责:
- 将请求参数名映射到对象属性名
- 调用 Converter 做类型转换
- 调用 Validator 做数据校验
@RestController
public class OrderController {
@PostMapping("/api/orders/form")
public Order createOrderFromForm(@ModelAttribute OrderForm form) {
// Spring 自动将请求参数绑定到 OrderForm 对象
return orderService.create(form);
}
}
自定义属性编辑器(PropertyEditor)
@InitBinder
public void initBinder(WebDataBinder binder) {
// 注册自定义编辑器
binder.registerCustomEditor(Date.class, new CustomDateEditor(
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"), true
));
// 指定允许绑定的字段(防止恶意字段注入)
binder.setAllowedFields("userId", "productId", "quantity", "remark");
// 必填字段
binder.setRequiredFields("userId", "productId", "quantity");
}
编程式使用 DataBinder
@Service
public class OrderImportService {
public OrderForm bindFromMap(Map<String, String> params) {
OrderForm form = new OrderForm();
DataBinder binder = new DataBinder(form);
binder.registerCustomEditor(Long.class, "productId", new FeixiangProductIdEditor());
MutablePropertyValues pvs = new MutablePropertyValues(params);
binder.bind(pvs);
// 获取绑定结果
BindingResult result = binder.getBindingResult();
if (result.hasErrors()) {
throw new IllegalArgumentException("参数绑定失败: " + result.getAllErrors());
}
return form;
}
}
乐途场景:订单表单绑定
乐途公司的运营后台需要导入批量订单,数据来自 Excel 解析后的 Map 列表:
public class OrderForm {
private Long userId;
private Long productId; // 支持 FX-10086 格式
private Integer quantity;
private LocalDate deliveryDate;
private BigDecimal discount; // 支持 ¥10.00 格式
private String remark;
// getters / setters...
}
配置转换器:
@Configuration
public class FeixiangWebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// 商品 ID 转换
registry.addConverter(new FeixiangProductIdConverter());
// 日期转换
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
registry.addFormatterForFieldType(LocalDate.class,
new TemporalAccessorFormatter(dateFormatter));
// 货币转换
registry.addFormatterForFieldType(BigDecimal.class, new CurrencyFormatter());
}
}
Controller 绑定:
@PostMapping("/api/orders/batch")
public List<Order> createBatchOrders(@RequestBody List<Map<String, String>> rows) {
return rows.stream()
.map(orderImportService::bindFromMap)
.map(orderService::create)
.collect(Collectors.toList());
}
注意事项
| 注意点 | 说明 |
|---|---|
| 类型转换失败 | 默认会抛出 TypeMismatchException,可被 @ExceptionHandler 捕获返回 400 |
| 字段安全 | 用 setAllowedFields() 白名单限制可绑定字段,防止恶意注入(如 isAdmin=true) |
| 空值处理 | 空字符串 "" 默认转换为 null,可用 binder.registerCustomEditor(String.class, new StringTrimmerEditor(true)) 控制 |
| 集合绑定 | 支持 List、Set、Map 绑定,参数名格式:items[0].name、params['key'] |
| 嵌套对象 | 支持多级嵌套绑定,如 address.city、address.zipCode |
常见面试题
Q1:Converter 和 Formatter 有什么区别?
Converter是通用类型转换(S → T),不考虑本地化。Formatter是特化的 Converter,专门处理String ↔ T,且考虑Locale(如日期、数字的本地化格式)。Spring MVC 的 Web 层优先使用 Formatter。
Q2:Spring 默认注册了哪些 Converter?
Spring 核心容器默认注册了 100+ 个转换器,覆盖:基本类型(String ↔ int/long/boolean)、集合类型、数组类型、枚举类型、
Properties、Resource等。Web 层额外注册了日期、数字的 Formatter。
Q3:@InitBinder 的作用范围是什么?
@InitBinder标注的方法只对当前 Controller 生效。如果想全局生效,在@ControllerAdvice中定义@InitBinder方法。
Q4:如何防止数据绑定中的安全漏洞(如字段注入攻击)?
使用
DataBinder.setAllowedFields()白名单限制可绑定字段,或setDisallowedFields()黑名单排除敏感字段(如password、isAdmin、role)。Spring Boot 2.x+ 默认已加强此方面的防护。