Перейти к основному содержимому

Spring Framework

Разработчику Архитектору

Spring

См. также: JVM, память и потоки · Жизненный цикл бина в Spring (кратко) · Первая программа на Spring · Hibernate и JPA · JUnit 5 · Gradle · Azure / App Service (Microsoft)


Как читать эту статью без перегруза

Материал объёмный, поэтому эффективнее идти по маршруту:

  1. Сначала "Основные концепции" (IoC, DI, Bean, ApplicationContext).
  2. Затем "Spring MVC" и "Spring Data" для понимания типового web-приложения.
  3. После этого "Spring Security" и "Управление транзакциями".
  4. Продвинутые разделы (Cloud, WebFlux, Native Image, Saga) изучайте по задачам проекта.

Для практики сразу после первых двух шагов переходите в первую программу на Spring, затем в Spring Security и JWT.


Что такое Spring и зачем он нужен?

Spring — это экосистема фреймворков и инструментов, предназначенных для построения надёжных, гибких и легко тестируемых корпоративных приложений на платформе Java SE и Java EE (ныне Jakarta EE). Изначально созданный в 2002 году Родом Джонсоном как альтернатива тяжеловесной и сложной в сопровождении модели Enterprise JavaBeans (EJB 2.x), Spring быстро стал де-факто стандартом для разработки серверных Java-приложений — от монолитов до распределённых микросервисов.

Главное предназначение Spring — упорядочить и упростить уже существующие возможности Java. Он оборачивает, координирует и делегирует Java API. Spring не требует наследования от его базовых классов и не навязывает жёсткой архитектуры; вместо этого он предлагает парадигмы проектирования, реализованные через инверсию управления и аспектно-ориентированное программирование. Это делает приложения, построенные на Spring, слабосвязанными, модульными и поддающимися тестированию вне контейнера — качества, критически важные в промышленной разработке.

Следует сразу провести терминологическое различие:

  • Spring Framework — это ядро экосистемы — набор модулей, реализующих базовые механизмы — управление объектами, внедрение зависимостей, интеграция с БД, транзакции, веб-обработка. Он полностью независим от Servlet API, хотя и умеет с ним работать.
  • Spring Boot, Spring Security, Spring Data и другие — это проекты, построенные на основе Spring Framework, решающие конкретные задачи (ускорение старта, безопасность, работа с данными и пр.). Они не являются частью ядра, но тесно с ним интегрированы и часто используются совместно.

Таким образом, "Spring" в широком смысле — это целая платформа, а в узком — её фундаментальный компонент: Spring Framework.


Исторический контекст и мотивация дизайна

Для понимания логики Spring важно вспомнить состояние Java-разработки в начале 2000-х. Платформа Java EE (тогда J2EE) предоставляла мощный, но чрезвычайно комплексный и вербозный стек — Enterprise JavaBeans (EJB), Java Naming and Directory Interface (JNDI), Java Transaction API (JTA), Java Message Service (JMS) и др. Простое приложение требовало десятков XML-файлов, наследования от фреймворковых классов и запуска внутри полноценного application server’а (WebLogic, WebSphere). Тестирование вне контейнера было затруднено, а изменения конфигурации — трудоёмки.

Spring предложил радикальный поворот: отказ от требований к среде выполнения. Spring-приложение можно запустить как обыкновенное Java-приложение (main()), без application server’а. Он заменил необходимость наследования — композицией и внедрением зависимостей, а жёсткую связку с инфраструктурой — абстракциями (например, JdbcTemplate вместо "голого" java.sql.Connection). Spring реализовал идеи Java EE проще и легче — и, в конечном итоге, сильно повлиял на эволюцию самой Jakarta EE (например, внедрение CDI — Contexts and Dependency Injection — во многом вдохновлено Spring DI).

Ключевая философская установка Spring — "non-invasive" (ненавязчивость): ваши классы не обязаны знать о Spring. Они могут быть обычными POJO (Plain Old Java Objects). Вся "магия" — внешняя, управляется фреймворком.


Архитектура Spring Framework

Spring Framework спроектирован как модульная иерархия. Он состоит из более чем 20 модулей, объединённых в несколько слоёв. Каждый модуль — независимая JAR-библиотека, которую можно подключить отдельно, без остальных. Это позволяет собирать приложение "по частям", избегая избыточных зависимостей.

Ниже приведена структура по слоям, от ядра к периферии, с пояснением роли каждого компонента:


1. Core Container (Ядро контейнера)

Это фундамент всей экосистемы. Всё, что касается жизненного цикла объектов, управления конфигурацией и зависимостями, находится здесь.

  • spring-core — базовые утилиты, используемые во всём фреймворке — Resource (унифицированный доступ к файлам, URL, classpath), TypeConverter, аннотация @Nullable, механизм обработки свойств (PropertyEditor, Environment). Ядро, на котором строится остальное.
  • spring-beans — реализация IoC-контейнера. Содержит интерфейсы BeanFactory и ApplicationContext, а также всю логику создания, настройки, связывания и уничтожения бинов (beans) — объектов, управляемых Spring.
  • spring-context — расширение контейнера, добавляющее поддержку международизации (MessageSource), событийной модели (ApplicationEventPublisher), планировщиков (TaskExecutor, TaskScheduler), валидации (Validator). Именно ApplicationContext (а не BeanFactory) используется в подавляющем большинстве приложений.
  • spring-expression (SpEL) — мощный язык выражений, встроенный во время выполнения. Позволяет динамически вычислять значения в конфигурации: @Value("#{systemProperties['user.region']}"), @Value("#{T(java.lang.Math).random() * 100}"), вызов методов, доступ к коллекциям. Используется в XML/Java-конфигурации, Spring Security, Spring Data и др.

2. Data Access / Integration (Доступ к данным и интеграция)

Слой, обеспечивающий единый и упрощённый способ взаимодействия с внешними системами — базами данных, очередями сообщений, транзакционными менеджерами.

  • spring-jdbc — абстракция над JDBC. Устраняет шаблонный код: открытие/закрытие соединений, обработка SQLException. JdbcTemplate позволяет писать запросы одной строкой, не заботясь о ручном управлении ресурсами.
  • spring-orm — интеграция с ORM-фреймворками — Hibernate, JPA, JDO, iBatis/MyBatis. Spring управляет жизненным циклом ORM (сессии, фабрики), обёртывает исключения (HibernateException → DataAccessException) и координирует транзакции.
  • spring-oxm — Object/XML Mapping. Упрощает сериализацию/десериализацию Java-объектов в XML и обратно с помощью JAXB, Castor, XMLBeans, JiBX, XStream.
  • spring-jms — абстракция над Java Message Service. Позволяет отправлять и получать сообщения через JmsTemplate, а также реализовывать слушателей (MessageListener) без работы с Connection, Session и MessageConsumer вручную.
  • spring-txдекларативное и программное управление транзакциями. Позволяет объявить метод как транзакционный (@Transactional) и не думать о begin(), commit(), rollback(). Поддерживает как локальные (DataSourceTransactionManager), так и распределённые (JtaTransactionManager) транзакции, вложенные транзакции и точки сохранения (savepoints).

3. Web (Веб-слой)

