JavaRush /Курси /Spring Boot /Перенесення констант catal...

Перенесення констант catalog-service у YAML

Spring Boot
Рівень 13 , Лекція 4
Відкрита

1. Що переносимо в YAML

Якщо подивитися на живий catalog-service, то в ньому вже є вебшар, сервіс, репозиторій і стартова логіка через ApplicationRunner. У кожному такому шарі легко завести «зручні маленькі константи»: ліміт featured-курсів, типовий фільтр published-only, заголовок каталогу, прапорець maintenance-mode. Вони виглядають безневинно — рівно доти, доки ви не захочете змінити їх без зміни логіки (наприклад, щоб у локальному запуску була інша назва або інший ліміт).

Але поки ці ключі живуть лише в YAML, самі по собі вони ще нічого не змінюють. Справжній виграш починається тоді, коли той самий набір значень починає керувати реальними біномами catalog-service.

Важливо вловити критерій: у конфіг ми переносимо значення, які справді можуть змінюватися між запусками, але не потребують переписування алгоритму. Наприклад, «за замовчуванням показувати лише опубліковані курси» — це налаштування поведінки, а «як саме фільтрувати за track і level» — уже логіка; її в YAML не виносять. Інакше ви ризикуєте винести в конфіг половину застосунку, а потім героїчно намагатися «програмувати YAML» (спойлер: YAML не хоче бути мовою програмування — він хоче бути YAML).

Щоб було наочно, зафіксуємо невеликий «контракт» налаштувань, які переносимо сьогодні. Він буде не філософським, а прикладним — і безпосередньо зав’язаний на поведінку наявних класів.

Налаштування Приклад ключа Тип Де впливає в проєкті
Заголовок каталогу app.catalog.title String Підсумок запуску (і далі — будь-які ідеї для «про застосунок»/landing)
Режим обслуговування app.catalog.maintenance-mode boolean Сервіс може повертати порожні відповіді або «заморожувати» видачу
Ліміт featured app.catalog.max-featured-count int Ендпоінт /api/catalog/featured не повертає нескінченний список
Типово — лише published app.catalog.default-published-only boolean Коли параметр запиту publishedOnly не передано
Чи ввімкнено звіт під час старту app.catalog.startup-report-enabled boolean StartupSummaryRunner вирішує: друкувати звіт чи мовчати

Список спеціально маленький. Сьогодні ми не чіпаємо «контент каталогу» (список курсів) і не будуємо складні структури конфігурації — це буде пізніше. Зараз нам потрібен простий, відчутний крок: перестати зберігати змінні параметри всередині Java-класів.

2. application.yaml як контракт налаштувань

До цього місця базовий application.yaml уже зафіксовано: spring.application.name і набір ключів під app.catalog.*. Переписувати весь файл удруге немає сенсу — важливіше тримати в голові, який із наявних класів починає читати ці значення.

CourseCatalogService забирає maintenance-mode, max-featured-count і default-published-only. StartupSummaryRunnerstartup-report-enabled, title і spring.application.name. Тобто конфіг перестає бути «абстрактним YAML про проєкт узагалі» і стає дуже конкретним контрактом між ключем і поведінкою класу.

Саме це й відрізняє корисний конфіг від блокнота зі рядками. Поки ключі не прив’язані до поведінки, вони просто лежать у файлі. Щойно клас починає отримувати їх через залежності, конфігурація стає частиною поведінки застосунку під час виконання.

3. @Value: як Spring підставляє значення в біни

Анотація @Value — це мінімалістичний місток між «світом конфігурації» і «світом Java-об’єктів». Вона дозволяє сказати Spring: «Коли будеш створювати цей бін — візьми підсумкове значення за ключем ... із конфігураційного середовища, сконвертуй у потрібний тип і передай у конструктор». Тобто це не «магія у вакуумі», а цілком конкретний момент життя застосунку: створення біна в ApplicationContext.

Дуже важлива домовленість для курсу (і для нормального життя загалом): @Value краще ставити на параметри конструктора, а не на поля. Так ви зберігаєте стиль constructor injection, а клас залишається чесним: його залежності й налаштування видно прямо в конструкторі, а не заховані десь посеред файлу.

