Введение
Всем привет, друзья! Это вторая статья про обертку ответов контроллеров. Сегодня хочу рассказать про то, как использовать BeanPostProcessor и для чего это может быть нужно - это основной посыл статьи. Сделаем мы это немного доработав проект из предыдущей статьи.
Под прошлой статьей пользователь с ником @maxzh83 указал на логическую недоработку проекта - невозможность реализовать несколько раз сервис IWrapperService, что происходит из-за того, что идет инжект только одной реализации сервиса.
private final IWrapperService wrapperService;
Сегодня посмотрим на то, как можно решить данную недоработку используя уже готовый функционал спринга - внедрение коллекций. И как можно немного доработать функционал спринга с помощью BeanPostProcessor для более удобного внедрения зависимостей для нашей конкретной ситуации.
Вспоминаем, что готово на данный момент
Предлагаю вспомнить, что мы сделали в прошлой статье. Код на GitHub на ветке master.
Немного про то, что уже было создано
Мы создали стартер, в котором присутствует 2 аннотации: @DisableResponseWrapper и @EnableResponseWrapper, а также 2 интерфейса: IWrapperModel и IWrapperService, используя которые мы можем обернуть все необходимые ответы контроллеров в новый класс.
Для реализации такого функционала, в стартере мы создали класс, реализующий интерфейс ResponseBodyAdvice<Object> и аннотированный с помощью @ControllerAdvice(annotations = EnableResponseWrapper.class)
@AllArgsConstructor @ControllerAdvice(annotations = EnableResponseWrapper.class) public class ResponseWrapperAdvice implements ResponseBodyAdvice<Object> { ... }
Аннотация
@ControllerAdvice(annotations = EnableResponseWrapper.class)указывает, что методы данного компонента будут использоваться сразу несколькими контроллерами. Также указываем, что наши методы будут обрабатывать только те контроллеры, которые помечены @EnableResponseWrapper.Класс реализует интерфейс ResponseBodyAdvice<>, который позволяет настраивать ответ, после его возвращения методом @ResponseBody или контроллером ResponseEntity, но до того, как тело будет записано с помощью HttpMessageConverter.
В классе необходимо было реализовать 2 метода:
public boolean supports(MethodParameter returnType, @NonNull Class converterType)
- метод вызывается для каждой точки входа контроллера и является фильтром к дальнейшей обработке. В случае, если этот метод возвращает false - дальнейшая обработка точки входа контроллера не происходит.
public Object beforeBodyWrite(@Nullable Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType, @NonNull Class selectedConverterType, @NonNull ServerHttpRequest request, @NonNull ServerHttpResponse response)
- метод обработки точки входа контроллера. Вернуть в методе необходимо объект, который будет возвращен api.
Этот механизм мы положили в стартер и написали демо-проект под данный функционал.
Представленный в статье механизм возможно использовать и для иных задач, связанных с обработкой ответов контроллеров. Подобный подход используется для обработки исключений в контроллерах. (Хорошая статья об обработки исключений https://habr.com/ru/post/528116/)
С помощью использования ControllerAdvice+ResponseBodyAdvice и аннотаций вы можете более гибко настроить обработку любых ответов контроллеров с использованием большого количества информации о методе, контроллере, запросе и ответе контроллера.
Внедрение коллекции
Мы хотим в результате получить возможность реализовывать для каждого класса-обертки свой сервис.
Самый простой и быстрый способ - воспользоваться тем, что уже умеет spring - внедрять коллекции зависимостей.
Создадим аннотацию для сервисов:
@Service @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface WrapperService { Class<? extends IWrapperModel<?, ?>> wrapperModel(); }
Так как наша аннотация всегда висит над сервисом - то вешаем над нашей аннотацией @Service - это позволит использовать @WrapperService вместо @Service.
В качестве аргумента будем принимать модель-обертку для дальнейшего получения по ней сервиса из списка.
Небольшие доработки
Сразу сделаем небольшие доработки по проекту для того, чтобы стартер был более гибким.
Добавим generic-и. Интерфейс модели:
public interface IWrapperModel<Body, Data> { void setData(@Nullable Data data, @NonNull MethodInformation methodInformation); default <DataHelper> void setDataHelper(@Nullable DataHelper data, @NonNull MethodInformation methodInformation){ @SuppressWarnings("unchecked") Data data1 = (Data) data; setData(data1, methodInformation); } void setBody(@NonNull Body body, @NonNull MethodInformation methodInformation); default <BodyHelper> void setBodyHelper(@NonNull BodyHelper body, @NonNull MethodInformation methodInformation) { @SuppressWarnings("unchecked") Body body1 = (Body) body; setBody(body1, methodInformation); } }
Создаем функции-хелперы setBodyHelper и setDataHelper для того, чтобы иметь возможность работать с Wildcard. Подробнее про helper-методы и зачем они нужны можно прочесть в официальной документации.
Аналогично делаем для интерфейса сервиса:
public interface IWrapperService<Body, Data> { @Nullable Data getData(@NonNull Body body); default <BodyHelper> Data getDataHelper (@NonNull BodyHelper body) { @SuppressWarnings("unchecked") Body body1 = (Body) body; return getData(body1); } }
Также для того, чтобы производить обертку на основе данных о запросе, создадим класс данных.
@Getter @AllArgsConstructor public class MethodInformation { @NonNull private final MethodParameter returnType; @NonNull private final MediaType selectedContentType; @NonNull private final Class<?> selectedConverterType; @NonNull private final ServerHttpRequest request; @NonNull private final ServerHttpResponse response; }
Изменяем ResponseWrapperAdvice
Теперь все, что нам необходимо сделать - это написать инжект коллекции в обработчике контроллеров ResponseWrapperAdvice.
Вместо:
private final IWrapperService wrapperService;
Пишем:
private final List<IWrapperService<?, ?>> wrapperServiceList;
Таким образом spring самостоятельно найдет и соберет в список все классы, реализующие IWrapperService с любыми Body и Data.
Напишем метод получения из списка того сервиса, который относится к конкретно нашему классу-обертке.
... @NonNull private IWrapperService<?, ?> getWrapperService( @NonNull Class<? extends IWrapperModel<?, ?>> wrapperClass ) { IWrapperService<?, ?> wrapperService = null; for (IWrapperService<?, ?> iWrapperService : wrapperServiceList) { for (Annotation annotation : iWrapperService.getClass().getAnnotations()) { if (annotation.annotationType() == WrapperService.class && ((WrapperService) annotation).wrapperModel()==wrapperClass ){ wrapperService = iWrapperService; break; } } } if (wrapperService==null) { throw new RuntimeException("Обертка без сервиса"); } return wrapperService; } ...
С помощью iWrapperService.getClass().getAnnotations() получаем все аннотации для каждого из сервисов и ищем среди них аннотацию @WrapperService. Из нее получаем класс-обертку, которую сравниваем с той, с которой работаем сейчас сами.
Полный код
@AllArgsConstructor @ControllerAdvice(annotations = EnableResponseWrapper.class) public class ResponseWrapperAdvice implements ResponseBodyAdvice<Object> { private final List<IWrapperService<?, ?>> wrapperServiceList; /** * Метод не будет обработан, если помечен аннотацией {@link DisableResponseWrapper} <br/> <br/> * * @param returnType the return type * @param converterType the selected converter type * @return {@code true} if {@link #beforeBodyWrite} should be invoked; * {@code false} otherwise */ @Override public boolean supports(MethodParameter returnType, @NonNull Class converterType) { for (Annotation a : returnType.getMethodAnnotations()) { if (a.annotationType() == DisableResponseWrapper.class) { return false; } } return true; } /** * Оборачиваем ответ * * @param body the body to be written * @param returnType the return type of the controller method * @param selectedContentType the content type selected through content negotiation * @param selectedConverterType the converter type selected to write to the response * @param request the current request * @param response the current response * @return the body that was passed in or a modified (possibly new) instance */ @SneakyThrows @Override @Nullable public Object beforeBodyWrite( @Nullable Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType, @NonNull Class selectedConverterType, @NonNull ServerHttpRequest request, @NonNull ServerHttpResponse response ) { MethodInformation methodInformation = new MethodInformation(returnType, selectedContentType, selectedConverterType, request, response); if (body == null) { return null; } // получаем wrapperClass из аннотации Class<? extends IWrapperModel<?, ?>> wrapperClass = null; for (Annotation annotation : returnType.getContainingClass().getAnnotations()) { if (annotation.annotationType() == EnableResponseWrapper.class) { wrapperClass = ((EnableResponseWrapper) annotation).wrapperClass(); break; } } if (wrapperClass == null) { return body; } // проверяем, был ли передан Collection или наследник Collection if (Collection.class.isAssignableFrom(body.getClass())) { try { Collection<?> bodyCollection = (Collection<?>) body; // проверяем, что collection не пустой if (bodyCollection.isEmpty()) { return body; } // оборачиваем каждый элемент коллекции return generateListOfResponseWrapper(bodyCollection, wrapperClass, methodInformation); } catch (Exception e) { return body; } } // если не collection return generateResponseWrapper(body, wrapperClass, methodInformation); } /** * Генерируем список оберток для коллекции (те информация добавляется внутрь списка) * * @param bodyCollection список объектов, которые необходимо обернуть * @param wrapperClass объект обертки * @return список оберток */ @NonNull private List<Object> generateListOfResponseWrapper( @NonNull Collection<?> bodyCollection, @NonNull Class<? extends IWrapperModel<?, ?>> wrapperClass, @NonNull MethodInformation methodInformation ) { return bodyCollection.stream() .map((t) -> t == null ? null : generateResponseWrapper(t, wrapperClass, methodInformation) ) .collect(Collectors.toList()); } /** * Генерируем обертку вокруг объекта * * @param body объект который необходимо поместить в обертку * @param wrapperClass объект обертки * @return обертка */ @NonNull @SneakyThrows private IWrapperModel<?, ?> generateResponseWrapper( @NonNull Object body, @NonNull Class<? extends IWrapperModel<?, ?>> wrapperClass, @NonNull MethodInformation methodInformation ) { // wrapperClass должен иметь конструктор без параметров - получаем объект IWrapperModel IWrapperModel<?, ?> wrapper = wrapperClass.getDeclaredConstructor().newInstance(); wrapper.setBodyHelper(body, methodInformation); wrapper.setDataHelper(getWrapperService(wrapperClass).getDataHelper(body), methodInformation); return wrapper; } /** * Получаем нужный сервис для конкретной обертки * * @param wrapperClass обертка, для которой необходимо найти сервис * @return сервис для обертки * @throws RuntimeException в коде присутствует обертка без сервиса */ @NonNull private IWrapperService<?, ?> getWrapperService( @NonNull Class<? extends IWrapperModel<?, ?>> wrapperClass ) { IWrapperService<?, ?> wrapperService = null; for (IWrapperService<?, ?> iWrapperService : wrapperServiceList) { for (Annotation annotation : iWrapperService.getClass().getAnnotations()) { if (annotation.annotationType() == WrapperService.class && ((WrapperService) annotation).wrapperModel()==wrapperClass ){ wrapperService = iWrapperService; break; } } } if (wrapperService==null) { throw new RuntimeException("Обертка без сервиса"); } return wrapperService; } }
Данный подход вполне оптимальный, но не лучший. Во-первых, метод getWrapperService будет вызываться каждый раз при запросе, в чем нет необходимости. Это можно поправить кешированием, например.
Во-вторых, как мне кажется, куда логичнее инжектить не список сервисов и затем по нему искать нужный перебором, а сразу мапу, в которой будет класс-обертка против сервиса.
Спринг умеет инжектить мапы, но не сможет (и не должен) сам догадаться брать класс-обертку из аннотации. Для таких случаев спринг предоставляет инструменты внедрения в процесс создания и настройки бинов (а также добавления их в контекст). Воспользуемся инструментом донастройки бинов - реализацией интерфейса BeanPostProcessor.
Полный код проекта с реализацией через внедрение коллекций: GitHub
Немного теории
В спринге присутствуют следующие этапы инициализации контекста, в каждый из которых, при желании, можно вклиниться самому:
Парсирование кофигурации (XML, JavaConfig и тд). После парсирования конфигурации создается BeanDefenition - мета информация, описывающая будущий бин;
Настройка уже созданных BeanDefinition - на этом этапе мы можем повлиять на то, какими будут наши бины еще до их создания;
Создание кастомных FactoryBean - можно самостоятельно создать фабрику, которая будет создавать бины определенного типа;
Создание экземпляров бинов - тут происходит создание классов или проксей. На этом этапе можно организовать инжект поля через конструктор;
Настройка созданных бинов - BeanPostProcessor - именно то, что нам нужно. На этом этапе конструктор бина уже выполнился и бин создан, но бин еще не попал в контекст.
Хорошая и подробная статья про этапы инициализации контекста.
Класс, реализующий BeanPostProcessor - это структурный компонент - его бин должен быть декларирован одним из 4 способов. Интерфейс предоставляет 2 метода postProcessBeforeInitialization и postProcessAfterInitialization. Первый вызывается до PostConstruct, второй после. Каждый из методов должен вернуть бин.
Реализация задачи через BeanPostProcessor
Во-первых, для удобства, нам понадобится новая аннотация, которая будет обозначать место для инжекта нашей мапы:
@Target({ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface InjectWrapperServiceMap { }
Во-вторых, нам нужен сам класс, реализующий BeanPostProcessor:
@Component @AllArgsConstructor public class InjectWrapperServiceMapBeanPostProcessor implements BeanPostProcessor { private final ApplicationContext applicationContext; ... }
Для получения бинов в мапу нам понадобится заинжектить контекст. Так как мы реализуем архитектурный бин - то инжектить контекст имеем право.
Из двух методов интерфейса нам понадобится только один. Выполнять наполнение полей будем до выполнения PostConstruct, тк метод, аннотированный PostConstruct считается инициализир��ющим, работющим тогда, когда все бины были подключены к классу.
... @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { setFieldInjects(bean); setMethodInject(bean); return bean; } ...
Метод получения мапы класса обертки против сервиса:
... private Map<Class<? extends IWrapperModel<?, ?>>, IWrapperService> getWrapperServiceMap() { Map<String, IWrapperService> beansOfType = applicationContext.getBeansOfType(IWrapperService.class); return beansOfType.values().stream() .collect(Collectors.toMap( (t) -> { if (!t.getClass().isAnnotationPresent(WrapperService.class)) { throw new RuntimeException("Не все сервисы, реализующие IWrapperService, аннотированы @WrapperService"); } return t.getClass().getAnnotation(WrapperService.class).wrapperModel(); }, (t) -> t ) ); } ...
applicationContext.getBeansOfType(IWrapperService.class) - получаем все бины из контекста, которые реализуют IWrapperService.
t.getClass().getAnnotation(WrapperService.class).wrapperModel() - получаем класс-обертку для сервиса.
Методы инжекта в метод и в переменную:
... @SneakyThrows private void setMethodInject(Object bean) { Set<Method> methods = Arrays.stream(bean.getClass().getDeclaredMethods()) .filter(method -> method.isAnnotationPresent(InjectWrapperServiceMap.class)) .collect(Collectors.toSet()); for (Method method : methods) { method.invoke(bean, getWrapperServiceMap()); } } @SneakyThrows private void setFieldInjects(Object bean) { Set<Field> fields = Arrays.stream(bean.getClass().getDeclaredFields()) .filter(field -> field.isAnnotationPresent(InjectWrapperServiceMap.class)) .collect(Collectors.toSet()); for (Field field : fields) { field.setAccessible(true); field.set(bean, getWrapperServiceMap()); } } ...
Вначале получаем все методы/поля класса и фильтруем их по наличию аннотации @InjectWrapperServiceMap.
Для методов вызываем выполнение метода, передавая в него мапу method.invoke(bean, getWrapperServiceMap());
Для поля обязательно устанавливаем доступность для того, чтобы могли в поле что-либо записывать и производим запись field.set(bean, getWrapperServiceMap());
Полный код InjectWrapperServiceMapBeanPostProcessor
@Component @AllArgsConstructor public class InjectWrapperServiceMapBeanPostProcessor implements BeanPostProcessor { private final ApplicationContext applicationContext; @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { setFieldInjects(bean); setMethodInject(bean); return bean; } @SneakyThrows private void setMethodInject(Object bean) { Set<Method> methods = Arrays.stream(bean.getClass().getDeclaredMethods()) .filter(method -> method.isAnnotationPresent(InjectWrapperServiceMap.class)) .collect(Collectors.toSet()); for (Method method : methods) { method.invoke(bean, getWrapperServiceMap()); } } @SneakyThrows private void setFieldInjects(Object bean) { Set<Field> fields = Arrays.stream(bean.getClass().getDeclaredFields()) .filter(field -> field.isAnnotationPresent(InjectWrapperServiceMap.class)) .collect(Collectors.toSet()); for (Field field : fields) { field.setAccessible(true); field.set(bean, getWrapperServiceMap()); } } private Map<Class<? extends IWrapperModel<?, ?>>, IWrapperService> getWrapperServiceMap() { Map<String, IWrapperService> beansOfType = applicationContext.getBeansOfType(IWrapperService.class); return beansOfType.values().stream() .collect(Collectors.toMap( (t) -> { if (!t.getClass().isAnnotationPresent(WrapperService.class)) { throw new RuntimeException("Не все сервисы, реализующие IWrapperService, аннотированы @WrapperService"); } return t.getClass().getAnnotation(WrapperService.class).wrapperModel(); }, (t) -> t ) ); } }
Регестрируем BeanPostProcessor
В конфигурационном файле настройки бинов создаем новый бин:
... @Bean @ConditionalOnMissingBean public InjectWrapperServiceMapBeanPostProcessor responseWrapperBeanPostProcessor() { return new InjectWrapperServiceMapBeanPostProcessor(applicationContext); } ...
К бину ResponseWrapperAdvice необходимо добавить @DependsOn(value = "responseWrapperBeanPostProcessor") для того, чтобы бин конфигурировался после создания бина BPP.
Для работы @DependsOn необходимо над классом конфигурации поставить аннотацию @ComponentScan("ru.emilnasyrov.lib.response.wrapper")
Полный код ResponseWrapperAutoConfiguration
@Configuration @AutoConfigureAfter(WebMvcAutoConfiguration.class) @RequiredArgsConstructor @ComponentScan("ru.emilnasyrov.lib.response.wrapper") public class ResponseWrapperAutoConfiguration { private final ApplicationContext applicationContext; @Bean @ConditionalOnMissingBean @DependsOn(value = "responseWrapperBeanPostProcessor") public ResponseWrapperAdvice responseWrapperAdvice() { return new ResponseWrapperAdvice(); } @Bean @ConditionalOnMissingBean public InjectWrapperServiceMapBeanPostProcessor responseWrapperBeanPostProcessor() { return new InjectWrapperServiceMapBeanPostProcessor(applicationContext); } }
Теперь аннотацию @InjectWrapperServiceMap для инжекта мапы сервисов можем использовать как в внутри нашего стартера, так и снаружи.
Дополняем обработчик контроллеров
В ResponseWrapperAdvice заинжектим мапу:
... private Map<Class<? extends IWrapperModel<?, ?>>, IWrapperService<?, ?>> wrapperServiceMap; @InjectWrapperServiceMap public void setWrapperServiceMap(Map<Class<? extends IWrapperModel<?, ?>>, IWrapperService<?, ?>> wrapperServiceMap) { this.wrapperServiceMap = wrapperServiceMap; } ...
Используем ее в методе generateResponseWrapper следующим образом:
private IWrapperModel<?, ?> generateResponseWrapper(...){ ... wrapper.setDataHelper(wrapperServiceMap.get(wrapperClass).getDataHelper(body), methodInformation); ... }
Полный код ResponseWrapperAdvice
@NoArgsConstructor @ControllerAdvice(annotations = EnableResponseWrapper.class) public class ResponseWrapperAdvice implements ResponseBodyAdvice<Object> { private Map<Class<? extends IWrapperModel<?, ?>>, IWrapperService<?, ?>> wrapperServiceMap; @InjectWrapperServiceMap public void setWrapperServiceMap(Map<Class<? extends IWrapperModel<?, ?>>, IWrapperService<?, ?>> wrapperServiceMap) { this.wrapperServiceMap = wrapperServiceMap; } /** * Метод не будет обработан, если помечен аннотацией {@link DisableResponseWrapper} <br/> <br/> * * @param returnType the return type * @param converterType the selected converter type * @return {@code true} if {@link #beforeBodyWrite} should be invoked; * {@code false} otherwise */ @Override public boolean supports(MethodParameter returnType, @NonNull Class converterType) { for (Annotation a : returnType.getMethodAnnotations()) { if (a.annotationType() == DisableResponseWrapper.class) { return false; } } return true; } /** * Оборачиваем ответ * * @param body the body to be written * @param returnType the return type of the controller method * @param selectedContentType the content type selected through content negotiation * @param selectedConverterType the converter type selected to write to the response * @param request the current request * @param response the current response * @return the body that was passed in or a modified (possibly new) instance */ @SneakyThrows @Override @Nullable public Object beforeBodyWrite( @Nullable Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType, @NonNull Class selectedConverterType, @NonNull ServerHttpRequest request, @NonNull ServerHttpResponse response ) { MethodInformation methodInformation = new MethodInformation(returnType, selectedContentType, selectedConverterType, request, response); if (body == null) { return null; } // получаем wrapperClass из аннотации Class<? extends IWrapperModel<?, ?>> wrapperClass = null; for (Annotation annotation : returnType.getContainingClass().getAnnotations()) { if (annotation.annotationType() == EnableResponseWrapper.class) { wrapperClass = ((EnableResponseWrapper) annotation).wrapperClass(); break; } } if (wrapperClass == null) { return body; } // проверяем, был ли передан Collection или наследник Collection if (Collection.class.isAssignableFrom(body.getClass())) { try { Collection<?> bodyCollection = (Collection<?>) body; // проверяем, что collection не пустой if (bodyCollection.isEmpty()) { return body; } // оборачиваем каждый элемент коллекции return generateListOfResponseWrapper(bodyCollection, wrapperClass, methodInformation); } catch (Exception e) { return body; } } // если не collection return generateResponseWrapper(body, wrapperClass, methodInformation); } /** * Генерируем список оберток для коллекции (те информация добавляется внутрь списка) * * @param bodyCollection список объектов, которые необходимо обернуть * @param wrapperClass объект обертки * @return список оберток */ @NonNull private List<Object> generateListOfResponseWrapper( @NonNull Collection<?> bodyCollection, @NonNull Class<? extends IWrapperModel<?, ?>> wrapperClass, @NonNull MethodInformation methodInformation ) { return bodyCollection.stream() .map((t) -> t == null ? null : generateResponseWrapper(t, wrapperClass, methodInformation) ) .collect(Collectors.toList()); } /** * Генерируем обертку вокруг объекта * * @param body объект который необходимо поместить в обертку * @param wrapperClass объект обертки * @return обертка */ @NonNull @SneakyThrows private IWrapperModel<?, ?> generateResponseWrapper( @NonNull Object body, @NonNull Class<? extends IWrapperModel<?, ?>> wrapperClass, @NonNull MethodInformation methodInformation ) { // wrapperClass должен иметь конструктор без параметров - получаем объект IWrapperModel IWrapperModel<?, ?> wrapper = wrapperClass.getDeclaredConstructor().newInstance(); wrapper.setBodyHelper(body, methodInformation); wrapper.setDataHelper(wrapperServiceMap.get(wrapperClass).getDataHelper(body), methodInformation); return wrapper; } }
В коде остается нерешенный момент с использованием одного сервиса для разных оберток, что предлагаю, при необходимости, реализовать вам самим. Мне кажется, это довольно редкий кейс и нет необходимости его рассматривать в рамках данной статьи.
А также инжект мапы через конструктор, который, думаю, я рассмотрю в последующих статьях.
Вот и все. Остается только протестировать наш стартер.
Код проекта с реализацией BeanPostProcessor: GitHub
Демо
Добавим следующие классы в демо проект:
Passport - класс дополнительных данных
@Data @AllArgsConstructor @NoArgsConstructor public class Passport { private String series; private String number; }
Wrapper - реализация через generic-и
@Data @AllArgsConstructor @NoArgsConstructor public class Wrapper implements IWrapperModel<MainModel, String> { @JsonUnwrapped MainModel main; String someInfo; @Override public void setBody(@NonNull MainModel body, @NonNull MethodInformation methodInformation) { main = body; } @Override public void setData(String data, @NonNull MethodInformation methodInformation) { someInfo = data; } // переопределяем toString, hashCode, equals }
Wrapper2 - второй класс-оберта
@Data @AllArgsConstructor @NoArgsConstructor public class Wrapper2 implements IWrapperModel<MainModel, Passport> { @JsonUnwrapped MainModel main; Passport passport; @Override public void setData(Passport passport, @NonNull MethodInformation methodInformation) { this.passport = passport; } @Override public void setBody(@NonNull MainModel body, @NonNull MethodInformation methodInformation) { this.main = body; } }
Wrapper2ServiceImpl - вторая реализация сервиса
@WrapperService(wrapperModel = Wrapper2.class) public class Wrapper2ServiceImpl implements IWrapperService<MainModel, Passport> { @Override public Passport getData(@NonNull MainModel body) { return new Passport("series", "number"); } }
WrapperServiceImpl остался из прошлой статьи с небольшими аналогичными изменениями
Controller2 - контроллер для второй обертки
@EnableResponseWrapper(wrapperClass = Wrapper2.class) @RequestMapping("/test2") public class Controller2 { @GetMapping public MainModel test() { return new MainModel("Name", "Surname"); } @GetMapping("/collection") public Collection<MainModel> testList() { Collection<MainModel> mainModels = new ArrayList<>(); mainModels.add(new MainModel("Name1", "Surname1")); mainModels.add(new MainModel("Name2", "Surname2")); return mainModels; } @DisableResponseWrapper @GetMapping("/unwrapped") public MainModel unwrapped() { return new MainModel("Name", "Surname"); } }
Controller остался таким же, как и в прошлой статье
Результат в Postman




Итог
В данной статье на примере стартера мы рассмотрели использование BeanPostProcessor и то, какие вещи с его помощью можно делать.
Не всегда spring может дать нам то, чего мы хотим, но часто, если нас что-то не устраивает, то есть возможность дополнить spring.
Ссылка на полный код проекта: GitHub