Предназначен для построения веб-приложений и RESTful API.

  • spring-web — базовая инфраструктура — клиенты HTTP (RestTemplate, WebClient), кодеки, обработка multipart-запросов, поддержка WebSocket.
  • spring-webmvc — реализация Model-View-Controller на основе Servlet API. Содержит DispatcherServlet — центральный контроллер, распределяющий запросы по обработчикам, а также всё, что связано с маршрутизацией (@Controller, @RequestMapping), преобразованием данных (HttpMessageConverter), резолверами представлений (ViewResolver).
  • spring-webfluxреактивный стек для построения асинхронных, неблокирующих веб-приложений (альтернатива MVC). Использует Project Reactor (Mono, Flux) и Netty в качестве встроенного сервера.

4. AOP и инструментирование

  • spring-aop — поддержка аспектно-ориентированного программирования. Позволяет выносить сквозную функциональность (логгирование, безопасность, кэширование, транзакции) в отдельные аспекты, не засоряя бизнес-код. Реализован через прокси-объекты (JDK Dynamic Proxies или CGLIB).
  • spring-aspects — интеграция с AspectJ — более мощным, но более сложным AOP-фреймворком, способным модифицировать байткод.
  • spring-instrument — поддержка класс-лоадеров для инструментирования (например, для отслеживания времени выполнения методов во время загрузки классов).

5. Тестирование

  • spring-test — инструменты для интеграционного тестирования — загрузка контекста в JUnit (@SpringBootTest), мокинг зависимостей (@MockBean), транзакционные тесты (@Transactional), тестирование веб-слоя (@WebMvcTest). Позволяет проверять взаимодействие компонентов в условиях, максимально приближенных к production.

Основные концепции

Чтобы понять Spring, необходимо глубоко освоить четыре взаимосвязанных понятия.


Inversion of Control (IoC) — Инверсия управления

Это принцип проектирования. Суть: вместо того чтобы объект сам создавал или искал свои зависимости, их ему предоставляет внешний субъект — "контейнер". Традиционно объект контролирует поток выполнения: он решает, когда и какие другие объекты создавать. При IoC этот контроль инвертируется: поток управления передаётся контейнеру. Объект становится пассивным участником — он объявляет, что ему нужно, а контейнер заботится о том, как это получить.

IoC снижает связность: класс зависит не от конкретной реализации PaymentService, а от интерфейса PaymentService. Конкретная реализация (CreditCardPaymentService, PayPalPaymentService) поставляется извне — и может меняться без изменения кода класса.


Dependency Injection (DI) — Внедрение зависимостей

Это механизм реализации IoC. DI — это процесс, при котором зависимости передаются объекту, а не запрашиваются им самим. В Spring существует три способа DI:

  1. Внедрение через конструктор — наиболее рекомендуемый. Гарантирует, что объект создаётся в полностью инициализированном состоянии. Неизменяемые зависимости.
@Service
public class OrderService {
private final PaymentService paymentService;

public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
  1. Внедрение через setter-метод — позволяет изменять зависимости после создания объекта (редко нужно).
@Service
public class OrderService {
private PaymentService paymentService;

@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
  1. Внедрение через поле — самый компактный, но нарушает инкапсуляцию и затрудняет unit-тестирование (нельзя передать зависимость вручную без рефлексии). Не рекомендуется, кроме тривиальных случаев.
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
}

Spring использует аннотацию @Autowired (или JSR-330 @Inject) для автоматического связывания. По умолчанию связывание происходит по типу; при конфликте — по имени бина или с помощью @Qualifier.


Bean — управляемый объект

В терминологии Spring бин — это любой Java-объект, жизненный цикл которого управляется Spring IoC-контейнером. Это не специальный тип объекта — это роль. Любой POJO может стать бином, если Spring знает о нём и берёт на себя его создание, настройку и уничтожение.

Бины описываются метаданными конфигурации. Ранее — в XML:


Разбор:
- В этом фрагменте ключевой объект — `OrderService`; через него показан конкретный кусок архитектуры из раздела.
- Аннотации `@Service, @Autowired, @Inject, @Qualifier` включают фреймворковое поведение: регистрация, биндинг, security или тестовая инфраструктура.
- Основной поток строится на вызовах ``OrderService()`, `setPaymentService()``: они задают путь от входа к результату.
- При расширении этого примера сначала проверяют контракты методов и тесты на граничные случаи — именно там чаще всего возникает регрессия.

Сегодня — в основном через аннотации и Java-конфигурацию:


Разбор:
- Этот блок работает как схема: строка `Сегодня — в основном через *аннотации* и *Java-конфигурацию*:` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Или автоматически — через компонентное сканирование:


Разбор:
- Этот блок работает как схема: строка `Или автоматически — через *компонентное сканирование*:` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Каждый бин имеет:

  • Имя (bean name) — уникальный идентификатор в контексте (по умолчанию — имя класса с маленькой буквы).
  • Область видимости (scope)singleton (по умолчанию, один экземпляр на контекст), prototype (новый экземпляр при каждом запросе), request, session, websocket.
  • Жизненный цикл — Spring вызывает @PostConstruct после инициализации и @PreDestroy перед уничтожением. Также можно реализовать InitializingBean / DisposableBean.

ApplicationContext — контекст приложения

Это самый важный интерфейс в Spring. ApplicationContext — это расширенная реализация BeanFactory, представляющая собой полную конфигурацию приложения в runtime. Его можно рассматривать как реестр бинов плюс инфраструктурные сервисы.

Когда приложение запускается, Spring:

  1. Читает конфигурацию (XML, @Configuration, @Component-классы).
  2. Создаёт метамодель — описание всех бинов, их зависимостей, настроек.
  3. Выполняет разрешение зависимостей (dependency resolution) — строит граф объектов.
  4. Создаёт бины в правильном порядке (уважая зависимости), применяет пост-процессоры (BeanPostProcessor), внедряет зависимости.
  5. Вызывает методы инициализации.
  6. Складывает готовые бины в ApplicationContext.

После этого любой компонент может получить доступ к любому другому бину через ApplicationContext (хотя прямой вызов context.getBean() считается плохой практикой — зависимости должны внедряться, а не извлекаться вручную).

Именно ApplicationContext обеспечивает:

  • Единое пространство имён для бинов.
  • Централизованное управление жизненным циклом.
  • Доступ к инфраструктурным сервисам — событиям, интернационализации, окружению.
  • Интеграцию с другими слоями (транзакции, AOP, веб).

Способы конфигурации

Spring прошёл путь от XML-диктатуры к полностью автоматизированной настройке. Сегодня существует три основных подхода — и они могут сосуществовать.


1. XML-конфигурация (устаревшая, но важная для понимания)

Исторически первый способ. Конфигурация описывается в отдельных XML-файлах (например, applicationContext.xml). Бины объявляются тегами <bean>, зависимости — через <property> или <constructor-arg>.

Преимущества — полная независимость от Java-кода, централизованное управление, возможность изменения без перекомпиляции.
Недостатки — многословность, отсутствие проверки типов на этапе компиляции, сложность поддержки при росте приложения.


Разбор:
- Этот блок работает как схема: строка `Каждый бин имеет:` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.


2. Java-конфигурация (@Configuration, @Bean)

Введена в Spring 3.0 как более типобезопасная и выразительная альтернатива XML. Конфигурация — это обычный Java-класс с аннотацией @Configuration, а методы, помеченные @Bean, возвращают экземпляры бинов.

Преимущества — проверка типов, автодополнение в IDE, рефакторинг, условная конфигурация (@Profile, @Conditional), возможность использовать всю мощь Java (циклы, логика).
Недостатки: необходимость компиляции при изменении.


Разбор:
- Этот блок работает как схема: строка `---` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.


3. Автоматическое конфигурирование (Component Scanning + Spring Boot)

Максимально декларативный подход. Разработчик помечает классы аннотациями (@Component, @Service, @Repository, @Controller), а Spring автоматически обнаруживает их в указанном пакете (@ComponentScan) и регистрирует как бины. Зависимости внедряются по типу.

Spring Boot радикально развивает эту идею — он анализирует classpath, находит зависимости (H2, MySQL, Redis), и автоматически создаёт и настраивает нужные бины (DataSource, RedisConnectionFactory, JpaTransactionManager и т.д.). Поведение можно кастомизировать через application.properties/application.yml.

Это позволяет писать приложения с нулевой конфигурацией:


Разбор:
- Этот блок работает как схема: строка `---` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Важно: автоматическая конфигурация не отменяет понимания. Она лишь скрывает рутину. Чтобы эффективно использовать Spring Boot, необходимо чётко представлять, какие бины создаются, как они связаны и как их можно переопределить.


Spring MVC

Spring MVC — это реализация шаблона проектирования Model-View-Controller, построенная поверх Java Servlet API. Его цель — чётко разделить три ответственности:

