Рубрики
Без рубрики

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

Узнайте, как создать и настроить пользовательскую привязку данных в Spring MVC.

Автор оригинала: baeldung.

1. Обзор

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

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

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

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

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

@Component
public class StringToLocalDateTimeConverter
  implements Converter {

    @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. Использование перечислений в качестве параметров запроса

Далее мы увидим как использовать e num в качестве параметра Запроса .

Здесь у нас есть простое перечисление Режимы :

public enum Modes {
    ALPHA, BETA;
}

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

public class StringToEnumConverter implements Converter {

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

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

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

Или в качестве переменной Path :

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

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

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

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

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
}

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

public class StringToAbstractEntityConverterFactory 
  implements ConverterFactory{

    @Override
    public  Converter getConverter(Class targetClass) {
        return new StringToAbstractEntityConverter<>(targetClass);
    }

    private static class StringToAbstractEntityConverter
      implements Converter {

        private Class targetClass;

        public StringToAbstractEntityConverter(Class 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 () , который возвращает конвертер для нужного типа. Затем процесс преобразования делегируется этому конвертеру.

Затем нам нужно зарегистрировать наш Конвертерный завод :

@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 getStringToFoo(@PathVariable Foo foo) {
        return ResponseEntity.ok(foo);
    }
    
    @GetMapping("/bar/{bar}")
    public ResponseEntity getStringToBar(@PathVariable Bar bar) {
        return ResponseEntity.ok(bar);
    }
}

4. Привязка Объектов Домена

Бывают случаи, когда мы хотим привязать данные к объектам, но они поступают либо не прямым способом (например, из Сеанса , заголовка или 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");
    }
}

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

@Configuration
public class WebConfig implements WebMvcConfigurer {

    //...

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

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

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

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

5. Заключение

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

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

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