1. Conceptual checkpoint: цель
Conceptual checkpoint — это не список терминов, которые нужно выучить как таблицу умножения, а проверка, что у вас в голове сложилась причинно-следственная цепочка. Типичный провал новичка выглядит так: он помнит @Component, @Bean, @Profile, но не может связать их в историю «зачем это нужно и что реально происходит при старте приложения». Чекпоинт — это попытка собрать из “слов” модель, которая держит Spring в руках, а не держит вас.
Чтобы было проще, представьте ситуацию «вас разбудили в 3 ночи» (не рекомендую, но методически полезно). Вам задают вопрос: «Почему сервис стал proxy?» или «Почему приложение падает на старте, хотя метод placeOrder() даже не вызвали?» Если вы отвечаете не фразой “ну Spring такой”, а связываете ответ с контейнером, фазами старта, lifecycle и wiring — вы прошли чекпоинт.
Здесь полезнее не коллекционировать термины, а на каждом узле быстро проверять себя: видите ли вы причину появления механизма, место этого механизма в старте приложения и типичную путаницу рядом с ним.
Ниже — компактная «карта разговора». Это не шпаргалка «как заклинание», а ориентир: какой смысловой блок за какой тянется.
| Блок понимания | Что вы должны уметь проговорить одним абзацем | Где это видно в ContextFlow |
|---|---|---|
| Manual wiring → DI | Почему new внутри сервисов ломает масштабирование и тестируемость | ранние версии OrderPlacementService и bootstrap |
| IoC / composition root | Где должна собираться система и почему это одна точка | AppConfig, затем модульные конфиги |
| ApplicationContext | Почему это “центр приложения”, а не просто фабрика объектов | main стартует контекст, достаёт ScenarioRunner |
| BeanDefinition и фазы | Что происходит до создания объектов и почему ошибки возникают “ещё до бизнеса” | startup failures, post-processors |
| Registration (scanning/@Bean) | Как класс превращается в bean и почему @Configuration — не просто @Component | @ComponentScan, @Import, @Bean |
| Dependency resolution | Что происходит при нескольких реализациях интерфейса | NotificationSender, DiscountPolicy |
| Lifecycle / scopes | Когда bean “готов”, как работает destroy, и почему scope — свойство контейнера | NotificationTemplateCatalog, prototype sessions |
| Environment / properties | Почему конфигурация — часть runtime, а не “строки в коде” | contextflow.properties, @Value |
| Profiles / conditions | Как менять состав бинов без if-else в бизнес-логике | dev/demo/test режимы |
| Resource / MessageSource | Чем “ресурс приложения” отличается от файла и зачем i18n без web | templates, message bundles |
| Events | Зачем события внутри приложения и почему они синхронны по умолчанию | OrderCreatedEvent, listeners |
| Extension points | BFPP/BPP/FactoryBean/Aware: что это и почему нельзя тащить в бизнес | support.* пакет |
| Proxies / AOP | Откуда берётся поведение “вокруг метода” и почему self-invocation больно | ServiceTimingAspect |
| XML + tests | XML как тот же контейнер и тесты как проверка сборки | legacy xml bridge, smoke tests |
2. Большая схема ContextFlow
В финале курса важно не утонуть в деталях и удержать “одну большую картинку”. Spring Core — это история о том, как приложение стартует, как контейнер читает метаданные, создаёт объектный граф, применяет инфраструктурные механики и только потом отдаёт вам точку входа уровня бизнес-сценариев. Если вы видите эту схему, почти любая аннотация перестаёт быть магией и становится просто меткой в нужном месте пайплайна.
Ниже — условная блок-схема (в жизни шагов больше, но нам важны смысловые вехи):
flowchart TD
A["main()"] --> B[создаём ApplicationContext]
B --> C[регистрируем конфигурацию / scanning]
C --> D["refresh(): читаем BeanDefinition"]
D --> E[post-processors: BFPP/BPP]
E --> F[создаём singleton beans]
F --> G[готовый контекст]
G --> H["ScenarioRunner.run()"]
H --> I["use-case публикует events"]
I --> J[listeners делают side effects]
Если эта схема действительно у вас в голове, вы должны без подсказки восстановить короткий pure Spring scaffold:
1. создать AnnotationConfigApplicationContext;
2. до refresh() выбрать активный profile;
3. зарегистрировать AppConfig;
4. вызвать refresh(), чтобы контейнер прочитал definitions и собрал singleton-граф;
5. получить ScenarioRunner и запустить сценарий;
6. закрыть context, чтобы отработали destroy-callbacks.
Если на этом месте путаются шаги 2, 3 и 4, значит ещё не до конца собралась причинно-следственная цепочка между конфигурацией, startup pipeline и runtime-поведением приложения.
3. Manual wiring и IoC/DI
Первый вопрос к себе: можете ли вы за полминуты объяснить, почему new внутри сервиса ломает рост проекта и где после этого должен жить composition root? Если ответ сводится к “Spring потом подставит зависимость”, значит узел ещё не собран.
Смысл не в том, что new как слово запрещён. Смысл в том, что business-class не должен сам решать, какую реализацию ему создавать: при росте проекта это превращается в tight coupling и делает тесты дороже.
Плохая версия выглядит примерно так: бизнес-сервис сам решает, какие реализации ему нужны, и сам их создаёт. Это прямая дорога к tight coupling.
public class OrderPlacementService {
public void placeOrder(String orderId) {
// Плохо: сервис сам создаёт зависимость и жёстко привязывается к конкретной реализации.
// Это усложняет тестирование (нельзя подменить sender) и масштабирование (появляются if-else и копипаста).
NotificationSender sender = new ConsoleNotificationSender();
// Плохо: бизнес-логика начинает «знать», как именно отправлять уведомления.
sender.send("Order created: " + orderId);
}
}
Хорошая версия — зависимости видны в конструкторе. Класс честно говорит: «Я работаю через контракт NotificationSender, а не через конкретный Console...». И вот это уже DI-friendly дизайн, который Spring потом просто автоматизирует.
public class OrderPlacementService {
private final NotificationSender sender;
public OrderPlacementService(NotificationSender sender) {
// Хорошо: зависимость приходит извне (DI), её можно подменить в тесте или в другом профиле.
this.sender = sender;
}
}
Хороший короткий ответ здесь звучит так: зависимости становятся явными, класс проще подменять в тестах, а Spring лишь автоматизирует сборку такого графа. Если в ответе нет слов “явные зависимости”, “подменяемость” и “testability”, фундамент ещё шатается.
4. BeanFactory и две фазы старта
Следующий вопрос: можете ли вы без путаницы развести BeanFactory, ApplicationContext и две фазы старта? Если в ответе нет метаданных до объектов, startup failures будут казаться магией.
BeanFactory — базовый контейнер. ApplicationContext — его “повседневная расширенная версия”: она умеет Environment, ресурсы, сообщения, события и многое из того, что вы реально используете в приложении. Ключевая мысль при этом одна: bean сначала живёт как definition, и только потом становится реальным объектом.
Пусть пример и диагностический, но он делает модель наблюдаемой:
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
// Здесь мы смотрим на метаданные (definition), а не на реальный объект-сервис.
BeanDefinition bd = context.getBeanFactory()
// Имя "orderPlacementService" — это bean name, по нему definition хранится в фабрике.
.getBeanDefinition("orderPlacementService");
// Для @Component это обычно будет имя класса, которое Spring собирается создать.
System.out.println(bd.getBeanClassName()); // com.example.contextflow.application.service.OrderPlacementService
}
Если вы спокойно проговариваете “сначала definition, потом instance”, перестают быть странными BFPP, profiles и ошибки на старте. Именно эта связка и объясняет, почему контейнер умеет падать ещё до первого бизнес-вызова.
5. Регистрация бинов и модули
Здесь проверьте две вещи. Во-первых, можете ли вы быстро назвать, из каких модулей складывается финальный composition root. Во-вторых, понимаете ли вы, почему @Configuration — это место сборки графа, а не склад случайных аннотаций.
Когда проект маленький, хочется держать всё в одном AppConfig. Когда проект чуть-чуть подрос — хочется сделать ещё один AppConfig. Когда проект вырос нормально — хочется спрятаться под стол. Поэтому мы и учили дисциплину: модульная конфигурация и понятные границы scanning.
Финальная сборка ContextFlow выглядит спокойно: один верхний конфиг, который импортирует модули.
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
// Верхний конфиг (composition root): он собирает приложение из модулей и задаёт явные границы.
@Import({
// База приложения: доменные сервисы/порты/репозитории и т.п.
CoreConfig.class,
// Профили (dev/demo/test) и вариативность поведения.
ProfilesConfig.class,
// Отчёты, форматирование и вывод.
ReportingConfig.class,
// Мост к легаси XML-фрагменту.
LegacyBridgeConfig.class,
// Инфраструктурные расширения Spring: BFPP/BPP/FactoryBean/AOP и диагностика.
SupportConfig.class
})
public class AppConfig {
}
Если, восстанавливая эту картину, вы забываете SupportConfig, значит из головы выпадают post-processors, FactoryBean, диагностика и AOP. А рядом полезно удерживать вторую мысль: scanning — это одна из registration strategies, а не магия “само всё найдётся”.
6. Разрешение зависимостей
Теперь вопрос про wiring: что делает контейнер, когда у интерфейса несколько реализаций? И чем Map<String, NotificationSender> лучше ручного if-else new ...?
Хороший ответ должен звучать так: контейнер либо выбирает кандидата по умолчанию, либо мы делаем выбор явным, либо получаем набор стратегий и маршрутизируем уже готовые beans.
Очень хороший checkpoint — уметь объяснить, почему Map<String, NotificationSender> — это не “странная фишка Spring”, а контейнерный способ построить реестр стратегий. Ключ в Map — это имя bean-а, а не “рандомная строка из воздуха”. Пример:
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class NotificationDispatchService {
private final Map<String, NotificationSender> senders;
public NotificationDispatchService(Map<String, NotificationSender> senders) {
// Spring соберёт сюда все NotificationSender-бины, которые есть в контексте.
// Ключ в Map — это bean name (например, "emailNotificationSender"), а значение — сам bean.
this.senders = senders;
}
}
Дальше вы либо выбираете отправителя по конфигурации (например, defaultChannel), либо делаете маленький router. И ключевой момент: вы не создаёте реализации сами, вы не делаете if (channel == EMAIL) new EmailSender(), вы просите контейнер дать вам готовые стратегии.
Optional зависимости — это отдельная зона осторожности. Здесь важно уметь объяснить, что Optional/ObjectProvider — это не “обход DI”, а способ честно сказать: “эта интеграция может отсутствовать”. И ещё важнее — уметь отличать это от service locator, где бизнес-сервис “вдруг” начинает искать бины в рантайме.
7. Lifecycle и scopes
Здесь проверьте, можете ли вы объяснить, когда bean действительно готов к работе и почему shutdown — не декоративный финал. Если в ответе нет init/destroy и scope как свойства контейнера, картина ещё плавает.
Пока вы не знаете lifecycle, вы воспринимаете приложение как “я создал объект — он работает”. В Spring это недостаточно: контейнер должен создать объект, внедрить зависимости, выполнить init-callbacks, и только потом вы можете считать bean готовым. А на shutdown он должен корректно вызвать destroy-callbacks — если вы не забыли закрыть контекст. Это особенно важно для ресурсных штук: каталогов шаблонов, менеджеров файлового вывода, кешей и т.п.
Ниже — короткий, почти “учебник для будущего себя” пример:
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Component;
@Component
public class NotificationTemplateCatalog {
@PostConstruct
void init() {
// init-callback: выполняется после создания bean-а и внедрения зависимостей
System.out.println("templates loaded"); // templates loaded
}
@PreDestroy
void shutdown() {
// destroy-callback: выполнится при закрытии ApplicationContext (например, try-with-resources)
System.out.println("templates released"); // templates released
}
}
Теперь scopes. Вы должны уметь проговорить, что Spring-singleton — это singleton per container, а не GoF Singleton на весь JVM-мир. И уметь объяснить ловушку “prototype в singleton”: prototype создаётся при инъекции, а не “на каждый вызов”. Поэтому контролируемое создание prototype обычно делается через deferred lookup (например, ObjectProvider), а не через наивную инъекцию “одного prototype-объекта”.
Если вы можете на словах объяснить, почему “singleton — должен быть stateless”, и как состояние переносится в method-local данные или короткоживущие сессии, значит вы не построите себе будущий баг “иногда работает, иногда нет”.
8. Environment, properties, profiles
Если попросить вас коротко объяснить Environment, properties и profiles, хороший ответ должен звучать не “это строки из файла”, а “это часть runtime-сборки приложения”. Следующий контрольный вопрос — понимаете ли вы, что profile меняет состав контекста, а не просто значение переменной.
В какой-то момент любой проект упирается в то, что значения “вшиты” в код: путь вывода отчёта, режим аудита, дефолтный канал уведомлений. В этот момент появляются либо хардкод и страдания, либо нормальная конфигурация. В Spring Core мы учились именно второму: Environment, property sources, @Value, и аккуратная типизация через conversion, чтобы не тащить по проекту if ("CSV".equalsIgnoreCase(format)).
Самый простой checkpoint: вы можете объяснить, откуда берётся значение для ${contextflow.app-name}, какой у него порядок поиска, и почему дефолтные значения важны, если у пользователя нет файла конфигурации.
Профили — следующая ступень. Здесь важно уметь объяснить, что profiles — это не “переключатель фич”, а режим сборки приложения. В test-профиле мы хотим детерминированный генератор id и no-op уведомления, чтобы тесты были стабильными. В demo — предсказуемое поведение для демонстрации. В dev — максимально простой console-run.
Минимальный пример профильного бина выглядит так:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
public class ProfilesConfig {
@Bean
@Profile("test")
OrderIdGenerator orderIdGenerator() {
return new DeterministicOrderIdGenerator();
}
}
Если вы можете объяснить, что профили меняют состав контекста, а не “просто значение переменной”, и что это чище, чем if (mode.equals("test")) внутри бизнес-сервиса, то вы унесли правильную практику из курса.
9. Resources и MessageSource
Проверьте себя на двух различениях: Resource vs File и MessageSource vs “строки прямо в коде”. Если эти пары сливаются, потом трудно понять, почему classpath-ресурс нельзя мыслить как произвольный файл на диске и почему тексты лучше держать вне Java-классов.
Почти у каждого новичка есть фаза “я читаю файлы через new File("...") и абсолютные пути”. И почти у каждого новичка есть фаза “почему в jar это сломалось?”. Spring Resource abstraction как раз создана, чтобы вы перестали связывать “ресурс приложения” и “файл на диске” в одну сущность. В нашем курсе это проявилось в шаблонах уведомлений и заголовках отчётов: они живут в classpath, а артефакты мы пишем только в build/.
Параллельно MessageSource даёт i18n даже без web. В non-web приложении Locale — это не “язык браузера”, а явно выбранная настройка: по клиенту или по дефолту. В checkpoint вы должны уметь объяснить, почему текст сообщений выносится из кода, и как работает fallback, когда локализованного ключа нет.
Вот маленький “обёрточный” сервис для сообщений:
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Service;
import java.util.Locale;
@Service
public class ContextFlowMessages {
private final MessageSource messageSource;
public ContextFlowMessages(MessageSource messageSource) {
this.messageSource = messageSource;
}
public String orderCreated(Locale locale, String orderId) {
return messageSource.getMessage("order.created", new Object[]{orderId}, locale);
}
}
Если вы можете проговорить, что MessageSource — это часть ApplicationContext, а не “какая-то отдельная библиотека”, и что ключи сообщений — это контракт (примерно как API-контракт, только текстовый), то вы действительно понимаете роль контекста как runtime среды.
10. Application events
Здесь главный checkpoint-вопрос такой: что вы публикуете — факт или команду? И помните ли вы, что listeners по умолчанию работают синхронно?
События — это место, где очень легко “переборщить”. Поэтому чекпоинт здесь не “знать аннотацию”, а уметь объяснить границу применимости. В ContextFlow мы вынесли side effects (аудит, уведомления, статистику) из use-case сервисов в listeners, чтобы основной сценарий стал читабельным. Но при этом мы не превращали весь бизнес-процесс в цепочку событий, где невозможно понять, что происходит и в каком порядке.
Публикация события в use-case сервисе выглядит максимально скучно — и это хорошо:
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
@Service
public class OrderPlacementService {
private final ApplicationEventPublisher publisher;
public OrderPlacementService(ApplicationEventPublisher publisher) {
// Паблишер — инфраструктурная зависимость: через него мы публикуем события в ApplicationContext.
this.publisher = publisher;
}
public void place(String orderId) {
// Публикуем доменное событие: мы сообщаем "что случилось", а не "как делать side effects".
publisher.publishEvent(new OrderCreatedEvent(orderId));
}
}
А обработчик side effect — отдельный bean. И ключевое: по умолчанию этот обработчик вызовется синхронно, в том же потоке, до завершения place().
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class AuditOrderEventsListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// По умолчанию слушатель вызывается синхронно в потоке, который публикует событие.
// Поэтому тяжёлые операции здесь могут замедлить основной use-case.
System.out.println("AUDIT " + event.orderId()); // AUDIT ORD-001
}
}
Если вы можете объяснить разницу “events внутри приложения” vs “messaging между сервисами” (Kafka/RabbitMQ), и не путаете синхронные listeners с асинхронностью, значит вы держите архитектуру в руках, а не просто “делаете модно”.
11. Extension points Spring
Эта часть проверяет не любовь к API, а понимание pipeline. Можете ли вы спокойно развести BFPP, BPP, FactoryBean и Aware по месту и роли? И можете ли сразу сказать, почему всё это держим в support, а не в business layer?
BeanFactoryPostProcessor работает на уровне метаданных и запускается до создания объектов. Это идеальное место для sanity-check на конфигурацию, но плохое место для “делать бизнес”.
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;
@Component
public class ContextFlowSanityCheckBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) {
System.out.println("Bean definitions: " + bf.getBeanDefinitionCount()); // Bean definitions: 87
}
}
BeanPostProcessor трогает уже созданные beans и может оборачивать их, добавлять поведение, модифицировать. Через BPP растут многие “магические” механики, включая auto-proxying.
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
@Component
public class TrackedComponentBeanPostProcessor implements BeanPostProcessor {
public Object postProcessAfterInitialization(Object bean, String name) throws BeansException {
return bean;
}
}
FactoryBean — особый случай: bean в контейнере является фабрикой, а “наружу” контейнер отдаёт продукт. Вы должны уметь объяснить отличие FactoryBean от обычного @Bean-метода и помнить про &beanName, когда нужен именно объект-фабрика.
import org.springframework.beans.factory.FactoryBean;
public class ReportFormatterFactoryBean implements FactoryBean<ReportFormatter> {
public ReportFormatter getObject() { return new TextReportFormatter(); }
public Class<?> getObjectType() { return ReportFormatter.class; }
}
Aware — самый опасный из “удобных” инструментов, потому что им легко заразить business layer контейнерными деталями. В курсе мы держали его в диагностике: да, иногда нужно узнать Environment, имя bean-а или контекст, но это не повод превращать сервисы в “контейнеро-зависимые”.
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Component
public class ContextDiagnosticsBean implements EnvironmentAware {
public void setEnvironment(Environment environment) {
System.out.println(environment.getActiveProfiles().length); // 1
}
}
Если вы можете объяснить, что BFPP — “до объектов”, BPP — “после создания”, FactoryBean — “bean производит другой объект”, а Aware — “контейнер сообщает что-то bean-у”, и при этом добавляете фразу “это не должно течь в business layer”, то вы прошли один из самых важных conceptual узлов Spring.
12. Proxy-модель и AOP basics
Проверьте, можете ли вы объяснить proxy-модель без слова “магия”: кто такой proxy, кто target и почему self-invocation обходит advice.
Понимание proxy-модели — это то, что делает будущие темы (@Transactional, security interception) не магией, а “ну да, прокси вокруг метода”. Чекпоинт тут предельно практичный: вы должны различать proxy и target object, понимать, что перехватываются вызовы через container-managed reference, и помнить ограничения: final, private, self-invocation.
Минимальный аспект в нашем проекте — измерение времени выполнения сервисных методов. Он не должен содержать бизнес-логику, только техническую обвязку.
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ServiceTimingAspect {
@Around("execution(* com.example.contextflow.application.service..*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
return pjp.proceed();
}
}
Даже если этот пример “ничего не логирует”, он уже позволяет вам объяснить главное: Spring не переписывает ваши классы в рантайме “как хочет”, он создаёт proxy, который перехватывает вызовы и делает что-то до/после. И если внутри класса вы вызвали свой же метод напрямую, прокси не участвовал — значит advice не сработал. Это не баг Spring, это честная модель.
13. Legacy XML и тесты контекста
Финальный технический checkpoint: умеете ли вы спокойно читать XML как тот же bean model и понимаете ли вы, что context smoke test проверяет сборку, а не бизнес-математику?
Финальный checkpoint должен снять страх перед двумя вещами: XML и “тесты со Spring-контекстом”. XML — это просто другой синтаксис описания тех же beans и тех же зависимостей. Вы должны уметь посмотреть на такую запись и прочитать её как bean definition, а не как “древнюю магию”.
<bean id="consoleAuditWriter"
class="com.example.contextflow.infrastructure.audit.ConsoleAuditWriter"/>
Подключение маленького legacy-фрагмента в современный Java-config проект делается мостом, и вы должны уметь объяснить: “мы не переезжаем в XML, мы временно импортируем кусок, потому что legacy так живёт”.
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
@Configuration
@ImportResource("classpath:legacy/legacy-notification-context.xml")
public class LegacyBridgeConfig {
}
А тесты контекста — это не “тесты всего подряд через Spring”, а проверка того, что сборка контейнера реальна, воспроизводима и работает в нужном профиле. Иногда лучший тест — это тот, который просто поднимает контекст и не падает.
import org.junit.jupiter.api.Test; // В курсе используем JUnit Jupiter; версия фиксируется репозиторием.
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(AppConfig.class)
@ActiveProfiles("test")
class ContextSmokeTest {
@Test
void contextStarts() {
// Smoke test: проверяет сборку контекста (wiring/profiles/properties),
// а не бизнес-логику создания заказа.
}
}
Если вы можете объяснить, что этот тест проверяет wiring/profiles/properties, а не “бизнес-логику создания заказа”, вы понимаете границу unit vs context testing, а значит не превратите проект в медленный и хрупкий тестовый монолит.
14. Типичные ошибки
Ошибка №1: отвечать списком терминов вместо истории.
Очень частый “псевдо-успех”: студент говорит «BeanFactory, ApplicationContext, BeanDefinition, @Component, @Bean…», но не может связать это в объяснение “как приложение стартует”. Лечится просто: каждый ответ начинайте с проблемы, потом механизм, потом эффект в проекте. Не “какие аннотации”, а “какой шаг контейнера”.
Ошибка №2: путать BeanFactoryPostProcessor и BeanPostProcessor.
В голове это часто сливается в “какой-то процессор”. Стабильная опора: BFPP работает с метаданными до создания объектов, BPP — с уже созданными объектами (до/после init). Если вы эту фразу можете произнести спокойно, вы уже не перепутаете их в реальной работе.
Ошибка №3: делать вид, что proxy — это “только в AOP-лекции”.
Потом при встрече с @Transactional легко решить, что транзакции “встроены в метод”. Нет: proxy-модель — это фундамент. Если вы не держите в голове “есть proxy и есть target”, вы будете ловить странности instanceof, self-invocation и “почему не сработало”.
Ошибка №4: называть bean-ом всё, что движется.
Доменные объекты (Order, OrderItem, команды) обычно не должны быть bean-ами. Bean — это то, чем управляет контейнер: сервисы, инфраструктура, конфигурационные штуки. Если вы делаете из каждого доменного экземпляра bean, вы быстро получаете кашу из scopes, состояния и неявных зависимостей.
Ошибка №5: скатываться в service locator под видом “так быстрее”.
Как только в бизнес-коде появляется ApplicationContext.getBean(...) или статический holder контекста, вы теряете DI как принцип. Код начинает “искать мир вокруг себя”, а не получать зависимости. На небольшом проекте оно даже будет работать… пока не начнёт ломаться там, где вы не ждёте.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