  • Model — данные и бизнес-логика (обычно представлены POJO, сервисами, репозиториями).
  • View — представление результатов (JSP, Thymeleaf, Freemarker, или просто JSON/XML для REST).
  • Controller — посредник — принимает HTTP-запрос, вызывает модель, выбирает представление.

В отличие от "классического" MVC (например, в Swing), веб-MVC не предполагает прямого обновления View моделью. Веб — это stateless-протокол: каждый запрос независим. Поэтому Controller собирает данные из модели, помещает их в контекст представления и передаёт управление View.


Ядро архитектуры — DispatcherServlet

Все запросы в Spring MVC проходят через один сервлет — DispatcherServlet. Это фронт-контроллер, централизующий обработку. Его работа — координировать взаимодействие компонентов.

До DispatcherServlet запрос уже прошёл встроенный HTTP-сервер (в Spring Boot — обычно Tomcat) и цепочку сервлет-фильтров (javax.servlet.Filter). Туда же входит Spring Security — проверка JWT, сессии, прав на URL. Фильтры работают на уровне Servlet API; если фильтр вернул ответ (например, 401 Unauthorized), до диспетчера запрос не дойдёт.

Жизненный цикл запроса в Spring Boot

На практике удобно держать в голове семь этапов — от входа в приложение до завершения обработки:

ЭтапЧто происходит
1Запрос у DispatcherServletЦентральный обработчик принимает HTTP-запрос от контейнера после фильтров.
2HandlerMapping находит контроллерПо URL и методу (GET, POST…) выбирается handler — обычно метод с @GetMapping / @PostMapping.
3preHandle() у перехватчиковКлассы с HandlerInterceptor выполняют подготовку: логирование, проверка заголовка, запись в MDC. Возврат false обрывает цепочку до контроллера.
4Контроллер обрабатывает запросВызов метода @Controller / @RestController, сервисы, репозитории, бизнес-логика.
5postHandle() у перехватчиковОбработка после контроллера, но до финального ответа (актуально для MVC с шаблонами и Model).
6Формирование ответа@RestController — сериализация в JSON через HttpMessageConverter; классический MVC — рендеринг View (Thymeleaf, JSP).
7afterCompletion() у перехватчиковЗавершение и очистка: снятие атрибутов, закрытие ресурсов, метрики; вызывается даже при исключении в контроллере.

Перехватчики и фильтры. Фильтры видят "сырой" запрос раньше Spring MVC; перехватчики (HandlerInterceptor) привязаны к конкретному handler и знают, какой контроллер будет вызван. Для сквозной аутентификации и CSRF чаще хватает Spring Security; для логирования времени обработки конкретного API — HandlerInterceptor. См. Chain of Responsibility в Java и паттерн "Посредник" (DispatcherServlet как медиатор).

Внутренние шаги диспетчера

Внутри DispatcherServlet тот же путь разбивается на компоненты Spring MVC:

  1. Получение запроса от контейнера (Tomcat, Jetty).
  2. Определение обработчика (handler) через HandlerMapping. По умолчанию используется RequestMappingHandlerMapping, который сопоставляет URL/HTTP-метод с методом класса, помеченного @Controller или @RestController.
  3. Подготовка аргументов вызова метода через HandlerAdapter (обычно RequestMappingHandlerAdapter). Здесь происходит:
    • привязка параметров запроса (@RequestParam, @PathVariable);
    • десериализация тела (@RequestBody);
    • валидация (@Valid);
    • внедрение специальных объектов (HttpServletRequest, Model).
  4. Вызов метода контроллера (между этапами 3–4 в таблице выше выполняются preHandle() / после вызова — postHandle()).
  5. Обработка возвращаемого значения:
    • строка → имя представления (для @Controller);
    • объект → сериализация в JSON/XML (для @RestController);
    • ResponseEntity → полный контроль над HTTP-ответом.
  6. Рендеринг View (если применимо) через ViewResolver, который по имени (например, "home") находит шаблон (home.html) и заполняет его данными из Model.

Этот процесс полностью настраиваем: любой этап можно заменить своей реализацией — и это ключевое преимущество Spring MVC перед фреймворками с "жёсткой" архитектурой.


Контроллеры — @Controller и @RestController

Класс, помеченный @Controller, сообщает Spring, что он содержит методы для обработки веб-запросов. Методы аннотируются метками маршрутизации:

  • @GetMapping("/users/{id}") — GET-запрос к /users/123;
  • @PostMapping("/orders") — POST-запрос для создания заказа;
  • @PutMapping, @DeleteMapping — для остальных HTTP-глаголов.

Аргументы метода могут включать:

  • @PathVariable("id") Long id — извлечение из URL (/users/{id});
  • @RequestParam("page") int page — из query-параметров (?page=2);
  • @RequestBody User user — десериализация JSON/XML в объект (через Jackson или JAXB);
  • Model model — контейнер для передачи данных во View;
  • HttpServletResponse response — прямой доступ к ответу (редко нужно, нарушает абстракцию).

Возвращаемое значение:

  • String → имя представления (например, "user-profile"user-profile.html);
  • ModelAndView → явное указание View и Model;
  • void → имя View выводится из URL (редко);
  • Любой POJO + @ResponseBody (или класс с @RestController) → автоматическая сериализация в JSON/XML.

@RestController — это просто @Controller + @ResponseBody на уровне класса. Он предназначен исключительно для построения RESTful API, где ответ всегда — данные (не HTML).


Обработка исключений

Spring MVC предоставляет централизованный механизм обработки ошибок через @ControllerAdvice. Это компонент, который "ловит" исключения, выброшенные любым контроллером, и преобразует их в структурированный HTTP-ответ.


Разбор:
- Этот блок работает как схема: строка `> **Важно**: автоматическая конфигурация *не отменяет* понимания. Она лишь скрывает рутину. Чтобы эффективно использовать Spring Boot, необходимо чётко представлять, *какие бины создаются*, *как они связаны* и *как их можно переопределить*.` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Такой подход избавляет от дублирования try-catch в каждом методе и гарантирует единообразный формат ошибок — критически важно для клиентских приложений.


Spring Data

Одна из самых успешных абстракций Spring — Spring Data. Его задача — устранить рутину CRUD-операций и повторяющегося SQL/JPA-кода, сохранив при этом гибкость.


Основная идея — Repository как интерфейс

Разработчик объявляет интерфейс, наследуемый от JpaRepository<T, ID> (для JPA) или MongoRepository<T, ID> (для MongoDB), и Spring динамически создаёт реализацию этого интерфейса во время выполнения.


Разбор:
- Этот блок работает как схема: строка `Такой подход избавляет от дублирования `try-catch` в каждом методе и гарантирует единообразный формат ошибок — критически важно для клиентских приложений.` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Spring Data анализирует:

