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

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

Spring REST & MVC
Рівень 28 , Лекція 4
Відкрита

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

На цьому етапі в нас уже є всі складові: runtime-параметри винесені з коду, базовий application.yml став читабельним, app.attachments.* доходить до storage через AttachmentProperties, а локальне перевизначення накладається поверх базової конфігурації. Тепер лишилося зібрати це в один відтворюваний базовий стан проєкту та швидко перевірити три речі: чи немає хардкоду, чи не суперечать одне одному multipart-ліміти й чи не зʼявилося друге джерело істини для storage-dir. Тож почнімо.

Коли проєкт ще невеликий, дуже легко повірити в міф: «Головне — щоб запускалося, а конфіг потім підчистимо». Це приблизно як сказати: «Головне — щоб лампочка світилася, а проводку потім акуратно вкладемо». Світло справді буде. Але одного дня ви випадково зачепите дріт ногою — і раптом дізнаєтеся, що означає «чому воно вчора працювало».

Зараз важливо не знову сперечатися про те, що саме виносити в YAML. Це питання ми вже закрили. Натомість завдання практичне: зробити так, щоб Task Tracker API запускався однаково для всієї команди, а конфіг не ховав у собі другу копію логіки. Налаштування фреймворку живуть під spring.*, налаштування застосунку — під app.*, runtime-параметри змінюються через базову конфігурацію, профіль і перевизначення, а 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) рівень фреймворку (spring.*)
spring.servlet.multipart.max-file-size 10MB Обмежує розмір одного файла під час завантаження рівень фреймворку (spring.*)
spring.servlet.multipart.max-request-size 12MB Обмежує загальний розмір multipart-запиту (файл + метадані + службові частини) рівень фреймворку (spring.*)
app.attachments.storage-dir ./data/attachments Підказує шару зберігання, де тримати файли рівень застосунку (app.*)

Цього набору досить, щоб базовий стан проєкту був явним, а не розсіяним у пам’яті розробників.

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. Явні значення за замовчуванням

Значення за замовчуванням у конфігурації — це місце, де новачки найчастіше потрапляють у пастку: вони або не задають дефолт узагалі («нехай буде як вийде»), або задають його так, що проєкт прив’язується до однієї машини («у мене на диску D усе гарно»). Правда тут проста: базове значення має бути переносним і має бути видно прямо в базовій конфігурації.

Для app.attachments.storage-dir таким значенням у нас є ./data/attachments. Це не таємне резервне значення в Java-класі, а явний default проєкту. Його видно відразу після відкриття application.yml, і тому будь-який розробник розуміє, куди за замовчуванням підуть файли.

app:
  attachments:
    # Базовий шлях сховища; локальні відмінності перекриваються профілем або змінною середовища
    storage-dir: ./data/attachments

Саме тому AttachmentProperties не повинен вигадувати друге резервне значення всередині коду. Якщо storage-dir — критична властивість, базова конфігурація задає її явно, а @Validated і @NotBlank страхують від порожнього значення під час старту. Інакше у вас зʼявляються дві істини: YAML говорить одне, а Java-клас мовчки підставляє інше.

5. Прибираємо хардкод у storage

Конфігурація вважається зробленою не тоді, коли ви додали гарний YAML, а тоді, коли прибрали з коду останні «магічні рядки», що суперечать ідеї зовнішніх налаштувань. Дуже типова ситуація: ви додали app.attachments.storage-dir, але десь у storage все ще стоїть Path.of("/tmp/..."), бо «я просто тестував». Через тиждень ви вже не пам’ятаєте, що це був тест, і починаєте шукати: «Чому файли не там?»

Після AttachmentProperties у storage залишається одне чесне завдання: взяти типізований об’єкт конфігурації і працювати з ним. Він не повинен сам обирати шлях, тримати запасну константу або сперечатися з 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) {
        // Єдине джерело істини про директорію — типізований об’єкт конфігурації
        this.root = Path.of(props.storageDir());
    }
}

Так, тут можна додати нормалізацію шляху (normalize()), перетворення в абсолютний (toAbsolutePath()), створення директорії (Files.createDirectories(root)). Але це вже деталі реалізації файлового сховища. У контексті сьогоднішньої лекції важливо зафіксувати дисципліну: storage не обирає шлях сам і не зберігає його у вихідному коді.

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

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

Цей вузол у нас уже тримається на @Validated і @NotBlank у AttachmentProperties. Для фінального базового стану важлива одна думка: storage-dir залишається обов’язковим ключем рівня застосунку, базовий application.yml дає йому явне значення, а fail-fast на старті не дає застосунку перейти в напівпрацездатний стан.

Ще одна корисна межа: не змішуйте в голові «валідацію конфігу» і «валідацію запитів». Коли користувач надіслав некоректний JSON — це 400 Bad Request і наш контракт обробки помилок. Коли в застосунку зламаний конфіг — це проблема запуску, і правильна поведінка найчастіше одна: застосунок не має запускатися.

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:
    # Базова директорія зберігання; профіль і змінні середовища можуть її перевизначити
    storage-dir: ./data/attachments

У цьому фрагменті важлива не лише конкретика значень, а й те, що тепер їх легко прочитати як документ. З YAML одразу видно: проєкт очікує Problem Details для помилок, завантаження файлів до 10MB і зберігання вкладень у ./data/attachments. Це і є мета дисципліни: щоб конфігурація перестала бути «таємним знанням автора проєкту» і стала явною частиною репозиторію.

8. Типові помилки в конфігурації

Помилка №1: залишити частину хардкоду «про всяк випадок».
Дуже часто після впровадження @ConfigurationProperties у коді залишається стара константа, наприклад Path.of("/tmp/task-tracker/uploads"), або тихе резервне значення всередині AttachmentProperties, і вона починає сперечатися з YAML. У результаті конфіг є, типізований об’єкт є, а файли все одно летять у старе місце. Щойно шлях винесений у конфігурацію, у storage-реалізації та в класі конфігурації не має залишатися другого прихованого джерела істини про директорію.

Помилка №2: зробити max-request-size меншим за max-file-size.
Така конфігурація формально коректна, але логічно суперечлива. Вона призводить до того, що завантаження починає ламатися раніше за контролер, а ви намагаєтеся виправити не те місце. Гарне правило: якщо multipart несе один файл і невеликий блок метаданих, то 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: не валідовати критичні властивості рівня застосунку.
Якщо storage-dir є обов’язковим, відсутність валідації означає, що застосунок може стартувати, а потім зламатися в момент завантаження файла. Це найгірший сценарій: проблему виявляють пізно, у непередбачуваному місці й зазвичай із менш зрозумілою помилкою. @Validated і @NotBlank на AttachmentProperties — простий спосіб зробити поведінку такою: або запускаємося коректно, або не запускаємося взагалі.

1
Опитування
Налаштування Spring, рівень 28, лекція 4
Недоступний
Налаштування Spring
Налаштування, профілі та валідація
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