JavaRush /Курсы /Spring REST & MVC /Финальная дисциплина конфигурации

Финальная дисциплина конфигурации

Spring REST & MVC
28 уровень , 4 лекция
Открыта

1. Дисциплина конфигурации

К этому месту у нас уже есть все детали: runtime-параметры вынесены из кода, базовый application.yml стал читаемым, app.attachments.* доходит до storage через AttachmentProperties, а local-override живёт поверх base-конфига. Теперь осталось собрать это в один воспроизводимый baseline проекта и быстро проверить три вещи: нет ли хардкода, не спорят ли друг с другом multipart-лимиты и не появился ли второй источник истины для storage-dir. Так что приступим.

Когда проект маленький, очень легко поверить в миф: “главное — чтобы запускалось, а конфиг потом подчистим”. Это примерно как сказать: “главное — чтобы лампочка горела, а проводку потом аккуратно уложим”. Технически свет будет, но однажды вы случайно заденете провод ногой — и внезапно узнаете, что такое “почему оно вчера работало”.

Сейчас важно не заново спорить, что вообще выносить в YAML. Этот вопрос мы уже закрыли. Здесь задача практичнее: сделать так, чтобы Task Tracker API запускался одинаково у всей команды, а конфиг не прятал в себе вторую копию логики. Framework-настройки живут под spring.*, настройки приложения — под app.*, runtime-параметры меняются через base-конфиг, профиль и overrides, а API-контракт и доменные правила остаются в коде.

2. Обязательные ключи запуска Task Tracker API

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

В нашем проекте есть хорошее естественное разделение: всё, что относится к Spring MVC и servlet-инфраструктуре, лежит под spring.*, а всё, что относится к правилам запуска именно нашего приложения, лежит под app.*. Это не “правило ради правила”, а способ сделать файл читаемым: даже человек, который впервые открыл application.yml, быстро поймёт, где “настройки платформы”, а где “настройки проекта”.

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

Ключ Пример значения Зачем нужен Уровень
spring.mvc.problemdetails.enabled true Включает Problem Details как базовый формат ошибок (application/problem+json) framework-level (spring.*)
spring.servlet.multipart.max-file-size 10MB Ограничивает размер одного файла при upload framework-level (spring.*)
spring.servlet.multipart.max-request-size 12MB Ограничивает общий размер multipart-запроса (файл + metadata + служебные части) framework-level (spring.*)
app.attachments.storage-dir ./data/attachments Говорит storage-слою, где хранить содержимое файлов application-level (app.*)

Этого набора достаточно, чтобы runtime-baseline проекта был явным, а не распределённым по памяти разработчиков.

3. Согласованные multipart-лимиты

Multipart-лимиты — это тот случай, когда конфиг может быть “правильным по синтаксису”, но “неправильным по смыслу”. И это особенно неприятно, потому что ошибка проявляется не там, где вы ожидаете: запрос даже не дойдёт до вашего контроллера, а вы будете смотреть на лог и думать “почему мой endpoint не вызывается, я же его точно написал”.

В multipart у нас есть как минимум две величины: максимальный размер одного файла и максимальный размер всего запроса. Если упростить до бытовой аналогии, max-file-size — это ограничение “размер одного чемодана”, а max-request-size — ограничение “общий вес багажа”. Логично, что общий багаж должен быть не меньше одного чемодана, иначе вы разрешили чемодан на 20 кг, но багажник — на 10 кг. Формально вы ничего не нарушили, но поездка закончится на первом же посте контроля.

Плохой пример, который выглядит “как будто нормально”, но на практике создаёт непредсказуемость:

spring:
  servlet:
    multipart:
      # Противоречие: один файл можно 20MB...
      max-file-size: 20MB
      # ...но весь запрос целиком — только 10MB
      max-request-size: 10MB

Теперь вы сами себе противоречите: одному файлу можно 20MB, но весь запрос — только 10MB. В итоге для пользователя это будет выглядеть как “то ли 413 Payload Too Large, то ли какая-то странная ошибка загрузки”, а для вас — как “почему не работает, если я разрешил 20MB”.

