分类
spring

了解Spring类型转换

spring-type-conversions

译者注

原文

https://www.baeldung.com/spring-type-conversions

Demo

https://github.com/eugenp/tutorials/tree/master/spring-boot-modules/spring-boot-basic-customization-2

一、介绍

在这篇文章中,我们来学习Spring类型转换。
Spring为内建的类型提供了各种开箱即用的转换器,这意味着我们可以轻松的转换基本类型,例如String、Integer、Boolean或一些其他的类型。

除此之外,Spring也提供了一个固定的类型转换器SPI,以便我们用来自定义我们的转换器。

二、内建转换器

我们从Spring提供的开箱即用的转换器开始,先看看Spring内置的整数转换器:

@Autowired
ConversionService conversionService;

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

我们需要做的就是:自动装入(autowire)Spring提供的转换器服务ConversionService,并且调用这个转换方法convert()。第一个参数是“我们想要转换的值”,第二个参数是“我们想要转换的目标类型”

此外,还可以从Spring转换为Integer,还有多种不同的组合可以使用。

3. 创建自定义转换器

我们看一个转换的例子,在这个例子中,我们把代表Employee对象的字符串转换为Employee实例对象。

先给出Employee类:

public class Employee {

    private long id;
    private double salary;

    // standard constructors, getters, setters
}

这个字符串的形式是“逗号分隔值”,分别表示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中,来告诉Spring我们用到了这个类。具体的做法就是实现WebMvcConfigurer类,并且重写Formatters()方法:

@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 隐式转换

除了使用转换服务ConversionService来实现显式转换,Spring能在控制器方法中,使用所有已注册的转换器来实现隐式转换:

@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,这在创建枚举类型转换器的时候尤其有用。

我们来看一个简单的枚举:

public enum Modes {
    ALPHA, BETA;
}

接下来,创建一个“字符串到枚举类型转换器工厂”StringToEnumConverterFactory,这个工厂可以创建字符串到枚举的转换器:

@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去演示用法,但我们在StringToEnumConverterFactory工厂的任何地方都没有提及这个枚举。我们这个工厂类会按需创建对于任何枚举类型的转换器。

下一步是注册这个工厂类,操作就像注册我们的Converter转换器一样,例子如下:

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

现在这个转换服务ConversionService可以把字符串转换成枚举了:

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

5. 创建一个通用转换器

通用转换器GenericConverter让我们在牺牲安全性的前提下,获得更大的灵活性,来创建一个更通用的转换器。
我们来设计一个可以转换Integer、Double、StringBigDecimal的转换器。
不需要去写三个转换器,而是使用通用转换器GenericConverter来达到这个目的。

第一步是告诉Spring我们希望这个转换器支持什么类型。操作时创建一个ConvertiblePair的集合(每个元素是一对类型,表示可以转换):

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);
    }
}

转换方法很简单,但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的类型转换系统,并且给出了一些示例。
和往常一样,你可以在Github上找到本文的示例代码。