JavaRush /Курси /Spring Boot /HTTP‑запит у контролері Spring MVC

HTTP‑запит у контролері Spring MVC

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

1. Життєвий цикл запиту: практична користь

Тепер уже видно, що web starter підіймає не лише порт, а й усю MVC‑інфраструктуру. Наступне природне запитання дуже приземлене: як конкретний HTTP‑запит проходить крізь цю інфраструктуру і зрештою опиняється в методі контролера?

Коли ви тільки починаєте писати вебкод, дуже хочеться вірити в просту казку: «Я написав @GetMapping — отже, запит туди потрапить». Іноді так і буває. А іноді — ні. Саме тут у новачка часто вмикається режим «комп’ютер зламався», «Spring дивний», «я не створений для бекенду». Насправді найчастіше ламається щось приземлене: шлях не збігся, метод не той, параметр не розпарсився, сервер узагалі не піднявся, контролер не став біном.

Тому наше завдання сьогодні — зібрати зрозумілий ланцюжок обробки: хто першим зустрічає HTTP‑запит, хто вирішує, який метод викликати, хто дістає slug з URL і чому publishedOnly=false раптом перетворюється на boolean без вашого ручного Boolean.parseBoolean(...). Це не глибокі «внутрішності Spring заради внутрішностей», а найпрактичніша навичка: уміти пояснити, чому запит потрапив — або не потрапив — у контролер.

Щоб було простіше тримати це в голові, уявіть, що запит — це посилка, а Spring MVC — сортувальний центр. Посилка приїжджає в місто (порт), потім потрапляє до центрального термінала (DispatcherServlet), там за адресою вирішують, у який відділ її відправити (HandlerMapping), потім співробітник оформлює видачу (HandlerAdapter), а на виході посилку пакують у потрібний формат (HttpMessageConverter). Так, це звучить як логістика. Але так усе ж приємніше, ніж «магія».

2. Запит: Tomcat → HttpServletRequest

Найважливіше, що потрібно прийняти: HTTP‑запит спочатку потрапляє не в Spring, а в вебсервер/контейнер. У нашому випадку (після підключення spring-boot-starter-webmvc) Spring Boot підіймає вбудований Tomcat, який починає слухати порт (зазвичай 8080) і приймати TCP‑з’єднання. Tomcat — це «двері» в застосунок. Він не знає про @GetMapping, @PathVariable та інші принади — він просто обслуговує HTTP і вміє викликати сервлети.

На цьому етапі «байти з мережі» перетворюються на об’єкти Servlet API:

  • jakarta.servlet.http.HttpServletRequest — те, що прийшло від клієнта;
  • jakarta.servlet.http.HttpServletResponse — те, що ми будемо відправляти назад.

Корисно один раз розкласти URL на частини, бо 50% проблем новачка починаються з плутанини між path і query parameters:

# Повний URL: path + query string
http://localhost:8080/api/catalog/courses/spring-boot?publishedOnly=true&limit=10
\______/ \___________/ \______________________________/ \______________________/
 schema     host:port                шлях                  рядок запиту

HttpServletRequest містить (спрощено) приблизно таку інформацію: HTTP‑метод (GET), шлях (/api/catalog/...), query string (publishedOnly=true&limit=10), заголовки (Accept, User-Agent тощо), а також інші деталі запиту.

Можна уявити це у вигляді невеликої таблиці:

Що потрібно зрозуміти Де це живе в запиті Як Spring зазвичай дістає
HTTP‑метод GET / ... за @GetMapping, @PostMapping тощо
Шлях /api/catalog/courses/{slug} за шаблоном маршруту
Параметр шляху фрагмент шляху @PathVariable
Параметр запиту після ? @RequestParam
Тіло запиту (не сьогодні) (не сьогодні)

І ось тепер ключовий момент: Tomcat повинен зрозуміти, якому сервлету передати запит. І в Spring MVC є один сервлет, який «зустрічає майже все».

3. DispatcherServlet — головний «диспетчер» Spring MVC

У Spring MVC центральна фігура — це DispatcherServlet. Його роль найпростіше описати так: це один вхід у MVC‑світ, який далі розбирає, який контролер і який метод повинні обробити запит.

