Введение

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

Итог

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

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

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