Spring Framework
Spring
См. также: JVM, память и потоки · Жизненный цикл бина в Spring (кратко) · Первая программа на Spring · Hibernate и JPA · JUnit 5 · Gradle · Azure / App Service (Microsoft)
Как читать эту статью без перегруза
Материал объёмный, поэтому эффективнее идти по маршруту:
- Сначала "Основные концепции" (IoC, DI, Bean, ApplicationContext).
- Затем "Spring MVC" и "Spring Data" для понимания типового web-приложения.
- После этого "Spring Security" и "Управление транзакциями".
- Продвинутые разделы (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:
- Внедрение через конструктор — наиболее рекомендуемый. Гарантирует, что объект создаётся в полностью инициализированном состоянии. Неизменяемые зависимости.
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
- Внедрение через setter-метод — позволяет изменять зависимости после создания объекта (редко нужно).
@Service
public class OrderService {
private PaymentService paymentService;
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
- Внедрение через поле — самый компактный, но нарушает инкапсуляцию и затрудняет 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:
- Читает конфигурацию (XML,
@Configuration,@Component-классы). - Создаёт метамодель — описание всех бинов, их зависимостей, настроек.
- Выполняет разрешение зависимостей (dependency resolution) — строит граф объектов.
- Создаёт бины в правильном порядке (уважая зависимости), применяет пост-процессоры (
BeanPostProcessor), внедряет зависимости. - Вызывает методы инициализации.
- Складывает готовые бины в
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-запрос от контейнера после фильтров. |
| 2 | HandlerMapping находит контроллер | По URL и методу (GET, POST…) выбирается handler — обычно метод с @GetMapping / @PostMapping. |
| 3 | preHandle() у перехватчиков | Классы с HandlerInterceptor выполняют подготовку: логирование, проверка заголовка, запись в MDC. Возврат false обрывает цепочку до контроллера. |
| 4 | Контроллер обрабатывает запрос | Вызов метода @Controller / @RestController, сервисы, репозитории, бизнес-логика. |
| 5 | postHandle() у перехватчиков | Обработка после контроллера, но до финального ответа (актуально для MVC с шаблонами и Model). |
| 6 | Формирование ответа | @RestController — сериализация в JSON через HttpMessageConverter; классический MVC — рендеринг View (Thymeleaf, JSP). |
| 7 | afterCompletion() у перехватчиков | Завершение и очистка: снятие атрибутов, закрытие ресурсов, метрики; вызывается даже при исключении в контроллере. |
Перехватчики и фильтры. Фильтры видят "сырой" запрос раньше Spring MVC; перехватчики (HandlerInterceptor) привязаны к конкретному handler и знают, какой контроллер будет вызван. Для сквозной аутентификации и CSRF чаще хватает Spring Security; для логирования времени обработки конкретного API — HandlerInterceptor. См. Chain of Responsibility в Java и паттерн "Посредник" (DispatcherServlet как медиатор).
Внутренние шаги диспетчера
Внутри DispatcherServlet тот же путь разбивается на компоненты Spring MVC:
- Получение запроса от контейнера (Tomcat, Jetty).
- Определение обработчика (handler) через
HandlerMapping. По умолчанию используетсяRequestMappingHandlerMapping, который сопоставляет URL/HTTP-метод с методом класса, помеченного@Controllerили@RestController. - Подготовка аргументов вызова метода через
HandlerAdapter(обычноRequestMappingHandlerAdapter). Здесь происходит:- привязка параметров запроса (
@RequestParam,@PathVariable); - десериализация тела (
@RequestBody); - валидация (
@Valid); - внедрение специальных объектов (
HttpServletRequest,Model).
- привязка параметров запроса (
- Вызов метода контроллера (между этапами 3–4 в таблице выше выполняются
preHandle()/ после вызова —postHandle()). - Обработка возвращаемого значения:
- строка → имя представления (для
@Controller); - объект → сериализация в JSON/XML (для
@RestController); ResponseEntity→ полный контроль над HTTP-ответом.
- строка → имя представления (для
- Рендеринг 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 = ?; - Параметры:
StartingWith→LIKE ?%,Between→BETWEEN ? 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, что воспринимается как его часть. Он решает две задачи:
- Аутентификация — "Кто вы?" (проверка учётных данных — логин/пароль, JWT, OAuth2-токен).
- Авторизация — "Что вы можете?" (проверка прав доступа к ресурсу — 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 — безопасность в продакшене. Практический старт с SecurityFilterChain — Spring Security — практический старт.
Управление транзакциями
Транзакции — критически важный механизм для обеспечения целостности данных. Spring предоставляет единый API для работы с транзакциями, независимо от низкоуровневой технологии (JDBC, JPA, JTA).
@Transactional — простота и мощь
Аннотация @Transactional — основной инструмент. Её можно ставить на класс (все публичные методы) или на отдельный метод.
Разбор:
- Этот блок работает как схема: строка `Разбор:` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.
Spring создаёт прокси вокруг OrderService. При вызове createOrder():
- Открывается транзакция (через
PlatformTransactionManager); - Выполняется метод;
- При успехе —
commit(); - При исключении —
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 — это готовый автомобиль с предустановленными опциями. Его три кита:
- Auto-configuration — анализ classpath и автоматическое создание бинов. Наличие
spring-boot-starter-Data-jpaиHikariCP→ Spring Boot создаётDataSource,EntityManagerFactory,JpaTransactionManager. - Starter-зависимости — предопределённые наборы библиотек для конкретных задач:
spring-boot-starter-web— веб (Tomcat, Spring MVC, Jackson);spring-boot-starter-Data-jpa— JPA + Hibernate + транзакции;spring-boot-starter-security— Spring Security.
- Встроенные серверы — приложение — это executable JAR с Tomcat/Jetty/Undertow внутри. Не требует развёртывания в application server.
Механизм автонастройки
Каждая автонастройка — это @Configuration-класс с аннотацией @Conditional*. Например, DataSourceAutoConfiguration активируется только если:
- В classpath есть
DataSource; - Пользователь не предоставил свой
DataSource(@ConditionalOnMissingBean); - Заданы свойства
spring.datasource.url.
Это позволяет гибко переопределять поведение: достаточно объявить свой DataSource — и автонастройка отключится.
Внешнее конфигурирование
Spring Boot поддерживает 17+ источников конфигурации (в порядке приоритета):
- Параметры командной строки (
--server.port=8081); - Переменные окружения (
SERVER_PORT=8081); application.properties/application.ymlв classpath;- Профиль-специфичные файлы (
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 решает это:
- Создаётся отдельный сервис (
config-server), который читает конфигурации из Git-репозитория (application.yml,user-service-prod.yml); - Клиентские сервисы при старте запрашивают у Config Server свои настройки;
- Изменения в 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 объект с:
principal—subиз токена;authorities— преобразованныеscopeвGrantedAuthority(например,SCOPE_read:users→read: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)
-
Инстантация
- Spring создаёт объект через конструктор
- на этом шаге зависимости ещё не подставлены
-
Заполнение зависимостей
AutowiredAnnotationBeanPostProcessorвнедряет зависимости через@Autowired,@Value,@Resource- значения берутся из
Environment, например${db.url} - при необходимости применяется преобразование типов через
Converter
-
Обработка до инициализации
- вызываются все
BeanPostProcessor.postProcessBeforeInitialization() - здесь отрабатывают
@PostConstructи другие механизмы, завязанные на post-processor
- вызываются все
-
Инициализация
- выполняется
InitializingBean.afterPropertiesSet(), если интерфейс реализован - затем выполняется метод из
init-methodили@Bean(initMethod = "...")
- выполняется
-
Обработка после инициализации
- вызываются
BeanPostProcessor.postProcessAfterInitialization() - на этом шаге часто создаются прокси для AOP и
@Transactional
- вызываются
-
Рабочая стадия
- бин внедряется в другие компоненты
- бизнес-логика выполняется уже через готовый экземпляр или его прокси
-
Уничтожение
- при закрытии контекста вызываются
@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, но не при конструкторном внедрении.
Как это работает:
- создаётся ранний экземпляр A без финальной инициализации
- этот экземпляр временно кладётся в
early singleton cache - при создании B Spring берёт A из раннего кеша и завершает сборку графа
- после этого 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
- Открытие пакетов для рефлексии
Spring читает аннотации и создаёт бины через рефлексию. Пакеты с компонентами должны бытьopen:
open module com.example.app {
requires spring.context;
requires spring.beans;
// ...
}
open — разрешает deep reflection для всего модуля. Альтернатива — opens com.example.service to spring.core, но это многословно.
- Экспорт API-пакетов
Если модуль предоставляет интерфейсы для других, их пакеты должны бытьexported:
exports com.example.api;
- Обработка автоматических модулей
Большинство библиотек (включая 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 добавляют поддержку:
-
Конфигурация времени сборки (
@Configuration(proxyBeanMethods = false))
Упрощает анализ: Spring не создаёт CGLIB-прокси для@Configuration-классов. -
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 { }
- Ограничения
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 — это последовательность локальных транзакций, где каждая сопровождается компенсирующей операцией на случай сбоя.
Пример оформления заказа:
ReserveInventory→ если успех, идти дальше; иначе — стоп.ProcessPayment→ если успех, идти дальше; иначе — вызватьCancelInventoryReservation.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
- В той же транзакции записываем событие в специальную таблицу
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());
- Отдельный реляционный триггер или асинхронный воркер читает
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
Для диагностики:
Разбор:
- Этот блок работает как схема: строка `---` задаёт каркас потока без деталей реализации.
- Сначала видна общая последовательность этапов, а затем уже разбираются конкретные классы и методы.
- Такой формат уменьшает когнитивную нагрузку и помогает быстрее локализовать проблемный шаг.
- Практически это карта для отладки и чтения следующих кодовых фрагментов.