JavaRush /Курси /Spring Boot /Web- binding і confi...

Web- binding і config- binding у Spring

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

1. Термін binding: одне слово, різні механізми

Точку розширення MVC ми вже підготували. Але перш ніж під’єднувати до неї конвертери та форматувальники, важливо не переплутати два дуже схожі на вигляд потоки даних: рядок із HTTP-запиту та рядок із конфігурації застосунку. Зовні обидва схожі на «якийсь текст», а всередині в них різні правила, різний час життя і різні місця налаштування.

Слово binding у Spring — це як слово «ключ» в українській мові: ключ від дверей, ключ у музиці й ключ у криптографії наче пишуться однаково, але краще не намагатися відкрити квартиру нотним ключем, хоч ідея драматична. У Spring під «binding» зазвичай розуміють «перетворення зовнішніх даних на Java-типи»: рядки в int, текст у enum, JSON в об’єкт, YAML у налаштування тощо.

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

Джерела рядків: URL запиту та application.yaml

Дуже корисно одразу й чесно розділити: звідки саме прийшов рядок, який Spring зараз намагається перетворити на LocalDate або CourseTrack. Для web-binding джерело майже завжди одне й те саме: HTTP-запит. Query-параметри (?limit=20&track=SPRING) і path variables (/courses/spring-boot) фізично приходять як текст в URL. Плюс є заголовки, кукі, тіло запиту — але сьогодні нам важливі саме query-параметри.

Для конфігураційного binding джерело інше: файли конфігурації (application.yaml), змінні середовища, system properties, CLI args. Вони теж текстові за природою, навіть якщо YAML виглядає «структурно». І ось тут мозок новачка робить логічний стрибок: «О! І там рядки, і тут рядки. Отже, конвертація одна й та сама». А Spring такий: «Чудово. Але ні».

Щоб закріпити, порівняймо на одному прикладі — рядок у URL і рядок у YAML.

URL (web):

/api/catalog/courses?limit=20&track=SPRING

YAML (config):

app:
  catalog:
    max-featured-count: 4 # ліміт "featured" курсів, політика сервісу

В обох випадках є 20, SPRING і 4. Але в першому випадку це введення користувача або клієнта в конкретному запиті, а в другому — налаштування застосунку як системи. І далі ці дані мандрують по різних конвеєрах.

2. Web-binding у MVC: ?limit=20 перетворюється на int

Розберімо шлях query-параметра без магії. Браузер або клієнт надсилає запит, в URL лежать рядки, і Spring MVC має розв’язати дві задачі: який метод контролера викликати та як підготувати аргументи цього методу. Для @RequestParam Spring бере значення параметра як рядок і намагається перетворити його на тип параметра методу: int, boolean, enum, LocalDate тощо. Це відбувається на кожному запиті, безпосередньо перед викликом вашого контролера.

Схематично, дуже спрощено, web-binding виглядає так:

