Java Spring Framework: уровень Middle/Middle+

Углубленный курс для разработчиков, желающих освоить внутреннее устройство Spring Boot, микросервисную архитектуру, безопасность и высокопроизводительную работу с данными. Программа охватывает продвинутые паттерны, реактивный стек и инструменты мониторинга.

1. Глубокое погружение в Spring Boot: магия автоконфигурации, жизненный цикл бинов и AOP

Глубокое погружение в Spring Boot: магия автоконфигурации, жизненный цикл бинов и AOP

Добро пожаловать в курс Java Spring Framework: уровень Middle/Middle+. Если вы читаете эту статью, значит, вы уже умеете создавать REST-контроллеры, подключать базы данных и писать бизнес-логику. Но чтобы перейти от уровня Junior к Middle и выше, недостаточно знать, как использовать аннотации. Необходимо понимать, что именно происходит под капотом, когда вы запускаете приложение.

В этой статье мы разберем три фундаментальных столпа Spring Boot, которые часто воспринимаются как «магия»: механизм автоконфигурации, сложный жизненный цикл бинов и реализацию AOP (Aspect Oriented Programming).

Магия автоконфигурации: как Spring понимает, что нам нужно?

Многие разработчики привыкли к тому, что достаточно добавить зависимость spring-boot-starter-web в pom.xml или build.gradle, и приложение автоматически поднимает Tomcat и настраивает DispatcherServlet. Но как это работает?