Дуже корисно запам’ятати патерн, який тут використовується: Front Controller. Замість того щоб реєструвати в контейнері тисячу маленьких сервлетів на кожен шлях, Spring реєструє один великий «фронтальний» сервлет, який уже всередині себе робить маршрутизацію і викликає ваш код.

Якщо намалювати шлях запиту зовсім схематично, вийде так:

flowchart TD
    A[Клієнт: браузер / Postman / curl] -->|HTTP‑запит| B[Embedded Tomcat]
    B --> C[DispatcherServlet]
    C --> D[HandlerMapping: шукаємо відповідний метод]
    D --> E[HandlerAdapter: готуємо виклик]
    E --> F[Controller method]
    F --> G[ReturnValueHandler + HttpMessageConverter]
    G -->|HTTP‑відповідь| A

Важливо розуміти: DispatcherServlet не існує сам по собі. Він живе всередині Spring Boot застосунку як частина інфраструктури, яку Boot підіймає для вас. Тобто ви зазвичай не створюєте його вручну. Але ви можете побачити його в логах старту — наприклад, рядки про ініціалізацію сервлета dispatcherServlet. Для новачка це добрий сигнал: «ага, MVC‑інфраструктура справді піднялася».

І ще один дуже важливий момент: controller ми не викликаємо вручну. Жодних new CourseCatalogController() і controller.findAll() у main(). Контролер — це бін у ApplicationContext, і Spring викликає його метод як частину обробки запиту.

4. Пошук handler-методу за маршрутом

Тепер наймеханічніше і водночас найкорисніше: як Spring вирішує, який саме Java‑метод потрібно викликати для конкретного запиту.

Коли застосунок стартує, Spring MVC сканує ваші controller‑класи і будує таблицю відповідностей: (HTTP‑метод + шлях + додаткові умови) → метод. У повсякденній роботі це виглядає як знайомі анотації @GetMapping, @PostMapping, @RequestMapping тощо.

У межах поточної лекції нам достатньо @GetMapping: вона говорить Spring MVC, що цей метод — обробник GET‑запитів за вказаним шляхом.

Давайте створимо невеликий навчальний endpoint у catalog-service, щоб на нього можна було спокійно дивитися в браузері чи через Postman, не заважаючи майбутнім маршрутам каталогу. Покладемо його в пакет catalog.web (ми вже домовилися, що web‑шар живе там).

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

@RestController
class CatalogDebugController {

    // Простий debug-endpoint: якщо ви бачите відповідь — отже mapping і контролер піднялися
    @GetMapping("/api/catalog/debug/courses")
    String courses() {
        // Повертаємо рядок: Spring MVC покладе його в тіло HTTP-відповіді
        return "courses";
    }
}

Тут важливо одразу кілька речей.

По-перше, шлях повинен збігатися символ у символ (включно зі слешами). Якщо ви випадково напишете "/api/catalog/debug/course" замість "/api/catalog/debug/courses", жодна магія не «здогадається». Ви отримаєте 404, і це буде чесно.

По-друге, враховується HTTP‑метод. Якщо ви відправите POST на цей шлях, Spring може сказати: «маршрут є, але POST не підтримано» — це вже не 404, а інша історія.

По-третє, Spring викликає цей метод не тому, що ви десь явно його зареєстрували, а тому що контролер потрапив у component scan і став біном. Тобто працездатність маршруту залежить і від того, де лежить клас (пакет), і від того, чи застосунок узагалі стартував.

5. Аргументи методу: @PathVariable і @RequestParam

До цього моменту ми говорили: Spring вибрав метод. Але зазвичай нам потрібно не просто вибрати метод, а ще й передати йому дані із запиту: slug, прапорці фільтрації, ліміт тощо. І ось тут починається та частина MVC, яка найбільше схожа на фразу «у мене рядок, а стало число — як?!».

@PathVariable: дані зі шляху

Якщо частина даних логічно є частиною адреси ресурсу, ми зазвичай кладемо її в path. Класичний приклад — отримати курс за slug.

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

@RestController
class CatalogDebugBySlugController {