flowchart TD
    A["HTTP-запит
...?limit=20"] --> B["Spring MVC
аргументи методу"] B --> C["ConversionService
рядок -> тип"] C --> D["@RequestParam int limit"] D --> E["Виклик методу контролера"]

Найзручніше те, що якщо ви одразу оголошуєте типи в сигнатурі методу, контролер стає коротким і чесним. Він не «парсить рядок», а описує контракт: «мені потрібні int limit і CourseTrack track».

Наприклад:

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

import java.util.List;

@GetMapping("/api/catalog/courses") // кінцева точка каталогу курсів
public List<CourseCard> findCourses(
        @RequestParam(defaultValue = "20") int limit, // MVC перетворить рядок на int (або поверне 400 у разі помилки)
        @RequestParam(required = false) CourseTrack track // null, якщо параметр не передали
) {
    // Контролер не займається парсингом і валідацією "вручну" — він приймає типи
    return service.find(limit, track);
}

Тут ключовий момент: limit і track у запиті приходять рядками, але ваш метод не бачить їх як рядки. Цю «брудну роботу» робить MVC.

І ще один момент, який часто дивує: якщо Spring не зміг перетворити рядок на потрібний тип, ваш метод взагалі не викликається. Контролер навіть не встигає почати роботу. Зазвичай це закінчується стандартною відповіддю з помилкою, часто 400 Bad Request, — без того, щоб ви вручну ловили NumberFormatException. Це не «Spring злий», а спроба захистити вас від ручного парсингу в кожному методі.

3. MVC ConversionService: перекладач між рядком і типом

Якщо уявити Spring MVC як аеропорт, то ConversionService — це та сама людина біля стійки, яка перекладає «пасажирську» мову, тобто рядки в запиті, на «службову» мову, тобто Java-типи. Ви майже не бачите її в коді контролера, але саме вона пояснює, чому можна написати @RequestParam int limit і не займатися Integer.parseInt(...) під кожною кінцевою точкою.

Важливо не переплутати: ConversionService не про JSON і не про Jackson. Jackson підключається, коли йдеться про тіло запиту або відповіді в JSON, і то через HTTP message converters. Query-параметри — це переважно про conversion і formatting, а не про серіалізацію.

Якщо спробувати зробити все «вручну», контролер миттєво перетворюється на «String-центр», де життя складається з parseInt, trim, toUpperCase і тихої ненависті до користувачів, які надіслали limit= 20 з пробілами.

Анти-приклад (так можна, але так сумно):

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

import java.util.List;

@GetMapping("/api/catalog/courses")
public List<CourseCard> findCourses(@RequestParam String limit) {
    // Вручну тягнути рядок із запиту — майже завжди погана ідея: це розповзеться по контролерах
    int value = Integer.parseInt(limit.trim()); // може впасти, і буде не дуже красиво
    return service.find(value, null); // доменні параметри ви втрачаєте, поки намагаєтеся "розпарсити" життя
}

Коли ви бачите такий код, майже завжди варто зупинитися і запитати себе: «А я точно хочу, щоб кожен мій контролер займався ручним розбором рядків?» Зазвичай відповідь така: «Ні. Я хочу жити».

І тут згадуємо, що в лекції 1 ми завели WebConfiguration implements WebMvcConfigurer. Саме сюди, за потреби, додають правила web-конвертації, якщо стандартних можливостей не вистачає. Важливо: це стосується web-шару, тобто значень, що приходять із запиту. Це не «універсальний конвертер на весь застосунок».

4. Config-binding у Spring Boot: рядки в налаштування застосунку

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

Схема, теж спрощена, виглядає так:

flowchart TD
    A["application.yaml / env vars / args"] --> B["Spring Boot binder"]
    B --> C["Java-об'єкти налаштувань"]
    C --> D["Біни отримують налаштування
через DI"]

І так, тут теж є «binding»: рядкові значення з YAML мають стати int, boolean, enum, Duration тощо. Але цей binding живе у світі конфігурації та стартового створення застосунку, а не у світі HTTP-запитів.

Для ілюстрації, чисто щоб побачити інший контекст, можна уявити майбутню конфігураційну модель CatalogProperties:

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("app.catalog") // префікс ключів у application.yaml
public class CatalogProperties {

    // Політика застосунку як системи, а не "параметр запиту"
    private int maxFeaturedCount;

    public int getMaxFeaturedCount() {
        return maxFeaturedCount; // те, що прочиталося з YAML/env/args на старті
    }

    public void setMaxFeaturedCount(int maxFeaturedCount) {
        // Якщо тут опиниться "banana" замість числа — це спливе на старті, а не на конкретному HTTP-запиті
        this.maxFeaturedCount = maxFeaturedCount;
    }
}

І відповідний YAML:

app:
  catalog:
    max-featured-count: 4

Зверніть увагу на дві відмінності, які часто вислизають.

По-перше, в YAML ключ max-featured-count (kebab-case), а в Java поле maxFeaturedCount (camelCase). Для конфігураційного binding це нормальна ситуація: Spring Boot уміє пов’язувати такі імена. У web-binding такого автоматичного «перейменування» зазвичай не мають на увазі, тому що web-binding працює з іменами параметрів запиту (?limit=), а не з «ключами налаштувань».

По-друге, якщо конфігурацію зламано, наприклад замість числа в YAML лежить banana, це проявиться на старті. Це не «поганий запит», а «застосунок як система не може стартувати». І поведінка тут інша: частіше ви отримаєте помилку старту, а не акуратну реакцію на один конкретний HTTP-запит.

5. Порівняння: web-binding vs config-binding

Щоб перестати плутатися, корисно один раз порівняти механіки як дві різні підсистеми. Вони справді обидві конвертують рядки, але мета в них різна, і тому правила та точки кастомізації теж відрізняються.

Характеристика Web-binding (Spring MVC) Config-binding (Spring Boot)
Джерело даних HTTP-запит (query params, path variables тощо) application.yaml, env vars, system properties, args
Коли відбувається На кожен запит, перед викликом контролера Під час старту застосунку, під час створення контексту
Основна мета Перетворити вхідні дані клієнта на типи для обробника запиту Налаштувати поведінку застосунку без правки коду
Де ви бачите результат У параметрах методу контролера В об'єктах налаштувань, які впроваджуються через DI
Що при помилці конвертації Зазвичай запит не доходить до методу контролера, клієнт отримує помилку Часто проблему видно під час старту: застосунок не зібрано або зібрано некоректно
Куди додають правила конвертації Точки розширення MVC (WebMvcConfigurer, converters/formatters) Конфігураційна модель і механіки Boot binder (у межах курсу — пізніше)

Головний практичний висновок дуже простий: якщо ви зараз дивитеся на @RequestParam, думайте «MVC». Якщо дивитеся на YAML і налаштування сервісу, думайте «Boot config». Не намагайтеся лікувати одне через налаштування іншого, як не намагаються лагодити принтер налаштуваннями мікрохвильової печі, хоча в офісі іноді і таке буває.

6. catalog-service: типи в контролері і типи в конфігу

Тепер приземлимо теорію на наш catalog-service, де є доменні типи CourseTrack, CourseLevel і фільтри в кінцевих точках каталогу. У web-шарі ми хочемо, щоб контролер приймав доменні типи, а не рядки. Це робить сигнатуру читабельною: метод одразу показує, які фільтри підтримує кінцева точка.

Наприклад, умовно так:

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

import java.time.LocalDate;
import java.util.List;

@GetMapping("/api/catalog/courses")
public List<CourseCard> findCourses(
        @RequestParam(required = false) CourseTrack track, // прийде рядком із URL, MVC перетворить у enum/тип
        @RequestParam(required = false) CourseLevel level, // аналогічно: web-binding, а не читання з YAML
        @RequestParam(required = false) LocalDate launchedAfter // формат дати визначають правила MVC conversion/formatting
) {
    // Тут уже типи, а не "сирий текст": сервісу простіше жити
    return service.find(track, level, launchedAfter);
}

Тут track, level, launchedAfter — це web-binding. Вони беруться з URL, і Spring MVC намагається їх перетворити.

А тепер уявіть конфігураційний шар, не заглиблюючись у деталі: наприклад, ліміт featured-курсів. Це не введення користувача, а політика сервісу. Сьогодні ліміт 4, завтра 6 — і ми хочемо змінювати це без правки коду. Такий параметр логічно має жити в конфігурації застосунку, а не в query-параметрі кожного запиту.

Наприклад, ось так, чисто як ідея конфігурації:

app:
  catalog:
    max-featured-count: 4

І тут народжується класична плутанина: новачок дивиться на max-featured-count і думає, що MVC автоматично застосує це правило до @RequestParam int limit. Ні, це різні шари. MVC не читає ваші app.catalog.* як «значення за замовчуванням для запиту». Для запиту значення за замовчуванням задається або defaultValue прямо в @RequestParam, або логікою в сервісі, або окремою доменною політикою, але це все не «магічно з YAML на кожну кінцеву точку».

7. Де налаштовувати конвертацію

Після цієї лекції хочеться поставити в голові два маленькі дорожні знаки. Перший: WebMvcConfigurer та його методи — це про web-шар. Якщо ви додаєте правила в addFormatters, ви впливаєте на те, як Spring MVC перетворює рядок із URL на Java-тип. Це чудово, корисно і безпечно, але не робить вашу конфігурацію «раптом типобезпечною» і не налаштовує те, як Boot читає application.yaml.

Другий знак: конфігураційні механізми Boot — це про те, як застосунок збирається і живе як система. Вони не зобов’язані збігатися з правилами web-binding, бо задачі різні. Конфіг — це керованість сервісу, web-binding — це зручний і передбачуваний контракт кінцевих точок.

Якщо хочеться запам’ятати це однією картинкою, можна так:

flowchart TD
    subgraph R["Під час виконання: кожен HTTP-запит"]
      Q["URL: ?track=SPRING"] --> MVC["MVC binding
(ConversionService)"] MVC --> C["Аргументи методу контролера"] end
flowchart TD
    subgraph S["Startup: старт застосунку"]
      Y["YAML / env / args"] --> B["Boot config binding
(binder)"] B --> P["Властивості/налаштування"] end

Щойно ви починаєте думати: «А можна одним налаштуванням змусити і запити розбиратися, і config binding працювати однаково?», — найімовірніше, ви намагаєтеся склеїти дві різні підсистеми. У Java це зазвичай закінчується тим, що ви отримуєте «універсальний» код, який складніший і менш передбачуваний, ніж два прості рішення на своїх місцях.

8. Типові помилки під час web- та config-binding

Перед тим як ми підемо далі за планом, корисно спокійно проговорити типові помилки, на які наступають майже всі. Це не «соромні помилки» — це нормальна частина дорослішання у Spring, тому що термінологія схожа, а механіки різні. Але якщо впізнавати ці ситуації заздалегідь, ви зекономите собі багато часу й трохи нервових клітин, а вони, як відомо, у бекенді не відновлюються, хоча логів ми напишемо достатньо.

Помилка №1: вважати, що web-binding і config-binding — це один механізм, бо «і там рядки, і тут рядки».
Зазвичай це проявляється так: розробник додає налаштування в application.yaml і чекає, що @RequestParam почне оброблятися «за тими самими правилами». У реальності MVC і Boot binder — різні підсистеми, і зміни в одній не зобов’язані впливати на іншу. Лікується дуже просто: завжди ставте собі запитання «це значення прийшло з HTTP чи з конфігурації застосунку?».

Помилка №2: залишати параметри контролера рядками й вручну парсити їх у кожному методі.
Такий код швидко розповзається: trim, parseInt, toUpperCase починають жити в контролері, і контролер перетворюється на суміш «контракт API» і «бібліотека парсингу рядків». Spring MVC уміє конвертувати базові типи сам, а для доменних форматів є правила, які можна централізувати. Контролеру краще залишатися тонкою межею, а не фабрикою з виробництва NumberFormatException.

Помилка №3: очікувати, що defaultValue у @RequestParam — це те саме, що «дефолт у конфігурації».
defaultValue — це правило для конкретної кінцевої точки та конкретного параметра запиту: якщо параметр не прийшов, MVC підставить рядок і спробує перетворити його на тип. Конфігураційне значення за замовчуванням — це правило поведінки сервісу як системи. Це різні рівні. Якщо змішати їх, ви отримаєте API, у якому частина значень за замовчуванням «розмазана» по контролерах, а частина живе в налаштуваннях, — і дуже важко зрозуміти, що насправді є політикою застосунку.

Помилка №4: намагатися «полагодити» незрозумілу поведінку bindingʼа вмиканням @EnableWebMvc.
Це як лікувати дивний звук у машині зняттям дверей: іноді звук справді зникає, але тепер у вас інша проблема. @EnableWebMvc перемикає MVC у більш ручний режим і легко ламає корисні Boot defaults. Якщо ваша мета — точкове налаштування конвертації вхідних значень, коректніше триматися WebMvcConfigurer і невеликих додавань поверх автонастроювання.

Помилка №5: думати, що якщо тип один і той самий (CourseTrack), то й правила введення мають бути «універсальними».
CourseTrack може приходити із запиту, де користувачі люблять писати java-backend, і з конфігурації, де зазвичай пишуть акуратні значення на кшталт JAVA_BACKEND. Ці джерела поводяться по-різному, і ви маєте право обробляти їх по-різному. Уніфікація будь-якою ціною часто робить гірше: ви ускладнюєте систему заради краси, а не заради зрозумілої поведінки.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