Нижче — найпростіший приклад, який показує і @Value, і значення за замовчуванням. Зверніть увагу на синтаксис ${ключ:дефолт}. Якщо ключа в конфігурації немає, Spring візьме дефолт. Якщо дефолту немає, а ключ не знайдено — застосунок завершиться з помилкою на старті (і це, чесно кажучи, добре: краще впасти одразу, ніж тихо працювати «незрозуміло як»).

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class DemoConfigReader {

    // Налаштування приходить ззовні: з application.yaml / env / system properties
    private final int limit;

    public DemoConfigReader(@Value("${app.catalog.max-featured-count:4}") int limit) {
        // Якщо ключ не задано, буде використано значення за замовчуванням (4)
        this.limit = limit;
    }

    public int limit() {
        return limit;
    }
}

Ще один важливий бонус: Spring уміє робити базову конвертацію типів. Якщо ви просите int або boolean, а в YAML лежить число або true/false, то ви отримаєте коректне значення без ручного Integer.parseInt(...). Саме тому @Value так зручний як «перший крок» — він дає швидкий результат і не потребує окремої архітектури конфігурації.

4. Рефакторимо CourseCatalogService

Сервісний шар — ідеальне місце, щоб показати цінність конфігурації без архітектурних перекосів. Контролер має залишатися тонким, репозиторій — джерелом даних, а сервіс — місцем, де застосовується політика видачі. Ліміти й прапорці поведінки — це якраз «політика», і її зручно задавати через конфігурацію. Так ми не ховаємо налаштування у вебшарі й не розмазуємо їх по проєкту.

Уявімо типову версію «до»: ліміт featured захардкожений просто в класі, і якщо завтра ви захочете зробити «у локальному запуску 2, у dev-середовищі 5», вам доведеться правити код. Це й є мініприклад того, як зашиті константи створюють зайву зв’язність.

До (спрощено):

import java.util.List;

import org.springframework.stereotype.Service;

@Service
public class CourseCatalogService {

    // "Константа в коді" — щоб змінити, потрібно змінювати Java і перекомпілювати/перекочувати сервіс
    private static final int MAX_FEATURED_COUNT = 4;

    public List<String> featuredSlugs() {
        return List.of("spring-boot", "spring-core"); // просто приклад
    }
}

Тепер зробімо «після»: перенесемо параметри в конструктор, а значення читатимемо через @Value. Почнемо з полів і конструктора, щоб не перевантажувати приклад.

Файл: src/main/java/.../catalog/service/CourseCatalogService.java (фрагмент: поля й конструктор)

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class CourseCatalogService {

    // Обмеження видачі featured-курсів беремо з конфігурації
    private final int maxFeaturedCount;

    // Прапорець "обслуговування": за true можна повертати порожні відповіді
    private final boolean maintenanceMode;

    // Значення за замовчуванням для publishedOnly, якщо параметр запиту не задано
    private final boolean defaultPublishedOnly;

    public CourseCatalogService(
            @Value("${app.catalog.max-featured-count:4}") int maxFeaturedCount,
            @Value("${app.catalog.maintenance-mode:false}") boolean maintenanceMode,
            @Value("${app.catalog.default-published-only:true}") boolean defaultPublishedOnly) {

        // Тут "контракт" стає явним: налаштування приходять у конструктор
        this.maxFeaturedCount = maxFeaturedCount;
        this.maintenanceMode = maintenanceMode;
        this.defaultPublishedOnly = defaultPublishedOnly;
    }
}

Так, поки це виглядає як «три @Value підряд». Це нормально на цьому кроці: ми вчимося переносити значення з коду в YAML. Для кількох параметрів такий варіант іще читабельний. Але якщо конфігурація почне розростатися, цей стиль швидко стане шумним — тримайте це відчуття в голові вже зараз. Сьогодні нам важливіше відчути, як змінюється мислення: сервіс перестав «сам вирішувати» свої параметри й почав отримувати їх ззовні.

Тепер додамо метод featured(...) (або аналогічний), який реально обмежує видачу. Покажемо приклад акуратно, без зайвих деталей предметної моделі.

Файл: CourseCatalogService.java (фрагмент: featured-видача)

import java.util.List;

