Перейти к основному содержимому

Пользовательское связывание данных в Spring MVC

· 5 мин. чтения

Задача: Сумма двух

Дано массив целых чисел и целая сумма. Нужно найти индексы двух чисел, сумма которых равна заданной ...

ANDROMEDA

1. Обзор

В этой статье будет показано, как мы можем использовать механизм привязки данных Spring, чтобы сделать наш код более понятным и читабельным, применяя автоматические преобразования примитивов в объекты.

По умолчанию Spring умеет преобразовывать только простые типы. Другими словами, как только мы отправим данные в контроллер данных Int , String или Boolean , они будут автоматически привязаны к соответствующим типам Java.

Но в реальных проектах этого будет недостаточно, так как нам может понадобиться привязать более сложные типы объектов .

2. Привязка отдельных объектов к параметрам запроса

Давайте начнем с простого и сначала привяжем простой тип; нам нужно будет предоставить пользовательскую реализацию интерфейса Converter<S, T> , где S — тип, из которого мы конвертируем, а T — тип, в который мы конвертируем:

@Component
public class StringToLocalDateTimeConverter
implements Converter<String, LocalDateTime> {

@Override
public LocalDateTime convert(String source) {
return LocalDateTime.parse(
source, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
}

Теперь мы можем использовать следующий синтаксис в нашем контроллере:

@GetMapping("/findbydate/{date}")
public GenericEntity findByDate(@PathVariable("date") LocalDateTime date) {
return ...;
}

2.1. Использование перечислений в качестве параметров запроса

Далее мы увидим, как использовать enum в качестве RequestParameter .

Здесь у нас есть простое перечисление Modes :

public enum Modes {
ALPHA, BETA;
}

Мы создадим конвертер String to enum следующим образом:

public class StringToEnumConverter implements Converter<String, Modes> {

@Override
public Modes convert(String from) {
return Modes.valueOf(from);
}
}

Затем нам нужно зарегистрировать наш конвертер :

@Configuration
public class WebConfig implements WebMvcConfigurer {

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

Теперь мы можем использовать наш Enum в качестве RequestParameter :

@GetMapping
public ResponseEntity<Object> getStringToMode(@RequestParam("mode") Modes mode) {
// ...
}

Или как PathVariable :

@GetMapping("/entity/findbymode/{mode}")
public GenericEntity findByEnum(@PathVariable("mode") Modes mode) {
// ...
}

3. Привязка иерархии объектов

Иногда нам нужно преобразовать все дерево иерархии объектов, и имеет смысл иметь более централизованную привязку, а не набор отдельных преобразователей.

В этом примере у нас есть AbstractEntity наш базовый класс:

public abstract class AbstractEntity {
long id;
public AbstractEntity(long id){
this.id = id;
}
}

И подклассы Foo и Bar :

public class Foo extends AbstractEntity {
private String name;

// standard constructors, getters, setters
}
public class Bar extends AbstractEntity {
private int value;

// standard constructors, getters, setters
}

В этом случае мы можем реализовать ConverterFactory<S, R>, где S будет типом, из которого мы преобразуем, а R — базовым типом , определяющим диапазон классов, в которые мы можем преобразовать:

public class StringToAbstractEntityConverterFactory 
implements ConverterFactory<String, AbstractEntity>{

@Override
public <T extends AbstractEntity> Converter<String, T> getConverter(Class<T> targetClass) {
return new StringToAbstractEntityConverter<>(targetClass);
}

private static class StringToAbstractEntityConverter<T extends AbstractEntity>
implements Converter<String, T> {

private Class<T> targetClass;

public StringToAbstractEntityConverter(Class<T> targetClass) {
this.targetClass = targetClass;
}

@Override
public T convert(String source) {
long id = Long.parseLong(source);
if(this.targetClass == Foo.class) {
return (T) new Foo(id);
}
else if(this.targetClass == Bar.class) {
return (T) new Bar(id);
} else {
return null;
}
}
}
}

Как мы видим, единственный метод, который должен быть реализован, это getConverter() , который возвращает преобразователь для нужного типа. Затем процесс преобразования делегируется этому преобразователю.

Затем нам нужно зарегистрировать нашу ConverterFactory :

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(new StringToAbstractEntityConverterFactory());
}
}

Наконец, мы можем использовать его как угодно в нашем контроллере:

@RestController
@RequestMapping("/string-to-abstract")
public class AbstractEntityController {

@GetMapping("/foo/{foo}")
public ResponseEntity<Object> getStringToFoo(@PathVariable Foo foo) {
return ResponseEntity.ok(foo);
}

@GetMapping("/bar/{bar}")
public ResponseEntity<Object> getStringToBar(@PathVariable Bar bar) {
return ResponseEntity.ok(bar);
}
}

4. Связывание объектов домена

Бывают случаи, когда мы хотим привязать данные к объектам, но они приходят либо непрямым путем (например, из переменных Session , Header или Cookie ), либо вообще сохраняются в источнике данных. В этих случаях нам нужно использовать другое решение.

4.1. Пользовательский преобразователь аргументов

В первую очередь определим аннотацию для таких параметров:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Version {
}

Затем мы реализуем собственный HandlerMethodArgumentResolver :

public class HeaderVersionArgumentResolver
implements HandlerMethodArgumentResolver {

@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterAnnotation(Version.class) != null;
}

@Override
public Object resolveArgument(
MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory) throws Exception {

HttpServletRequest request
= (HttpServletRequest) nativeWebRequest.getNativeRequest();

return request.getHeader("Version");
}
}

Последнее, что нужно сделать, это сообщить Spring, где их искать:

@Configuration
public class WebConfig implements WebMvcConfigurer {

//...

@Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new HeaderVersionArgumentResolver());
}
}

Вот и все. Теперь мы можем использовать его в контроллере:

@GetMapping("/entity/{id}")
public ResponseEntity findByVersion(
@PathVariable Long id, @Version String version) {
return ...;
}

Как мы видим, метод resolveArgument () класса HandlerMethodArgumentResolver возвращает объект. Другими словами, мы могли бы вернуть любой объект, а не только String .

5. Вывод

В результате мы избавились от многих рутинных преобразований и позволили Spring сделать большую часть работы за нас. В конце подведем итог:

  • Для отдельного простого преобразования типа в объект мы должны использовать реализацию Converter
  • Для инкапсуляции логики преобразования для диапазона объектов мы можем попробовать реализацию ConverterFactory .
  • Для любых данных, поступающих косвенно или требующих применения дополнительной логики для извлечения связанных данных, лучше использовать HandlerMethodArgumentResolver .

Как обычно, все примеры всегда можно найти в нашем репозитории GitHub .