1. 概述

本文将深入探讨 Spring 的类型转换机制。Spring 开箱即用地提供了多种内置类型转换器,支持 StringIntegerBoolean 等基础类型之间的转换。除此之外,Spring 还提供了强大的类型转换 SPI,方便我们开发自定义转换器。

2. 内置转换器

先来看看 Spring 自带的转换器。以 StringInteger 为例:

@Autowired
ConversionService conversionService;

@Test
public void whenConvertStringToIntegerUsingDefaultConverter_thenSuccess() {
    assertThat(
      conversionService.convert("25", Integer.class)).isEqualTo(25);
}

只需注入 Spring 提供的 ConversionService,调用 convert() 方法即可。第一个参数是待转换值,第二个参数是目标类型。除了 StringInteger,Spring 还支持大量其他类型组合。

3. 自定义转换器

现在看个更复杂的例子:将 String 表示的 Employee 转换为 Employee 对象。

Employee 类如下:

public class Employee {

    private long id;
    private double salary;

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

String 格式为逗号分隔的 idsalary(如 "1,50000.00")。要创建自定义转换器,需实现 Converter<S, T> 接口并重写 convert() 方法:

public class StringToEmployeeConverter
  implements Converter<String, Employee> {

    @Override
    public Employee convert(String from) {
        String[] data = from.split(",");
        return new Employee(
          Long.parseLong(data[0]), 
          Double.parseDouble(data[1]));
    }
}

还没完!需要将 StringToEmployeeConverter 注册到 FormatterRegistry。通过实现 WebMvcConfigurer 并重写 addFormatters() 方法:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToEmployeeConverter());
    }
}

✅ 大功告成!新转换器已加入 ConversionService,使用方式与内置转换器完全一致:

@Test
public void whenConvertStringToEmployee_thenSuccess() {
    Employee employee = conversionService
      .convert("1,50000.00", Employee.class);
    Employee actualEmployee = new Employee(1, 50000.00);
    
    assertThat(conversionService.convert("1,50000.00", 
      Employee.class))
      .isEqualToComparingFieldByField(actualEmployee);
}

3.1. 隐式转换

除了显式调用 ConversionServiceSpring 还能在 Controller 方法中自动应用已注册的转换器

@RestController
public class StringToEmployeeConverterController {

    @GetMapping("/string-to-employee")
    public ResponseEntity<Object> getStringToEmployee(
      @RequestParam("employee") Employee employee) {
        return ResponseEntity.ok(employee);
    }
}

这是更自然的用法。测试一下效果:

@Test
public void getStringToEmployeeTest() throws Exception {
    mockMvc.perform(get("/string-to-employee?employee=1,2000"))
      .andDo(print())
      .andExpect(jsonPath("$.id", is(1)))
      .andExpect(jsonPath("$.salary", is(2000.0)))
}

测试会打印完整请求/响应信息。返回的 JSON 格式 Employee 对象如下:

{"id":1,"salary":2000.0}

4. 创建转换器工厂

还可以创建 ConverterFactory 按需生成转换器,这对 Enum 类型转换特别有用。

先看个简单的枚举:

public enum Modes {
    ALPHA, BETA;
}

接下来创建 StringToEnumConverterFactory,能生成任意 StringEnum 的转换器:

@Component
public class StringToEnumConverterFactory 
  implements ConverterFactory<String, Enum> {

    private static class StringToEnumConverter<T extends Enum> 
      implements Converter<String, T> {

        private Class<T> enumType;

        public StringToEnumConverter(Class<T> enumType) {
            this.enumType = enumType;
        }

        public T convert(String source) {
            return (T) Enum.valueOf(this.enumType, source.trim());
        }
    }

    @Override
    public <T extends Enum> Converter<String, T> getConverter(
      Class<T> targetType) {
        return new StringToEnumConverter(targetType);
    }
}

⚠️ 注意:虽然用 Modes 演示,但工厂类本身是通用的,能按需为任意 Enum 类型生成转换器

注册方式与之前相同:

@Override
public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(new StringToEmployeeConverter());
    registry.addConverterFactory(new StringToEnumConverterFactory());
}

现在 ConversionService 已支持 StringEnum 的转换:

@Test
public void whenConvertStringToEnum_thenSuccess() {
    assertThat(conversionService.convert("ALPHA", Modes.class))
      .isEqualTo(Modes.ALPHA);
}

5. 创建通用转换器

GenericConverter 提供更灵活的通用转换能力,但会牺牲部分类型安全性。

Integer/Double/StringBigDecimal 为例。无需写三个转换器,一个 GenericConverter 即可搞定。

首先定义支持的转换类型组合:

public class GenericBigDecimalConverter 
  implements GenericConverter {

@Override
public Set<ConvertiblePair> getConvertibleTypes () {

    ConvertiblePair[] pairs = new ConvertiblePair[] {
          new ConvertiblePair(Number.class, BigDecimal.class),
          new ConvertiblePair(String.class, BigDecimal.class)};
        return ImmutableSet.copyOf(pairs);
    }
}

然后重写 convert() 方法:

@Override
public Object convert (Object source, TypeDescriptor sourceType, 
  TypeDescriptor targetType) {

    if (sourceType.getType() == BigDecimal.class) {
        return source;
    }

    if(sourceType.getType() == String.class) {
        String number = (String) source;
        return new BigDecimal(number);
    } else {
        Number number = (Number) source;
        BigDecimal converted = new BigDecimal(number.doubleValue());
        return converted.setScale(2, BigDecimal.ROUND_HALF_EVEN);
    }
}

convert() 方法逻辑简单,但 TypeDescriptor 提供了获取源/目标类型细节的强大能力。

最后注册转换器:

@Override
public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(new StringToEmployeeConverter());
    registry.addConverterFactory(new StringToEnumConverterFactory());
    registry.addConverter(new GenericBigDecimalConverter());
}

使用方式与其他转换器一致:

@Test
public void whenConvertingToBigDecimalUsingGenericConverter_thenSuccess() {
    assertThat(conversionService
      .convert(Integer.valueOf(11), BigDecimal.class))
      .isEqualTo(BigDecimal.valueOf(11.00)
      .setScale(2, BigDecimal.ROUND_HALF_EVEN));
    assertThat(conversionService
      .convert(Double.valueOf(25.23), BigDecimal.class))
      .isEqualByComparingTo(BigDecimal.valueOf(Double.valueOf(25.23)));
    assertThat(conversionService.convert("2.32", BigDecimal.class))
      .isEqualTo(BigDecimal.valueOf(2.32));
}

6. 总结

本文通过多个示例演示了 Spring 类型转换系统的使用与扩展。关键点总结:

内置转换器:开箱即用,覆盖常见基础类型
自定义转换器:实现 Converter<S,T> 接口即可
隐式转换:Controller 参数自动应用转换器
转换器工厂:按需生成转换器,适合枚举等场景
通用转换器:灵活处理多类型转换,但需注意类型安全

完整源码可在 GitHub 获取。


原始标题:Guide to Spring Type Conversions

« 上一篇: Java Weekly, 第202期
» 下一篇: Groovy Bean定义详解