1. Користь метаданих для app.catalog.*
Коли проєкт маленький, вам здається, що ви й так усе памʼятаєте: «ну title, ну maxFeaturedCount, що тут забувати». Але щойно конфігурація розростається, а вона розростається завжди — це не загроза, а статистика, — починається класичний атракціон: помилки в YAML, «чому не підхопилося?», «чому IDE не підказує?», «чому воно працює лише якщо я тричі перезапущу все й принесу каву в жертву». Метадані — це спосіб зробити конфігурацію помітною для IDE та зручною для підказок ще на етапі написання, а не під час падіння застосунку.
Давайте чесно визнаємо: YAML — це текст. Текст дуже дружній, доки ви не зробите одну зайву літеру. Особливо весело, коли у вас є:
app:
catalog:
# Скільки відібраних курсів віддаємо в API
max-featured-count: 4
…і одного дня, у темряві та втомі, зʼявляється:
app:
catalog:
# Помилка в ключі: властивість не привʼяжеться, і ви отримаєте значення за замовчуванням
max-feautred-count: 4
Компілятор вам нічого не скаже, бо він узагалі не знає, що таке YAML. Застосунок теж може не впасти, бо значення просто не привʼяжеться, і ви отримаєте значення за замовчуванням (або 0, або false) — а потім будете думати, що у вас «зламався Spring». Насправді проблема в конфігу, який хтось набрав із помилкою, — тобто в нас із вами.
Метадані конфігурації вирішують цей біль як інженерний інструмент: вони дозволяють IDE розуміти ваші властивості так само, як вона розуміє стандартні spring.* властивості. І це різко зменшує кількість прикрих помилок.
2. Метадані конфігурації: що це
Метадані конфігурації — це не «налаштування Spring Boot», а опис ваших налаштувань для інструментів розробки. Уявіть, що ваші @ConfigurationProperties — це коробка з дротами. Вона працює, але в темряві незрозуміло, куди що встромити. Метадані — це наліпка на коробці: «червоний дріт — живлення, синій — земля, а це взагалі краще не чіпати руками». Застосунок від наліпки напряму не змінює поведінку, зате розробник перестає гадати.
В екосистемі Spring Boot метадані найчастіше мають вигляд JSON-файлу, по суті — «довідника властивостей», який описує:
- імена ваших властивостей (у канонічному вигляді, наприклад app.catalog.max-featured-count);
- їхні типи (наприклад, java.lang.Integer, java.time.Duration, java.lang.Boolean);
- іноді — опис і підказки;
- іноді — значення за замовчуванням та інформацію про застарілість.
Важливо одразу сказати: metadata не замінює привʼязування і не замінює валідацію. Вона відповідає за інший шар — за досвід розробника.
Щоб не плутати ці три речі, зручно тримати в голові таку таблицю:
| Механізм | Коли працює | Що дає | Чого не робить |
|---|---|---|---|
| @ConfigurationProperties binding | під час запуску застосунку | перетворює YAML на типи Java | не захищає від логічно неправильних значень |
| Bean Validation (@Validated, @NotBlank і т. ін.) | під час запуску застосунку | перевіряє коректність значень, fail-fast | не допомагає вам швидше писати YAML без описок |
| Configuration metadata | під час збирання (build-time) | підказки IDE, зручність пошуку, документація властивостей | не впливає на поведінку застосунку під час виконання |
І так: Spring Boot уже постачається з метаданими для своїх стандартних властивостей (server.port, spring.jackson.*, management.endpoints.* і т. д.), тому IDE часто підказує їх «з коробки». Наше завдання — зробити так, щоб наші app.catalog.* теж були повноцінними учасниками, а не випадковими рядками.
3. Хто створює метадані: spring-boot-configuration-processor
Тепер — головний герой лекції. spring-boot-configuration-processor — це процесор анотацій, який запускається під час компіляції та аналізує ваш код у пошуках класів або records, позначених @ConfigurationProperties. На основі знайдених моделей він генерує файл метаданих, який потім використовують IDE та інші інструменти.
Важливо: це саме історія build-time. Тобто це не той компонент, який живе всередині Spring ApplicationContext, не бін, не автоконфігурація, не магія під час старту. Це радше помічник на етапі збирання, як людина, яка перед виставою підписує реквізит: «це меч», «це стілець», «це корона, не переплутай».
Якщо описати потік максимально просто, виходить така схема:
flowchart TD
A["Ваші вихідні Java @ConfigurationProperties records/classes"] --> B["javac"]
A --> C["Обробка анотацій spring-boot-configuration-processor"]
C --> D["META-INF/spring-configuration-metadata.json"]
B --> E[".class файли"]
D --> F["IDE читає метадані та підказує властивості в YAML"]
Із схеми видно головне: processor читає ваш код, генерує опис, і цей опис допомагає вам писати конфігурацію. Усе.
І тут новачки часто плутаються: «Але якщо це processor, значить без нього застосунок не стартує?» Ні. Застосунок стартує. Просто YAML ви будете писати так, ніби на дворі 2003 рік: на око, на інтуїції та з молитвою.
4. Підключення процесора в Gradle
Найчастіша помилка з spring-boot-configuration-processor — підключити його як звичайну runtime-залежність, а потім дивуватися, чому «нічого не змінилося» (або чому залежностей у проєкті стало більше, ніж у типовій соціальній мережі мікросервісів).
Правильна думка така: раз processor потрібен під час компіляції, отже він має бути в Gradle-конфігурації annotationProcessor, а не в implementation.
Мінімально коректний варіант виглядає так:
dependencies {
// Потрібен лише на етапі компіляції: генерує метадані для IDE
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
}
Якщо десь ви бачили ще compileOnly поруч із цим процесором — не лякайтеся. Це не обовʼязкова частина для нашого курсу, але іноді її додають, щоб IDE стабільніше бачила processor під час своїх внутрішніх механізмів імпорту проєкту:
dependencies {
// Щоб процесор було видно як залежність (іноді допомагає IDE під час імпорту)
compileOnly("org.springframework.boot:spring-boot-configuration-processor")
// Запуск annotation processor під час компіляції
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
}
Ключовий момент, який важливо закріпити: ми не пишемо версії вручну. У нашому проєкті версіями керує Spring Boot BOM, і spring-boot-configuration-processor теж підтягується у погодженій версії.
Ще один маленький, але важливий нюанс: processor має сенс лише тоді, коли Gradle/IDE справді запускають обробку анотацій. Зазвичай у сучасному Gradle + IntelliJ IDEA після імпорту проєкту все працює, але якщо ви бачите, що IDE взагалі не підказує app.catalog.*, то часто проблема не в Spring, а у вимкненій обробці анотацій у налаштуваннях IDE. Це один із тих випадків, коли причина не в коді, а в прапорці, який ховається десь глибоко.
5. Runtime і build-time: різні світи
Дуже хочеться зліпити в голові дві думки в одну: «Я додав @ConfigurationPropertiesScan, отже в мене буде metadata». І навпаки: «Я додав configuration processor, отже мої properties-класи зареєструвалися». Ні й ні. Це різні механіки, які розвʼязують різні завдання.
Щоб не плутати, корисно побачити обидві частини на одному прикладі з catalog-service.
Runtime-частина: застосунок має вміти створити бін CatalogProperties і привʼязати в нього YAML. Для цього нам потрібен scan:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@SpringBootApplication // основна точка входу в Spring Boot застосунок
@ConfigurationPropertiesScan // підхоплюємо @ConfigurationProperties як біни
public class CatalogServiceApplication {
public static void main(String[] args) {
// запускаємо Spring ApplicationContext
SpringApplication.run(CatalogServiceApplication.class, args);
}
}
Build-time-частина: Gradle має під час компіляції запустити processor і згенерувати метадані. Це взагалі не видно в runtime-коді — це живе в build.gradle.kts:
dependencies {
// Генерація spring-configuration-metadata.json під час компіляції
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
}
Якщо спростити до людського: @ConfigurationPropertiesScan відповідає за те, щоб застосунок розумів конфігурацію під час запуску. Processor відповідає за те, щоб розробнику було зручно писати конфігурацію до запуску.
І ось тут зʼявляється корисна інженерна звичка: якщо ви ловите себе на думці «чому не працює?», спочатку уточніть, про що саме йдеться — runtime чи build-time. Це дуже економить години життя й рятує клавіатури від ударів.
6. Що генерується і де лежать метадані
У метаданих є цілком конкретний результат: JSON-файл із відомою назвою. Зазвичай він називається spring-configuration-metadata.json і лежить у META-INF у вихідному каталозі класів.
У термінах Gradle-проєкту ви зазвичай знайдете його приблизно тут:
build/classes/java/main/META-INF/spring-configuration-metadata.json
Вміст файлу доволі обʼємний, але за змістом там є два головні масиви: groups і properties. Для розуміння достатньо побачити мініфрагмент, який стосується однієї властивості:
{
"properties": [
{
"name": "app.catalog.max-featured-count",
"type": "java.lang.Integer",
"description": "Максимальна кількість відібраних курсів, що показуються в API."
}
]
}
Будь ласка, не сприймайте цей JSON як щось, що потрібно писати вручну або додавати в git. Це артефакт збирання. Він має зʼявлятися автоматично.
І важливий анти-магічний момент: processor нічого не вигадує з повітря. Якщо він не бачить @ConfigurationProperties, він не розуміє, що це конфігураційна модель. Якщо клас лежить у проєкті, але не позначений анотацією, для нього це просто звичайний клас. Тому metadata — це продовження вашої ж дисципліни: якщо ви хочете, щоб властивості були «офіційними», оформлюйте їх як @ConfigurationProperties, а не як випадковий набір @Value.
7. Корисні метадані: структура і JavaDoc
Найсумніші метадані — ті, що є, але не допомагають. Зазвичай вони не допомагають з однієї причини: у властивостей немає описів, і IDE може підказати лише імʼя та тип. Це все одно краще, ніж нічого, але можна зробити приємніше.
Гарна новина: processor уміє підхоплювати описи з JavaDoc. І record тут несподівано зручний: ви можете документувати компоненти прямо в заголовку, поруч із полем. Виходить майже як документація до параметрів функції — компактно й по суті.
Приклад у контексті catalog-service:
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("app.catalog")
public record CatalogProperties(
/** Назва для сторінки-заставки та відповідей API. */
String title,
/** Максимальна кількість відібраних курсів, що показуються в /api/catalog/featured. */
int maxFeaturedCount
) { }
Сенс тут не в тому, що «JavaDoc — це красиво». Сенс у тому, що за кілька тижнів ви забудете, чому maxFeaturedCount стосується саме добірки featured, а не ліміту загального списку курсів. Підказка IDE з description — це як маленьке рятівне коло.
Ще один нюанс: метадані добре працюють, коли ви тримаєте структуру акуратною й у просторі імен. Ми вже вибрали префікс app.catalog.*. Це не просто «щоб було модно». Це робить вашу конфігурацію схожою на нормальний модуль: усе, що стосується каталогу, лежить в одному просторі імен. Для IDE це теж корисно: ви набираєте app. — і бачите весь ваш «прикладний namespace».
Нарешті, корисно памʼятати, що метадані не зобовʼязані бути гігантськими. Вони не мають описувати кожен чих проєкту, якщо ви не збираєтеся це справді конфігурувати. У нашому курсі конфігурація має залишатися керованою: краще мати менше властивостей, але добре описаних, ніж сотню прапорців у стилі enableAdvancedSuperMode, які ніхто не памʼятає, навіщо додали.
8. Типові помилки під час роботи з метаданими
Помилка № 1: підключити spring-boot-configuration-processor через implementation.
Так ви перетворюєте інструмент етапу збирання на runtime-залежність, хоча він не потрібен застосунку під час запуску. У найкращому випадку нічого страшного не станеться, але проєкт буде важчим, а в новачка зʼявиться хибне враження, що це частина runtime-магії. Правильне місце — annotationProcessor.
Помилка № 2: чекати, що метадані змінять поведінку застосунку.
Метадані роблять життя зручнішим в IDE, але вони не перевіряють значення, не змінюють пріоритети, не вмикають профілі й не додають біни. Якщо конфігурацію зламано логічно, це ловить Bean Validation; якщо властивість не привʼязалася, питання в привʼязуванні, структурі або ключах. Метадані — про підказки та документацію, а не про «полагодити все».
Помилка № 3: плутати runtime scan і build-time processor.
@ConfigurationPropertiesScan потрібен, щоб CatalogProperties зʼявився як бін і заповнився значеннями під час запуску. Processor потрібен, щоб IDE підказувала ваші ключі. Можна мати scan без processor (застосунок працюватиме, але писати YAML буде незручно). Можна мати processor без scan (IDE підкаже ключі, але застосунок не створить бін). Потрібні обидва, бо вони закривають різні частини життя проєкту.
Помилка № 4: розраховувати на метадані, коли клас не позначений @ConfigurationProperties.
Processor дивиться на @ConfigurationProperties, бо саме вона визначає: «це конфігураційна модель». Якщо ви залишили клас без анотації (або взагалі читаєте властивості через @Value), метадані для таких розрізнених рядків не зʼявляться. І це чесно: не можна навести лад там, де ви його не створюєте.
Помилка № 5: очікувати підказок в IDE, але при цьому вимкнути annotation processing у налаштуваннях.
Іноді проєкт зібрано правильно, залежність додано правильно, а підказок усе одно немає. У таких випадках проблема нерідко не в Spring Boot, а в тому, що IDE не запускає обробку анотацій або не підхоплює її з Gradle-моделі. Це неприємно, але лікується налаштуваннями, а не переписуванням CatalogProperties «про всяк випадок».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