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.