  • Имя метода: findByEmail → генерирует JPQL/HQL-запрос WHERE email = ?;
  • Параметры: StartingWithLIKE ?%, BetweenBETWEEN ? AND ?;
  • Аннотацию @Query: позволяет писать произвольные запросы на JPQL, SQL или (для NoSQL) нативные запросы.

Все операции (save(), findById(), delete()) уже реализованы в базовом интерфейсе. Нет необходимости писать классы UserRepositoryImpl.


Поддержка различных хранилищ

Spring Data — это фасад. За ним скрываются специализированные модули:

  • spring-Data-jpa — для реляционных БД через Hibernate, EclipseLink;
  • spring-Data-mongodb — для документных БД;
  • spring-Data-redis — для in-memory хранилищ;
  • spring-Data-elasticsearch — для поисковых движков;
  • spring-Data-jdbc — упрощённая альтернатива JPA без lazy loading и кэширования.

Все они используют единый подход: интерфейс Repository → автоматическая реализация → расширение через имена методов или аннотации.


Транзакционность и пагинация

Любой метод репозитория по умолчанию выполняется в транзакции (благодаря @Transactional, который Spring Data добавляет в реализацию). Пагинация поддерживается через типы Page<T> и Pageable:


Разбор:
- Этот блок работает как схема: строка `Spring Data анализирует:` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Фреймворк автоматически добавляет LIMIT/OFFSET (или эквивалент) и выполняет отдельный запрос COUNT(*) для расчёта общего числа страниц.


Spring Security

Spring Security — это независимый фреймворк, но настолько тесно интегрированный с Spring, что воспринимается как его часть. Он решает две задачи:

  1. Аутентификация — "Кто вы?" (проверка учётных данных — логин/пароль, JWT, OAuth2-токен).
  2. Авторизация — "Что вы можете?" (проверка прав доступа к ресурсу — URL, методу, данным).

Архитектура — цепочка фильтров

Все запросы к защищённому приложению проходят через цепочку сервлет-фильтров (FilterChain). Spring Security регистрирует свой главный фильтр — FilterChainProxy, который делегирует работу конкретным SecurityFilter’ам:

  • UsernamePasswordAuthenticationFilter — обрабатывает форму входа;
  • BearerTokenAuthenticationFilter — извлекает JWT из заголовка Authorization;
  • ExceptionTranslationFilter — перехватывает AccessDeniedException и AuthenticationException;
  • FilterSecurityInterceptor — финальная проверка: имеет ли аутентифицированный пользователь право на доступ к данному URL.

Каждый фильтр может разрешить запрос (передача дальше по цепочке), отклонить (отправить 401/403) или инициировать аутентификацию (перенаправить на страницу логина).


Конфигурация — Java DSL

Современный подход — декларативная настройка через SecurityFilterChain:

Код ITЗагрузка примера кода…

Разбор:

  • Класс SecurityConfig собирает правила доступа в одном месте, чтобы авторизация не размазывалась по контроллерам.
  • Аннотации @Configuration, @EnableWebSecurity и @Bean включают регистрацию security-конфигурации и создают SecurityFilterChain как управляемый бин.
  • Цепочка authorizeHttpRequests(...) задаёт матрицу доступа — permitAll() открывает публичные пути, hasRole("ADMIN") ограничивает admin-маршруты, anyRequest().authenticated() закрывает остальное.
  • Блок formLogin(...) включает страницу входа /login, а logout(...) настраивает безопасный выход; это базовый stateful-сценарий для web-интерфейсов.
  • Вызов http.build() фиксирует итоговую конфигурацию фильтров; без него правила не будут применены контейнером.
  • На практике этот фрагмент обычно дополняют обработчиками ошибок 401/403 и интеграцией с user-store, чтобы правила соответствовали доменной модели прав.

Здесь:

  • authorizeHttpRequests — правила доступа по URL;
  • hasRole("ADMIN") — проверка наличия роли ROLE_ADMIN (префикс ROLE_ добавляется автоматически);
  • formLogin — настройка стандартной формы аутентификации;
  • logout — обработка выхода.

Для REST API вместо formLogin используется httpBasic() или настройка OAuth2/JWT.


Защита на уровне методов

Помимо URL, Spring Security позволяет ограничивать доступ к методам с помощью аннотаций:

  • @PreAuthorize("hasRole('USER')") — проверка до вызова метода;
  • @PostAuthorize("returnObject.owner == principal.username") — проверка после вызова (редко, дорого);
  • @Secured("ROLE_ADMIN") — упрощённый вариант @PreAuthorize.

Это особенно полезно в сервисном слое, где один и тот же метод может вызываться из разных точек входа (веб, CLI, scheduler).


Поддержка стандартов

Spring Security не изобретает протоколы — он реализует их:

  • OAuth2 / OpenID Connect — для делегированной аутентификации (Google, GitHub);
  • JWT — для stateless-аутентификации в микросервисах;
  • LDAP / Active Directory — для интеграции с корпоративными каталогами;
  • CSRF-защита — включена по умолчанию для stateful-приложений (форм), отключается для REST.

Чеклист для production (HTTPS, CSP, OIDC, секреты, SCA, пентест) — отдельная статья Spring Boot — безопасность в продакшене. Практический старт с SecurityFilterChainSpring Security — практический старт.


Управление транзакциями

Транзакции — критически важный механизм для обеспечения целостности данных. Spring предоставляет единый API для работы с транзакциями, независимо от низкоуровневой технологии (JDBC, JPA, JTA).


@Transactional — простота и мощь

Аннотация @Transactional — основной инструмент. Её можно ставить на класс (все публичные методы) или на отдельный метод.


Разбор:
- Этот блок работает как схема: строка `Разбор:` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Spring создаёт прокси вокруг OrderService. При вызове createOrder():

  1. Открывается транзакция (через PlatformTransactionManager);
  2. Выполняется метод;
  3. При успехе — commit();
  4. При исключении — rollback() (если исключение непроверяемое или явно указано в rollbackFor).

Ключевые параметры

  • propagation — как вести себя при вложенном вызове:
    • REQUIRED (по умолчанию) — использовать существующую транзакцию или создать новую;
    • REQUIRES_NEW — всегда новая транзакция (приостанавливает текущую);
    • NESTED — вложенная транзакция с точкой сохранения (поддерживается не всеми БД).
  • isolation — уровень изоляции (READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE);
  • readOnly = true — подсказка СУБД для оптимизации (только для операций чтения);
  • timeout = 30 — максимальное время выполнения в секундах.

Программное управление

Для сложных сценариев (например, частичный rollback) доступен TransactionTemplate:


Разбор:
- Этот блок работает как схема: строка `Spring создаёт *прокси* вокруг `OrderService`. При вызове `createOrder()`:` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.


Тестирование

Spring уделяет тестированию особое внимание. Он различает:

  • Unit-тесты: проверяют один класс, все зависимости замоканы (Mockito). Spring здесь не участвует.
  • Интеграционные тесты: проверяют взаимодействие компонентов, включая Spring-контекст.

Инструменты Spring Test