public List<CourseCard> featured(List<CourseCard> allCourses) {
    // Якщо ввімкнено maintenance-mode — "заморожуємо" видачу
    if (maintenanceMode) {
        return List.of();
    }

    // Ліміт беремо з maxFeaturedCount, який прийшов із YAML через @Value
    return allCourses.stream()
            .filter(CourseCard::featured)
            .limit(maxFeaturedCount)
            .toList();
}

Якщо ви зараз подумали «стоп, а звідки allCourses?», то ви мислите правильно: у реальному проєкті сервіс візьме дані з репозиторію. Просто щоб не роздувати приклад, ми залишили «джерело даних» за кадром. Логіка, яка нас цікавить, ось вона: за ввімкненого maintenance-mode сервіс віддає порожній результат, а ліміт featured береться з YAML.

Тепер другий практичний момент: дефолт publishedOnly. У вебшарі зазвичай параметр publishedOnly можуть і не передати. І тоді маємо два варіанти: або зашити поведінку в код, або зробити дефолт налаштовуваним. Ми якраз хочемо другий варіант.

Нехай сервіс приймає Boolean publishedOnly, де null означає «користувач параметр не надіслав». І тоді ми застосовуємо defaultPublishedOnly із конфігу.

Файл: CourseCatalogService.java (фрагмент: published-дефолт)

public boolean resolvePublishedOnly(Boolean publishedOnly) {
    // null = параметр був відсутній у запиті, отже використовуємо налаштування з конфігурації
    if (publishedOnly != null) {
        return publishedOnly;
    }
    return defaultPublishedOnly;
}

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

Щоб це реально використовувалося у вебшарі, контролеру достатньо приймати nullable-параметр (якщо він іще не так зроблений). Це не нова тема, просто акуратна форма для нашого сценарію.

Файл: src/main/java/.../catalog/web/CourseCatalogController.java (фрагмент)

import java.util.List;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CourseCatalogController {

    private final CourseCatalogService service;

    public CourseCatalogController(CourseCatalogService service) {
        this.service = service;
    }

    @GetMapping("/api/catalog/courses")
    public List<CourseCard> courses(@RequestParam(required = false) Boolean publishedOnly) {
        // publishedOnly може бути null, якщо query param не передали
        boolean finalPublishedOnly = service.resolvePublishedOnly(publishedOnly);

        return List.of(); // тут у вас буде реальна видача, це лише фрагмент
    }
}

Зверніть увагу: навіть тут ми не читаємо конфіг у контролері. Контролер лише приймає вхід і передає його в сервіс. Налаштування — усередині сервісу, тобто там, де їм і місце.

5. Рефакторимо StartupSummaryRunner

Startup-логіка — чудове місце, щоб відчути, що конфігурація керує застосунком ще до першого HTTP-запиту. Раніше ми додавали ApplicationRunner, щоб робити короткий звіт про старт. Але якщо цей звіт завжди ввімкнено, він швидко почне дратувати (особливо коли ви часто перезапускаєте застосунок). Тому прапорець startup-report-enabled — дуже життєва настройка: він не змінює «що вміє сервіс», але змінює «як він поводиться під час старту».

У цьому блоці ми акуратно впровадимо два значення: app.catalog.startup-report-enabled і app.catalog.title. І заодно покажемо, що можна читати й spring.application.name — просто щоб поєднати «наші налаштування» і «налаштування Boot» в одній картині.

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class StartupSummaryRunner implements ApplicationRunner {

    // Керуємо друком звіту під час старту через конфіг
    private final boolean enabled;

    // Заголовок каталогу беремо з app.catalog.title
    private final String title;

    // Дістаємо стандартне налаштування Spring Boot: spring.application.name
    private final String applicationName;

    public StartupSummaryRunner(
            @Value("${app.catalog.startup-report-enabled:true}") boolean enabled,
            @Value("${app.catalog.title:Catalog}") String title,
            @Value("${spring.application.name:application}") String applicationName) {

        // Усі налаштування й залежності видно прямо в конструкторі
        this.enabled = enabled;
        this.title = title;
        this.applicationName = applicationName;
    }

    @Override
    public void run(ApplicationArguments args) {
        // Якщо прапорець вимкнено — просто мовчки виходимо
        if (!enabled) {
            return;
        }

        System.out.println(applicationName + " started");     // catalog-service запущено
        System.out.println("Catalog title = " + title);        // Заголовок каталогу = Spring+ Catalog
    }
}