Всё начинается с аннотации @SpringBootApplication. Это не просто маркер, а мета-аннотация, объединяющая в себе три другие:

  • @Configuration — разрешает регистрацию бинов в контексте.
  • @ComponentScan — сканирует пакеты на наличие компонентов.
  • @EnableAutoConfiguration — именно здесь происходит магия.
  • Механизм загрузки конфигураций

    Аннотация @EnableAutoConfiguration импортирует класс AutoConfigurationImportSelector. Его задача — найти и загрузить конфигурационные классы, которые могут быть полезны приложению.

    До версии Spring Boot 2.7 основным источником был файл META-INF/spring.factories внутри jar-файлов зависимостей. В современных версиях (Spring Boot 3.x) используется файл META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.

    Spring сканирует classpath, находит эти файлы во всех подключенных библиотеках и собирает список потенциальных конфигураций. Это могут быть сотни классов: DataSourceAutoConfiguration, JacksonAutoConfiguration, SecurityAutoConfiguration и другие.

    Условные аннотации (Conditionals)

    Если бы Spring загружал все найденные конфигурации, приложение падало бы из-за конфликтов и запускалось бы вечность. Здесь в игру вступают условные аннотации @Conditional.... Они работают как фильтры.

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

    Логика фильтрации следующая:

    * @ConditionalOnClass: Конфигурация активируется только если нужный класс найден в зависимостях. Если вы не добавили драйвер БД, Spring проигнорирует эту настройку. * @ConditionalOnMissingBean: Это ключевой момент для переопределения. Spring говорит: «Я создам этот бин по умолчанию, только если разработчик не объявил свой такой же». * @ConditionalOnProperty: Активация зависит от наличия настройки в application.properties.

    [VISUALIZATION: Схема процесса автоконфигурации Spring Boot. Слева стопка jar-файлов с зависимостями. От них стрелки идут к блоку

    2. Продвинутая работа с данными: Spring Data JPA, управление транзакциями и оптимизация производительности

    Продвинутая работа с данными: Spring Data JPA, управление транзакциями и оптимизация производительности

    В предыдущей статье мы разобрали, как работает «магия» Spring Boot, включая AOP (аспектно-ориентированное программирование). Сегодня мы увидим одно из самых мощных применений AOP на практике — управление транзакциями.

    Переход от уровня Junior к Middle в работе с базами данных означает отказ от мышления «лишь бы работало» в пользу «работает эффективно и предсказуемо». Мы углубимся в недра Hibernate (который скрыт за Spring Data JPA), разберем проблему N+1, научимся правильно управлять транзакциями и использовать проекции.

    Жизненный цикл Entity и Persistence Context

    Многие начинающие разработчики воспринимают методы репозитория (save, findById) как прямые SQL-запросы. Однако между вашим кодом и базой данных стоит мощный посредник — Persistence Context (контекст персистентности), который управляется EntityManager.

    Persistence Context — это кэш первого уровня. Когда вы загружаете сущность из БД, она сохраняется в этом контексте.

    Dirty Checking (Грязная проверка)

    Одной из ключевых особенностей JPA является механизм Dirty Checking. Если сущность находится в управляемом состоянии (Managed), вам не нужно явно вызывать метод save() для сохранения изменений.

    Рассмотрим пример:

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

  • При загрузке user Hibernate сохраняет его копию (snapshot).
  • При завершении транзакции Hibernate сравнивает текущее состояние объекта с сохраненной копией.
  • Если есть различия, он автоматически генерирует SQL UPDATE.
  • !Визуализация механизма Dirty Checking и кэша первого уровня

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

    Аннотация @Transactional — это декларативный способ управления транзакциями, реализованный через AOP-прокси. Когда вы ставите эту аннотацию над методом, Spring оборачивает ваш бин в прокси-объект.

    Проблема самовызова (Self-invocation)

    Классическая ошибка на собеседованиях и в коде:

    Почему это не работает? Потому что вы вызываете метод saveTransactionally() внутри того же класса через this. Вызов идет напрямую к методу объекта, минуя прокси Spring, который отвечает за открытие и закрытие транзакции. Чтобы транзакция заработала, вызов должен прийти снаружи бина.

    Уровни изоляции и распространения (Propagation)

    Параметр propagation определяет, как транзакция должна вести себя, если она уже существует.

    * REQUIRED (по умолчанию): Если транзакция есть — используем её. Если нет — создаем новую. Самый частый сценарий. * REQUIRES_NEW: Всегда создает новую транзакцию, приостанавливая текущую. Полезно для логирования ошибок или аудита, который должен сохраниться, даже если основная бизнес-операция откатится (rollback). * MANDATORY: Требует наличия активной транзакции, иначе бросает исключение.

    Проблема N+1 и стратегии выборки

    Проблема N+1 — главный враг производительности ORM. Она возникает, когда фреймворк выполняет 1 запрос для получения списка родительских сущностей, а затем N запросов для получения связанных дочерних сущностей для каждого родителя.

    Предположим, у нас есть Author и список его книг List<Book>.

    Если авторов 100, то Hibernate выполнит:

    где — общее количество запросов, — запрос списка авторов, — количество авторов (запросы за книгами).

    Решение 1: EntityGraph

    @EntityGraph позволяет декларативно указать, какие связи нужно подтянуть сразу.

    Решение 2: JPQL и JOIN FETCH

    Более гибкий способ — написать запрос вручную:

    Ключевое слово FETCH заставляет Hibernate загрузить связанные данные в одном SQL-запросе, используя INNER JOIN или LEFT JOIN.

    Оптимизация производительности

    FetchType: LAZY vs EAGER

    Золотое правило Middle-разработчика: Всегда используйте FetchType.LAZY для связей @OneToMany и @ManyToMany.

    * EAGER (жадная загрузка) загружает данные всегда, даже если они вам не нужны. Это приводит к вытягиванию огромных графов объектов и переполнению памяти. * LAZY (ленивая загрузка) загружает данные только при первом обращении к ним.

    > По умолчанию в JPA: @OneToMany и @ManyToMany — LAZY, а @ManyToOne и @OneToOne — EAGER. Рекомендуется явно ставить LAZY везде.

    Использование DTO (Projections)

    Часто нам не нужна вся сущность целиком. Если вам нужно отобразить только имя пользователя и его email в выпадающем списке, нет смысла тянуть из БД тяжелый объект с десятком связей.

    Используйте интерфейсные проекции (Interface-based Projections) или DTO:

    Spring Data настолько умен, что сгенерирует SQL-запрос, выбирающий только эти два поля (SELECT first_name, email FROM ...), что существенно снижает нагрузку на сеть и БД.

    Read-Only транзакции

    Если метод только читает данные, помечайте транзакцию как readOnly = true:

    Это дает ряд оптимизаций:

  • Hibernate может отключить Dirty Checking (экономия ресурсов CPU).
  • Драйвер БД может использовать оптимизации для транзакций только на чтение.
  • Заключение

    Эффективная работа со Spring Data JPA требует понимания того, что происходит «под капотом». Использование JOIN FETCH для решения N+1, правильная настройка транзакций и работа с DTO вместо сущностей — это те навыки, которые отличают профессионала от новичка.

    В следующей части курса мы перейдем к вопросам безопасности и разберем Spring Security.

    3. Безопасность приложений: архитектура Spring Security, OAuth2, JWT и защита REST API

    Безопасность приложений: архитектура Spring Security, OAuth2, JWT и защита REST API

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

    Переход к уровню Middle требует понимания того, что безопасность — это не просто добавление зависимости spring-boot-starter-security и формы логина. Это глубокое понимание архитектуры фильтров, протоколов аутентификации и принципов защиты Stateless (безсостоянийных) API.

    Архитектура Spring Security: Это просто фильтры

    Многие разработчики боятся Spring Security из-за его кажущейся сложности. Однако в основе этого фреймворка лежит простая концепция — цепочка фильтров сервлетов (Servlet Filter Chain).

    Когда HTTP-запрос приходит в ваше приложение, он проходит через ряд перехватчиков. Spring Security встраивается в этот процесс с помощью одного главного фильтра — DelegatingFilterProxy.

    Как это работает под капотом?

  • DelegatingFilterProxy: Это стандартный сервлет-фильтр, который делегирует управление бину Spring, обычно называемому FilterChainProxy.
  • FilterChainProxy: Это «диспетчер безопасности». Он решает, какая именно цепочка безопасности (SecurityFilterChain) должна применяться к текущему запросу.
  • SecurityFilterChain: Это набор фильтров безопасности, которые выполняют конкретные задачи: проверка заголовков, аутентификация, авторизация, защита от атак.
  • !Визуализация прохождения запроса через цепочку фильтров безопасности перед попаданием в контроллер

    Authentication vs Authorization

    Важно четко разделять два понятия:

    * Аутентификация (Authentication): Ответ на вопрос «Кто ты?». Процесс проверки учетных данных (логин/пароль, токен). * Авторизация (Authorization): Ответ на вопрос «Что тебе можно делать?». Проверка прав доступа к ресурсу.

    В Spring Security за аутентификацию отвечает интерфейс AuthenticationManager. Чаще всего используется его реализация ProviderManager, которая опрашивает список AuthenticationProvider. Если хотя бы один провайдер узнает пользователя, аутентификация считается успешной.

    Защита REST API: От сессий к токенам

    Классические веб-приложения используют сессии (JSESSIONID в cookies). Сервер хранит состояние пользователя в памяти. Для REST API архитектуры микросервисов этот подход плох по двум причинам:

  • Масштабируемость: Если у вас 10 экземпляров сервиса, вам нужно синхронизировать сессии между ними (например, через Redis).
  • Мобильные клиенты: Нативным приложениям сложнее работать с куками, чем с заголовками.
  • Стандартом индустрии для REST API стал JWT (JSON Web Token).

    Анатомия JWT

    JWT — это строка, состоящая из трех частей, разделенных точкой: Header.Payload.Signature.

  • Header: Информация об алгоритме шифрования.
  • Payload (Claims): Полезная нагрузка (ID пользователя, роли, срок действия).
  • Signature: Цифровая подпись, гарантирующая, что данные не были изменены.
  • Математически создание подписи можно выразить формулой:

    Где: * — итоговая подпись (Signature). * — алгоритм хеширования с ключом (например, HMACSHA256). * — функция кодирования в Base64Url. * — заголовок (header). * — полезная нагрузка (payload). * — операция конкатенации строк. * — секретный ключ, который хранится только на сервере.

    Сервер не хранит токены. При получении запроса он берет заголовок и payload из токена, снова хеширует их своим секретным ключом и сравнивает результат с пришедшей подписью . Если они совпадают — токен валиден.

    Настройка Spring Security 6+ (Spring Boot 3)

    В новых версиях Spring Security мы больше не наследуемся от WebSecurityConfigurerAdapter. Теперь мы регистрируем бин SecurityFilterChain.

    Почему мы отключаем CSRF?

    CSRF (Cross-Site Request Forgery) — атака, использующая доверие сайта к браузеру пользователя через cookies. Поскольку в REST API с JWT мы не используем cookies для аутентификации и не храним сессию на сервере, этот вектор атаки становится неактуальным, и защиту можно безопасно отключить.

    OAuth2 и OpenID Connect

    Если вы не хотите хранить пароли пользователей или вам нужно интегрироваться с Google/GitHub/Keycloak, вы используете OAuth2.

    OAuth2 — это протокол делегирования доступа. В нем есть 4 роли:

  • Resource Owner: Пользователь.
  • Client: Ваше приложение.
  • Authorization Server: Сервер, выдающий токены (например, Google).
  • Resource Server: Сервер, где лежат данные (Ваш REST API).
  • В контексте микросервисов ваше Spring Boot приложение часто выступает в роли Resource Server. Оно не выдает токены, оно их только проверяет.

    Настройка Resource Server в Spring Boot минимальна:

    Spring автоматически скачает публичные ключи (JWK Set) с сервера авторизации (указанного в application.properties) и будет валидировать входящие JWT токены.

    SecurityContextHolder

    Как получить данные текущего пользователя в коде бизнес-логики? Spring Security хранит аутентификацию в ThreadLocal переменной.

    Важно: Так как данные хранятся в ThreadLocal, при использовании @Async или параллельных стримов контекст безопасности не передается в новые потоки автоматически. Для этого нужно настраивать DelegatingSecurityContextExecutor.

    Заключение

    Безопасность на уровне Middle — это понимание того, как запросы проходят через цепочку фильтров, умение реализовывать Stateless-архитектуру с помощью JWT и знание протокола OAuth2.

    Мы рассмотрели:

  • Архитектуру фильтров Spring Security.
  • Разницу между аутентификацией и авторизацией.
  • Принцип работы и математику подписи JWT.
  • Настройку SecurityFilterChain в Spring Boot 3.
  • В следующей части курса мы выйдем за пределы одного приложения и поговорим о микросервисной архитектуре, Service Discovery и API Gateway.

    4. Микросервисы и асинхронность: экосистема Spring Cloud, Apache Kafka и распределенные системы

    Микросервисы и асинхронность: экосистема Spring Cloud, Apache Kafka и распределенные системы

    В предыдущих частях курса мы прошли путь от создания монолитного приложения с «магией» Spring Boot до оптимизации работы с базой данных и настройки безопасности через OAuth2. Теперь пришло время разрушить монолит.

    Переход от уровня Junior к Middle+ часто знаменуется сменой парадигмы: вы перестаете думать в рамках одного JVM-процесса и начинаете мыслить категориями распределенных систем. Вызов метода превращается в сетевой запрос, транзакция в базе данных — в сагу, а гарантированная доставка сообщений становится головной болью.

    В этой статье мы разберем экосистему Spring Cloud, научимся проектировать асинхронное взаимодействие через Apache Kafka и узнаем, как не уронить весь продакшн из-за одного зависшего сервиса.

    Экосистема Spring Cloud: Клей для микросервисов

    Когда вы разбиваете приложение на десятки сервисов, возникают инфраструктурные проблемы, которых не было в монолите. Как сервисам находить друг друга? Как хранить конфигурации? Как маршрутизировать запросы?

    Spring Cloud — это не отдельный фреймворк, а набор проектов, решающих эти задачи.

    Service Discovery (Eureka)

    В облачной среде IP-адреса сервисов динамичны. Вы не можете просто прописать http://192.168.1.50:8080 в коде, потому что через минуту сервис может перезапуститься на другом порту или другом сервере.

    Здесь на помощь приходит Service Discovery (например, Netflix Eureka). Это телефонная книга вашего кластера.

  • Регистрация: При старте микросервис отправляет запрос в Eureka: «Я сервис order-service, мой IP такой-то, порт такой-то».
  • Обнаружение: Когда gateway хочет отправить запрос в order-service, он спрашивает у Eureka актуальный адрес.
  • API Gateway (Spring Cloud Gateway)

    Выставлять все 50 микросервисов в интернет — плохая идея. Клиент (фронтенд или мобильное приложение) должен знать только одну точку входа.

    API Gateway выполняет роль швейцара: * Маршрутизация: Перенаправляет запрос /api/orders на order-service, а /api/users на user-service. * Cross-cutting concerns: Здесь удобно реализовать аутентификацию (проверку JWT), rate limiting (ограничение частоты запросов) и логирование.

    !Архитектура с использованием API Gateway и Service Discovery

    Синхронное vs Асинхронное взаимодействие

    Самая частая ошибка при переходе на микросервисы — создание «распределенного монолита». Это происходит, когда сервисы общаются друг с другом синхронно по HTTP (REST или Feign Client).

    Представьте цепочку вызовов: Shop -> Order -> Inventory -> Payment. Если каждый сервис отвечает 200 мс, то пользователь будет ждать почти секунду. Но хуже то, что надежность такой системы падает катастрофически.

    Рассчитаем доступность системы при синхронной цепочке вызовов:

    Где: * — общая доступность системы (вероятность успешного выполнения запроса). * — доступность -го сервиса (например, 0.99). * — количество сервисов в цепочке. * — знак произведения.

    Если у нас 4 сервиса с доступностью 99% (), то общая надежность:

    То есть каждый 25-й запрос будет падать. Решение? Асинхронность.

    Apache Kafka: Кровеносная система микросервисов

    Apache Kafka — это не просто очередь сообщений (как RabbitMQ), это распределенный лог событий. Главное отличие в том, что Kafka хранит сообщения на диске и позволяет перечитывать их многократно.

    Основные понятия

  • Topic (Топик): Категория сообщений (например, orders-created).
  • Partition (Партиция): Топик разбивается на части для масштабирования. Это единица параллелизма.
  • Producer: Тот, кто пишет в топик.
  • Consumer Group: Группа читателей. Kafka гарантирует, что одно сообщение из партиции будет прочитано только одним консьюмером в рамках одной группы.
  • Spring for Apache Kafka

    Подключение Kafka в Spring Boot максимально упрощено. Нам нужен KafkaTemplate для отправки и @KafkaListener для чтения.

    Отправка сообщения (Producer):

    Чтение сообщения (Consumer):

    Масштабирование Consumer Group

    Важно понимать математику масштабирования в Kafka. Количество активных консьюмеров в одной группе не может превышать количество партиций.

    Где: * — количество экземпляров вашего микросервиса (слушателей). * — количество партиций в топике. * — знак «меньше или равно».

    Если у вас 3 партиции и вы запустите 5 экземпляров сервиса, то 2 экземпляра будут простаивать (idle), так как им не достанется партиций для чтения. Это фундаментальное ограничение модели Kafka.

    !Распределение партиций между консьюмерами в группе

    Паттерны отказоустойчивости (Resilience)

    В распределенной системе сбои неизбежны. Сеть моргнет, база данных зависнет, сторонний API упадет. Если ваш сервис просто ждет ответа, он может забить все свои потоки ожиданием и тоже упасть. Это называется каскадный сбой.

    Для защиты используется паттерн Circuit Breaker (Автоматический выключатель), реализованный в библиотеке Resilience4j.

    Состояния Circuit Breaker

  • CLOSED (Закрыт): Все хорошо, запросы проходят.
  • OPEN (Открыт): Ошибок слишком много. Запросы моментально отклоняются без попытки вызова удаленного сервиса.
  • HALF-OPEN (Полуоткрыт): Пробный режим. Система пропускает пару запросов, чтобы проверить, ожил ли сервис.
  • Пример использования:

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

    Когда запрос проходит через 5 сервисов и падает на третьем, найти причину по логам невозможно, если логи не связаны.

    Инструменты вроде Micrometer Tracing (ранее Spring Cloud Sleuth) и Zipkin/Jaeger добавляют к каждому запросу два идентификатора:

  • Trace ID: Общий ID для всей цепочки запросов (от входа в Gateway до последнего сервиса).
  • Span ID: ID конкретного этапа обработки внутри одного сервиса.
  • Благодаря Trace ID вы можете вбить его в систему агрегации логов (ELK, Graylog) и увидеть полную картину прохождения запроса.

    Заключение

    Микросервисная архитектура дает гибкость и возможность независимого масштабирования, но требует высокой инженерной культуры.

    Мы разобрали: * Как Spring Cloud помогает сервисам находить друг друга. * Почему асинхронность через Kafka повышает надежность системы по сравнению с синхронным REST. * Как защитить систему от каскадных сбоев с помощью Circuit Breaker.

    Это завершает наш цикл статей о разработке на Spring Framework уровня Middle+. Теперь у вас есть полный набор инструментов: от глубокого понимания бинов и баз данных до построения безопасных и отказоустойчивых распределенных систем.

    5. Качество и наблюдаемость: тестирование с Testcontainers, мониторинг через Actuator и введение в WebFlux

    Качество и наблюдаемость: тестирование с Testcontainers, мониторинг через Actuator и введение в WebFlux

    Мы прошли долгий путь: от разбора внутренней кухни Spring Boot и оптимизации JPA до построения защищенных микросервисов на Kafka. Но создание сложной распределенной системы — это только половина дела. Вторая половина — убедиться, что она работает корректно в условиях, приближенных к боевым, и знать о её состоянии в любой момент времени.

    Уровень Middle+ подразумевает отказ от принципа «на моем компьютере работает». В этой статье мы поднимем планку качества кода с помощью Testcontainers, научимся следить за здоровьем приложения через Spring Boot Actuator и заглянем в будущее высокой нагрузки с Spring WebFlux.

    Честное тестирование: Testcontainers

    В мире Junior-разработки для тестов часто используют H2 (in-memory базу данных). Это удобно: она быстрая и не требует установки. Но в мире Middle+ H2 — это ловушка.

    Почему? Потому что H2 — это не PostgreSQL и не Oracle. У них разный синтаксис SQL, разные механизмы блокировок и разные типы данных. Тест, который проходит на H2, может с треском провалиться на реальной базе в продакшене. То же самое касается брокеров сообщений: мокать Kafka — значит не проверять реальную сериализацию и сетевое взаимодействие.

    Что такое Testcontainers?

    Testcontainers — это Java-библиотека, которая позволяет запускать одноразовые Docker-контейнеры прямо из JUnit-тестов.

    Вместо того чтобы мокать базу данных, вы поднимаете настоящий PostgreSQL в Docker-контейнере на время теста. После завершения теста контейнер уничтожается.

    !Схема взаимодействия JUnit тестов с Docker через библиотеку Testcontainers

    Практика: Spring Boot 3.1+ и @ServiceConnection

    Раньше настройка Testcontainers требовала много ручного кода для проброса динамических портов в application.properties. Начиная со Spring Boot 3.1, это делается элементарно с помощью spring-boot-testcontainers.

    Пример интеграционного теста:

    Аннотация @ServiceConnection — это магия Spring Boot. Она автоматически находит нужный контейнер и подставляет его параметры (URL, username, password) в конфигурацию Spring. Вам больше не нужно заботиться о портах.

    Наблюдаемость (Observability): Spring Boot Actuator

    Когда ваше приложение запущено в Kubernetes, у вас нет возможности подключиться к нему отладчиком. Вы не видите консоль. Как понять, живо ли приложение? Хватает ли ему памяти? Не переполнен ли пул соединений с БД?

    Spring Boot Actuator предоставляет готовые endpoints (ручки) для мониторинга и управления приложением.

    Ключевые энпоинты

    Подключив зависимость spring-boot-starter-actuator, вы получаете доступ к /actuator:

  • /health: Показывает статус здоровья (UP или DOWN). Это не просто «приложение запущено». Actuator проверяет соединение с БД, брокером сообщений и дисковым пространством. Если БД упала, статус станет DOWN, и Kubernetes перезапустит под.
  • /metrics: Огромное количество метрик (CPU, JVM memory, GC, HTTP requests).
  • /loggers: Позволяет просматривать и менять уровень логирования в рантайме без перезагрузки. Это спасение, когда нужно отладить проблему на проде.
  • Метрики и Micrometer

    Actuator собирает данные, но не хранит их. Для этого используется фасад Micrometer. Это как SLF4J, но для метрик. Вы пишете код один раз, а Micrometer отправляет метрики в Prometheus, Datadog, Graphite или New Relic.

    Важный аспект мониторинга — расчет доступности (Availability) или SLA. Формула доступности сервиса выглядит так:

    где — доступность в процентах, — время, когда сервис работал корректно, — время простоя, — знак умножения.

    Actuator в связке с Prometheus позволяет строить графики, основанные на подобных метриках, и настраивать алерты (оповещения), если количество ошибок превышает порог.

    > Важно: Никогда не выставляйте Actuator в публичный интернет без защиты. Злоумышленник может узнать версии библиотек, пути к файлам и даже сделать дамп кучи (Heap Dump). Используйте Spring Security для защиты /actuator/**.

    Введение в Spring WebFlux: Реактивное программирование

    До сих пор мы работали с классическим стеком Spring MVC. Он построен на модели Thread-per-Request (один поток на один запрос).

    Проблема блокирующего I/O

    Представьте, что ваш сервис делает запрос к внешней API, который длится 2 секунды. В Spring MVC поток Tomcat будет просто ждать эти 2 секунды. Он заблокирован.

    Если у вас 200 потоков (стандарт для Tomcat), то 201-й запрос будет ждать в очереди. Это называется проблемой масштабируемости I/O.

    Реактивное решение

    Spring WebFlux — это реактивный веб-фреймворк, построенный на Project Reactor и Netty. Он использует модель Event Loop (как в Node.js или Nginx).

    Вместо того чтобы блокировать поток, WebFlux инициирует операцию и «отпускает» поток заниматься другими делами. Когда данные придут, сработает callback (событие), и обработка продолжится.

    !Сравнение блокирующей модели Spring MVC и неблокирующей модели Spring WebFlux

    Mono и Flux

    В реактивном мире мы не возвращаем User или List<User>. Мы возвращаем «обещание» данных.

  • Mono<T>: Контейнер для 0 или 1 элемента. (Аналог Optional или CompletableFuture).
  • Flux<T>: Контейнер для 0 или N элементов. (Аналог Stream).
  • Пример реактивного контроллера:

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

    Переход на WebFlux — это смена парадигмы. Код становится сложнее для написания и отладки (stacktrace становится нечитаемым).

    Используйте WebFlux, если: * Вам нужно держать тысячи открытых соединений (WebSocket, чаты, IoT). * Вы пишите API Gateway или высоконагруженный прокси. * Вам нужна потоковая передача данных (Streaming).

    Для обычных CRUD-приложений Spring MVC (особенно с появлением Virtual Threads в Java 21) остается предпочтительным выбором.

    Заключение курса

    В этой статье мы закрыли важные пробелы в качестве и производительности.

  • Testcontainers дали нам уверенность, что код работает с реальной инфраструктурой.
  • Actuator дал нам глаза и уши в продакшене.
  • WebFlux показал путь к экстремальной масштабируемости.
  • Вы прошли путь от понимания автоконфигурации до построения реактивных микросервисов. Теперь вы обладаете набором инструментов уровня Middle+, позволяющим создавать не просто работающие, а надежные, наблюдаемые и эффективные системы на Spring Framework.

    Итоги

  • Testcontainers обеспечивают надежное интеграционное тестирование, запуская реальные базы данных и брокеры сообщений в Docker-контейнерах, что исключает проблемы несовместимости окружений.
  • Spring Boot Actuator предоставляет готовые инструменты для мониторинга состояния приложения, сбора метрик и управления логированием в реальном времени без перезагрузки сервиса.
  • Spring WebFlux предлагает неблокирующую реактивную модель для создания высоконагруженных систем, эффективно использующих ресурсы при большом количестве одновременных соединений.