1. 简介

Spring 在底层大量使用 属性编辑器(Property Editor) 来实现 String 类型与自定义对象类型之间的自动转换,其基础是 Java Beans 规范中的 PropertyEditor 机制。

本文将通过两个典型场景,带你掌握:

✅ 自动发现的属性编辑器绑定
✅ 手动注册的自定义属性编辑器绑定

这两种方式在实际开发中非常实用,尤其是在处理 URL 路径参数、表单提交等需要类型转换的场景下,能极大简化代码逻辑。


2. 自动属性编辑器绑定

原理说明

JavaBeans 的内省机制支持一种“约定优于配置”的自动发现策略:
只要你的 PropertyEditor 类与目标类 位于同一包下,并且类名是目标类名 + Editor 后缀,Spring 就会自动注册它。

例如:

  • 目标类:CreditCard
  • 编辑器类:CreditCardEditor

满足这两个条件后,无需任何额外配置,Spring 会在类型转换时自动使用该编辑器。

实战示例

我们来模拟一个需求:前端传入格式化的信用卡号(如 1234-1234-1111-0019),后端自动解析为 CreditCard 对象。

✅ 定义模型类

public class CreditCard {

    private String rawCardNumber;
    private Integer bankIdNo;      // 前6位:发卡行标识
    private Integer accountNo;     // 第7到15位:账户号
    private Integer checkCode;     // 最后一位:校验码

    // 标准构造函数、getter、setter 省略
}

✅ 编写属性编辑器

编辑器需继承 PropertyEditorSupport,并重写两个核心方法:

  • setAsText(String):字符串 → 对象
  • getAsText():对象 → 字符串(序列化用)
public class CreditCardEditor extends PropertyEditorSupport {

    @Override
    public String getAsText() {
        CreditCard creditCard = (CreditCard) getValue();
        return creditCard == null ? "" : creditCard.getRawCardNumber();
    }
    
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        if (!StringUtils.hasLength(text)) {
            setValue(null);
        } else {
            CreditCard creditCard = new CreditCard();
            creditCard.setRawCardNumber(text);
            
            String cardNo = text.replaceAll("-", "");
            if (cardNo.length() != 16) {
                throw new IllegalArgumentException("Credit card format should be xxxx-xxxx-xxxx-xxxx");
            }
            
            try {
                creditCard.setBankIdNo(Integer.valueOf(cardNo.substring(0, 6)));
                creditCard.setAccountNo(Integer.valueOf(cardNo.substring(6, cardNo.length() - 1)));
                creditCard.setCheckCode(Integer.valueOf(cardNo.substring(cardNo.length() - 1)));
            } catch (NumberFormatException nfe) {
                throw new IllegalArgumentException(nfe);
            }
            
            setValue(creditCard);
        }
    }
}

⚠️ 注意:setValue() 是父类方法,用于设置转换后的结果对象,必须调用!

✅ 接口定义

Spring 会自动识别 CreditCardEditor 并完成绑定:

@GetMapping(value = "/credit-card/{card-no}", produces = MediaType.APPLICATION_JSON_VALUE)
public CreditCard parseCreditCardNumber(@PathVariable("card-no") CreditCard creditCard) {
    return creditCard;
}

✅ 请求示例

请求 URL:

/property-editor/credit-card/1234-1234-1111-0019

响应结果:

{
    "rawCardNumber": "1234-1234-1111-0019",
    "bankIdNo": 123412,
    "accountNo": 341111001,
    "checkCode": 9
}

✅ 效果:路径变量直接转为复杂对象,Controller 层代码干净利落。


3. 自定义属性编辑器绑定

使用场景

当你的编辑器和目标类不在同一个包,或命名不符合 XXXEditor 规范时,自动发现机制失效。这时就需要手动注册。

典型场景包括:

  • 第三方类无法修改包结构
  • 多个编辑器共用一个类型
  • 想要更灵活的控制绑定逻辑

实战示例

我们来处理一个简单的类型转换:将路径中的字符串转为首字母大写的 ExoticType 对象。

✅ 定义模型类

public class ExoticType {
    private String name;
    
    // 构造函数、getter、setter 省略
}

✅ 编写自定义编辑器

public class CustomExoticTypeEditor extends PropertyEditorSupport {

    @Override
    public String getAsText() {
        ExoticType exoticType = (ExoticType) getValue();
        return exoticType == null ? "" : exoticType.getName();
    }
    
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        ExoticType exoticType = new ExoticType();
        exoticType.setName(text.toUpperCase());  // 强制转大写
        
        setValue(exoticType);
    }
}

✅ 手动注册编辑器

使用 @InitBinder 注解在 Controller 中注册绑定规则:

@InitBinder
public void initBinder(WebDataBinder binder) {
    binder.registerCustomEditor(ExoticType.class, new CustomExoticTypeEditor());
}

📌 @InitBinder 方法会在每次请求前执行,用于设置数据绑定规则。
📌 registerCustomEditor() 明确告诉 Spring:遇到 ExoticType 类型时,用 CustomExoticTypeEditor 处理。

✅ 接口定义

@GetMapping(value = "/exotic-type/{value}", produces = MediaType.APPLICATION_JSON_VALUE)
public ExoticType parseExoticType(@PathVariable("value") ExoticType exoticType) {
    return exoticType;
}

✅ 请求示例

请求 URL:

/property-editor/exotic-type/passion-fruit

响应结果:

{
    "name": "PASSION-FRUIT"
}

✅ 成功实现自动转换,并应用了业务逻辑(转大写)。


4. 总结

方式 是否需要手动注册 适用场景
✅ 自动绑定 ❌ 不需要 编辑器与目标类同包且命名规范
✅ 自定义绑定 ✅ 需要 @InitBinder 灵活控制、跨包、第三方类等

关键点回顾

  • 🔧 PropertyEditorSupport 是编写编辑器的基础
  • 🔄 setAsText()getAsText() 必须实现
  • 📦 自动发现依赖 包名一致 + Editor 后缀
  • 🛠 @InitBinder + registerCustomEditor() 是手动绑定的黄金组合
  • 💡 适用于 @RequestParam, @PathVariable, 表单字段等多种场景

踩坑提醒:别忘了调用 setValue(),否则转换结果为空!

本文完整代码已托管至 GitHub:

👉 https://github.com/example/spring-boot-data-demo

这类机制虽然属于“老派”API,但在某些场景下依然简单粗暴有效,值得集合备用。


原始标题:Spring Custom Property Editor