JavaRush /Курсы /Spring Core /Точки расширения ContextFlow

Точки расширения ContextFlow

Spring Core
19 уровень , 4 лекция
Открыта

1. Проектирование точек расширения

Когда впервые видишь BFPP/BPP, возникает естественное желание: «О, круто! Сейчас я тут всё проверю, тут всё поправлю, тут всё подменю… и вообще, зачем мне архитектура, если есть пост-процессоры?» Это примерно как увидеть скотч и решить, что теперь можно не использовать гвозди. Да, можно. Но потом в какой-то момент дверь вместе со скотчем остаётся у вас в руках, а вы стоите и делаете вид, что так и было задумано.

В ContextFlow у нас цель другая: не превратить проект в цирк контейнерных трюков, а получить два очень прагматичных эффекта. Первый эффект — ранние проверки, которые дают понятную ошибку на старте, если сборка контекста неправильная (например, вообще нет ни одного NotificationSender). Второй эффект — диагностика, которая помогает увидеть, что именно контейнер выдал наружу как bean: какой beanName, какой реальный Class, какие объекты прошли инициализацию.

После карты refresh(), BFPP, BPP и разбора аннотационной инфраструктуры осталось собрать всё это в одну стабильную wiring-схему проекта. Иначе post-processors так и останутся набором отдельных демо-трюков, а не нормальной частью приложения.

И здесь важен тонкий момент: BFPP и BPP — это не «бизнес-логика, только в другом месте». Это инфраструктурные крючки. Они должны работать как турникет в метро: турникет проверяет, что билет есть и валиден, но он не решает, куда вам ехать и зачем вы вообще вышли из дома. Если BFPP начинает «подкручивать» бизнес-решения, вы получаете приложение, которое невозможно читать без знания внутренних фаз refresh().

2. Правило слоёв: support и application

Чтобы BFPP/BPP не превратились в “второй Spring внутри Spring”, в проекте нужно заранее принять простое архитектурное правило: бизнес-слой (domain и application) не обязан знать, что у контейнера вообще есть какие-то пост-процессоры. Это знание — строго инфраструктурное. Поэтому у нас и выделены пакеты support.* и config.*: они как «комната системных администраторов», куда обычные пользователи (бизнес-сервисы) не ходят.

В ContextFlow это удобно фиксировать прямо структурой пакетов. Сверху — домен и порты, которые описывают “что мы хотим” (уведомить, записать аудит, сохранить заказ). Дальше — application-сервисы, которые оркестрируют сценарии. И отдельно — infrastructure и support, которые занимаются “как именно это работает” и “как это диагностировать”. BFPP/BPP — типичный житель support.postprocessor, потому что они используют Spring SPI (BeanFactoryPostProcessor, BeanPostProcessor) и напрямую общаются с контейнером.

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

Слой проекта Что там живёт Может ли зависеть от Spring? Примеры
domain.* сущности, события, порты (интерфейсы) лучше минимально Order, Customer, OrderCreatedEvent, NotificationSender
application.* use-case сервисы и сценарии допустимо, но без “контейнерного мозга” OrderPlacementService, ScenarioRunner
infrastructure.* реализации портов да, но без SPI-экзотики ConsoleNotificationSender, FileAuditWriter
support.* инфраструктурные расширения контейнера да, это их работа BFPP/BPP, конвертеры, lifecycle helpers
config.* wiring и регистрация beans да AppConfig, SupportConfig

Если очень коротко, то BFPP/BPP должны ощущаться как “проводка в стенах”: она важна, без неё ничего не работает, но вы не принимаете бизнес-решение «создать заказ» на уровне проводки.

3. SupportConfig: регистрация BFPP/BPP

Одна из самых неприятных проблем новичка в Spring — «почему оно сработало?» или «почему оно внезапно перестало работать?». И почти всегда ответ связан с тем, что где-то “включилась инфраструктура”, но она не была оформлена как часть понятной конфигурации. Поэтому в ContextFlow мы не регистрируем post-processors “где попало”, а делаем отдельный модуль конфигурации: SupportConfig. Один модуль, один импорт в AppConfig, один понятный вход для всей container-specific инфраструктуры.