Хорошая базовая стратегия для нашего проекта простая: раз мы загружаем один файл и небольшой JSON-метаданный блок, то max-request-size должен быть чуть больше max-file-size. Например, 10MB и 12MB — это понятная пара. Плюс она хорошо читается глазами: видно, что запрос включает не только файл.

spring:
  servlet:
    multipart:
      # Один файл
      max-file-size: 10MB
      # Весь запрос (файл + метаданные)
      max-request-size: 12MB

Ещё один нюанс: очень легко “на всякий случай” поставить огромные значения, чтобы “точно не мешало”. Но в учебном проекте это контрпродуктивно: вам важнее научиться контролировать границы, чем сделать “бесконечный upload”. Большие лимиты увеличивают риск, что вы начнёте хранить в памяти или логах то, что хранить не надо, и в целом усложняют отладку.

4. Явные defaults

Defaults в конфигурации — это место, где новички чаще всего попадают в ловушку: они либо не задают дефолт вообще (“пусть будет как получится”), либо задают дефолт так, что проект становится привязан к одной машине (“у меня на диске D всё красиво”). Истина тут простая: базовое значение должно быть переносимым и должно быть видно прямо в base-конфиге.

Для app.attachments.storage-dir таким значением у нас служит ./data/attachments. Это не “тайный fallback в Java-классе”, а явный default проекта. Его видно сразу после открытия application.yml, и поэтому любой разработчик понимает, куда по умолчанию пойдут файлы.

app:
  attachments:
    # Базовый путь для репозитория; локальные отличия перекрываются профилем или env var
    storage-dir: ./data/attachments

Именно поэтому AttachmentProperties не должен придумывать второй fallback внутри кода. Если storage-dir — критичное свойство, base-конфиг задаёт его явно, а @Validated и @NotBlank страхуют от пустого значения при старте. Иначе у вас появляются две истины: YAML говорит одно, а Java-класс молча подставляет другое.

5. Убираем хардкод в storage

Конфигурация считается сделанной не тогда, когда вы добавили красивый YAML, а тогда, когда вы убрали из кода последние “магические строки”, которые противоречат идее внешних настроек. Очень типичная ситуация: вы добавили app.attachments.storage-dir, но где-то в storage всё ещё стоит Path.of("/tmp/..."), потому что “я просто тестил”. Через неделю вы уже не помните, что это было тестом, и начинаете искать “почему файлы не там”.

После AttachmentProperties у storage остаётся одна честная задача: взять typed config object и работать с ним. Он не должен сам выбирать путь, держать запасную константу или спорить с YAML.

package com.example.tasktracker.infrastructure.storage;

import com.example.tasktracker.config.AttachmentProperties;
import java.nio.file.Path;
import org.springframework.stereotype.Service;

@Service
public class LocalAttachmentStorage {
    private final Path root;

    public LocalAttachmentStorage(AttachmentProperties props) {
        // Единственный источник истины о директории — typed config object
        this.root = Path.of(props.storageDir());
    }
}

Да, тут можно добавить нормализацию пути (normalize()), перевод в абсолютный (toAbsolutePath()), создание директории (Files.createDirectories(root)). Но это уже детали file storage реализации. В контексте сегодняшней лекции важно зафиксировать дисциплину: storage не выбирает путь сам и не хранит его в исходниках.

6. Валидация конфигурации

Валидация конфигурации — это “страховка от человеческого фактора”. И она особенно полезна там, где без настройки подсистема физически не может работать. Для storage-dir это ровно наш случай: пустой или отсутствующий путь должен ломать запуск сразу, а не всплывать в первом upload как Path.of(null) или странный 500.

Этот узел у нас уже держится на @Validated и @NotBlank в AttachmentProperties. Для финального baseline важна одна мысль: storage-dir остаётся required application-level key, base application.yml даёт ему явное значение, а fail-fast на старте не даёт приложению перейти в полурабочее состояние.

Ещё одна полезная граница: не смешивайте в голове “валидацию конфига” и “валидацию запросов”. Когда пользователь прислал плохой JSON — это 400 Bad Request и наш error contract. Когда у приложения сломан конфиг — это проблема запуска, и правильное поведение чаще всего одно: приложение не должно стартовать.