  • @SpringBootTest — загружает полный контекст приложения. Тяжеловесно, но необходимо для end-to-end проверки.
  • @DataJpaTest — загружает только слой данных — репозитории, DataSource, транзакции. Использует in-memory БД (H2) по умолчанию. Отлично для тестирования запросов.
  • @WebMvcTest — загружает только веб-слой — контроллеры, ObjectMapper, фильтры безопасности. Сервисы мокаются. Идеален для проверки маршрутов и статус-кодов.
  • @TestConfiguration — добавляет временные бины только для тестов (например, мок-реализацию внешнего API).

Пример — тестирование контроллера


Разбор:
- Этот блок работает как схема: строка `---` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

MockMvc эмулирует HTTP-запрос без запуска сервера — быстро и надёжно.


Spring Boot — концепция "convention over configuration"

Если Spring Framework — это конструктор, то Spring Boot — это готовый автомобиль с предустановленными опциями. Его три кита:

  1. Auto-configuration — анализ classpath и автоматическое создание бинов. Наличие spring-boot-starter-Data-jpa и HikariCP → Spring Boot создаёт DataSource, EntityManagerFactory, JpaTransactionManager.
  2. Starter-зависимости — предопределённые наборы библиотек для конкретных задач:
    • spring-boot-starter-web — веб (Tomcat, Spring MVC, Jackson);
    • spring-boot-starter-Data-jpa — JPA + Hibernate + транзакции;
    • spring-boot-starter-security — Spring Security.
  3. Встроенные серверы — приложение — это executable JAR с Tomcat/Jetty/Undertow внутри. Не требует развёртывания в application server.

Механизм автонастройки

Каждая автонастройка — это @Configuration-класс с аннотацией @Conditional*. Например, DataSourceAutoConfiguration активируется только если:

  • В classpath есть DataSource;
  • Пользователь не предоставил свой DataSource (@ConditionalOnMissingBean);
  • Заданы свойства spring.datasource.url.

Это позволяет гибко переопределять поведение: достаточно объявить свой DataSource — и автонастройка отключится.


Внешнее конфигурирование

Spring Boot поддерживает 17+ источников конфигурации (в порядке приоритета):

  1. Параметры командной строки (--server.port=8081);
  2. Переменные окружения (SERVER_PORT=8081);
  3. application.properties / application.yml в classpath;
  4. Профиль-специфичные файлы (application-prod.yml).

Это позволяет использовать один и тот же JAR в dev, test, prod — меняя только окружение.


Наблюдаемость

Для промышленного применения недостаточно, чтобы приложение работало — необходимо понимать, как оно работает. Spring Boot предоставляет встроенные механизмы наблюдаемости через Spring Boot Actuator — модуль, exposing операционные конечные точки (endpoints) через HTTP или JMX.


Что такое endpoint?

Endpoint — это программный интерфейс, предоставляющий информацию о состоянии приложения:

  • /actuator/health — общее состояние здоровья (UP, DOWN, OUT_OF_SERVICE);
  • /actuator/metrics — список доступных метрик (CPU, память, HTTP-запросы, пул БД);
  • /actuator/env — все свойства окружения и конфигурации;
  • /actuator/beans — полный список бинов в контексте;
  • /actuator/mappings — зарегистрированные URL-маршруты;
  • /actuator/threaddump — дамп потоков выполнения;
  • /actuator/heapdump — дамп кучи (требует явного включения).

По умолчанию многие endpoint’ы отключены в production (/env, /heapdump) из соображений безопасности. Их можно включить выборочно, настроив права доступа через Spring Security.


Здоровье (Health) как контракт

Endpoint /health — это не просто "сервер жив / мёртв". Он интегрируется с внешними зависимостями — БД, Redis, RabbitMQ, внешние API. Для каждой зависимости Spring Boot регистрирует HealthIndicator:


Разбор:
- Этот блок работает как схема: строка ``MockMvc` эмулирует HTTP-запрос без запуска сервера — быстро и надёжно.` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Если хотя бы один HealthIndicator возвращает DOWN, общий статус становится DOWN. Это позволяет оркестраторам (Kubernetes, Docker Swarm) автоматически перезапускать нездоровые экземпляры.


Метрики и интеграция с системами мониторинга

Spring Boot использует Micrometer — фасад над системами метрик (Prometheus, Graphite, Datadog, New Relic). Он предоставляет типизированные метрики:

  • Counter — монотонно возрастающее значение (число запросов);
  • Gauge — мгновенное значение (размер пула соединений);
  • Timer — гистограмма времени выполнения (латентность запросов);
  • DistributionSummary — гистограмма объёмов (размер ответа в байтах).

Каждый HTTP-запрос автоматически генерирует метрики:

  • http.server.requests — таймер по методу, URI, статусу;
  • jdbc.connections.active — активные соединения с БД;
  • jvm.memory.used — использование памяти.

Эти метрики экспортируются в Prometheus в формате текста по /actuator/prometheus, после чего визуализируются в Grafana. Таким образом, Spring Boot обеспечивает сквозную трассировку от запроса до ресурсов JVM без ручного кода.


Spring Cloud

Когда приложение растёт, оно перестаёт быть монолитом и превращается в систему сервисов. Spring Cloud — это набор инструментов для управления сложностью распределённых систем — обнаружение сервисов, конфигурация, отказоустойчивость, маршрутизация.


Service Discovery — как найти нужный сервис?

В микросервисной архитектуре экземпляры сервисов динамически появляются и исчезают. Жёстко прописывать IP-адреса в коде невозможно. Решение — центральный реестр сервисов.

Spring Cloud интегрируется с:

  • Eureka (Netflix) — сервер регистрации и discovery;
  • Consul (HashiCorp) — multi-purpose tool (discovery, config, KV-store);
  • Zookeeper (Apache) — координация распределённых систем.

Сервис при старте регистрируется в реестре, передавая:

  • имя (например, user-service);
  • IP и порт;
  • метаданные (версия, зона доступности).

Клиенты (другие сервисы) запрашивают у реестра список экземпляров user-service и выбирают один (обычно через round-robin). Spring Cloud автоматизирует это через @LoadBalanced RestTemplate или WebClient:


Разбор:
- Этот блок работает как схема: строка `Если хотя бы один `HealthIndicator` возвращает `DOWN`, общий статус становится `DOWN`. Это позволяет оркестраторам (Kubernetes, Docker Swarm) автоматически перезапускать нездоровые экземпляры.` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Здесь user-service — логическое имя, а не хост. Spring Cloud под капотом преобразует его в реальный URL через discovery-клиент.


Centralized Configuration

Когда сервисов десятки, изменение конфигурации в каждом application.yml — кошмар. Spring Cloud Config Server решает это:

  1. Создаётся отдельный сервис (config-server), который читает конфигурации из Git-репозитория (application.yml, user-service-prod.yml);
  2. Клиентские сервисы при старте запрашивают у Config Server свои настройки;
  3. Изменения в Git → автоматическая перезагрузка (@RefreshScope) без перезапуска.

Конфигурация становится версионируемой, аудируемой и централизованной.


Отказоустойчивость

Сети ненадёжны. Вызов внешнего сервиса может зависнуть на секунды. Без защиты это приведёт к исчерпанию потоков и полному падению системы. Spring Cloud Circuit Breaker (на базе Resilience4j или Sentinel) реализует паттерн аварийного выключателя:

  • Закрытое состояние — запросы проходят нормально;
  • При превышении порога ошибок/таймаутов → открытое состояние — все вызовы сразу падают с исключением;
  • Через заданное время → половинчатое состояние — пробные запросы; при успехе — возврат в закрытое.

Пример:


Разбор:
- Этот блок работает как схема: строка `Здесь `user-service` — логическое имя, а не хост. Spring Cloud под капотом преобразует его в реальный URL через discovery-клиент.` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Дополняет это Spring Retry — автоматические повторные попытки при временных сбоях (например, сетевой таймаут).


API Gateway

Клиенты (браузеры, мобильные приложения) не должны знать о внутренней топологии сервисов. Spring Cloud Gateway — это реактивный reverse proxy, который:

  • маршрутизирует запросы (/users/**user-service);
  • применяет фильтры (аутентификация, rate limiting, логгирование);
  • агрегирует ответы от нескольких сервисов (GraphQL-like);
  • преобразует протоколы (HTTP → gRPC).

Фильтры — это цепочка обработчиков, где каждый может модифицировать запрос/ответ до передачи дальше.


Реактивное программирование

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


Основы Reactive Streams

Reactive Streams — это стандарт (JSR-379) для обработки асинхронных потоков данных с обратным давлением (backpressure). Его реализует Project Reactor — основа WebFlux:

  • Mono<T> — поток, который эмитит 0 или 1 элемент (аналог CompletableFuture<T>);
  • Flux<T> — поток, который эмитит 0..N элементов (аналог Stream<T> + асинхронность).

Пример контроллера:


Разбор:
- Этот блок работает как схема: строка `Дополняет это **Spring Retry** — автоматические повторные попытки при временных сбоях (например, сетевой таймаут).` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Важно: реактивность распространяется по цепочке. Если репозиторий возвращает Mono, а сервис вызывает блокирующий метод — вы получите ложную реактивность. Для полного эффекта нужны:

  • неблокирующий драйвер БД (R2DBC вместо JDBC);
  • реактивные клиенты (WebClient вместо RestTemplate);
  • сервер, поддерживающий реактивность (Netty, Undertow — не Tomcat).

Когда использовать WebFlux?

  • Высокая конкуренция, низкая задержка (тысячи соединений на одном сервере);
  • Долгие операции ввода-вывода (streaming, SSE, WebSocket);
  • Интеграция с реактивными системами (Kafka, Cassandra).

Для типичных CRUD-приложений с умеренной нагрузкой MVC остаётся проще и производительнее — потоки JVM оптимизированы для коротких блокирующих операций.


Продвинутая безопасность

Spring Security предоставляет базовые механизмы, но реальные системы требуют глубокой настройки.


Валидация JWT — собственный JwtDecoder

По умолчанию Spring Security использует NimbusJwtDecoder, но его можно кастомизировать для:

  • валидации подписи через JWKS (JSON Web Key Set) удалённого IdP;
  • проверки кастомных claim’ов ("scope": ["read:users", "write:orders"]);
  • интеграции с внутренними системами (например, проверка статуса пользователя в БД по sub).

Разбор:
- Этот блок работает как схема: строка `Важно: реактивность *распространяется по цепочке*. Если репозиторий возвращает `Mono`, а сервис вызывает блокирующий метод — вы получите *ложную реактивность*. Для полного эффекта нужны:` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.


OAuth2 Resource Server — защита API

В микросервисной архитектуре клиенты получают токен от Authorization Server (Auth0, Keycloak), а сервисы выступают как Resource Server — проверяют токен, но не выдают его.

Конфигурация:


Разбор:
- Этот блок работает как схема: строка `---` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

После валидации токена Spring создаёт Authentication объект с:

  • principalsub из токена;
  • authorities — преобразованные scope в GrantedAuthority (например, SCOPE_read:usersread:users);
  • details — полезная нагрузка токена.

Аудит и логгирование безопасности

Spring Security позволяет регистрировать события безопасности:

  • AuthenticationSuccessEvent — успешный вход;
  • AuthenticationFailureBadCredentialsEvent — ошибка логина;
  • AuthorizationDeniedEvent — отказ в доступе.

Эти события можно перехватывать через @EventListener и отправлять в SIEM-систему (Splunk, ELK) для анализа атак.


Разбор:
- Этот блок работает как схема: строка `После валидации токена Spring создаёт `Authentication` объект с:` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.


Производительность

Безопасность и надёжность важны, но не в ущерб скорости. Spring предоставляет инструменты для оптимизации.


Декларативное кэширование

Аннотации @Cacheable, @CachePut, @CacheEvict позволяют кэшировать результаты методов:


Разбор:
- Этот блок работает как схема: строка `---` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Поддерживаемые провайдеры — Caffeine (in-memory), Redis, Hazelcast, Ehcache. Spring абстрагирует API — смена кэша требует только изменения конфигурации.


Пул соединений с БД

JDBC-соединения — дорогостоящий ресурс. Всегда используйте пул:

  • HikariCP — самый быстрый и лёгкий (используется по умолчанию в Spring Boot);
  • Tomcat JDBC Pool, Commons DBCP2 — альтернативы.

Ключевые настройки:

  • maximum-pool-size — максимум одновременных соединений (обычно 10–20);
  • connection-timeout — время ожидания свободного соединения (30 сек);
  • idle-timeout, max-lifetime — управление "устаревшими" соединениями.

Неправильная настройка пула — частая причина отказов под нагрузкой.


Lazy Loading и N+1 проблема

JPA по умолчанию загружает связи @OneToMany лениво (LAZY). Это экономит ресурсы, но может привести к N+1 запросам: один запрос на сущность + N запросов на каждую связанную коллекцию.

Решения:

  • JOIN FETCH в JPQL — загрузка связанных данных одним запросом;
  • @EntityGraph — декларативное указание графа загрузки;
  • DTO-проекции через @Query — возврат плоских объектов.

Spring Data JPA поддерживает EntityGraph:


Разбор:
- Этот блок работает как схема: строка `Поддерживаемые провайдеры: Caffeine (in-memory), Redis, Hazelcast, Ehcache. Spring абстрагирует API — смена кэша требует только изменения конфигурации.` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.


Тонкая настройка жизненного цикла

Этот раздел нужен, когда в проекте появляются сложные ошибки:

  • циклические зависимости
  • бин создаётся, но работает не так, как ожидается
  • нужно добавить собственную логику до или после инициализации

Базовая версия процесса уже есть в JVM, память и потоки. Ниже расширенная схема для production-задач.


Этапы жизненного цикла бина (singleton scope)

  1. Инстантация

    • Spring создаёт объект через конструктор
    • на этом шаге зависимости ещё не подставлены
  2. Заполнение зависимостей

    • AutowiredAnnotationBeanPostProcessor внедряет зависимости через @Autowired, @Value, @Resource
    • значения берутся из Environment, например ${db.url}
    • при необходимости применяется преобразование типов через Converter
  3. Обработка до инициализации

    • вызываются все BeanPostProcessor.postProcessBeforeInitialization()
    • здесь отрабатывают @PostConstruct и другие механизмы, завязанные на post-processor
  4. Инициализация

    • выполняется InitializingBean.afterPropertiesSet(), если интерфейс реализован
    • затем выполняется метод из init-method или @Bean(initMethod = "...")
  5. Обработка после инициализации

    • вызываются BeanPostProcessor.postProcessAfterInitialization()
    • на этом шаге часто создаются прокси для AOP и @Transactional
  6. Рабочая стадия

    • бин внедряется в другие компоненты
    • бизнес-логика выполняется уже через готовый экземпляр или его прокси
  7. Уничтожение

    • при закрытии контекста вызываются @PreDestroy, DisposableBean.destroy() и destroy-method

Расширение через BeanPostProcessor и BeanFactoryPostProcessor

Оба интерфейса расширяют поведение контейнера, но работают на разных этапах.

  • BeanPostProcessor
    • работает с уже созданным экземпляром бина
    • подходит, когда нужно обернуть бин в прокси или изменить его перед использованием
    • типичный пример это логирование вызовов методов

Код ITЗагрузка примера кода…

  • BeanFactoryPostProcessor
    • работает с метаданными бинов до создания экземпляров
    • позволяет добавить, удалить или изменить BeanDefinition
    • полезен для условной регистрации компонентов на старте приложения

Пример BeanFactoryPostProcessor:

@Component
public class ConditionalBeanRegistrar implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
if (isFeatureFlagEnabled()) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
GenericBeanDefinition def = new GenericBeanDefinition();
def.setBeanClass(FeatureService.class);
registry.registerBeanDefinition("featureService", def);
}
}
}

Эти механизмы лежат в основе @ConfigurationProperties, @Conditional и @Profile.


Разрешение циклических зависимостей

Spring может пережить цикл при внедрении через поле или setter, но не при конструкторном внедрении.

Как это работает:

  1. создаётся ранний экземпляр A без финальной инициализации
  2. этот экземпляр временно кладётся в early singleton cache
  3. при создании B Spring берёт A из раннего кеша и завершает сборку графа
  4. после этого A проходит оставшиеся этапы инициализации

Риск в том, что B может обратиться к A до полной инициализации A. Поэтому для нового кода лучше использовать конструкторное DI и разрывать цикл архитектурно. Если цикл найден в конструкторе, Spring выбросит BeanCurrentlyInCreationException.


Миграция legacy-конфигураций

Многие корпоративные системы до сих пор используют XML-конфигурацию. Миграция — архитектурное обновление.


Шаг 1 — Гибридная конфигурация

Spring позволяет смешивать XML и Java Config:


Разбор:
- Этот блок работает как схема: строка `---` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Это позволяет постепенно переводить компоненты, не переписывая всё сразу.


Шаг 2 — Замена @Autowired field injection

Field injection (@Autowired на поле) мешает unit-тестированию. Лучшая практика — конструкторная инъекция. Современные IDE (IntelliJ IDEA) позволяют автоматически рефакторить:


Разбор:
- Этот блок работает как схема: строка `Это позволяет постепенно переводить компоненты, не переписывая всё сразу.` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Если зависимостей много (>3), стоит пересмотреть SRP (Single Responsibility Principle) — возможно, класс делает слишком много.


Шаг 3 — Устранение ApplicationContextAware

Прямые вызовы context.getBean() нарушают DI. Вместо:


Разбор:
- Этот блок работает как схема: строка `Если зависимостей много (>3), стоит пересмотреть SRP (Single Responsibility Principle) — возможно, класс делает слишком много.` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Используется lookup method injection или ObjectProvider:


Разбор:
- Этот блок работает как схема: строка `Используется *lookup method injection* или `ObjectProvider`:` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

ObjectProvider — ленивый, безопасный способ получения бина по требованию, без жёсткой связи с контекстом.


Шаг 4 — Замена PropertyPlaceholderConfigurer на @ConfigurationProperties

XML-стиль:


Разбор:
- Этот блок работает как схема: строка ``ObjectProvider` — ленивый, безопасный способ получения бина по требованию, без жёсткой связи с контекстом.` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Java-стиль:


Разбор:
- Этот блок работает как схема: строка `Java-стиль:` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Преимущества — типобезопасность, IDE-поддержка, автоматическая валидация через @Validated.


Spring и Java Platform Module System (JPMS)

С появлением модулей в Java 9 возникла проблема: Spring — reflection-heavy фреймворк, а JPMS ограничивает доступ к внутренностям модулей. Совместимость требует явных открытых пакетов.


Ключевые требования к module-info.java

  1. Открытие пакетов для рефлексии
    Spring читает аннотации и создаёт бины через рефлексию. Пакеты с компонентами должны быть open:
open module com.example.app {
requires spring.context;
requires spring.beans;
// ...
}

open — разрешает deep reflection для всего модуля. Альтернатива — opens com.example.service to spring.core, но это многословно.

  1. Экспорт API-пакетов
    Если модуль предоставляет интерфейсы для других, их пакеты должны быть exported:
exports com.example.api;
  1. Обработка автоматических модулей
    Большинство библиотек (включая Spring до 5.3) — automatic modules (имя модуля = имя JAR). Их можно require по имени, но без строгих гарантий. Spring Boot 2.4+ и Spring Framework 5.3+ поставляются как явные модули, что упрощает сборку.

Проблемы и решения

  • InaccessibleObjectException — при попытке доступа к private-полям. Решение: флаги JVM (--add-opens java.base/java.lang=ALL-UNNAMED), но это обход, а не решение. Лучше — явное open в module-info.java.
  • Spring AOP и CGLIB — генерация подклассов в runtime. Требует --add-opens для пакетов с целевыми классами.
  • Spring Boot и Fat JAR — JPMS не поддерживает "fat" JAR’ы. Решение: использовать jlink для сборки custom runtime или оставить приложение как classpath-приложение (стандартный путь для Spring Boot).

На практике большинство Spring-приложений продолжают собираться как classpath-приложения, а не модульные — из-за сложности и ограниченной выгоды. JPMS оправдан в библиотеках, но не в бизнес-приложениях.


Native Image — Spring Boot и GraalVM

Традиционные JVM-приложения страдают от долгого старта и высокого потребления памяти. GraalVM Native Image компилирует JVM-байткод в standalone native binary, устраняя overhead JIT и garbage collection.


Как это работает с Spring?

GraalVM требует статического анализа — всё, что может быть вызвано через рефлексию, должно быть явно зарегистрировано. Spring Boot 3+ и Spring Native добавляют поддержку:

  1. Конфигурация времени сборки (@Configuration(proxyBeanMethods = false))
    Упрощает анализ: Spring не создаёт CGLIB-прокси для @Configuration-классов.

  2. Hint’ы для рефлексии
    Spring Native генерирует reflect-config.json, resource-config.json автоматически через @NativeHint:

@NativeHint(
types = {User.class, Order.class},
options = "--enable-url-protocols=http"
)
@SpringBootApplication
public class MyApp { }
  1. Ограничения
    • Thread.stop(), Object.finalize() — не поддерживаются;
    • Динамическая загрузка классов (Class.forName()) — только для заранее объявленных;
    • Прокси через java.lang.reflect.Proxy — работают, CGLIB — нет (используется JDK Proxy);
    • @Scheduled — работает, @Async — требует явной настройки пула.

Преимущества и издержки

  • Плюсы:

    • Старт за 20–100 мс (вместо 2–5 сек);
    • Потребление памяти в 2–5 раз меньше;
    • Более предсказуемый runtime (нет JIT warm-up).
  • Минусы:

    • Время сборки — 2–10 минут;
    • Бинарь привязан к ОС и архитектуре;
    • Отладка — сложнее (нет hot reload, профилирование через perf);
    • Не все библиотеки совместимы (например, некоторые JDBC-драйверы).

Native Image оправдан для:

  • serverless-функций (AWS Lambda, Azure Functions);
  • edge-сервисов с ограничениями по памяти;
  • CLI-инструментов.

Для обычных серверных приложений JVM остаётся оптимальным выбором.


Архитектурные паттерны

Spring не навязывает архитектуру, но предоставляет инструменты для реализации сложных паттернов.


CQRS (Command Query Responsibility Segregation)

Разделение операций на:

  • Commands — изменяют состояние (мутирующие, идемпотентные);
  • Queries — читают состояние (идемпотентные, без побочных эффектов).

Реализация:

  • Разные интерфейсы/классы для команд и запросов:

Код ITЗагрузка примера кода…

  • Разные модели данных:
    • Write model — оптимизирована под запись (нормализованная, с транзакциями);
    • Read model — оптимизирована под чтение (денормализованная, возможно, в отдельной БД — например, Elasticsearch).

Spring Data позволяет легко поддерживать обе модели: JpaRepository для записи, ElasticsearchRepository для чтения.


Event Sourcing

Вместо хранения текущего состояния — хранение последовательности событий, приведших к этому состоянию.

Пример:

  • Событие UserCreated(id=1, email="a@b.com");
  • Событие UserEmailChanged(id=1, old="a@b.com", new="x@y.com").

Преимущества:

  • Аудит "из коробки";
  • Возможность восстановить состояние на любой момент времени (time travel);
  • Лёгкое построение read-моделей через обработку событий.

Spring поддерживает через Spring Cloud Stream и Eventuate Tram:


Разбор:
- Этот блок работает как схема: строка `Преимущества: типобезопасность, IDE-поддержка, автоматическая валидация через `@Validated`.` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.


Saga Pattern

В микросервисах нет глобальных транзакций (XA). Saga — это последовательность локальных транзакций, где каждая сопровождается компенсирующей операцией на случай сбоя.

Пример оформления заказа:

  1. ReserveInventory → если успех, идти дальше; иначе — стоп.
  2. ProcessPayment → если успех, идти дальше; иначе — вызвать CancelInventoryReservation.
  3. ShipOrder → если успех — завершить; иначе — вызвать RefundPayment, затем CancelInventoryReservation.

Реализация:

  • Orchestrated Saga — центральный оркестратор (OrderSagaOrchestrator) управляет шагами через очереди (RabbitMQ, Kafka).
  • Choreographed Saga — сервисы обмениваются событиями (InventoryReserved, PaymentFailed), без центрального узла.

Spring Cloud Stream + Kafka — естественный выбор для Choreographed Saga:


Разбор:
- Этот блок работает как схема: строка `---` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.


Практический кейс — обработка заказов через Kafka

Рассмотрим типичную задачу: сервис оформления заказов (order-service) должен уведомлять другие сервисы — резервирование товаров (inventory-service), обработку платежей (payment-service) и аналитику (analytics-service) — без жёсткой связки. Цель — обеспечить асинхронность, отказоустойчивость и сохранность данных при сбоях.


Почему Kafka, а не RabbitMQ или REST?

Выбор Kafka обусловлен требованиями:

  • Сохранность сообщений — Kafka хранит сообщения на диске, обеспечивая durability;
  • Повторное чтение — потребители могут перечитывать историю (например, для восстановления после бага);
  • Высокая пропускная способность — тысячи сообщений в секунду на один партицион;
  • Гарантии порядка в партиции — критично для событий одного заказа.

RabbitMQ подошёл бы для простых fire-and-forget задач, но не для событийной архитектуры. REST — для синхронных, а не асинхронных взаимодействий.


Архитектура системы


Разбор:
- Этот блок работает как схема: строка `---` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Ключевые решения:

  • Один топик orders, а не отдельные топики на сервис — упрощает аудит и мониторинг;
  • Разные consumer group’ы — каждый сервис читает все сообщения независимо (fan-out);
  • События в формате Avro + Schema Registry — строгая типизация, совместимость версий.

Шаг 1 — Производитель (order-service)

Сервис создаёт заказ и публикует событие только после коммита транзакции с БД. Это исключает рассогласование: сообщение не уйдёт, если заказ не сохранился.


Проблема —Dual Write

Наивный подход:


Разбор:
- Этот блок работает как схема: строка `Ключевые решения:` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Риск — если падение после шага 1, но до шага 2 — заказ есть в БД, но сообщения нет. Другие сервисы не узнают о нём.


Решение —Transactional Outbox Pattern

  1. В той же транзакции записываем событие в специальную таблицу outbox:
INSERT INTO orders (id, items) VALUES (1001, '[...]');
INSERT INTO outbox (event_id, aggregate_id, event_type, payload, created_at)
VALUES (uuid(), 1001, 'OrderCreated', '{"orderId":1001,...}', now());
  1. Отдельный реляционный триггер или асинхронный воркер читает outbox и отправляет события в Kafka.

Spring предоставляет Spring Integration и Spring for Apache Kafka для реализации воркера:


Разбор:
- Этот блок работает как схема: строка `**Риск**: если падение после шага 1, но до шага 2 — заказ есть в БД, но сообщения нет. Другие сервисы не узнают о нём.` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Ключевые моменты:

  • FOR UPDATE SKIP LOCKED — позволяет нескольким инстансам параллельно обрабатывать outbox без блокировок;
  • published = true проставляется только после успешной отправки;
  • Исключение в kafkaTemplate.send() не прерывает транзакцию — событие останется в outbox и будет повторно обработано.

Шаг 2 — Потребитель (inventory-service)

Потребитель должен быть идемпотентным: повторная обработка одного и того же сообщения не должна привести к ошибке (например, двойному резервированию).


Проблема —At-Least-Once Delivery

Kafka гарантирует доставку минимум один раз. При сбое потребителя между обработкой сообщения и коммитом оффсета сообщение будет перечитано.


Решение —Idempotency Key

Каждое событие содержит уникальный идентификатор (eventId), генерируемый на стороне производителя:


Разбор:
- Этот блок работает как схема: строка `Ключевые моменты:` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.

Потребитель ведёт журнал обработанных eventId:


Разбор:
- Этот блок работает как схема: строка `Потребитель ведёт журнал обработанных `eventId`:` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.


Обработка ошибок — Dead Letter Topic (DLT)

Если ошибка не поддаётся обработке (например, невалидный формат сообщения), сообщение отправляется в специальный топик orders.DLT для ручного анализа:


Разбор:
- Этот блок работает как схема: строка `---` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.


Шаг 3 — Наблюдаемость и отладка

Без мониторинга асинхронная система превращается в "чёрный ящик".


Распределённая трассировка (Distributed Tracing)

Spring Cloud Sleuth интегрируется с Kafka:

  • При публикации сообщения в заголовки Kafka (tracing_header) добавляется traceId и spanId;
  • Потребитель извлекает их и продолжает трассировку.

Результат — сквозной трейс в Jaeger или Zipkin:


Разбор:
- Этот блок работает как схема: строка `---` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.


Мониторинг отставания (Lag)

Критический метрик — consumer.lag: сколько сообщений непрочитано в партиции.
Spring Boot Actuator + Micrometer экспортирует:

  • kafka_consumer_fetch_manager_records_lag — лаг по топику/группе;
  • kafka_producer_record_send_total — количество отправленных сообщений.

Алерт при lag > 1000 — признак перегрузки потребителя.


Отладка через kafka-console-consumer

Для диагностики:


Разбор:
- Этот блок работает как схема: строка `---` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.