Представьте, что человек открывает репозиторий и пытается понять, что происходит. Если BFPP/BPP лежат рядом с бизнес-сервисами или включаются через случайный @Component в глубине пакета, то «магия» становится реальной магией — в плохом смысле. А если есть модуль SupportConfig, то расширения контейнера становятся явной частью сборки: как блок предохранителей в щитке. Хочешь — смотри, какие стоят “автоматы”, хочешь — временно отключай часть диагностики по профилю.

Пример минимального SupportConfig, который регистрирует BFPP и BPP в нашем проекте:

import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SupportConfig {

    @Bean
    static BeanFactoryPostProcessor contextFlowSanityChecks() {
        // BFPP должен подключиться как можно раньше, поэтому @Bean делаем static.
        // Так Spring сможет вызвать его до создания экземпляра конфигурационного класса.
        return new ContextFlowSanityCheckBeanFactoryPostProcessor();
    }

    @Bean
    BeanPostProcessor trackedComponents() {
        // BPP — это уже диагностика на уровне реальных объектов (после создания bean-ов).
        return new TrackedComponentBeanPostProcessor();
    }
}

Обратите внимание на static у @Bean для BFPP. Мы не превращаем это в мистический ритуал, но фиксируем важную идею: BFPP — “ранний участник старта”. Чем меньше шансов, что он будет создан “позже, чем надо”, тем меньше сюрпризов на ровном месте.

Теперь нужно подключить SupportConfig в корневую конфигурацию приложения. В реальном проекте AppConfig уже, скорее всего, собирает модули через @Import, и мы добавляем ещё один:

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({
        // Бизнес-ядро (use-cases, доменные сервисы и т.п.)
        CoreConfig.class,
        // Реализации портов (файлы, консоль, БД и т.д.)
        InfrastructureConfig.class,
        // Инфраструктурные расширения контейнера: BFPP/BPP и диагностика
        SupportConfig.class
})
public class AppConfig {
}

И наконец, точка входа (условно main()) остаётся честной и короткой: поднимаем контекст и запускаем сценарий через ScenarioRunner, как и раньше.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ContextFlowMain {
    public static void main(String[] args) {
        // try-with-resources гарантирует корректное закрытие контекста и вызов shutdown hooks.
        try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
            // Достаём верхнеуровневый сценарий из контейнера и запускаем.
            context.getBean(ScenarioRunner.class).run();
        }
    }
}

Да, это выглядит “скучно”. И это комплимент: скучный bootstrap — признак того, что у вас не прячется хаос там, где его потом невозможно отладить.

4. BFPP: ранние sanity checks

BFPP — отличный инструмент, чтобы сделать старт контекста максимально предсказуемым: если сборка сломана, лучше упасть сразу и объяснить по-человечески, что именно не так. Но чтобы BFPP не стал источником боли, мы используем его как “санитарный контроль”: проверяем структуру контекста, наличие обязательных ролей, минимальные требования к сборке — и всё. Без попыток “додумать за вас” бизнес-решения.

Ниже пример BFPP для ContextFlow, который проверяет, что в контейнере вообще существует хотя бы один NotificationSender. Это не бизнес-правило уровня “кому отправлять”, это структурное правило: проект заявлен как приложение с уведомлениями, значит, хотя бы один отправитель должен быть.

import com.example.contextflow.domain.ports.NotificationSender;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;

public class ContextFlowSanityCheckBeanFactoryPostProcessor
        implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) {
        // Важно: здесь мы проверяем только "что зарегистрировано", а не "что уже создано".
        // Поэтому используем getBeanNamesForType(...) вместо getBean(...).
        String[] senders = bf.getBeanNamesForType(NotificationSender.class, true, false);

        // true  -> учитываем не только singleton'ы, но и другие scopes / фабрики (по сути: "ищи шире").
        // false -> НЕ форсируем создание bean-ов ради проверки (иначе можно запустить пол-приложения).
        if (senders.length == 0) {
            // Сообщение специально "про проект", а не про Spring: так проще читать ошибку на старте.
            throw new IllegalStateException("ContextFlow: at least one NotificationSender is required");
        }
    }
}

Здесь специально используется getBeanNamesForType(...). На человеческом языке это означает: “посмотри, что зарегистрировано по типу, но не пытайся ради этого создавать beans”. Нам в sanity check нужен именно уровень сборки, а не запуск половины приложения в середине старта.

Обычно в ContextFlow хочется проверить не один тип, а несколько “опорных” портов. Тогда BFPP быстро превращается в копипасту, поэтому можно сделать маленький helper внутри класса — да, внутри BFPP тоже можно писать нормальный код, мы же не в храме минимализма:

import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;

// Фрагмент: helper-метод, который можно держать внутри BFPP-класса (например, как private static).
private static void requireType(ConfigurableListableBeanFactory bf, Class
   type) {
    // Проверяем "наличие по типу" без создания экземпляров.
    String[] names = bf.getBeanNamesForType(type, true, false);
    if (names.length == 0) {
        // Сообщение делаем максимально понятным: какой именно обязательный тип отсутствует.
        throw new IllegalStateException("ContextFlow: missing bean of type " + type.getSimpleName());
    }
}

И использовать так:

// Это именно "структурные опоры" приложения: без них проект не считается собранным.
requireType(bf, NotificationSender.class);
requireType(bf, AuditWriter.class);
requireType(bf, OrderStore.class);

Обратите внимание на стиль ошибок. В sanity checks важно, чтобы сообщение было “про проект”, а не “про внутренности Spring”. Новичок должен увидеть не NoSuchBeanDefinitionException на глубине 70 строк, а понятную фразу уровня: “не найден AuditWriter — проект требует хотя бы одну реализацию аудита”.

И ещё один практический нюанс: BFPP — плохое место для “молчаливых исправлений”. Если вы автоматически меняете половину BeanDefinition (делаете lazy, меняете scope, подставляете дефолты), то через пару недель никто не поймёт, почему приложение ведёт себя не так, как написано в конфигурации. В учебном проекте это особенно опасно: студент начинает думать, что Spring “сам как-то догадается”.

5. BPP: диагностика beans

Если BFPP — это проверка “плана здания” до начала строительства, то BPP — это момент, когда дом уже построили, и вы ходите с фонариком и смотрите, где у вас розетки, выключатели и почему в ванной вдруг оказалась люстра. BPP видит реальные экземпляры beans, и этим он очень полезен для диагностики: можно понять, какие объекты реально живут в контексте после init-фазы, и какие классы контейнер отдал наружу.

В ContextFlow нам не нужно “обрабатывать всё подряд” — иначе это быстро превращается в лог-шум и перфоманс-страдания. Мы выбираем узкий критерий: например, трекаем только сервисы (по соглашению — *Service). Тогда вывод остаётся читаемым и помогает понять, что контейнер собрал.

import org.springframework.beans.factory.config.BeanPostProcessor;

public class TrackedComponentBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        // Узкий фильтр: трекаем только то, что нам действительно интересно читать в консоли.
        if (beanName.endsWith("Service")) {
            // Диагностика: показываем beanName и реальный класс (полезно при прокси и декораторах).
            System.out.println("[track] " + beanName + " -> " + bean.getClass().getName());
            // пример вывода: [track] orderPlacementService -> com.example...OrderPlacementService
        }

        // Важно: BPP обязан вернуть объект (тот же или обёртку), иначе bean "пропадёт" из контекста.
        return bean;
    }
}

Тут важна одна “деталь на миллион долларов”, которую новички часто пропускают: BPP обязан вернуть объект, с которым контейнер будет жить дальше. Если вы вернули null, то дальше начнётся весёлое приключение под названием “почему у меня внезапно нет bean-а, хотя он был”. Поэтому мы всегда заканчиваем return bean; (или возвращаем обёртку — но это уже отдельная история, и мы здесь используем диагностику без изменения поведения).

Иногда вместо проверки по имени удобнее ограничивать по пакету, особенно если в проекте есть свои naming conventions. Тогда критерий можно записать так:

// Альтернатива фильтру по имени: фильтр по пакету (удобно, если имена нестрогие).
String pkg = bean.getClass().getPackageName();
if (pkg.startsWith("com.example.contextflow.application")) {
    // Печатаем короткое имя класса, чтобы вывод был компактнее.
    System.out.println("[app] " + beanName + " -> " + bean.getClass().getSimpleName()); // [app] ...
}

Смысл тот же: узко, предсказуемо, без попытки “обрабатывать вообще всё в мире”. В учебном проекте это ещё и защищает от ситуации, когда вы нечаянно начали печатать внутренние инфраструктурные beans Spring, и студент потом смотрит на консоль, как на матрицу, и спрашивает: “а где тут мой код?”.

Эффект в рантайме: порядок старта

