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 .