Введение

Всем привет, друзья! Это вторая статья про обертку ответов контроллеров. Сегодня хочу рассказать про то, как использовать 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

Немного теории

В спринге присутствуют следующие этапы инициализации контекста, в каждый из которых, при желании, можно вклиниться самому:

  1. Парсирование кофигурации (XML, JavaConfig и тд). После парсирования конфигурации создается BeanDefenition - мета информация, описывающая будущий бин;

  2. Настройка уже созданных BeanDefinition - на этом этапе мы можем повлиять на то, какими будут наши бины еще до их создания;

  3. Создание кастомных FactoryBean - можно самостоятельно создать фабрику, которая будет создавать бины определенного типа;

  4. Создание экземпляров бинов - тут происходит создание классов или проксей. На этом этапе можно организовать инжект поля через конструктор;

  5. Настройка созданных бинов - 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
Тест контроллера 1 с оберткой Wrapper
Тест контроллера 1 с оберткой Wrapper
Тест контроллера 1 с оберткой Wrapper для коллекции
Тест контроллера 1 с оберткой Wrapper для коллекции
Тест контроллера 2 с оберткой Wrapper2
Тест контроллера 2 с оберткой Wrapper2
Тест контроллера 2 с оберткой Wrapper2 для коллекций
Тест контроллера 2 с оберткой Wrapper2 для коллекций

Итог

В данной статье на примере стартера мы рассмотрели использование BeanPostProcessor и то, какие вещи с его помощью можно делать.

Не всегда spring может дать нам то, чего мы хотим, но часто, если нас что-то не устраивает, то есть возможность дополнить spring.

Ссылка на полный код проекта: GitHub