Java Development: от Core Java до Spring REST, БД, Security, тестов и контейнеризации

Курс охватывает ключевые темы современного Java-разработчика: многопоточность, коллекции и Stream API, а также построение учебного веб-приложения на Spring. В результате вы реализуете RESTful API с доступом к базе данных, безопасностью, тестами и запуском в контейнерах.

1. Core Java: коллекции, generics и Stream API

Core Java: коллекции, generics и Stream API

Эта статья — фундамент для всего последующего курса: в Spring и REST вы постоянно работаете со структурами данных, обобщёнными типами (List<User>, ResponseEntity<List<OrderDto>>), а Stream API часто используется в сервисном слое и при маппинге DTO. Здесь мы разберём, как правильно выбирать коллекции, как generics дают типобезопасность, и как строить пайплайны Stream.

Коллекции в Java: зачем и какие бывают

Коллекции — это стандартные структуры данных из Java Collections Framework, предназначенные для хранения, поиска, обхода и преобразования групп объектов.

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

  • java.util — основные интерфейсы и реализации (например, List, Map, ArrayList, HashMap)
  • java.util.concurrent — потокобезопасные коллекции и примитивы конкурентности
  • !Иерархия основных интерфейсов коллекций и типичных реализаций

    Интерфейсы: Collection и Map

    В Java есть две крупные «семьи» контейнеров:

  • Collection<E> — хранит элементы как последовательность/набор (List, Set, Queue)
  • Map<K, V> — хранит пары ключ–значение (ключи уникальны)
  • Важно: Map не наследуется от Collection, потому что его модель данных другая.

    List, Set, Queue/Deque, Map: назначение и отличия

    List

    List<E> — упорядоченная коллекция, допускает дубликаты, доступ по индексу.

    Основные реализации:

  • ArrayList — быстрый доступ по индексу, хороший выбор по умолчанию
  • LinkedList — удобнее для частых вставок/удалений в середине, но медленнее случайный доступ
  • Пример:

    Set

    Set<E> — множество: дубликаты запрещены.

    Основные реализации:

  • HashSet — быстро, порядок не гарантируется
  • LinkedHashSet — сохраняет порядок вставки
  • TreeSet — хранит элементы отсортированными (нужен Comparable или Comparator)
  • Критически важно: уникальность в Set определяется методами equals() и hashCode().

    Queue и Deque

    Queue<E> — очередь (FIFO), Deque<E> — двусторонняя очередь (можно добавлять/забирать с обоих концов).

    Основные реализации:

  • ArrayDeque — обычно лучший выбор для стека/очереди без конкурентности
  • PriorityQueue — «очередь с приоритетом» (извлекается минимальный/максимальный по компаратору)
  • Пример Deque как стека:

    Map

    Map<K, V> — быстрый доступ к значению по ключу.

    Основные реализации:

  • HashMap — быстрый, порядок не гарантируется
  • LinkedHashMap — сохраняет порядок вставки (часто полезно для предсказуемого вывода)
  • TreeMap — сортировка по ключу
  • Пример:

    Как выбрать коллекцию: практические критерии

    Выбор коллекции — это баланс требований к:

  • наличию дубликатов (List допускает, Set нет)
  • необходимости порядка (вставки или сортировки)
  • типичным операциям (частые get(i)ArrayList, быстрые проверки «содержит ли» → HashSet)
  • необходимости ключ–значение (Map)
  • Ниже — упрощённая шпаргалка.

    | Требование | Обычно выбирают | Почему | |---|---|---| | Нужен индекс и порядок | ArrayList | Быстрый доступ по индексу | | Нельзя дубликаты, важна скорость | HashSet | Быстрые add/contains | | Нельзя дубликаты, нужен порядок вставки | LinkedHashSet | Предсказуемый порядок | | Нужна сортировка элементов | TreeSet | Всегда отсортировано | | Ключ → значение | HashMap | Быстрый доступ по ключу | | Нужен порядок ключей | TreeMap | Сортировка по ключу | | Очередь/стек без потоков | ArrayDeque | Быстро и без лишних накладных |

    equals и hashCode: основа корректной работы Set и Map

    HashSet и HashMap опираются на контракт:

  • если a.equals(b) истинно, то a.hashCode() == b.hashCode() должен совпадать
  • equals() должен быть рефлексивным, симметричным, транзитивным и консистентным
  • Если вы используете свои классы как ключи в Map или элементы Set, обязательно реализуйте equals() и hashCode().

    Пример (класс-ключ):

    Модифицируемые и неизменяемые коллекции

    В современном Java-коде часто полезно явно отделять:

  • изменяемые коллекции (куда вы добавляете/удаляете элементы)
  • неизменяемые представления (которые безопасно отдавать наружу)
  • Практика:

  • List.of(...), Set.of(...), Map.of(...) создают неизменяемые коллекции (попытка изменить вызовет исключение)
  • Collections.unmodifiableList(list) создаёт неизменяемый вид на существующий список (если исходный список изменится, это отразится и в «unmodifiable»)
  • Пример:

    Итераторы и fail-fast поведение

    При обходе коллекций важно понимать разницу:

  • модификация коллекции во время обхода через for-each обычно приводит к ConcurrentModificationException
  • безопасный способ удаления во время итерации — через Iterator.remove()
  • Пример корректного удаления:

    Generics (обобщения): типобезопасность без кастов

    Generics позволяют параметризовать типы. Например, List<String> означает «список строк». Компилятор проверяет корректность типов, а вам почти не нужны приведения ((String) ...).

    Почему generics важны

  • меньше ошибок времени выполнения (ClassCastException)
  • более самодокументируемый код
  • сигнатуры методов становятся точными (особенно в API и библиотеках)
  • Пример без generics (так писать не надо):

    Пример с generics:

    Ограничения типов (bounds)

    Иногда нужно сказать: «тип должен быть числом» или «должен реализовывать интерфейс».

  • T extends Number — верхняя граница: T обязан быть Number или его наследником
  • Пример:

    Wildcards и принцип PECS

    Wildcard — это «неизвестный» параметр типа.

  • ? extends T — коллекция производит значения типа T (можно читать как T, но почти нельзя добавлять)
  • ? super T — коллекция потребляет значения типа T (можно добавлять T, но читать придётся как Object или как super-тип)
  • Запоминают это правилом PECS: Producer Extends, Consumer Super.

    Пример: копирование из источника в приёмник:

    Стирание типов (type erasure)

    Generics в Java работают через стирание типов: в байткоде в основном остаются «сырые» типы, а проверки обеспечиваются компилятором и вставленными приведениями.

    Практические последствия:

  • нельзя создать new T() (у типа T нет информации о конструкторе)
  • нельзя сделать new List<String>[10] (массивы хранят тип времени выполнения, а generics стираются)
  • иногда появляются предупреждения о небезопасных операциях (особенно при взаимодействии со старым кодом без generics)
  • Когда особенно важны generics в этом курсе

  • DTO и модели: List<UserDto>, Map<String, Object>
  • репозитории и сервисы
  • Spring-контроллеры: ResponseEntity<List<UserDto>>
  • тесты: типы в матчерах и ассёртах
  • Stream API: декларативная обработка данных

    Stream API — это способ выразить обработку коллекций как конвейер операций: что сделать, а не как именно бегать по циклам.

    Stream — это не коллекция

    Stream<T>:

  • не хранит данные
  • обычно одноразовый (после терминальной операции использовать нельзя)
  • вычисления ленивые: промежуточные операции не выполняются, пока не вызвана терминальная
  • !Конвейер Stream: источник → промежуточные операции → терминальная операция

    Создание стримов

    Частые варианты:

  • list.stream()
  • Stream.of(a, b, c)
  • Arrays.stream(array)
  • IntStream.range(0, n) (примитивные стримы без автобоксинга)
  • Промежуточные и терминальные операции

    Промежуточные (возвращают новый Stream):

  • filter, map, flatMap, distinct, sorted, peek, limit, skip
  • Терминальные (завершают стрим и дают результат):

  • collect, toList, reduce, forEach, count, min/max, anyMatch/allMatch/noneMatch, findFirst/findAny
  • Пример:

    map vs flatMap

  • map преобразует один элемент в один элемент
  • flatMap разворачивает один элемент в поток элементов и «сплющивает» результат
  • Пример: получить все теги из списка постов:

    Collectors: группировка и агрегации

    Collectors позволяют собирать данные в структуры.

    Пример: группировка пользователей по роли:

    Пример: собрать в Map по id:

    Важно: Collectors.toMap выбросит исключение при дубликатах ключей. Если дубликаты возможны, задавайте функцию слияния.

    Optional как результат поиска

    Optional<T> часто появляется в:

  • findFirst, findAny, min, max
  • репозиториях (например, в Spring Data)
  • Пример:

    Параллельные стримы: осторожно

    parallelStream() может ускорить вычисления, но не является «волшебной кнопкой».

    Типичные риски:

  • накладные расходы на распараллеливание выше выигрыша на небольших объёмах
  • нельзя безопасно писать в общий изменяемый объект внутри forEach
  • операции должны быть без побочных эффектов, иначе получите гонки данных
  • Если вы не уверены — используйте обычный stream() и измеряйте.

    Связь с дальнейшими темами курса

    После этой статьи будет проще:

  • проектировать DTO и сервисы с корректными типами (Generics)
  • выбирать структуры данных для кешей, индексов, очередей задач (Map/Set/Queue)
  • писать чистые преобразования данных в сервисном слое (Stream API)
  • понимать, почему в многопоточности появляются ConcurrentModificationException и почему нужны конкурентные коллекции (к ним мы вернёмся в теме про многопоточность)
  • Полезные источники

  • Java Collections Framework (Oracle)
  • Generics (Oracle)
  • Stream API (Oracle)
  • Class HashMap (Java SE)
  • 2. Многопоточность и конкурентность: синхронизация, Executors, CompletableFuture

    Многопоточность и конкурентность: синхронизация, Executors, CompletableFuture

    В предыдущей статье мы разобрали коллекции, generics и Stream API. В реальных приложениях (особенно web-сервисах на Spring) данные почти никогда не обрабатываются строго последовательно: запросы приходят параллельно, фоновые задачи выполняются одновременно, кэш и соединения с БД используются многими потоками. Поэтому следующий фундаментальный блок — конкурентность: как безопасно разделять данные между потоками и как организовать асинхронную работу без хаоса.

    Цели этой статьи:

  • понять, какие проблемы возникают при параллельном доступе к данным
  • научиться защищать общий изменяемый state (синхронизация, volatile, атомики, блокировки)
  • использовать высокоуровневые инструменты: ExecutorService, Future, CompletableFuture
  • связать всё это с практикой Spring-приложений
  • Конкурентность: что может пойти не так

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

    Ключевые проблемы:

  • Race condition — результат зависит от того, как именно потоки «перемешались» во времени.
  • Visibility — один поток изменил значение, но другой поток «не видит» изменения вовремя.
  • Atomicity — операция выглядит как одна, но на самом деле состоит из нескольких шагов и может быть «разорвана» другим потоком.
  • Deadlock — потоки навсегда ждут друг друга из-за неправильного порядка захвата блокировок.
  • Мини-пример гонки данных:

    value++ — это несколько шагов (прочитать, увеличить, записать). Если два потока делают это одновременно, часть инкрементов «теряется».

    !Иллюстрация, почему value++ в нескольких потоках приводит к потерянным обновлениям

    Java Memory Model и правило happens-before

    В Java важно понимать: потоки не обязаны мгновенно видеть изменения друг друга. Компилятор и процессор имеют право переупорядочивать инструкции, а значения могут кэшироваться.

    Чтобы программист мог рассуждать о корректности, в Java существует Java Memory Model (JMM) и отношение happens-before.

    Практическая интерпретация happens-before:

  • если действие A happens-before действия B, то эффекты A (записи в память) гарантированно будут видны для B
  • синхронизация (synchronized, Lock), volatile, завершение потока, публикация через потокобезопасные структуры создают такие гарантии
  • Ссылки для углубления:

  • Java Language Specification, JMM (раздел 17)
  • Низкоуровневые инструменты синхронизации

    synchronized: монитор и взаимное исключение

    सynchronized решает две задачи одновременно:

  • взаимное исключение — в критическую секцию заходит только один поток
  • видимость — вход/выход из монитора формируют нужные гарантии видимости (happens-before)
  • Пример:

    На практике synchronized отлично подходит для коротких критических секций.

    wait/notify/notifyAll: координация потоков

    Методы wait, notify, notifyAll используются для ожидания условия на объектном мониторе.

    Правила, без которых код становится некорректным:

  • wait вызывается только внутри synchronized по тому же объекту
  • ожидание условия всегда делается в цикле while, а не через if (возможны spurious wakeups)
  • Пример шаблона:

    На практике в прикладном коде чаще используют готовые высокоуровневые примитивы (BlockingQueue, CountDownLatch), а wait/notify оставляют для редких случаев.

    volatile: видимость без взаимного исключения

    volatile гарантирует:

  • запись в volatile поле становится видна другим потокам
  • чтения/записи не будут опасно переупорядочены вокруг volatile-операций
  • Но volatile не делает составные операции атомарными.

    Корректный пример использования — флаг остановки:

    Некорректный пример — счётчик на volatile int с ++ (гонка останется).

    Атомарные классы: AtomicInteger и CAS

    Пакет java.util.concurrent.atomic даёт атомарные операции без synchronized.

    Пример:

    Под капотом часто используется CAS (compare-and-set): обновить значение только если оно равно ожидаемому.

    Явные блокировки: Lock и ReadWriteLock

    ReentrantLock полезен, когда нужно то, чего нет у synchronized:

  • tryLock() (не ждать бесконечно)
  • ожидание с таймаутом
  • более гибкие условия через Condition
  • Пример:

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

    ThreadLocal: данные, привязанные к потоку

    ThreadLocal<T> — способ хранить значения «для каждого потока отдельно».

    Важно помнить про риски:

  • в пулах потоков поток живёт долго, и забытый ThreadLocal может приводить к утечкам и «протеканию» контекста
  • если используете ThreadLocal, почти всегда нужен remove() в finally
  • Конкурентные коллекции и связь с предыдущей темой

    В статье про коллекции мы говорили, что обычные ArrayList и HashMap не потокобезопасны. В многопоточном коде вместо ручной синхронизации часто лучше выбрать готовые конкурентные структуры из java.util.concurrent.

    Частые варианты:

  • ConcurrentHashMap — потокобезопасная Map для конкурентного доступа
  • CopyOnWriteArrayList — хороша для «много читаем, мало пишем» (цена записи высокая)
  • BlockingQueue (ArrayBlockingQueue, LinkedBlockingQueue) — основа схем producer-consumer
  • Пример безопасного обновления значения в ConcurrentHashMap:

    Это лучше, чем get + put вручную, потому что merge делает обновление корректно при конкурентном доступе.

    Справочник:

  • Package java.util.concurrent (Oracle)
  • Class ConcurrentHashMap (Oracle)
  • Executors: управление потоками через пулы

    Создавать new Thread(...) на каждую задачу в серверном приложении почти всегда плохая идея:

  • создание потоков дорого
  • слишком много потоков приводит к переключениям контекста и деградации
  • сложно управлять жизненным циклом и ошибками
  • Executor и ExecutorService решают это: вы отправляете задачи, а пул потоков выполняет их.

    Runnable и Callable, Future

  • Runnable не возвращает результат
  • Callable<V> возвращает результат и может бросать checked-исключения
  • Future<V> — «обещание» результата в будущем
  • Пример:

    Минус Future: нет удобной композиции (сложно «склеивать» несколько async-операций без блокировок). Для этого нужен CompletableFuture.

    Типы пулов и когда их применять

  • newFixedThreadPool(n) — фиксированное число потоков; хорошо для контролируемой нагрузки
  • newCachedThreadPool() — потоки создаются по мере необходимости; риск создать слишком много потоков
  • newSingleThreadExecutor() — последовательная обработка задач в одном потоке
  • newScheduledThreadPool(n) — задачи по расписанию (schedule, scheduleAtFixedRate)
  • ForkJoinPool — эффективен для задач, которые рекурсивно делятся на подзадачи
  • Важно: фабрики Executors удобны для обучения, но в продакшене часто создают ThreadPoolExecutor явно, чтобы контролировать очередь, лимиты и обработку отказов.

    Завершение пула

    Общий шаблон:

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

    Справочник:

  • Interface ExecutorService (Oracle)
  • Class ThreadPoolExecutor (Oracle)
  • CompletableFuture: асинхронные пайплайны без блокировок

    CompletableFuture<T> — это:

  • Future, которое можно вручную завершать (complete)
  • основа для построения асинхронных конвейеров (thenApply, thenCompose, thenCombine)
  • удобная обработка ошибок (exceptionally, handle)
  • Запуск задач и выбор пула

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

    thenApply vs thenCompose

  • thenApply — преобразует результат: T -> U
  • thenCompose — «плоская композиция», когда шаг возвращает ещё один CompletableFuture<U>
  • Пример цепочки запросов (условно: найти пользователя, затем загрузить его заказы):

    Если бы использовали thenApply, получили бы вложенный тип CompletableFuture<CompletableFuture<List<Order>>>.

    Комбинирование независимых задач

    thenCombine объединяет два независимых результата.

    Для набора задач используют:

  • CompletableFuture.allOf(...) — дождаться всех
  • CompletableFuture.anyOf(...) — дождаться первой завершившейся
  • Обработка ошибок

    exceptionally позволяет вернуть запасное значение:

    handle получает и результат, и исключение (одно из них будет null):

    Справочник:

  • Class CompletableFuture (Oracle)
  • Практика для Spring и REST-приложений

    Ключевая мысль: Spring-приложение почти всегда уже работает в многопоточном окружении, потому что сервер (например, Tomcat) обрабатывает запросы параллельно.

    Практические правила:

  • сервисы и контроллеры в Spring по умолчанию singleton, значит они должны быть потокобезопасными
  • не храните request-данные в полях singleton-бинов (используйте параметры методов, DTO, request scope или безопасность контекста)
  • для фоновых задач используйте управляемые пулы (например, через Spring TaskExecutor или @Async), а не new Thread()
  • Если вы используете CompletableFuture/пулы в бизнес-логике:

  • отделяйте CPU-bound и I/O-bound задачи разными пулами
  • не блокируйте поток без необходимости (Future.get() внутри web-потока часто ухудшает latency)
  • следите за контекстом (например, security context) при переходе между потоками: он не переносится автоматически
  • Типичные ошибки и рабочие привычки

    Ошибки, которые чаще всего встречаются у начинающих:

  • синхронизация «на всём подряд» (потеря параллелизма и риск дедлоков)
  • общий изменяемый ArrayList/HashMap в нескольких потоках
  • блокировка внутри parallelStream() или внутри задач общего commonPool
  • ожидание результата через .get() там, где можно построить цепочку CompletableFuture
  • забытый shutdown() у ExecutorService
  • Полезные привычки:

  • по умолчанию избегайте общего изменяемого состояния; предпочитайте неизменяемые объекты и передачу данных через параметры
  • если нужен shared state, сначала попробуйте конкурентные коллекции и атомики, а уже потом ручную синхронизацию
  • держите критические секции маленькими
  • явно выбирайте и ограничивайте пулы потоков
  • Что дальше по курсу

    После этой темы будет проще:

  • писать сервисный слой Spring так, чтобы он был потокобезопасным
  • корректно использовать кэши, очереди задач и конкурентные коллекции
  • строить асинхронные интеграции (вызовы внешних сервисов) и фоновые задачи
  • уверенно подходить к тестированию многопоточного кода и к контейнеризации, где ограничения ресурсов делают управление пулами особенно важным
  • 3. Проектирование и качество кода: SOLID, паттерны, обработка ошибок

    Проектирование и качество кода: SOLID, паттерны, обработка ошибок

    После тем про коллекции, generics, Stream API и многопоточность у вас уже есть инструменты, чтобы писать работающий код. Но в промышленной разработке важен следующий уровень: писать код, который легко расширять, тестировать и безопасно эксплуатировать в продакшене.

    В этом курсе дальше будет Spring REST, БД, Security, тесты и контейнеризация. Все эти темы резко усиливают требования к качеству архитектуры: ошибки должны быть предсказуемыми, слои отделены, зависимости управляемы, а решения повторяемы. Поэтому здесь разберём три опоры:

  • принципы проектирования (SOLID и связанные идеи)
  • паттерны как готовые схемы решения типовых задач
  • обработка ошибок: исключения, границы слоёв, ошибки REST API и логирование
  • !Слои приложения и где должны обрабатываться и трансформироваться ошибки

    Качество кода: критерии, которые реально важны

    Качество кода полезно оценивать через свойства, которые влияют на скорость разработки и стабильность:

  • поддерживаемость: насколько легко читать и менять код
  • расширяемость: насколько просто добавить фичу без переписывания старого
  • тестируемость: можно ли изолированно проверить поведение (в том числе без БД и сети)
  • надёжность: предсказуемое поведение при ошибках и нагрузке
  • согласованность: единые подходы к слоям, исключениям, логированию
  • Практическая мысль: качество кода в серверной разработке — это в первую очередь управление сложностью.

    Базовые понятия проектирования

    Связность и связанность

    Два термина звучат похоже, но означают противоположные вещи:

  • высокая связность (cohesion): класс делает одну понятную вещь
  • низкая связанность (coupling): классы мало зависят друг от друга
  • Пример плохой связанности: бизнес-логика прямо вызывает JDBC, форматирует JSON и пишет в лог в одном методе. Такой код сложно тестировать и менять.

    Инварианты и границы ответственности

    Инвариант — правило, которое должно оставаться истинным всегда.

    Примеры инвариантов домена:

  • заказ не может иметь отрицательную сумму
  • пользователь с ролью ADMIN должен существовать в системе
  • Хороший дизайн фиксирует инварианты в одном месте (обычно в домене/сервисе), а не размазывает проверки по контроллерам, репозиториям и утилитам.

    SOLID: принципы, которые делают код живым

    SOLID — набор принципов, которые снижают связанность и повышают расширяемость.

    | Принцип | Идея простыми словами | Типичный симптом нарушения | Что делать на практике | |---|---|---|---| | SRP (Single Responsibility) | у класса одна причина для изменения | класс разрастается в “God Object” | выделять сервисы, мапперы, валидаторы | | OCP (Open/Closed) | расширяем поведение без правок старого кода | switch по типам/ролям везде | полиморфизм, стратегии, регистрация обработчиков | | LSP (Liskov Substitution) | наследника можно подставить вместо родителя без сюрпризов | переопределение ломает контракт | наследование только при сохранении семантики | | ISP (Interface Segregation) | лучше много маленьких интерфейсов | “толстый” интерфейс, половина методов не нужна | делить интерфейсы по сценариям | | DIP (Dependency Inversion) | зависим от абстракций, а не от деталей | сервис создаёт new Jdbc... внутри | внедрение зависимостей, фабрики, порты/адаптеры |

    SRP на примере: разделение логики

    Плохой пример: один класс делает всё.

    Лучше: разделить обязанности и сделать зависимости явными.

    Здесь контроллер (позже в Spring) будет отвечать за HTTP и DTO, а сервис — за бизнес-логику.

    OCP: как уйти от бесконечных if/else

    Если у вас поведение зависит от типа или сценария, часто возникает цепочка условий:

    Это плохо масштабируется: добавление нового типа приводит к правкам существующего кода.

    Типичное решение: стратегия.

    Заметьте связь с темой коллекций: Map здесь используется как индекс по ключу, а не как “мешок данных”.

    LSP: наследование должно сохранять контракт

    Нарушение LSP часто проявляется так:

  • базовый класс обещает одно поведение
  • наследник “подкручивает” и ломает ожидания
  • Примерный антипример: Square extends Rectangle, где изменение одной стороны меняет обе. Это ломает ожидания к Rectangle.

    Практика:

  • предпочитайте композицию наследованию
  • наследуйте только если можете честно соблюдать контракт базового типа
  • ISP: интерфейсы по сценариям

    Если интерфейс заставляет зависимые классы реализовывать ненужные методы, его стоит разделить.

    Вместо:

    Лучше разделить на несколько интерфейсов по ролям использования.

    DIP: инверсия зависимостей и тестируемость

    DIP особенно важен для будущих тем курса:

  • в тестах вы подменяете БД моками/фейками
  • в Spring зависимости внедряются контейнером
  • при контейнеризации конфигурация меняется, но код остаётся прежним
  • Если сервис зависит от интерфейса, а не от конкретной реализации, тест становится простым.

    Паттерны: когда и зачем

    Паттерн — это не “обязательная архитектурная мода”, а имя для повторяемого решения. Важно знать когда применять и когда остановиться, чтобы не превратить проект в музей абстракций.

    Ниже — набор паттернов, которые чаще всего встречаются в Java и особенно в Spring.

    Порождающие паттерны

  • Factory Method: создание объектов через фабричный метод, чтобы скрыть детали.
  • Builder: удобное создание сложных объектов.
  • Singleton: единственный экземпляр (в Spring большинство бинов по умолчанию singleton).
  • Пример Builder (часто встречается через Lombok, но важно понимать идею):

    Структурные паттерны

  • Adapter: приводим чужой API к своему интерфейсу.
  • Decorator: добавляем поведение, не меняя класс.
  • Facade: упрощаем сложную подсистему единым входом.
  • Связь со Spring: многие механизмы Spring — это комбинации адаптеров и декораторов (например, фильтры безопасности, прокси транзакций).

    Поведенческие паттерны

  • Strategy: выбираем алгоритм по контексту.
  • Template Method: каркас алгоритма в базовом классе, детали в наследниках.
  • Observer: уведомления подписчиков о событиях.
  • Command: оформляем действие как объект.
  • Практика: Strategy и Command очень полезны для бизнес-логики, которую нужно расширять без switch.

    Антипаттерны: что убивает поддерживаемость

  • God Object: один класс знает и делает всё.
  • Spaghetti code: логика течёт без структуры, методы вызывают всё подряд.
  • Primitive obsession: всё строками и числами без доменных типов (например, String status вместо enum или value object).
  • Exception swallowing: поймали Exception и ничего не сделали.
  • Shared mutable state: общий изменяемый объект для многих потоков без защиты (связь с прошлой статьёй про конкурентность).
  • Обработка ошибок: как сделать поведение предсказуемым

    Ошибки в серверной разработке неизбежны:

  • неверный ввод пользователя
  • недоступна БД или внешний сервис
  • гонки данных и таймауты
  • ошибки конфигурации
  • Правильная обработка ошибок — это часть дизайна.

    Что считать ошибкой

    Полезно делить ошибки на классы:

  • ошибка клиента: неверные данные, нарушены правила (обычно это будет 4xx в REST)
  • ошибка системы: сеть, БД, баг, таймаут (обычно 5xx)
  • конфликт/состояние: корректный запрос, но текущее состояние не позволяет выполнить (часто 409)
  • Checked vs unchecked исключения

    В Java есть два основных вида исключений:

  • checked (наследники Exception, кроме RuntimeException): компилятор требует обработку
  • unchecked (наследники RuntimeException): компилятор не требует обработку
  • Практические рекомендации:

  • checked-исключения подходят для ожидаемых ошибок инфраструктуры в низкоуровневом коде, где вы реально можете восстановиться
  • в бизнес-слое чаще используют unchecked-исключения для сигнализации о нарушении инвариантов и некорректных сценариях
  • Официальная справка: The Java Tutorials: Exceptions.

    Не ловите слишком широко

    Антипаттерн:

    Проблемы:

  • теряется причина ошибки
  • выше по стеку невозможно корректно принять решение
  • в REST это часто превращается в “500 без объяснения” или, хуже, в “200 с null”
  • Лучше:

  • ловить конкретные типы
  • добавлять контекст
  • либо пробрасывать дальше, либо переводить в доменное исключение
  • Трансляция исключений между слоями

    Хорошее правило: каждый слой знает свой язык.

  • репозиторий знает про БД и её ошибки
  • сервис знает про доменные правила
  • контроллер знает про HTTP
  • Значит, исключения часто нужно переводить на границе слоя.

    Пример идеи:

    Optional здесь уместен, потому что “может не быть” — нормальная ситуация для поиска.

    Свои исключения: делайте их смысловыми

    Плохо:

  • throw new RuntimeException("error")
  • Лучше:

  • NotFoundException
  • ValidationException
  • ConflictException
  • Это сильно упростит следующий блок курса про REST: вы сможете централизованно маппить типы ошибок на HTTP-ответы.

    Сообщение ошибки и причина

    У исключения часто есть:

  • сообщение для разработчика (контекст)
  • исходная причина (cause) при оборачивании
  • Шаблон:

    Так вы сохраняете цепочку причин и упрощаете диагностику.

    try-with-resources: корректное закрытие ресурсов

    Если объект реализует AutoCloseable, используйте try-with-resources.

    Это особенно важно для работы с потоками ввода-вывода и позже для работы с JDBC.

    Логирование: что логировать и на каком уровне

    Логи — это инструмент эксплуатации. Плохое логирование не помогает найти проблему и увеличивает шум.

    Полезные правила:

  • не логируйте одно и то же исключение много раз на разных уровнях без добавления контекста
  • если вы пробрасываете исключение вверх, часто достаточно залогировать его один раз на границе (например, в обработчике ошибок контроллера)
  • не логируйте секреты: пароли, токены, приватные ключи (это напрямую связано с темой Security)
  • Ошибки и многопоточность

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

  • исключения из задач в ExecutorService часто прячутся в Future.get() как ExecutionException
  • в CompletableFuture ошибки распространяются по цепочке и обрабатываются через exceptionally или handle
  • Пример:

    Если не обработать ошибку, вы получите “тихий провал” или неожиданный статус в будущем REST-слое.

    Как это свяжется со Spring REST дальше

    В следующих модулях курса вы будете строить REST API. Этот дизайн напрямую ляжет на практику:

  • контроллеры будут тонкими: только HTTP, DTO и вызов сервиса
  • сервисы будут держать бизнес-инварианты и выбрасывать доменные исключения
  • централизованный обработчик ошибок (в Spring обычно через механизм advice) будет превращать исключения в единый формат HTTP-ошибок
  • Чтобы подготовиться, держите в голове два принципа:

  • доменная логика не должна знать про HTTP
  • инфраструктура (БД, сеть) не должна “просачиваться” в доменные правила
  • Мини-чеклист качества перед коммитом

  • класс имеет одну понятную ответственность (SRP)
  • зависимости направлены “внутрь” к абстракциям (DIP)
  • нет catch (Exception) без сильной причины
  • исключения либо обработаны, либо переведены в смысловой тип
  • нет общего изменяемого состояния без защиты (связь с конкурентностью)
  • структура слоёв читается без “прыжков” через уровни
  • Полезные источники

  • SOLID (Wikipedia)
  • Design Patterns: Elements of Reusable Object-Oriented Software (GoF) — Wikipedia
  • The Java Tutorials: Exceptions
  • Effective Java (Joshua Bloch) — Wikipedia
  • Refactoring (Martin Fowler) — сайт книги
  • 4. Spring Boot: архитектура приложения, DI, конфигурация и профили

    Spring Boot: архитектура приложения, DI, конфигурация и профили

    Spring Boot — это надстройка над Spring Framework, которая ускоряет создание приложений за счёт автоконфигурации, удобной системы внешней конфигурации и готовых интеграций. В этом курсе дальше будет REST API, доступ к БД, Security, тестирование и контейнеризация. Чтобы эти темы не превратились в набор «магических аннотаций», важно понять базовую механику: архитектуру слоёв, DI (dependency injection), жизненный цикл бинов, конфигурацию и профили.

    Связь с предыдущими темами:

  • Из SOLID нам особенно нужны SRP и DIP: контроллеры тонкие, бизнес-логика в сервисах, зависимости выражены через интерфейсы.
  • Из темы про многопоточность важно помнить, что Spring-бины часто singleton и вызываются параллельно, значит общий изменяемый state в полях бина — риск.
  • Что такое Spring Boot и чем он отличается от Spring

    Spring Framework даёт базовые механизмы:

  • IoC/DI контейнер (создание объектов, внедрение зависимостей)
  • AOP (прокси для транзакций, безопасности и прочего)
  • инфраструктуру для web, data access, тестирования
  • Spring Boot добавляет:

  • starter dependencies (подбор совместимых зависимостей)
  • auto-configuration (автоматическая настройка на основе classpath и свойств)
  • единый подход к externalized configuration (properties/yaml, env vars, command line)
  • удобный запуск приложений
  • Официальные источники:

  • Spring Boot Reference Documentation
  • Spring Framework Reference Documentation
  • Архитектура приложения: слои и пакеты

    Цель архитектуры в учебном проекте курса: сделать код расширяемым (новые фичи), тестируемым (без поднятия всей системы) и предсказуемым в эксплуатации.

    Типовая схема слоёв для Spring Boot REST приложения:

  • Controller layer: HTTP, DTO, коды статусов
  • Service layer: бизнес-правила, транзакционные границы, оркестрация
  • Repository layer: доступ к данным (позже: Spring Data/JPA)
  • Domain/model: сущности, value objects, инварианты
  • Configuration: конфигурационные классы, свойства, бины инфраструктуры
  • !Схема слоёв Spring Boot приложения и кросс-срезов

    Практическая рекомендация по пакетам (пример):

  • com.example.app
  • com.example.app.web (controllers, DTO, exception handlers)
  • com.example.app.service
  • com.example.app.repository
  • com.example.app.domain
  • com.example.app.config
  • Важно:

  • Контроллеры не должны содержать бизнес-логику.
  • Сервис не должен знать про HTTP (не возвращать ResponseEntity, не работать с HttpServletRequest).
  • Репозиторий не должен «протаскивать» детали БД наверх.
  • Application entry point и component scanning

    Минимальная точка входа обычно выглядит так:

    @SpringBootApplication объединяет три идеи:

  • @Configuration: класс может объявлять бины
  • @EnableAutoConfiguration: включает автоконфигурацию
  • @ComponentScan: сканирует компоненты в текущем пакете и подпакетах
  • Вывод для структуры проекта:

  • главный класс лучше держать в корневом пакете приложения, чтобы @ComponentScan увидел все подпакеты.
  • DI (Dependency Injection): как Spring создаёт и связывает объекты

    DI — это подход, при котором объект не создаёт свои зависимости сам (через new), а получает их извне. В Spring роль «внешнего» выполняет контейнер.

    Почему это критично для курса:

  • проще писать тесты (можно подменять зависимости)
  • проще менять реализацию (DIP)
  • конфигурация отделяется от логики
  • Bean, ApplicationContext и жизненный цикл

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

    ApplicationContext — контейнер, который:

  • создаёт бины
  • внедряет зависимости
  • применяет post-processors (в том числе создаёт прокси)
  • управляет жизненным циклом
  • Упрощённо жизненный цикл бина:

  • создание экземпляра
  • внедрение зависимостей
  • вызовы lifecycle callbacks (например, @PostConstruct)
  • бин готов к использованию
  • при остановке приложения контейнер вызывает destroy callbacks
  • Способы регистрации бинов

    Самые частые варианты:

  • @Component, @Service, @Repository, @Controller (сканирование классов)
  • @Bean метод внутри @Configuration класса (ручное объявление)
  • Пример через аннотации:

    Пример через @Bean:

    Constructor injection как базовый стандарт

    Основной стиль внедрения зависимостей в современном Spring — через конструктор:

    Почему это лучше:

  • зависимость обязательна и явно выражена
  • объект легче тестировать
  • проще сделать поля final
  • Ссылка:

  • Spring Framework: Dependency Injection
  • Singleton scope и потокобезопасность

    По умолчанию большинство бинов имеют scope singleton: один экземпляр на ApplicationContext.

    Это означает:

  • один и тот же сервис используется всеми запросами
  • хранить request-specific данные в полях сервиса опасно
  • Практическое правило:

  • сервисы должны быть либо stateless, либо защищать общий state (что обычно сложнее и реже нужно)
  • Это напрямую связано с темой конкурентности: приложение обрабатывает HTTP запросы параллельно.

    Несколько реализаций и выбор конкретной

    Если есть два бина одного интерфейса, Spring не сможет выбрать автоматически.

    Варианты решения:

  • @Primary на реализации по умолчанию
  • @Qualifier("name") при внедрении
  • Пример:

    Конфигурация в Spring Boot: externalized configuration

    Spring Boot придерживается принципа: приложение должно настраиваться без перекомпиляции.

    Источники конфигурации (частые):

  • application.properties или application.yml
  • переменные окружения
  • аргументы командной строки
  • Ссылки:

  • Spring Boot: Externalized Configuration
  • application.properties vs application.yml

    Оба варианта эквивалентны по смыслу, выбор чаще вкусовой и командный.

    Пример application.yml:

    Пример application.properties:

    @Value и почему чаще лучше @ConfigurationProperties

    @Value подходит для единичных параметров:

    Но для группы настроек обычно лучше @ConfigurationProperties, потому что:

  • настройки типобезопасны
  • проще валидировать
  • удобно тестировать
  • Пример:

    Чтобы Spring начал создавать такой бин, его нужно зарегистрировать. Варианты:

  • @EnableConfigurationProperties(PricingProperties.class) в @Configuration
  • или @ConfigurationPropertiesScan в главном классе
  • Пример через scan:

    Ссылка:

  • Spring Boot: Type-safe Configuration Properties
  • Relaxed binding: как маппятся имена свойств

    Spring Boot умеет «связывать» разные стили имён к одному полю:

  • discountPercent
  • discount-percent
  • DISCOUNT_PERCENT
  • Это особенно важно для Docker и Kubernetes, где конфигурация часто передаётся через env vars.

    Профили (profiles): dev, test, prod без условной логики в коде

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

    Типовые профили:

  • dev: удобные настройки для локальной разработки
  • test: настройки для интеграционных тестов
  • prod: безопасные настройки для продакшена
  • Файлы конфигурации для профилей

    Spring Boot поддерживает профильные файлы:

  • application-dev.yml
  • application-test.yml
  • application-prod.yml
  • Пример: в application-dev.yml включим подробные логи:

    Активация профиля

    Способы задать активный профиль:

  • переменная окружения SPRING_PROFILES_ACTIVE=dev
  • аргумент командной строки --spring.profiles.active=dev
  • иногда через базовый application.yml (обычно для локальных сценариев)
  • Ссылка:

  • Spring Boot: Profiles
  • @Profile на бинах

    Иногда удобно менять реализацию зависимости по профилю.

    Пример: реальный отправитель писем в prod и заглушка в dev.

    Так вы избегаете if (env == ...) в бизнес-коде и придерживаетесь SRP.

    Profile-specific beans и тестирование

    Профили особенно полезны для тестов:

  • можно поднять приложение с test профилем
  • можно использовать in-memory базы (позже в курсе) или тестовые настройки
  • В тестах (JUnit) профиль обычно задают аннотациями Spring Boot Test, но это будет подробно в модуле про тестирование.

    Автоконфигурация: откуда берётся «магия»

    Автоконфигурация — это набор условий, по которым Spring Boot создаёт бины автоматически.

    Типовые условия:

  • наличие класса в classpath (например, web starter)
  • наличие свойства в конфигурации
  • отсутствие вашего собственного бина того же типа
  • Практическая мысль:

  • Boot старается «сделать разумно по умолчанию», но вы всегда можете переопределить поведение, объявив свой бин.
  • Чтобы понимать, что именно включилось, полезны:

  • actuator endpoints (позже)
  • логирование автоконфигурации (позже)
  • Ссылка:

  • Spring Boot: Auto-configuration
  • Мини-чеклист перед тем, как писать REST API

  • Пакеты разложены по слоям (web/service/repository/domain/config).
  • Зависимости внедряются через конструктор.
  • Бины не хранят request-state в полях.
  • Настройки вынесены в application.yml и типизированы через @ConfigurationProperties.
  • Для разных окружений используются профили вместо условной логики в коде.
  • Что дальше по курсу

    Дальше мы начнём строить REST API на Spring MVC:

  • контроллеры, маршруты, DTO
  • единый формат ошибок и обработчики исключений
  • подключение БД и транзакции
  • Если архитектура и конфигурация заложены правильно уже сейчас, следующие модули будут добавляться как слои, а не как хаотичные правки существующего кода.

    5. RESTful API: контроллеры, DTO, валидация, документация и обработка исключений

    RESTful API: контроллеры, DTO, валидация, документация и обработка исключений

    В прошлой статье мы разобрали базовую архитектуру Spring Boot приложения, DI, конфигурацию и профили. Теперь мы добавляем внешний API: HTTP-контроллеры, входные и выходные модели (DTO), валидацию, единый формат ошибок и документацию. Это слой, который связывает ваш домен и сервисы с миром клиентов: фронтендом, мобильными приложениями и другими сервисами.

    Ключевая цель темы: научиться строить REST API так, чтобы он был предсказуемым, удобным для клиентов и безопасным для эволюции.

    !Поток запроса и обработка ошибок по слоям

    Что значит RESTful на практике

    REST как стиль не сводится к аннотациям Spring. Для прикладной разработки достаточно набора правил, которые делают API понятным и совместимым с HTTP.

    Основные практики:

  • Ресурсы выражаются существительными во множественном числе: GET /users, GET /orders/{id}
  • HTTP-методы отражают операцию: GET чтение, POST создание, PUT полная замена, PATCH частичное изменение, DELETE удаление
  • HTTP-статусы отражают исход: 200, 201, 204, 400, 401, 403, 404, 409, 422, 500
  • Тело запроса и ответа содержит данные, а не “команды”
  • Ошибки имеют единый формат
  • Полезная справка по семантике в Spring MVC: Документация Spring Framework по Spring MVC.

    Роль контроллера в архитектуре

    Контроллер в нашем курсе отвечает за HTTP-аспекты и не отвечает за бизнес-логику.

    Контроллер делает:

  • Маршрутизацию: путь, метод, параметры
  • Десериализацию входных данных в DTO
  • Валидацию входных DTO
  • Вызов сервиса
  • Сериализацию результата сервиса в выходной DTO
  • Установку HTTP-кодов и заголовков
  • Контроллер не делает:

  • Принятие бизнес-решений и вычисления
  • Работу с БД
  • Транзакции “вручную”
  • Логирование секретов и персональных данных
  • Это продолжение идей SRP и DIP из темы про качество кода.

    Spring MVC контроллеры: базовые аннотации и стиль

    Чаще всего в REST API используется @RestController.

  • @RestController эквивалентен @Controller + @ResponseBody
  • @RequestMapping задаёт общий префикс
  • @GetMapping, @PostMapping, @PutMapping, @PatchMapping, @DeleteMapping задают HTTP-метод и путь
  • Пример каркаса контроллера:

    Практические замечания:

  • Для POST создания часто возвращают 201 Created и заголовок Location
  • Для простых GET можно возвращать DTO напрямую, но ResponseEntity полезен, когда нужно управлять статусом и заголовками
  • DTO: зачем нужны и как их проектировать

    DTO (Data Transfer Object) нужны, чтобы отделить внешний контракт API от внутренней модели.

    Почему нельзя “отдавать сущность JPA наружу” (и почему мы вообще говорим об этом заранее, до модуля про БД):

  • Модель БД и модель API меняются с разной скоростью
  • Можно случайно раскрыть лишние поля
  • Внутренние связи и ленивые загрузки плохо сочетаются с сериализацией
  • Сущности часто содержат инварианты и служебные поля, которые не должны быть частью контракта
  • Рекомендуемая структура DTO

    Обычно полезно иметь отдельные DTO для разных направлений:

  • Входные: CreateUserRequestDto, UpdateUserRequestDto
  • Выходные: UserResponseDto
  • Ошибки: ErrorResponse или формат Problem Details
  • В современном Java удобно использовать record:

    Маппинг DTO и домена

    На небольших проектах допустим ручной маппинг в сервисе или отдельном маппере.

    Пример идеи через отдельный mapper (SRP):

    Правило слоя:

  • Контроллер оперирует DTO
  • Сервис может принимать DTO команд (если так проще в учебном проекте), но доменные сущности и бизнес-правила должны жить в домене/сервисе
  • Валидация: где и как проверять данные

    Валидация в REST обычно имеет два уровня:

  • Валидация формы данных на границе (контроллер): обязательные поля, формат email, длины строк
  • Валидация бизнес-правил в сервисе: уникальность email, ограничения домена, состояние ресурса
  • В Spring Boot валидация обычно строится на Jakarta Bean Validation.

    Ссылки:

  • Jakarta Bean Validation
  • Spring Boot Reference: Validation
  • Валидация тела запроса

    Чтобы включить проверку аннотаций на DTO, используйте @Valid.

    Если данные невалидны, Spring выбросит исключение, которое мы должны превратить в понятный клиенту ответ.

    Валидация path/query параметров

    Для проверки параметров метода используется @Validated на контроллере (или на конкретном методе).

    Что валидировать на границе, а что в сервисе

    На границе (DTO) хорошо проверять:

  • Наличие (@NotNull, @NotBlank)
  • Формат (@Email, @Pattern)
  • Границы размеров (@Size, @Min, @Max)
  • В сервисе хорошо проверять:

  • Уникальность (например, email не занят)
  • Права и ограничения домена
  • Конфликты состояния (например, ресурс уже удалён или заблокирован)
  • Так вы избегаете ситуации, когда бизнес-правила “размазаны” по контроллерам.

    Обработка исключений: единый формат ошибок

    Если вы не сделаете централизованную обработку, клиент будет получать:

  • разные форматы ошибок для разных ситуаций
  • “500” там, где на самом деле “404”
  • стек-трейсы в логах без контекста и без корреляции с запросом
  • Наша цель: перевести исключения сервиса и инфраструктуры в единый HTTP-ответ.

    Доменные исключения

    В сервисном слое удобно иметь смысловые исключения:

  • NotFoundException
  • ValidationException или BusinessRuleViolationException
  • ConflictException
  • Пример:

    @ControllerAdvice и @ExceptionHandler

    @RestControllerAdvice позволяет перехватывать исключения и формировать ответы.

    Надёжный подход для публичного API: использовать Problem Details (RFC 7807).

  • RFC 7807: Problem Details for HTTP APIs
  • Пример обработчика:

    Практические замечания:

  • Общий @ExceptionHandler(Exception.class) полезен как последняя линия обороны, но не должен скрывать все ошибки без логирования
  • Для публичного API обычно не возвращают внутренние детали (имена таблиц, SQL, стек-трейсы)
  • Как выбирать HTTP-статусы для ошибок

    Мини-таблица соответствий, которую стоит применять последовательно:

    | Ситуация | Исключение (пример) | HTTP статус | |---|---|---| | Ресурс не найден | NotFoundException | 404 Not Found | | Невалидный ввод (форма данных) | MethodArgumentNotValidException | 400 Bad Request | | Нарушение бизнес-правила | BusinessRuleViolationException | 422 Unprocessable Entity или 400 Bad Request | | Конфликт состояния | ConflictException | 409 Conflict | | Нет аутентификации | будет в Security модуле | 401 Unauthorized | | Нет прав | будет в Security модуле | 403 Forbidden |

    Важно: 422 удобен, когда DTO формально валиден, но нарушает бизнес-инвариант.

    Ошибки и многопоточность

    Из темы про конкурентность важно помнить:

  • контроллеры и сервисы по умолчанию используются параллельно многими запросами
  • обработчик исключений (@RestControllerAdvice) тоже singleton, значит не должен хранить состояние запроса в полях
  • Если вы добавляете корреляцию (например, request id), передавайте её через параметры/контекст запроса, а не через поля singleton-бина.

    Документация API: OpenAPI и springdoc

    Документация нужна не только “для красоты”. Она помогает:

  • клиентам быстро понять контракт
  • вам самим поддерживать консистентность
  • автоматизировать генерацию клиентов и тестов
  • Стандарт OpenAPI:

  • OpenAPI Specification
  • Для Spring Boot распространённый инструмент: springdoc-openapi.

  • springdoc-openapi
  • Что документировать в первую очередь

    Чтобы документация была полезной, фиксируйте:

  • пути и методы
  • модели запросов и ответов (DTO)
  • коды ответов, включая ошибки
  • требования к авторизации (в модуле Security)
  • Пример аннотаций для контроллера (на уровне идеи):

    Практика: не пытайтесь покрыть аннотациями всё сразу. Минимально важно описывать публичные эндпоинты и ошибки.

    Частые элементы REST API: пагинация, сортировка, фильтрация

    В реальном API списки почти никогда не отдают “всё сразу”. Нужны ограничения.

    Типовой подход:

  • GET /api/users?limit=20&offset=0
  • GET /api/users?page=0&size=20&sort=name,asc (часто вместе со Spring Data позже)
  • Важно, чтобы поведение было стабильным:

  • фиксируйте значения по умолчанию
  • ограничивайте максимальный размер страницы
  • документируйте сортировку и фильтры
  • Версионирование API

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

    Практические варианты:

  • Версия в пути: /api/v1/users
  • Версия в заголовке: Accept: application/vnd.company.v1+json
  • Для учебного проекта чаще достаточно версии в пути, потому что это проще для клиентов и документации.

    Мини-чеклист перед тем, как коммитить контроллер

  • Контроллер тонкий: только HTTP и DTO
  • DTO не совпадают с внутренней моделью “случайно”, а являются осознанным контрактом
  • @Valid включён для входных DTO
  • Ошибки централизованы в @RestControllerAdvice
  • Статусы соответствуют смыслу (404 для отсутствия, 409 для конфликта)
  • Документация OpenAPI отражает хотя бы основные сценарии и ошибки
  • Что дальше по курсу

    Следующий логичный шаг после REST слоя:

  • подключить БД и репозитории
  • добавить транзакции и миграции схемы
  • затем наложить Security (аутентификация, авторизация)
  • Если слой API сделан правильно уже сейчас, подключение БД и Security будет добавлением инфраструктуры, а не переписыванием контрактов и обработчиков ошибок.

    6. Работа с БД: SQL, транзакции, JPA/Hibernate, миграции и оптимизация запросов

    Работа с БД: SQL, транзакции, JPA/Hibernate, миграции и оптимизация запросов

    После слоя REST API у приложения появляется следующий фундаментальный компонент: хранилище данных. В типичном Spring Boot сервисе это реляционная БД (PostgreSQL, MySQL и др.). В этой теме вы научитесь:

  • мыслить данными: таблицы, связи, ключи, индексы
  • понимать, что реально делает SQL и почему это важно даже при использовании ORM
  • управлять целостностью через транзакции
  • использовать JPA/Hibernate так, чтобы не получить лавину запросов и странные баги
  • поддерживать схему БД через миграции
  • находить и устранять проблемы производительности запросов
  • Связь с предыдущими модулями курса:

  • Из темы про архитектуру Spring Boot нам важны слои: контроллеры не должны знать про БД, а репозитории не должны протаскивать детали хранения наверх.
  • Из темы про обработку ошибок нам нужно корректно переводить инфраструктурные ошибки БД в смысловые исключения сервиса.
  • Из темы про конкурентность важно помнить: запросы выполняются параллельно, а значит транзакции и блокировки в БД становятся реальными конкурентными примитивами.
  • !Схема: как REST слой вызывает сервис, сервис — репозиторий, а дальше ORM и БД

    Реляционная БД и SQL: минимальная база

    Таблицы, строки, ключи

    Реляционная модель хранит данные в таблицах.

  • Строка (row) представляет одну запись (например, пользователя).
  • Первичный ключ (primary key) однозначно идентифицирует строку.
  • Внешний ключ (foreign key) связывает строки разных таблиц и поддерживает ссылочную целостность.
  • Пример домена для учебного сервиса:

  • users — пользователи
  • orders — заказы пользователя
  • order_items — позиции заказа
  • Нормализация и зачем она нужна

    Нормализация — это организация таблиц так, чтобы избежать дублирования и аномалий обновления.

    Практическое правило для старта:

  • повторяющиеся сущности выносите в отдельные таблицы
  • связи выражайте внешними ключами
  • Основные операции SQL

    CRUD в SQL обычно выглядит так:

  • INSERT — создать запись
  • SELECT — прочитать
  • UPDATE — изменить
  • DELETE — удалить
  • Пример упрощённого SQL:

    JOIN: как связываются таблицы

    JOIN соединяет строки двух таблиц по условию.

  • INNER JOIN — только строки, где есть совпадение
  • LEFT JOIN — все строки слева плюс совпадения справа
  • Пример: получить заказы с email пользователя:

    Индексы: почему WHERE может быть быстрым или медленным

    Индекс — структура данных, ускоряющая поиск по колонке.

    Практические следствия:

  • WHERE email = ? обычно должен опираться на индекс по email
  • индекс ускоряет чтение, но замедляет запись (потому что индекс тоже надо обновлять)
  • индекс не спасёт, если вы пишете запрос так, что он не может быть использован (например, из-за неподходящих функций над полем)
  • Справочник по индексам в PostgreSQL: PostgreSQL Documentation: Indexes

    Подключение БД в Spring Boot: DataSource и пул соединений

    Почему нужен пул соединений

    Соединение к БД — дорогой ресурс. В web-приложении нельзя создавать соединение на каждый запрос и тут же закрывать.

    Пул соединений:

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

    Документация пула: HikariCP

    Минимальная конфигурация

    Пример application.yml для PostgreSQL:

    Пояснения:

  • ddl-auto: validate говорит Hibernate: проверь схему, но не создавай и не меняй её автоматически.
  • изменение схемы лучше делать миграциями (будет дальше).
  • JPA и Hibernate: что это и как оно работает

    JPA vs Hibernate

  • JPA — спецификация (набор интерфейсов и правил) для ORM в Java.
  • Hibernate — самая распространённая реализация JPA.
  • Ссылки:

  • Jakarta Persistence (JPA)
  • Hibernate ORM Documentation
  • ORM простыми словами

    ORM (Object-Relational Mapping) сопоставляет:

  • таблицы и строки в БД
  • классы и объекты в Java
  • Идея: вы работаете с объектами, а ORM генерирует SQL.

    Критически важная мысль: SQL никуда не исчезает. Вы либо контролируете его, либо отлаживаете неожиданное поведение в продакшене.

    Entity, идентификатор и маппинг

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

    Практические правила:

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

    В JPA есть контекст персистентности (persistence context) — набор сущностей, которые ORM отслеживает внутри текущей транзакции.

    Из этого следует:

  • если вы загрузили сущность и поменяли поле, Hibernate может сделать UPDATE при завершении транзакции без явного save
  • повторная загрузка той же сущности по id в рамках одной транзакции часто не делает новый SQL, а берёт объект из контекста
  • Это удобно, но требует дисциплины по границам транзакций.

    Репозитории: Spring Data JPA

    Spring Data JPA создаёт реализации репозиториев автоматически.

    Документация: Spring Data JPA Reference

    Пример репозитория:

    Плюсы:

  • минимум шаблонного кода
  • типовые операции и пагинация из коробки
  • Риски:

  • легко написать метод, который неожиданно генерирует тяжёлый запрос
  • легко попасть в N+1 проблему при связях
  • Транзакции: целостность данных и конкурентность

    Зачем нужны транзакции

    Транзакция — это логическая единица работы с БД.

    Внутри транзакции вы хотите, чтобы изменения были:

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

    @Transactional: где ставить и почему

    В Spring транзакции обычно объявляют на сервисном слое.

    Почему это лучше, чем ставить транзакцию в контроллере:

  • контроллер отвечает за HTTP и DTO, а транзакция — часть бизнес-операции
  • сервис проще тестировать и переиспользовать
  • Документация: Spring Framework: Transaction Management

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

    Для чтения можно использовать @Transactional(readOnly = true):

  • это сигнал для инфраструктуры и иногда даёт оптимизации
  • также защищает от случайных изменений в коде чтения
  • Изоляция и блокировки: базовая ориентация

    Когда два запроса параллельно обновляют данные, БД должна решить, кто кого ждёт.

    Практические понятия:

  • изоляция — насколько изменения одной транзакции видны другой
  • блокировки — механизм, из-за которого одна транзакция может ждать другую
  • Подробно изоляции зависят от БД, но для старта важно:

  • если у вас есть конкурентные обновления, вы должны продумать стратегию (например, блокировки на уровне строк или оптимистическая блокировка)
  • Оптимистическая блокировка через @Version

    Оптимистическая блокировка подходит, когда конфликты редки.

    Идея:

  • у записи есть поле версии
  • при UPDATE Hibernate добавляет условие по версии
  • если кто-то уже изменил запись, UPDATE не затронет строку и ORM поймёт конфликт
  • Пример:

    Связи между сущностями и типичные ловушки

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

  • @ManyToOne — многие к одному (много заказов у одного пользователя)
  • @OneToMany — один ко многим (у пользователя коллекция заказов)
  • @OneToOne — один к одному
  • @ManyToMany — многие ко многим (часто лучше заменять на отдельную сущность-связку)
  • Пример: заказ принадлежит пользователю:

    Ключевая настройка здесь: fetch = FetchType.LAZY.

    LAZY vs EAGER

  • LAZY означает, что связанный объект загрузится не сразу, а при обращении.
  • EAGER означает, что связанный объект загрузится сразу.
  • Практика для серверных приложений:

  • по умолчанию выбирайте LAZY
  • загрузку связанных данных делайте явной на уровне запросов
  • Почему:

  • EAGER легко приводит к большим непредсказуемым запросам
  • LAZY легко приводит к N+1 проблеме, если вы не контролируете запросы
  • N+1 проблема

    Симптом:

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

    Способы решения:

  • JOIN FETCH в JPQL
  • @EntityGraph
  • запросы, которые сразу возвращают DTO (проекции)
  • Пример JOIN FETCH:

    Важно: join fetch меняет форму данных, поэтому его нужно применять осознанно, особенно со списками и пагинацией.

    Миграции схемы: Flyway и Liquibase

    Автоматическое изменение схемы через hibernate.ddl-auto=update удобно на первых минутах обучения, но опасно в реальных проектах.

    Правильный подход: миграции схемы.

    Почему миграции обязательны

    Миграции дают:

  • повторяемость развёртывания (у всех одинаковая схема)
  • контроль изменений в Git
  • предсказуемые апдейты между версиями
  • Flyway

    Flyway основан на версионированных SQL-скриптах.

    Документация: Flyway Documentation

    Типовая структура:

  • src/main/resources/db/migration/V1__init.sql
  • src/main/resources/db/migration/V2__add_orders.sql
  • Пример V1__init.sql:

    Liquibase

    Liquibase поддерживает разные форматы миграций (XML, YAML, JSON, SQL) и ориентирован на более декларативный стиль.

    Документация: Liquibase Documentation

    Выбор между Flyway и Liquibase часто командный. Для учебного проекта обычно проще Flyway.

    Оптимизация запросов: что делать, когда стало медленно

    Золотое правило: сначала измеряйте

    Оптимизация без измерений часто приводит к усложнению без эффекта.

    Практический подход:

  • включите логирование SQL в dev
  • зафиксируйте конкретный эндпоинт, который медленный
  • посмотрите реальный SQL и количество запросов
  • используйте EXPLAIN в БД для анализа плана
  • Для PostgreSQL: PostgreSQL Documentation: EXPLAIN

    Типовые причины медленных запросов

  • отсутствие индексов на фильтрах и связях
  • выборка лишних колонок и строк
  • N+1
  • сортировка и пагинация без индекса
  • сложные LIKE и функции по полям, которые ломают использование индекса
  • Логирование SQL в Hibernate

    Для разработки полезно видеть SQL.

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

    Замечание:

  • подробное логирование параметров может содержать персональные данные, поэтому включайте его только в dev.
  • Пагинация

    Не отдавайте большие списки целиком.

    В Spring Data JPA есть Pageable.

    Практические правила:

  • ограничивайте максимальный size
  • стабилизируйте сортировку (одинаковый порядок при одинаковых данных)
  • DTO-проекции вместо сущностей

    Если REST API возвращает небольшой набор полей, часто выгоднее загрузить сразу DTO, а не целую сущность с графом связей.

    Плюсы:

  • меньше данных
  • меньше риск случайно триггернуть ленивую загрузку
  • Минус:

  • DTO становится частью запроса, нужно следить за пакетами и рефакторингом
  • Кэширование: осторожно

    Кэширование может помочь, но добавляет сложность.

    Для старта держите правила простыми:

  • сначала устраните N+1 и добавьте индексы
  • потом думайте о кэше на уровне приложения или БД
  • Практические соглашения для учебного проекта

    Чтобы следующие темы (Security, тестирование, контейнеризация) легли ровно, зафиксируйте базовые правила проекта:

  • схема БД управляется миграциями
  • транзакции объявляются в сервисах
  • репозитории возвращают сущности или DTO-проекции, но контроллеры не работают напрямую с сущностями
  • в dev вы видите SQL, в prod вы не логируете чувствительные данные
  • проблемы производительности решаются через анализ реального SQL и планов
  • Что дальше по курсу

    После подключения БД вы готовы к темам, которые зависят от данных:

  • Security: пользователи, роли, хранение паролей, авторизация на уровне эндпоинтов и домена
  • Тестирование: репозитории и сервисы с тестовой БД, тестирование транзакций
  • Контейнеризация: запуск приложения вместе с БД, миграциями и профилями окружений
  • 7. Безопасность, тестирование и контейнеризация: Spring Security, JUnit, Testcontainers, Docker

    Security, testing, and containerization: Spring Security, JUnit, Testcontainers, Docker

    After building a REST API and connecting a database, your application becomes useful—and therefore becomes a target for mistakes, misuse, and attacks. At the same time, you need fast feedback (tests) and repeatable environments (containers) to ship changes safely.

    This module ties together three production-critical concerns:

  • Security: who can call your API and what they are allowed to do
  • Testing: how to prove behavior and prevent regressions
  • Containerization: how to run the service and its dependencies reproducibly (locally and in CI)
  • The ideas from previous modules still apply:

  • From SOLID and error handling: security checks and failures must be consistent, centralized where possible, and expressed via clear exceptions and HTTP responses.
  • From Spring Boot architecture: controllers stay thin; security belongs to configuration and cross-cutting layers; services stay testable.
  • From DB and transactions: authentication data (users, roles) is persisted; database constraints matter; concurrency affects security-sensitive operations.
  • !Where Spring Security sits in the request flow

    Spring Security fundamentals

    Spring Security is the de-facto standard security framework in the Spring ecosystem. It integrates with Spring MVC, method invocations, and many authentication mechanisms.

    Recommended references:

  • Spring Security Reference
  • Spring Boot Reference: Security
  • Authentication vs authorization

  • Authentication answers: Who are you?
  • Authorization answers: Are you allowed to do this?
  • A common pitfall is to mix these concerns.

  • Authentication is usually performed early in the request lifecycle, often in the Security Filter Chain.
  • Authorization can be enforced at multiple layers, commonly:
  • 1. At the HTTP layer (endpoint access rules) 2. At the method layer (service methods)

    The Security Filter Chain in Spring Boot

    Spring Security intercepts requests using a chain of filters. Each filter can:

  • read credentials (cookies, headers)
  • validate them
  • populate the SecurityContext (the authenticated principal)
  • enforce or defer authorization
  • Key concept:

  • SecurityContext is a per-request holder of the currently authenticated user (principal) and authorities (roles/permissions).
  • Session-based auth vs token-based auth

    Two typical approaches for REST APIs:

  • Session-based authentication
  • - Browser-oriented - Server stores session state - Often combined with CSRF protection
  • Token-based authentication (typically JWT)
  • - Common for APIs consumed by mobile apps, SPAs, or other services - Stateless on the server (usually) - Token is sent via Authorization: Bearer <token>

    Practical guidance for a course project:

  • If you build a pure REST backend with a separate frontend, token-based auth is usually simpler to scale.
  • If you build a server-rendered app or browser-heavy flows, session-based auth can be more straightforward.
  • Password storage: hashing, never encryption

    Passwords must be stored as hashes, not plaintext, and not reversible encryption.

  • Use a slow, adaptive hashing algorithm.
  • In Spring Security, the typical default is BCrypt via BCryptPasswordEncoder.
  • Reference:

  • OWASP Password Storage Cheat Sheet
  • Example configuration bean:

    Roles and authorities

    Spring Security uses authorities as strings. Roles are a convention: authorities prefixed with ROLE_.

    Typical patterns:

  • ROLE_USER, ROLE_ADMIN
  • Fine-grained authorities like order:read, order:write
  • Choose one consistent approach:

  • For small apps: roles may be enough.
  • For larger apps: prefer explicit permissions (authorities) to avoid role explosion.
  • Minimal security setup for a REST API

    Spring Security configuration style in modern Spring Boot is typically bean-based (no inheritance from old adapters).

    Define endpoint authorization rules

    Example SecurityFilterChain that:

  • allows access to OpenAPI/Swagger endpoints
  • requires authentication for the rest
  • Notes:

  • httpBasic() is fine for learning and internal services; for public APIs you typically move to token auth.
  • Disabling CSRF is acceptable for stateless token APIs; for session/cookie-based apps you usually keep CSRF enabled.
  • CSRF and why it exists

    CSRF (Cross-Site Request Forgery) is an attack where a browser can be tricked into sending authenticated requests to your server.

    Practical interpretation:

  • If your API uses cookies for authentication, CSRF is a real threat and you usually keep CSRF protection enabled.
  • If your API uses Authorization: Bearer ... tokens and does not rely on cookies, CSRF is typically less relevant.
  • Reference:

  • Spring Security: CSRF
  • CORS and REST APIs

    CORS (Cross-Origin Resource Sharing) is a browser-enforced policy. It is not primarily a security boundary for servers, but a browser mechanism.

    Typical scenario:

  • Frontend is hosted on https://app.example.com
  • Backend API is on https://api.example.com
  • Browser requires CORS headers to allow frontend JavaScript to call the API
  • Reference:

  • Spring Framework: CORS
  • Method-level security

    HTTP rules are often not enough. You may need authorization inside services, for example:

  • Admins can delete users
  • Users can only access their own orders
  • Enable method security:

    Use annotations like @PreAuthorize:

    Important design rule:

  • Controllers should not contain authorization logic beyond simple access constraints.
  • Service-level checks better protect you against accidental exposure of internal methods.
  • Common security mistakes in Spring REST services

  • Putting secrets in logs
  • Returning internal exception details to clients
  • Storing request-specific state in singleton beans
  • Implementing custom crypto instead of using established libraries
  • Skipping authorization on “internal” endpoints that later become public
  • A good habit is to treat security as a default requirement, not an optional add-on.

    Testing: what to test and why

    Tests are your main tool to ship changes safely.

    Recommended references:

  • JUnit 5 User Guide
  • Spring Boot Reference: Testing
  • Mockito Documentation
  • The testing pyramid for a Spring Boot service

    A practical split:

  • Unit tests: fast, isolated, no Spring context
  • Slice tests: focus on a layer, for example repository or controller
  • Integration tests: full application context and real dependencies (often via containers)
  • !Test pyramid for a Spring Boot REST service

    Unit tests with JUnit and Mockito

    Unit tests validate business rules in the service layer without Spring.

    Example service unit test (illustrative):

    With mocks (repository mocked, service tested):

    Key rules:

  • Unit tests should be deterministic and fast.
  • Mock only what you own at the boundary, typically repositories or external clients.
  • Spring MVC controller tests

    If you want to test HTTP layer behavior (status codes, validation, JSON), use Spring MVC testing support.

    Spring Security adds extra complexity because endpoints may require authentication.

    With Spring Security test helpers:

  • use @WithMockUser for authenticated calls
  • assert 401 or 403 for unauthorized cases
  • Reference:

  • Spring Security Test
  • Repository tests and DB behavior

    For JPA, your biggest risks are:

  • queries that do not match expectations
  • mapping problems
  • transactions and constraints
  • A common approach:

  • use @DataJpaTest
  • use a real database, preferably via Testcontainers
  • Testcontainers: realistic integration tests

    Testcontainers lets you run real dependencies (PostgreSQL, Redis, Kafka) in Docker containers during tests.

    References:

  • Testcontainers Documentation
  • Testcontainers for Java
  • Why it matters:

  • An in-memory DB is not equivalent to PostgreSQL behavior.
  • Migrations, indexes, constraints, and SQL quirks are real sources of bugs.
  • Minimal PostgreSQL Testcontainers example

    Important points:

  • Use a versioned image like postgres:16 for reproducibility.
  • Combine with migrations (Flyway/Liquibase) so schema is created the same way as in production.
  • Docker: packaging and running your service

    Docker is a standard way to package applications with consistent runtime behavior.

    References:

  • Docker Documentation
  • Dockerfile reference
  • Docker Compose
  • What containerization solves

    Containerizing your service helps you:

  • run the same artifact locally and in CI
  • run the app together with its dependencies (database)
  • avoid “works on my machine” problems
  • Build a Spring Boot image

    Modern Spring Boot can build images without a handwritten Dockerfile via Buildpacks.

    Reference:

  • Spring Boot: Container Images
  • Example:

    This produces a runnable image with sensible defaults.

    A simple Dockerfile approach

    If you want to control the image explicitly:

    Notes:

  • Use a JRE base image for runtime.
  • Use a pinned Java version to avoid surprises.
  • Running app and DB with Docker Compose

    A typical local setup uses Compose to start:

  • app
  • postgres
  • Example compose.yml:

    Important connection with the earlier profiles and configuration module:

  • In containers you usually configure via environment variables.
  • Spring Boot relaxed binding maps SPRING_DATASOURCE_URL to spring.datasource.url.
  • !Running the app with PostgreSQL using Docker Compose

    Putting it all together in the course project

    A realistic milestone for your training project after this module:

  • Security
  • 1. Define authentication mechanism (basic for learning, JWT for API-style) 2. Protect endpoints and add method-level authorization 3. Ensure password hashing and safe error responses
  • Tests
  • 1. Unit-test services (business rules) 2. Add controller tests for validation and security behavior 3. Add integration tests with Testcontainers and migrations
  • Containerization
  • 1. Build a reproducible image 2. Run app + DB via Compose 3. Confirm profiles and environment configuration behave as expected

    If you do these three steps correctly, later changes become safer and faster because:

  • security rules are explicit
  • regressions are caught automatically
  • environments are reproducible