Пользовательский связыватель данных в Spring MVC
1. обзор
В этой статье будет показано, как мы можем использовать механизм привязки данных Spring, чтобы сделать наш код более понятным и читаемым, применяя автоматические примитивы к преобразованию объектов.
По умолчанию Spring знает только как конвертировать простые типы. Другими словами, как только мы отправим данные контроллеру типа данныхInt,String илиBoolean, они будут автоматически связаны с соответствующими типами Java.
Но в реальных проектах этого недостаточно, так какwe might need to bind more complex types of objects.
2. Привязка отдельных объектов к параметрам запроса
Давайте начнем с простого и сначала свяжем простой тип; нам нужно будет предоставить индивидуальную реализацию интерфейсаConverter<S, T>, гдеS - это тип, из которого мы конвертируем, а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. Использование перечислений в качестве параметров запроса
Далее мы увидимhow to use enum as a RequestParameter.
Здесь мы имеем простойenumModes:
public enum Modes {
ALPHA, BETA;
}
Мы построимString доenum Converter следующим образом:
public class StringToEnumConverter implements Converter {
@Override
public Modes convert(String from) {
return Modes.valueOf(from);
}
}
Затем нам нужно зарегистрировать нашConverter:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToEnumConverter());
}
}
Теперь мы можем использовать нашEnum какRequestParameter:
@GetMapping
public ResponseEntity
Или как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
}
В этом случаеwe can implement ConverterFactory<S, R> where S will be the type we are converting from and R to be the base type определяет диапазон классов, в которые мы можем преобразовать:
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(), который возвращает преобразователь для необходимого типа. Процесс преобразования затем делегируется этому конвертеру.
Затем нам нужно зарегистрировать наш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
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 argumentResolvers) {
argumentResolvers.add(new HeaderVersionArgumentResolver());
}
}
Это оно. Теперь мы можем использовать его в контроллере:
@GetMapping("/entity/{id}")
public ResponseEntity findByVersion(
@PathVariable Long id, @Version String version) {
return ...;
}
Как мы видим, методHandlerMethodArgumentResolver ’sresolveArgument() возвращаетObject.. Другими словами, мы могли бы вернуть любой объект, а не толькоString.
5. Заключение
В результате мы избавились от многих рутинных преобразований и позволили Spring сделать большинство вещей для нас. В конце подведем итоги:
-
Для преобразования отдельного простого типа в объект мы должны использовать реализациюConverter
-
Для инкапсуляции логики преобразования для ряда объектов мы можем попробовать реализациюConverterFactory
-
Для любых данных, поступающих косвенно или требующих применения дополнительной логики для извлечения связанных данных, лучше использоватьHandlerMethodArgumentResolver
Как обычно, все примеры всегда можно найти в нашемGitHub repository.