1. Введение

В этой статье мы рассмотрим преобразования типов в Spring.

Spring предоставляет готовые конвертеры для встроенных типов; это означает преобразование в/из базовых типов, таких как String, Integer, Boolean и ряд других типов.

Помимо этого, Spring также предоставляет надежный SPI преобразования типов для разработки наших пользовательских конвертеров.

2. Встроенные конвертеры

Мы начнем с конвертеров, доступных в Spring; давайте взглянем на преобразование String в Integer:

Единственное, что нам нужно здесь сделать, это автоматически связать ConversionService, предоставляемый Spring, и вызвать метод convert(). Первый аргумент — это значение, которое мы хотим преобразовать, а второй аргумент — это целевой тип, в который мы хотим преобразовать.

@Autowired
ConversionService conversionService;

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

Помимо этого примера преобразования строки в целое, нам доступно множество других комбинаций.

3. Создание пользовательского конвертера

Давайте рассмотрим пример преобразования строкового представления Employee в экземпляр Employee.

Вот класс Employee:

Строка будет разделенной запятой парой, представляющей идентификатор и зарплату. Например, «1,50000,00».

public class Employee {

    private long id;
    private double salary;

    // standard constructors, getters, setters
}

Чтобы создать наш собственный конвертер, нам нужно реализовать интерфейс Converter\u003cS, T\u003e и реализовать метод convert():

Мы еще не закончили. Нам также нужно сообщить Spring об этом новом конвертере, добавив StringToEmployeeConverter в FormatterRegistry. Это можно сделать, реализуя WebMvcConfigurer и переопределяя метод addFormatters():

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

Вот и все. Наш новый Converter теперь доступен для ConversionService, и мы можем использовать его так же, как и любой другой встроенный Converter:

@Configuration
public class WebConfig implements WebMvcConfigurer {

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

3.1. Неявное преобразование

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

Помимо этого явного преобразования с использованием ConversionService, Spring также способен неявно преобразовывать значения прямо в методах контроллера для всех зарегистрированных преобразователей:

Это более естественный способ использования преобразователей. Давайте добавим тест, чтобы увидеть его в действии:

@RestController
public class StringToEmployeeConverterController {

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

Как видите, тест распечатает все детали запроса, а также ответ. Вот объект Employee в формате JSON, возвращаемый как часть ответа:

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

4. Создание ConverterFactory

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

Также можно создать ConverterFactory, который создает конвертеры по требованию. Это особенно полезно при создании конвертеров для Enum.

Давайте взглянем на действительно простое Enum:

Далее, давайте создадим StringToEnumConverterFactory, который может генерировать преобразователи для преобразования строки в любое Enum:

public enum Modes {
    ALPHA, BETA;
}

Как мы видим, фабрика class внутренне использует реализацию интерфейса Converter.

@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 для демонстрации использования, мы нигде не упоминали Enum в StringToEnumConverterFactory. Наш фабричный класс достаточно универсален, чтобы генерировать конвертеры по запросу для любого типа Enum.

Следующим шагом является регистрация этого фабричного класса, как мы зарегистрировали наш конвертер в предыдущем примере:

Теперь ConversionService готов преобразовать строки в перечисления:

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

5. Создание GenericConverter

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

GenericConverter дает нам больше гибкости для создания Converter для более универсального использования за счет потери некоторой безопасности типов.

Давайте рассмотрим пример преобразования Integer, Double или String в значение BigDecimal. Для этого нам не нужно писать три преобразователя. Простой GenericConverter может служить этой цели.

Первый шаг — сообщить Spring, какие типы преобразования поддерживаются. Мы делаем это, создавая набор ConvertiblePair:

Следующий шаг — переопределить метод convert() в том же классе:

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() настолько прост, насколько это возможно. . Однако TypeDescriptor предоставляет нам большую гибкость в плане получения сведений об исходном и целевом типах.

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

Как вы уже догадались, следующим шагом будет регистрация этого преобразователя:

«

@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.