    // {slug} у шляху — це "дірка", значення з неї Spring підставить в аргумент методу
    @GetMapping("/api/catalog/debug/courses/{slug}")
    String bySlug(@PathVariable String slug) {
        // Для наочності повертаємо те, що прийшло в path variable
        return slug;
    }
}

Що тут відбувається по кроках:

1. Приходить запит, наприклад GET /api/catalog/debug/courses/spring-boot.

2. DispatcherServlet отримує керування.

3. MVC знаходить mapping /api/catalog/debug/courses/{slug} і розуміє, що {slug} — це «дірка» у шляху.

4. Значення spring-boot витягується і потрапляє в аргумент slug.

@PathVariable — це пряма відповідь на запитання: «як витягнути фрагмент URL‑шляху у змінну». І тут теж є типова помилка новачка: забути фігурні дужки в шаблоні або назвати змінну не так.

Наприклад, якщо ви напишете @GetMapping("/.../{courseSlug}"), а в методі буде @PathVariable String slug, Spring не зможе здогадатися, що slug = courseSlug (він це вміє, але за певних умов; новачку краще на це не сподіватися). На перших порах краще, щоб імена збігалися.

@RequestParam: дані з query string

Query parameters — це те, що йде після ? в URL. Зазвичай туди кладуть фільтри, прапорці та інші необов’язкові параметри.

Зробімо ще один debug endpoint, який показує, як Spring перетворює publishedOnly=false на boolean.

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

@RestController
class CatalogDebugQueryController {

    @GetMapping("/api/catalog/debug/query")
    String query(
            // Якщо параметр не прийшов — використовуємо значення за замовчуванням
            @RequestParam(defaultValue = "true") boolean publishedOnly
    ) {
        // Spring сам перетворює рядок із query string на boolean (якщо це можливо)
        return "publishedOnly=" + publishedOnly;
    }
}

Запити до нього можуть виглядати так:

# Без параметра — спрацює defaultValue
GET /api/catalog/debug/query

# Явно передаємо параметр
GET /api/catalog/debug/query?publishedOnly=false

І ось тут проявляється один із найкорисніших для новачка фактів: у HTTP усе — рядки, але Spring MVC вміє їх конвертувати у прості типи. Тому boolean, int, long та інші базові речі зазвичай «просто працюють». Це не магія, а набір конвертерів усередині MVC. Деталі ми залишимо на наступний рівень, коли говоритимемо про тонке налаштування MVC, але сам факт важливо прийняти вже зараз.

Якщо параметр не передано, спрацьовує defaultValue = "true", і ви отримуєте передбачуване значення. Це значно простіше, ніж вручну перевіряти null, парсити рядок і думати, що робити за відсутності параметра.

Але є і зворотний бік: якщо ви передасте publishedOnly=banana, то конвертація в boolean не вдасться, і ви отримаєте помилку (зазвичай 400). Це нормально: Spring не може вгадати, що ви мали на увазі під «banana». Хоча іноді дуже хочеться.

6. Що відбувається після виконання методу

Досі ми описували маршрут «як дісталися до методу». Але для цілісної картини корисно розуміти, що відбувається далі, інакше ланцюжок обривається в найцікавішому місці: «метод повернув об’єкт — як він став відповіддю?».

Після того як MVC викликав ваш метод контролера і отримав результат, вмикається наступний шар: Spring повинен вирішити, як перетворити значення, що повертається, на HTTP‑відповідь. У нашому сьогоднішньому контексті (і взагалі в цьому курсі) ми зазвичай використовуємо @RestController, а отже значення, що повертається, розглядається як тіло відповіді, а не як назва HTML‑сторінки.

Далі в гру входить HttpMessageConverter. Його завдання — «пакувати» результат у потрібний формат: текст, JSON тощо. Якщо спростити:

  • якщо ви повертаєте String, то часто це стає текстом у відповіді;
  • якщо ви повертаєте об’єкт або колекцію, Boot (через Jackson) може перетворити це на JSON;
  • вибір формату залежить від типу значення, що повертається, і заголовків запиту (наприклад, Accept).