Тут важливо одразу кілька моментів. По-перше, ми використовуємо дефолти прямо в плейсхолдерах. Це допомагає пережити ситуацію «ключа ще немає, але клас уже написаний». По-друге, якщо ключі є в YAML, дефолти просто не знадобляться. По-третє, поведінка тепер змінюється одним рядком у application.yaml: можна вимкнути startup report, не торкаючись Java-коду взагалі.

Перевіряємо вплив конфігурації

Коли ви робите такий рефакторинг, мозок іноді підсовує сумнів: «А раптом це просто анотації для краси, і нічого не змінюється?» Найшвидший спосіб розвіяти сумнів — змінити YAML і побачити, що застосунок почав поводитися інакше. Не десь «усередині Spring», а у вашому коді: інший ліміт, інша назва, інше стартове повідомлення.

Уявіть, що ви змінюєте в application.yaml ліміт featured-курсів і вимикаєте звіт під час старту.

Файл: application.yaml (змінений фрагмент)

app:
  catalog:
    max-featured-count: 2
    startup-report-enabled: false

Після перезапуску застосунку ви побачите, що StartupSummaryRunner більше нічого не друкує. А якщо ваш ендпоінт /api/catalog/featured будується через max-featured-count, то видача стане коротшою — і це буде видно прямо в браузері або HTTP-клієнті. Саме такі перевірки формують правильну звичку: конфігурація — це частина поведінки, а не «окремий папірець поруч».

Для кількох параметрів такого маршруту через application.yaml і @Value вистачає з головою: видно, як зовнішній ключ справді змінює поведінку сервісів і раннерів. Але тут важливо не зробити хибний висновок, ніби вся конфігурація Boot — це «один YAML і пара @Value». Сам application.yaml — лише одне джерело значень, а коли налаштувань стає багато, розсип окремих @Value по класах уже заважає бачити конфігурацію як цілісну модель.

6. Типові помилки з YAML і @Value

Помилка №1: залишають значення і в коді, і в YAML (подвійне джерело істини).
Це найпідступніша проблема, бо спочатку здається, що так надійніше. Насправді виходить двовладдя: ви змінюєте application.yaml, але код продовжує використовувати стару константу — або навпаки. Лікується просто: якщо значення стало конфігураційним, у коді не має залишатися його зашитої копії, окрім хіба що дефолту в ${...:default} (і то свідомо).

Помилка №2: ставлять @Value на поле й поступово перетворюють клас на їжака з анотацій.
Полева ін’єкція виглядає коротше, але вона приховує залежності й ламає загальний стиль constructor injection. За кілька тижнів у вас буде клас, де згори десять полів із @Value, а конструктор порожній, і зрозуміти, що потрібно цьому об’єкту для життя, стає неприємно. Якщо тримати @Value на параметрах конструктора, клас читається значно чесніше.

Помилка №3: пишуть ключі абияк і без спільного префікса.
Сьогодні ми тримаємося app.catalog.*, і це не примха. Якщо почати додавати catalogTitle, catalog.title, myapp.catalogTitle, feature.limit — конфіг швидко перетворюється на «ринок у неділю». Один префікс на предметну область і один стиль ключів — це не бюрократія, а здоровий глузд.

Помилка №4: забувають про дефолт там, де він потрібен, і ловлять падіння на старті «Could not resolve placeholder …».
Сама по собі помилка корисна: застосунок чесно каже, що йому не дали потрібного параметра. Але новачок часто сприймає це як «Spring знову свариться незрозуміло на що». Якщо параметр обов’язковий — чудово, нехай падає. Якщо параметр м’який і має розумне значення за замовчуванням — задайте його через ${key:default} і позбавте себе зайвої крихкості на першому кроці.

Помилка №5: починають читати один і той самий ключ у п’яти різних класах.
Це виглядає безневинно, доки ключів мало. Потім ви змінюєте ключ або його зміст — і шукаєте по проєкту всі місця, де він використовується. Поки ключів мало, це ще терпимо, але важливо відчути: @Value — хороший мінімальний міст, але він погано масштабується, якщо перетворюється на основний стиль усієї конфігурації. Тому ми використовуємо його точково: для кількох базових параметрів поведінки, а не для всього підряд.

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