Когда вы добавили BFPP/BPP, очень хочется убедиться, что они действительно отработали, и отработали в правильной фазе. Самый простой способ — сделать вывод таким, чтобы его можно было “прочитать глазами”, не превращая запуск в расследование уровня криминалистики. Мы не строим production-логирование, но делаем сообщения достаточно стабильными, чтобы видеть порядок и смысл.

Полезно мысленно держать такую схему старта — она же поможет объяснить себе, почему BFPP и BPP не взаимозаменяемы:

flowchart TD
    A["Загружены BeanDefinition"] --> B["BFPP: sanity checks / правки метаданных"]
    B --> C["Создание singleton beans"]
    C --> D["BPP before init (если есть)"]
    D --> E["Init-фаза: @PostConstruct / initMethod"]
    E --> F["BPP after init (наш трекинг)"]
    F --> G["Контекст готов"]

Практическая проверка получается почти “самоочевидной”: если BFPP падает, вы увидите ошибку ещё до того, как начнут появляться сообщения BPP о созданных сервисах. А если BFPP проходит, то после создания beans вы увидите трекинг.

Например, такой вывод — сильно упрощённо — будет выглядеть примерно так:

[track] orderPlacementService -> com.example.contextflow.application.service.OrderPlacementService
[track] orderCancellationService -> com.example.contextflow.application.service.OrderCancellationService
[track] reportingService -> com.example.contextflow.application.reporting.ReportingService

И да, это тот случай, когда “лишний println” — полезный println: он превращает абстрактную модель пайплайна в наблюдаемое поведение. А потом, когда вы начнёте включать другие инфраструктурные механики, вам будет проще отличать “обычный объект” от “контейнерной штуки”.

6. Типичные ошибки при работе с BFPP/BPP

Ошибка №1: складывать BFPP/BPP рядом с бизнес-сервисами, потому что “так быстрее найти”.
Это почти всегда приводит к тому, что проект перестаёт читаться: рядом с OrderPlacementService внезапно появляется класс, который правит BeanDefinition, и мозг читателя начинает делать сальто. Правильнее держать processors в support.postprocessor, а регистрацию — в config модуле, чтобы было видно: это инфраструктура, а не часть домена.

Ошибка №2: пытаться в BFPP получать обычные beans через getBean() и использовать их как готовые объекты.
На BFPP-фазе вы должны думать “контейнером”: есть описания, есть набор definitions, есть структура. Если вы начнёте вытаскивать beans, вы либо спровоцируете раннюю инициализацию, либо получите странные полурабочие состояния, либо, что чаще, просто создадите себе проблему, которую потом будет невозможно объяснить новичку.

Ошибка №3: проверять в BFPP runtime-данные бизнес-сценария.
Иногда хочется написать “проверку”: а есть ли хотя бы один заказ в OrderStore? Но это уже не sanity check контейнера, а логика сценария. Контейнерный BFPP не должен знать, какие заказы будут создаваться и когда. Он отвечает только за то, что приложение собрано как минимум жизнеспособно: есть порты, есть реализации, есть ключевые инфраструктурные beans.

Ошибка №4: делать критерий BPP слишком широким и обрабатывать «вообще все beans».
Технически BPP имеет власть над каждым объектом в контексте. И если вы её используете без фильтров, то получаете консольный шум, непредсказуемые побочные эффекты и вопрос “почему стало медленно”. В ContextFlow трекайте только выбранные роли: сервисы, конкретные порты, конкретные пакеты. Узкий критерий — это не ограничение, это способ остаться в здравом уме.

Ошибка №5: забыть вернуть bean из метода BPP или вернуть несовместимую обёртку.
BPP — это место, где одна случайная ошибка превращает приложение в сюрреализм. Если вернуть null, bean исчезнет. Если вернуть объект другого типа, автосвязывание может сломаться в неожиданном месте. Поэтому в диагностическом BPP почти всегда правильный ответ — “вернул тот же объект, просто вывел информацию”.

1
Задача
Spring Core, 19 уровень, 4 лекция
Недоступна
Отдельный `SupportConfig` для ранней проверки структуры приложения
Отдельный `SupportConfig` для ранней проверки структуры приложения
1
Задача
Spring Core, 19 уровень, 4 лекция
Недоступна
Точечная диагностика service-beans из support-слоя
Точечная диагностика service-beans из support-слоя
1
Опрос
Поток контекста, 19 уровень, 4 лекция
Недоступен
Поток контекста
Этапы запуска Spring
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