Поки вам достатньо пам’ятати одне: controller‑метод повертає звичайні Java‑дані, а писати JSON вручну не потрібно. Ми вже бачили це в минулій лекції, а в наступній — застосуємо на практиці до доменної моделі каталогу.

7. Швидка діагностика: запит не дійшов до методу

Коли щось пішло не так, корисно не гадати, а швидко визначити, на якому відрізку маршруту сталася проблема. Для новачка це справді золота навичка: ви економите години «перепишу все заново» і переходите в режим «я розумію, що саме перевіряти».

Ось типова логіка діагностики, якщо ви ввели URL і не отримали очікуваного:

Якщо браузер пише щось на кшталт “Connection refused” або “Не вдається встановити з’єднання”, то запит узагалі не дійшов до застосунку. Або застосунок не запущений, або порт інший, або ви стукаєте не туди. Це рівень «Tomcat навіть не прийняв запит».

Якщо ви отримали 404 Not Found, це часто означає: DispatcherServlet працює, застосунок живий, але не знайшлося відповідного mapping. Тобто ваш контролер або не зареєструвався (не став біном), або шлях/слеш/описка не збіглися, або ви забули анотацію, або клас лежить поза component scan.

Якщо ви отримали 405 Method Not Allowed, це зазвичай означає: шлях існує (якийсь mapping схожий є), але HTTP‑метод не підходить. Наприклад, ви зробили POST, а у вас лише @GetMapping.

Якщо ви отримали 400 Bad Request, це часто означає, що mapping знайшовся, але не вдалося зв’язати параметри. Наприклад, ви очікували boolean, а прийшло publishedOnly=banana, або обов’язковий query param не передано, або path variable не вдалося витягнути.

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

8. Типові помилки під час обробки запитів

Помилка №1: плутанина між path і query parameters.
Дуже поширена історія: студент кладе slug у query string (/courses?slug=spring-boot), а потім намагається дістати його через @PathVariable, або навпаки — очікує @RequestParam, але пише маршрут /{slug}. Це не «трохи неправильно», це просто дві різні схеми URL. Якщо параметр є частиною ідентифікатора ресурсу, зазвичай він живе в path. Якщо це фільтр або опція — частіше в query.

Помилка №2: описка в маршруті, особливо в слешах.
/api/catalog/course і /api/catalog/courses — для людини це «ну майже», а для маршрутизації це два різні світи. Плюс іноді губиться початковий слеш або додається зайвий. Найпрактичніший спосіб: копіювати шлях із @GetMapping і вставляти його в запит, щоб не переписувати на око.

Помилка №3: забуті фігурні дужки в шаблоні path variable.
Новачки іноді пишуть @GetMapping("/courses/slug") і чекають, що slug стане змінною. Змінною він стає лише у вигляді /{slug}. Фігурні дужки — це не прикраса, а сигнал маршрутизатору: «тут буде значення із запиту».

Помилка №4: очікування, що контролер хтось повинен викликати вручну.
Після консольних застосунків хочеться написати «головний код», який викличе метод контролера. Але у вебсвіті контролер — це обробник подій. Його «викликає» не ваш main(), а Spring MVC у момент HTTP‑запиту. Якщо ви спіймали себе на думці «а де мені викликати цей метод?», значить ви ще не до кінця прийняли подієво-орієнтовану природу вебзастосунків.

Помилка №5: конфлікт маршрутів (ambiguous mapping).
Іноді студент копіює кілька прикладів із лекції і випадково робить два методи на один і той самий шлях та HTTP‑метод. Тоді під час старту Spring чесно скаже: «я не можу вибрати, який із них правильний». Це не баг і не прискіпливість — це захист від непередбачуваної поведінки. На перших порах тримайте правило: один шлях + один HTTP‑метод → один метод‑обробник.

1
Задача
Spring Boot, 10 рівень, 2 лекція
Недоступна
Отримання значення з URL через `@PathVariable`
Отримання значення з URL через `@PathVariable`
1
Задача
Spring Boot, 10 рівень, 2 лекція
Недоступна
Булевий параметр запиту з `defaultValue`
Булевий параметр запиту з `defaultValue`
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