Java 21 и Spring Framework: Путь к Strong Middle

Углубленный курс по современной бэкенд-разработке, охватывающий фичи Java 21 (Virtual Threads) и продвинутые аспекты экосистемы Spring. Вы научитесь проектировать масштабируемые API, работать с транзакциями и строить микросервисную архитектуру.

1. Modern Java и Spring Core: Виртуальные потоки, Records и магия IoC контейнера

Modern Java и Spring Core: Виртуальные потоки, Records и магия IoC контейнера

Добро пожаловать на курс Java 21 и Spring Framework: Путь к Strong Middle. Это не просто очередной туториал по синтаксису. Наша цель — углубиться в детали, которые отличают Junior-разработчика от уверенного Middle-специалиста: понимание того, как и почему работают инструменты, которыми мы пользуемся каждый день.

В этой вводной статье мы рассмотрим фундаментальные изменения в Java 21, которые меняют подход к написанию высоконагруженных приложений, и заглянем «под капот» Spring Framework, чтобы разобрать магию его IoC контейнера.

Java 21: Эволюция или Революция?

Долгое время Java развивалась эволюционно. Однако релиз Java 21 (LTS) принес изменения, которые можно назвать революционными, особенно в контексте конкурентного программирования.

Project Loom и Виртуальные потоки (Virtual Threads)

Главная звезда Java 21 — это Виртуальные потоки. Чтобы понять их ценность, нужно вспомнить, как Java работала с потоками раньше.

Традиционные потоки Java (java.lang.Thread) являются обертками над потоками операционной системы (OS Threads). Это ресурсы, которые дорого создавать и дорого переключать (context switching). В модели «один запрос — один поток» (thread-per-request), которую используют классические сервлеты (Tomcat, Jetty), количество одновременных пользователей жестко ограничено количеством доступных потоков ОС.

!Сравнение модели Platform Threads (1:1 с ОС) и Virtual Threads (M:N, где M виртуальных потоков работают на N потоках-носителях)

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

Когда классический поток выполняет запрос к базе данных или внешнему API, он блокируется. Поток ОС просто «спит», занимая память (около 1-2 МБ стека) и не выполняя полезной работы. Это неэффективно.

Решение: Виртуальные потоки

Виртуальные потоки — это легковесные сущности, управляемые JVM, а не операционной системой. Когда виртуальный поток встречает блокирующую операцию (I/O), JVM «отмонтирует» (unmount) его от потока-носителя (Carrier Thread). Поток-носитель освобождается и берет в работу другой виртуальный поток. Как только операция I/O завершается, виртуальный поток снова «монтируется» и продолжает выполнение.

Для оценки эффективности использования ресурсов в конкурентных системах часто применяют Закон Литтла:

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

В классической модели, если велико (из-за блокировок I/O), нам нужно огромное (количество потоков), чтобы поддерживать высокую . Потоки ОС заканчиваются быстро. Виртуальные потоки позволяют увеличить до миллионов, так как они почти ничего не стоят.

Пример создания виртуального потока:

В Spring Boot 3.2+ включение виртуальных потоков делается одной строкой в application.properties:

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

Records: Убийцы бойлерплейта

Второй важный инструмент для современного Java-разработчика — это Records (Записи). Они появились раньше Java 21, но именно сейчас стали стандартом де-факто для DTO (Data Transfer Objects).

До Records мы писали классы с кучей шаблонного кода или использовали Lombok. Records — это неизменяемые (immutable) носители данных.