7. Baseline application.yml

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

Ниже пример базового application.yml, который хорошо подходит для нашего текущего состояния проекта. Он задаёт Problem Details, multipart-лимиты и storage-dir. Всё остальное оставляется дефолтам, потому что либо не влияет на сценарий, либо требует отдельной дисциплины (которую мы в этом курсе сознательно не раздуваем).

spring:
  mvc:
    problemdetails:
      # Единый формат ошибок для API
      enabled: true
  servlet:
    multipart:
      # Один загружаемый файл
      max-file-size: 10MB
      # Весь multipart-запрос целиком (файл + метаданные)
      max-request-size: 12MB

app:
  attachments:
    # Базовая директория хранения; профиль и env vars могут её переопределить
    storage-dir: ./data/attachments

В этом фрагменте важна не только конкретика значений, а то, что их теперь легко “прочитать как документ”. Прямо по YAML видно, что проект ожидает: ошибки в Problem Details, загрузку файлов до 10MB и хранение вложений в ./data/attachments. Это и есть цель дисциплины: чтобы конфигурация перестала быть “тайным знанием автора проекта” и стала явной частью репозитория.

8. Типичные ошибки в конфигурации

Ошибка №1: оставить часть хардкода “на всякий случай”.
Очень часто после внедрения @ConfigurationProperties в коде остаётся старая константа, например Path.of("/tmp/task-tracker/uploads"), или тихий fallback внутри AttachmentProperties, и она начинает спорить с YAML. В итоге конфиг есть, typed object есть, а файлы всё равно летят в старое место. Как только путь вынесен в конфигурацию, в storage-реализации и в config-class не должно оставаться второй скрытой истины про директорию.

Ошибка №2: сделать max-request-size меньше max-file-size.
Такая конфигурация формально корректна, но логически противоречива. Она приводит к тому, что загрузка начинает ломаться “раньше контроллера”, а вы пытаетесь чинить не то место. Хорошее правило: если multipart несёт один файл и небольшой metadata-part, то max-request-size должен быть немного больше max-file-size, чтобы не возникало ощущения “сегодня загрузилось, завтра — нет”.

Ошибка №3: вынести в YAML то, что является частью API-контракта.
Переносить в конфиг ограничения DTO, правила статусов, допустимые переходы, ограничения на title — это ошибка архитектуры. Такие вещи должны жить в коде, потому что они описывают контракт API и предметную область, а не окружение запуска. Иначе вы получите ситуацию, где один разработчик “подкрутил YAML”, и клиенты вдруг увидели другое поведение без изменения версии API.

Ошибка №4: не различать spring.* и app.* и устроить “плоскую свалку ключей”.
Когда свойства смешаны в корне файла без структуры, YAML перестаёт быть документом. Человек читает “мешок ключей” и не понимает, что управляет чем. Дисциплина здесь простая: всё, что относится к платформе (MVC, multipart), остаётся в spring.*, всё, что относится к вашему прикладному домену (attachments.storage-dir), живёт в app.*.

Ошибка №5: не валидировать критичные application-level свойства.
Если storage-dir обязателен, отсутствие валидации означает, что приложение может стартовать, а потом ломаться в момент загрузки файла. Это худший сценарий: проблема обнаруживается поздно, в непредсказуемом месте и обычно с менее понятной ошибкой. @Validated и @NotBlank на AttachmentProperties — простой способ сделать поведение “или запускаемся корректно, или не запускаемся вообще”.

1
Задача
Spring REST & MVC, 28 уровень, 4 лекция
Недоступна
Минимальный baseline конфигурации для upload-сценария
Минимальный baseline конфигурации для upload-сценария
1
Задача
Spring REST & MVC, 28 уровень, 4 лекция
Недоступна
Upload endpoint с конфигурируемой директорией хранения
Upload endpoint с конфигурируемой директорией хранения
1
Опрос
Spring Настройки, 28 уровень, 4 лекция
Недоступен
Spring Настройки
Настройки, профили и валидация
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