Что мы получаем «из коробки»:

  • Все поля private final.
  • Автоматически сгенерированные геттеры (без префикса get, просто username()).
  • Корректные реализации equals(), hashCode() и toString().
  • Канонический конструктор.
  • Почему это важно для Middle разработчика? Использование Records гарантирует иммутабельность данных на уровне API. Это снижает количество багов, связанных со случайным изменением состояния объекта при передаче его между слоями приложения.

    Spring Core: Магия IoC Контейнера

    Перейдем к Spring. Многие разработчики умеют ставить аннотацию @Autowired, но Strong Middle должен понимать, как именно объект попадает в переменную.

    ApplicationContext vs BeanFactory

    В основе Spring лежит интерфейс BeanFactory — это простейший контейнер, обеспечивающий DI (Dependency Injection). Однако в 99% случаев мы работаем с ApplicationContext. Это расширение BeanFactory, которое добавляет: * События (Events). * AOP (Аспектно-ориентированное программирование). * Работу с ресурсами и интернационализацию.

    Жизненный цикл Бина (Bean Lifecycle)

    Понимание жизненного цикла — ключ к решению сложных проблем инициализации.

    !Этапы создания бина: от определения класса до готового к использованию объекта в контексте

    Ключевые этапы:

  • Bean Definition: Spring сканирует конфигурацию и создает «рецепты» создания бинов.
  • Instantiation: Java создает объект (вызывает конструктор).
  • Populate Properties: Spring внедряет зависимости (DI).
  • Initialization: Вызов методов инициализации (@PostConstruct).
  • BeanPostProcessor (BPP) — Серый кардинал Spring

    Вот здесь происходит настоящая магия. BeanPostProcessor — это интерфейс, который позволяет вклиниться в процесс создания бина до и после его инициализации.

    Именно через BPP работают такие аннотации, как @Autowired, @Value, и даже @Transactional.

    Как работают Прокси (Proxies)

    Вы когда-нибудь задумывались, как работает @Transactional? Как простая аннотация заставляет метод открывать транзакцию, делать commit или rollback?

    Ответ: Dynamic Proxy.

    Когда Spring видит аннотацию @Transactional над классом, специальный BeanPostProcessor в методе postProcessAfterInitialization подменяет ваш оригинальный объект на Прокси-объект.

    Этот Прокси содержит ссылку на ваш оригинальный объект и добавляет логику вокруг вызова методов:

  • Прокси перехватывает вызов метода.
  • Открывает транзакцию БД.
  • Вызывает реальный метод вашего класса.
  • Если исключение — делает rollback, если успех — commit.
  • Именно поэтому вызов транзакционного метода из другого метода того же самого класса (this.method()) не запустит транзакцию. Вызов идет напрямую через this, минуя Прокси.

    Заключение

    Становление Strong Middle разработчиком требует смены парадигмы мышления. Мы переходим от вопроса «как написать код, чтобы он работал» к вопросам «как это работает внутри» и «какова цена этого решения».

    Виртуальные потоки в Java 21 решают проблему масштабируемости I/O-bound приложений, позволяя нам писать простой блокирующий код, который работает так же эффективно, как и реактивный. Records помогают делать код чище и безопаснее. А понимание внутренностей Spring (Lifecycle, BPP, Proxies) дает полный контроль над поведением приложения.

    В следующей статье мы углубимся в продвинутую конфигурацию Spring Boot и работу с базами данных.

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

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

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

    Большинство проблем с производительностью (около 80%) в Java-приложениях возникают именно здесь. Junior-разработчик пишет код, который просто работает. Strong Middle пишет код, который не «положит» базу данных при нагрузке.

    Spring Data JPA vs Hibernate: Разделяй и властвуй

    Для начала устраним путаницу в терминологии, которая часто встречается на собеседованиях.

    JPA (Jakarta Persistence API) — это спецификация (набор интерфейсов). Это закон, который гласит, как* должно работать ORM. * Hibernate — это самая популярная реализация этой спецификации. Это «движок», который делает всю грязную работу: генерирует SQL, управляет кэшем первого уровня и сессиями. * Spring Data JPA — это еще один уровень абстракции над JPA. Он позволяет нам писать интерфейсы репозиториев (extends JpaRepository), а Spring сам генерирует их реализацию на лету.

    Понимание этой матрешки важно, потому что оптимизации, о которых мы будем говорить, касаются именно Hibernate, даже если мы используем Spring Data.

    Проблема N+1: Главный враг производительности

    Это классическая проблема ORM. Допустим, у нас есть сущность User (Пользователь) и Order (Заказ). Связь — один ко многим (@OneToMany).

    Мы хотим получить всех пользователей и вывести количество их заказов:

    Что происходит в логах SQL?

  • SELECT * FROM users — Hibernate загружает список пользователей.
  • Для каждого пользователя, когда мы обращаемся к user.getOrders(), Hibernate делает отдельный SELECT * FROM orders WHERE user_id = ?.
  • Если у нас 1000 пользователей, мы выполним 1001 запрос к базе данных. Это катастрофа для сети и БД.

    Математически количество запросов можно выразить формулой:

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

    !Визуализация потока запросов при проблеме N+1: один начальный запрос порождает лавину дополнительных запросов.

    Решение: JOIN FETCH и EntityGraph

    Чтобы решить проблему, нужно заставить Hibernate вытащить все данные одним запросом, используя JOIN.

    Способ 1: JPQL с JOIN FETCH

    Это сгенерирует один SQL запрос с INNER JOIN.

    Способ 2: @EntityGraph

    Более декларативный подход, удобный в Spring Data JPA.

    Это подсказка для JPA: «когда будешь выполнять этот метод, сразу подгрузи поле orders».

    Оптимизация Hibernate: Dirty Checking и Read Only

    Как Hibernate понимает, что нужно обновить запись в базе? Механизм называется Dirty Checking.

    Когда вы загружаете сущность внутри транзакции, Hibernate сохраняет её копию (snapshot) в контексте персистентности (Persistence Context). При коммите транзакции он сравнивает текущее состояние объекта со снэпшотом. Если есть различия — выполняется UPDATE.

    Это удобно, но затратно по памяти и процессору. Если вы просто вычитываете данные для отображения (например, список товаров), вам не нужен этот механизм.

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

    В этом случае Spring настроит Hibernate так, что он не будет создавать снэпшоты и не будет проверять изменения (Dirty Checking). Это дает заметный прирост производительности и экономию памяти.

    Управление транзакциями: Глубже, чем @Transactional

    В прошлой статье мы узнали, что @Transactional работает через прокси. Теперь разберем параметры этой аннотации, которые критически важны для бизнес-логики.

    Isolation Levels (Уровни изоляции)

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

  • READ_UNCOMMITTED: Видны даже незакоммиченные изменения других транзакций. Опасно (Dirty Read).
  • READ_COMMITTED (Default для Postgres/Oracle): Видны только закоммиченные данные. Защищает от Dirty Read.
  • REPEATABLE_READ (Default для MySQL): Гарантирует, что если вы прочитали строку дважды в одной транзакции, результат будет одинаковым. Защищает от Non-repeatable Read.
  • SERIALIZABLE: Полная изоляция. Транзакции выполняются последовательно. Самый медленный уровень.
  • Propagation (Распространение)

    Этот параметр определяет, как транзакции взаимодействуют друг с другом при вызове одного сервиса из другого.

    1. REQUIRED (По умолчанию) Если транзакция уже есть — используем её. Если нет — создаем новую.

    > Если во вложенном методе возникнет исключение (RuntimeException), вся транзакция, включая изменения в вызывающем методе, откатится (Rollback).

    2. REQUIRES_NEW Всегда создает новую транзакцию. Текущая транзакция (если есть) «ставится на паузу».

    Это жизненно необходимо для таких вещей, как логирование ошибок или аудит. Представьте, что вы сохраняете заказ. Произошла ошибка. Вы хотите сохранить запись об ошибке в таблицу audit_logs. Если вы сделаете это в той же транзакции (REQUIRED), то при откате заказа откатится и запись в логе! REQUIRES_NEW позволяет закоммитить лог, даже если основная транзакция упала.

    !Схема работы Propagation.REQUIRES_NEW: приостановка внешней транзакции ради выполнения независимой внутренней.

    Проблема FetchType.EAGER

    Никогда, запомните, никогда не используйте FetchType.EAGER (жадную загрузку) для коллекций (@OneToMany, @ManyToMany).

    Если у вас есть User и List<Order>, и стоит EAGER, то при загрузке пользователя Hibernate всегда будет тянуть все его заказы. Даже если они вам сейчас не нужны. Вы не сможете это отключить.

    Всегда используйте FetchType.LAZY и подгружайте данные точечно через JOIN FETCH или @EntityGraph там, где это действительно нужно.

    Заключение

    Работа с данными на уровне Strong Middle — это постоянный поиск баланса.

  • Мы используем Spring Data JPA для скорости разработки, но помним про Hibernate под капотом.
  • Мы боремся с N+1 через JOIN FETCH и графы сущностей.
  • Мы отключаем Dirty Checking через readOnly = true там, где не меняем данные.
  • Мы осознанно выбираем стратегию Propagation для транзакций, чтобы не потерять критически важные логи.
  • В следующей части курса мы поднимемся на уровень выше и поговорим о построении REST API, обработке исключений и валидации данных.

    3. Промышленная веб-разработка: REST API, валидация, Spring Security и OAuth2

    Промышленная веб-разработка: REST API, валидация, Spring Security и OAuth2

    Мы прошли путь от понимания виртуальных потоков и магии IoC контейнера до оптимизации работы с базой данных. Теперь у нас есть надежный «движок» и эффективная «трансмиссия». Пришло время построить интерфейс управления — API, с которым будет взаимодействовать внешний мир, будь то фронтенд на React, мобильное приложение или другие микросервисы.

    В этой статье мы разберем, как писать REST API уровня Strong Middle, как элегантно обрабатывать ошибки (а не просто возвращать 500), как валидировать данные и, самое главное, как защитить приложение с помощью Spring Security и OAuth2.

    REST API: За пределами CRUD

    Многие считают, что REST — это просто маппинг HTTP-методов на операции с БД: GET для чтения, POST для создания. Но промышленная разработка требует понимания таких концепций, как идемпотентность и уровни зрелости Ричардсона.

    Идемпотентность методов

    Это ключевое понятие для построения надежных распределенных систем. Операция считается идемпотентной, если её многократное выполнение приводит к тому же состоянию системы, что и однократное.

    Математически это можно записать так:

    где — это функция (наш HTTP-запрос), — начальное состояние системы. Если мы применяем функцию дважды, результат не меняется по сравнению с первым применением.

    * GET: Идемпотентен. Сколько бы раз вы ни запрашивали данные, состояние сервера не меняется (Safe method). * PUT: Идемпотентен. Если вы отправляете запрос «Установить имя пользователя = Alex», то, сколько бы раз вы его ни отправили, имя останется «Alex». * DELETE: Идемпотентен. Удаление уже удаленного объекта возвращает 200 или 404, но состояние системы (объект отсутствует) остается неизменным. * POST: НЕ идемпотентен. Если вы дважды отправите запрос на создание заказа, у вас будет два разных заказа.

    Почему это важно? В сетях бывают сбои. Если клиент не получил ответ на POST запрос (timeout), он не знает, создан ресурс или нет. Повтор запроса может привести к дубликатам. Для PUT повтор безопасен.

    Обработка ошибок и Problem Details (RFC 7807)

    Junior-разработчик часто пишет try-catch прямо в контроллере. Middle использует @ExceptionHandler. Strong Middle использует стандарт RFC 7807.

    Начиная со Spring Framework 6 (Spring Boot 3), поддержка этого стандарта встроена. Вместо того чтобы изобретать свой формат JSON для ошибок, мы используем класс ProblemDetail.

    Это позволяет фронтенду получать предсказуемую структуру ошибок от всех ваших микросервисов.

    Валидация: Fail Fast

    Никогда не доверяйте данным, пришедшим извне. Валидация должна происходить как можно раньше. В Spring для этого используется Bean Validation (Hibernate Validator).

    Группы валидации (Validation Groups)

    Частая проблема: при создании пользователя (POST) пароль обязателен, а при обновлении (PUT) — нет (если мы не хотим его менять). Создавать два разных DTO? Необязательно. Используйте группы.

    В контроллере мы указываем, какую группу проверять:

    Spring Security: Анатомия защиты

    Spring Security — это, пожалуй, самая сложная часть экосистемы Spring. Чтобы не копипастить конфигурации со StackOverflow, нужно понимать, как это работает внутри.

    Filter Chain (Цепочка фильтров)

    Spring Security основан на стандартных Servlet Filters. Но как Spring внедряется в контейнер сервлетов (Tomcat)?

    [VISUALIZATION: Схема потока HTTP запроса в Spring Security. Слева направо: Client -> Servlet Container (Tomcat) -> DelegatingFilterProxy -> FilterChainProxy -> SecurityFilterChain (список фильтров: CsrfFilter, UsernamePasswordAuthenticationFilter, AuthorizationFilter) -> DispatcherServlet -> Controller. Стрелки показывают направление запроса.]

  • DelegatingFilterProxy: Это стандартный фильтр сервлетов, который перенаправляет управление бину Spring.
  • FilterChainProxy: Это «сердце» безопасности. Он решает, какую цепочку фильтров (SecurityFilterChain) применить к текущему запросу (например, /api/ может иметь одну конфигурацию, а /admin/ — другую).
  • SecurityFilterChain: Набор фильтров, которые выполняют аутентификацию, проверку CSRF, CORS и авторизацию.
  • Modern Configuration (Lambda DSL)

    В Spring Security 6.0+ старый стиль конфигурации через наследование WebSecurityConfigurerAdapter был удален. Теперь мы регистрируем SecurityFilterChain как бин, используя Lambda DSL.

    OAuth2 и JWT: Stateless безопасность

    В монолитах мы часто использовали сессии (JSESSIONID). Сервер хранил состояние пользователя в памяти. В микросервисах это не работает: если запрос придет на другой инстанс сервиса, там не будет сессии.

    Решение — JWT (JSON Web Token). Это стандарт передачи информации, подписанной цифровой подписью.

    Структура JWT

    Токен состоит из трех частей, разделенных точкой: Header.Payload.Signature.

    Самая важная часть — подпись. Она гарантирует, что данные в токене не были изменены злоумышленником. Формула создания подписи:

    где — итоговая подпись (Signature), — алгоритм хеширования, — закодированный заголовок, — закодированная полезная нагрузка (Payload), а — секретный ключ, который знает только сервер.

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

    Refresh Token Rotation

    JWT (Access Token) обычно живет недолго (5-15 минут). Что делать, когда он протух? Заставлять пользователя логиниться снова? Нет.

    Используется пара токенов:

  • Access Token: Короткоживущий, для доступа к ресурсам.
  • Refresh Token: Долгоживущий (дни/недели), хранится в БД (или надежном хранилище).
  • Когда Access Token истекает, клиент отправляет Refresh Token на специальный эндпоинт. Сервер проверяет его в БД, и если он валиден — выдает новую пару токенов.

    Заключение

    Создание промышленного API — это не только бизнес-логика. Это:

  • Правильное использование HTTP-методов (идемпотентность).
  • Стандартизированная обработка ошибок (Problem Details).
  • Контекстная валидация (Groups).
  • Понимание архитектуры фильтров Spring Security.
  • Безопасная работа с токенами (JWT + Refresh).
  • Теперь, когда наш бэкенд безопасен и надежен, в следующей части курса мы поговорим о том, как вынести его в продакшн: Docker, Kubernetes и CI/CD пайплайны.

    4. Тестирование и качество кода: JUnit 5, Mockito и интеграционные тесты с Testcontainers

    Тестирование и качество кода: JUnit 5, Mockito и интеграционные тесты с Testcontainers

    В предыдущих частях курса мы создали мощный бэкенд на Java 21 и Spring Boot: настроили виртуальные потоки, оптимизировали работу с базой данных и защитили API с помощью OAuth2. Но как мы можем быть уверены, что всё это работает корректно? И, что ещё важнее, как гарантировать, что новые изменения не сломают старый функционал?

    Отличие Junior-разработчика от Strong Middle заключается в отношении к тестам. Для новичка тесты — это скучная обязанность, которую нужно «пройти», чтобы закрыть задачу. Для опытного инженера тесты — это актив, который позволяет рефакторить код без страха и служит живой документацией к системе.

    Сегодня мы разберем современный стек тестирования: JUnit 5, Mockito и стандарт де-факто для интеграционных тестов — Testcontainers.

    Пирамида тестирования: Теория и Реальность

    Прежде чем писать код, вспомним фундаментальную концепцию — пирамиду тестирования. Она определяет баланс между скоростью выполнения тестов, их стоимостью и степенью изоляции.

    !Классическая пирамида тестирования Майка Кона, показывающая оптимальное соотношение типов тестов в проекте.

  • Unit Tests (Модульные): Проверяют бизнес-логику в изоляции. Никаких баз данных, никаких HTTP-вызовов. Они должны выполняться за миллисекунды.
  • Integration Tests (Интеграционные): Проверяют, как компоненты Spring взаимодействуют друг с другом и с внешними системами (БД, брокеры сообщений).
  • E2E (End-to-End): Проверяют сценарий пользователя целиком, от клика в браузере до изменения в базе.
  • Математически надежность системы можно грубо оценить через вероятность безотказной работы компонентов. Если система состоит из последовательных компонентов, общая надежность рассчитывается как произведение надежностей каждого компонента:

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

    Эта формула показывает, почему модульные тесты так важны: если каждый маленький метод () не протестирован и имеет низкую надежность, общая надежность системы () стремительно падает к нулю. Unit-тесты позволяют нам гарантировать, что каждый «кирпичик» () прочен.

    JUnit 5: Больше, чем просто @Test

    JUnit 5 (Jupiter) — это модульная платформа, которая принесла в Java-мир современные практики тестирования. Рассмотрим инструменты, которые должен знать Middle-разработчик.

    Параметризованные тесты

    Копипаста в тестах — это зло. Если вам нужно проверить метод валидации email с десятью разными адресами, не пишите 10 методов с аннотацией @Test. Используйте @ParameterizedTest.

    Помимо @ValueSource, существуют: * @EnumSource — перебор значений Enum. * @CsvSource — передача набора аргументов. * @MethodSource — самый мощный вариант, позволяющий генерировать сложные объекты (например, Records или DTO) в специальном статическом методе.

    AssertJ: Читаемые проверки

    Стандартные assertEquals из JUnit часто неудобны. В современной разработке принято использовать библиотеку AssertJ. Она предоставляет Fluent API, который читается как английский текст.

    Это делает тесты понятными даже для тех, кто не пишет код (например, QA-инженеров).

    Mockito: Искусство имитации

    В Unit-тестах мы должны изолировать тестируемый класс от его зависимостей. Если мы тестируем UserService, нам не нужно, чтобы он реально ходил в UserRepository и делал запросы к БД. Нам нужно сымитировать поведение репозитория.

    Здесь на сцену выходит Mockito.

    @Mock vs @InjectMocks

    Это самый частый вопрос на собеседованиях и место частых ошибок.

    * @Mock: Создает «пустышку» (mock) класса. По умолчанию все методы мока возвращают null, 0 или false. @InjectMocks: Создает экземпляр реального* класса (который мы тестируем) и пытается внедрить в него созданные @Mock объекты.

    Strict Stubs (Строгие заглушки)

    Mockito в последних версиях стал строгим. Если вы настроили поведение мока (when(...)), но в ходе теста этот метод не был вызван, тест упадет с ошибкой UnnecessaryStubbingException. Это помогает держать тесты чистыми и не накапливать мусорный код настройки.

    Интеграционные тесты: Spring Boot Test

    Unit-тесты прошли, но поднимется ли контекст Spring? Правильно ли мы настроили бины? Работает ли SQL-запрос?

    Для этого используется аннотация @SpringBootTest. Она поднимает полный ApplicationContext.

    Проблема H2 Database

    Долгое время стандартом было использование in-memory базы данных H2 для тестов. Вы писали код под PostgreSQL, а тестировали на H2.

    Почему это плохо для Middle+ разработчика?

  • Разный синтаксис: H2 не поддерживает специфичные фичи Postgres (например, JSONB, специфичные индексы, оконные функции).
  • Ложное чувство безопасности: Тест проходит на H2, но падает в продакшене на Postgres из-за разницы в диалектах.
  • Решение: Тестировать на том же, на чем работаем. Тестировать в Docker.

    Testcontainers: Революция в интеграционном тестировании

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

    Spring Boot 3.1+ и ServiceConnection

    Раньше настройка Testcontainers требовала много ручной работы с DynamicPropertySource для подмены URL базы данных. В Spring Boot 3.1 появился механизм ServiceConnection, который делает магию за нас.

    Вам понадобятся зависимости: * org.springframework.boot:spring-boot-testcontainers * org.testcontainers:postgresql

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

    Что происходит под капотом:

  • JUnit запускает тест.
  • Testcontainers скачивает образ postgres:15-alpine (если нет) и запускает контейнер на случайном свободном порту.
  • @ServiceConnection автоматически настраивает бины DataSource в контексте Spring, направляя их на этот контейнер.
  • Выполняется тест.
  • Контейнер уничтожается (или переиспользуется, если включен режим reuse).
  • Singleton Containers Pattern

    Поднимать контейнер на каждый тестовый класс — долго. PostgreSQL может стартовать 5-10 секунд. Если у вас 50 тестовых классов, вы потеряете 8 минут просто на ожидание старта БД.

    Strong Middle использует паттерн Singleton Container или фичу Reusable Containers. Мы определяем конфигурацию контейнеров в абстрактном базовом классе, от которого наследуются все интеграционные тесты. Контейнер стартует один раз перед всеми тестами и гасится в самом конце.

    Тестируемая архитектура

    Качество кода — это не только наличие тестов, но и возможность их написать. Если ваш код сложно тестировать, значит, у него плохая архитектура.

    Признаки плохого кода (Code Smells) с точки зрения тестирования:

  • Использование new в бизнес-логике: Невозможно подменить объект моком. Решение: Внедрение зависимостей (DI).
  • Статические методы (static): Сложно мокать (требует Mockito-inline). Решение: Оборачивать статику в сервисы-фасады.
  • Божественные классы (God Objects): Если в setUp теста вам нужно замокать 15 зависимостей, ваш класс делает слишком много. Решение: Разделение ответственности (SRP).
  • Заключение

    Переход к уровню Strong Middle требует смены парадигмы: мы перестаем надеяться, что «оно заработает», и начинаем доказывать это кодом.

  • Используйте JUnit 5 и параметризованные тесты для сокращения бойлерплейта.
  • Применяйте Mockito аккуратно, мокая только внешние зависимости, а не внутреннюю логику.
  • Забудьте про H2. Используйте Testcontainers для честных интеграционных тестов в среде, максимально приближенной к боевой.
  • Следите за скоростью тестов. Медленные тесты — это мертвые тесты, их перестают запускать.
  • В следующей, заключительной части курса, мы поговорим о том, как упаковать наше идеально протестированное приложение и доставить его пользователям: Docker, CI/CD и Kubernetes.

    5. Микросервисы и HighLoad: Spring Cloud, Kafka, Docker и Observability

    Микросервисы и HighLoad: Spring Cloud, Kafka, Docker и Observability

    Мы прошли долгий путь: от изучения виртуальных потоков Java 21 до настройки безопасности через OAuth2. Наше приложение работает, данные защищены, тесты проходят. Но что произойдет, если завтра к нам придет миллион пользователей?

    Монолитная архитектура, которую мы строили до сих пор, имеет предел вертикального масштабирования (добавление CPU/RAM). В мире HighLoad правит горизонтальное масштабирование — добавление новых серверов. Здесь мы вступаем на территорию Микросервисов.

    В этой статье мы разберем, как распилить монолит, не потеряв рассудок, как организовать асинхронное общение через Kafka, упаковать всё в Docker и, самое главное, как следить за этим зоопарком сервисов.

    От Монолита к Микросервисам: Теорема CAP

    Переход на микросервисы — это не просто смена кода, это смена философии. Главная проблема распределенных систем описывается Теоремой CAP.

    Она гласит, что в любой распределенной системе данных невозможно одновременно обеспечить все три свойства:

  • Consistency (Согласованность): Во всех узлах в один момент времени данные одинаковы.
  • Availability (Доступность): Каждый запрос получает ответ (успех или ошибка), без гарантии, что данные самые свежие.
  • Partition Tolerance (Устойчивость к разделению): Система продолжает работать, даже если связь между узлами пропала.
  • Математически это можно представить как пересечение множеств, где невозможно найти элемент, принадлежащий всем трем одновременно в случае сетевого сбоя:

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

    !Визуализация теоремы CAP, показывающая невозможность достижения всех трех свойств одновременно в распределенной системе.

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

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

    API Gateway (Spring Cloud Gateway)

    Когда у вас 50 микросервисов, фронтенд не должен знать адреса каждого из них. Ему нужна единая точка входа. Gateway занимается маршрутизацией, балансировкой нагрузки и аутентификацией.

    Service Discovery (Netflix Eureka / Kubernetes)

    Сервисы не должны знать IP-адреса друг друга (они постоянно меняются). Раньше стандартом был Eureka — реестр, где сервисы регистрировались при старте. Сейчас, в эпоху Kubernetes, надобность в Eureka отпадает — K8s сам предоставляет DNS-имена для сервисов (http://order-service).

    Resilience (Resilience4j)

    Что будет, если сервис заказов вызовет сервис платежей, а тот «висит»? Потоки сервиса заказов забьются ожиданием, и вся система встанет. Это называется каскадный сбой.

    Для защиты используется паттерн Circuit Breaker (Предохранитель). Если количество ошибок превышает порог, предохранитель «размыкается», и запросы моментально отбиваются с ошибкой, давая упавшему сервису время на восстановление.

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

    В монолите вызовы методов синхронны. В микросервисах синхронный HTTP (REST) — это зло для записи данных.

    Представьте цепочку вызовов: Order -> Payment -> Warehouse -> Notification.

    Надежность последовательной системы рассчитывается как произведение надежностей компонентов:

    Где — общая надежность системы, — количество сервисов в цепочке, а — надежность -го сервиса. Если у вас 4 сервиса с надежностью 99% (), общая надежность будет . Чем больше сервисов, тем ниже надежность.

    Решение — Асинхронное взаимодействие через брокер сообщений. Стандартом де-факто является Apache Kafka.

    Топики и Партиции

    Kafka — это не просто очередь, это распределенный лог событий.

    * Topic (Топик): Категория сообщений (например, orders-new). Partition (Партиция): Топик делится на части для параллельной обработки. Kafka гарантирует порядок сообщений только внутри одной партиции*. * Consumer Group (Группа потребителей): Механизм масштабирования. Если у вас 10 партиций и вы запустите 10 инстансов микросервиса в одной группе, каждый будет читать свою партицию. Это позволяет обрабатывать миллионы сообщений в секунду.

    !Иллюстрация масштабирования чтения в Kafka через партиции и группы потребителей.

    Docker: Контейнеризация

    Чтобы деплоить микросервисы, нам нужен стандарт упаковки. Docker решает проблему «на моей машине работает».

    Для Java-разработчика важно понимать концепцию слоев (Layers). Docker-образ состоит из слоев, доступных только для чтения. Каждый шаг в Dockerfile создает новый слой.

    Оптимизация Dockerfile для Spring Boot

    Плохой пример:

    При любом изменении кода пересобирается весь JAR, и Docker создает новый тяжелый слой.

    Хороший пример (Multi-stage build + Layered Jar): Spring Boot поддерживает разделение зависимостей и кода. Зависимости меняются редко, код — часто. Мы должны класть зависимости в нижние слои, чтобы Docker использовал кэш.

    Observability: Глаза и уши

    Когда у вас 100 инстансов сервисов, найти причину ошибки по логам в файлах невозможно. Нам нужна Наблюдаемость (Observability). Она стоит на трех китах:

  • Logs (Логи): Что произошло? (ELK Stack, Loki).
  • Metrics (Метрики): Что происходит сейчас? (Prometheus, Grafana).
  • Tracing (Трассировка): Где это произошло? (Zipkin, Jaeger, Tempo).
  • Distributed Tracing

    В Spring Boot 3 проект Spring Cloud Sleuth был заменен на Micrometer Tracing.

    Суть трассировки: каждому входящему HTTP-запросу присваивается уникальный Trace ID. Этот ID передается в заголовках (traceparent) при всех вызовах между микросервисами (через Feign, RestTemplate или Kafka).

    В итоге вы видите в Grafana или Zipkin полную диаграмму Ганта: сколько времени запрос провел в базе, сколько в очереди Kafka, а сколько в стороннем API.

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

    Поздравляю! Мы прошли путь от синтаксиса Java 21 до архитектуры высоконагруженных систем.

  • Мы научились использовать Virtual Threads и Records.
  • Мы освоили Spring Data JPA и оптимизацию SQL.
  • Мы построили безопасный REST API.
  • Мы покрыли код тестами с Testcontainers.
  • И теперь мы знаем, как масштабировать это через Microservices, Kafka и Docker.
  • Вы больше не Junior. У вас есть набор инструментов и знаний Strong Middle разработчика. Дальше — только практика и углубление в нюансы конкретных технологий. Удачи в продакшене!