JavaRush /Курси /Spring Boot /Endpoint mappings: ...

Endpoint mappings: карта маршрутів

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

1. Карта маршрутів під час виконання й IDE

Коли ви тільки починаєте писати бекенд, світ здається простим: ось клас-контролер, ось @GetMapping — отже, endpoint існує. Але реальність любить підкидати сюрпризи: контролер може не потрапити до компонентного сканування, маршрут може виявитися іншим через спільний префікс, метод може очікувати інший HTTP-метод, а частину шляхів узагалі обслуговують не контролери, а статичні ресурси чи інфраструктура. І в цей момент ви починаєте… вгадувати.

Endpoint /actuator/mappings — це як «карта метро» вашого сервісу. Не з умовної схеми, а з реального стану сервісу: які станції (маршрути) існують, куди ведуть і які лінії (MVC/Actuator/static resources) там перетинаються. Дуже зручно, коли ви ловите 404/405 і хочете спочатку переконатися, що маршрут узагалі зареєстрований, а вже потім шукати проблему в контролері.

Що вважається mapping у Spring MVC

Якщо говорити по-людськи, “mapping” у Spring MVC — це правило «який код має обробити який HTTP-запит». І це правило зазвичай ширше, ніж просто рядок /api/catalog/courses. До mapping майже завжди входить HTTP-метод (GET, POST тощо), іноді — query-параметри, заголовки, Content-Type (що ми приймаємо) і Accept (що ми віддаємо). Новачки часто перевіряють лише шлях, а потім дивуються 405 або 415.

Давайте на короткому прикладі подивимося, як mapping працює в комплексі. Це один і той самий шлях, але різні правила, а отже потенційно — різні обробники:

package com.example.catalogservice.catalog.web;

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

@RestController // <-- Важливо: без цієї анотації Spring не зареєструє контролер, і mapping не зʼявиться під час виконання.
class DemoController {

    @GetMapping(
            path = "/api/demo",
            produces = MediaType.APPLICATION_JSON_VALUE // <-- Це теж частина mapping: ми явно кажемо, що віддаємо JSON.
    )
    String json() {
        // Так читається краще, ніж екранований рядок: одразу видно, що саме ми повернемо клієнту.
        return """
               {"ok":true}
               """;
    }
}

Навіть тут уже видно: mapping — це не просто "/api/demo", а «GET + шлях + produces JSON». А в реальному catalog-service ми ще додаємо @RequestMapping("/api/catalog"), path variables (/{slug}) і фільтри через query params. Тому іноді корисно мати інструмент, який скаже: «ось це правило реально існує під час виконання, і ось цей метод-обробник його обслуговує».

2. Як працює /actuator/mappings

Найважливіше: mappings не «сканує код» і не будує здогадки. Він показує те, що Spring уже зареєстрував під час запуску застосунку. Тобто це знімок реального стану вебшару на поточний момент: які обробники знайшлися, які маршрути склалися, які інфраструктурні речі теж беруть участь в обробці запитів.

Якщо уявити Spring MVC як диспетчера таксі, то DispatcherServlet — це диспетчер, який приймає заявку (HTTP-запит) і вирішує, яке «таксі» (метод-обробник) поїде. Endpoint mappings показує, які «таксі» взагалі зареєстровані і за якими адресами вони їздять.

Ось спрощена схема, де mappings допомагає підсвітити карту:

flowchart TD
    A["HTTP-запит"] --> B["DispatcherServlet"]
    B --> C["HandlerMappings: пошук відповідного обробника"]
    C --> D["Контролер або обробник ресурсу"]
    D --> E["Відповідь"]

    M["/actuator/mappings"] --> C
    M -->|"показує список зареєстрованих правил"| C

Практично це означає дві корисні речі. По-перше, /actuator/mappings показує не лише ваші маршрути @RestController, а й інфраструктурні: статичні ресурси, Actuator-маршрути тощо. По-друге, він допомагає перестати сперечатися з реальністю. Якщо endpoint не відображається в mappings, то він або не зареєструвався, або ви дивитеся не той контекст, порт чи профіль, або просто очікували не того.

3. Як увімкнути mappings у catalog-service

В Actuator дуже легко влаштувати собі пастку: «для зручності» відкрити *, а потім забути, що зручність була лише локально. Ми вже домовилися про здорову політику: у local/dev можна розширену діагностику, у prod — мінімум. Тому mappings зазвичай вмикають лише в local/dev і лише тоді, коли він справді потрібен для діагностики.

Якщо експонування для local/dev у вас уже налаштовано, просто додайте туди mappings. Нижче — фрагмент для поточного стану catalog-service: зберігаємо вже знайомі config-introspection і conditions, а не збираємо include заново.

management:
  endpoints:
    web:
      exposure:
        # Зберігаємо вже відкриті local/dev endpoints і додаємо mappings
        include: "health,info,env,configprops,conditions,mappings"

Зверніть увагу на характерну деталь: тут немає *. Вам майже ніколи не потрібно «все». Потрібно кілька конкретних інструментів, і mappings — один із них.

Якщо у вашому проєкті вже є розширена політика для local/dev (наприклад, ви відкриваєте ще conditions або configprops), це теж нормально. Важливо, щоб це було свідомо, і щоб у prod ви не винесли «діагностичний швейцарський ніж» назовні.

4. Як читати /actuator/mappings

Перший досвід із /actuator/mappings у новачка часто такий: «Ого, скільки всього… я відкрив — і спершу навіть злякався». Це нормальна реакція. Там справді багато інформації, тому що Spring показує усю вебкартину, включно з інфраструктурою. Але читати це потрібно не як книгу, а як довідку для конкретного запиту.

Корисна звичка — подумки тримати три кроки: ви шукаєте шлях, перевіряєте HTTP-метод, потім дивитеся, який обробник (клас і метод) призначено на це правило. Усе інше — другорядне.

Приклад того, як виглядає шматок відповіді (дуже спрощено і скорочено, бо повний JSON може бути величезним):

{
  "contexts": {
    "application": {
      "mappings": {
        "dispatcherServlets": {
          "dispatcherServlet": [
            {
              "handler": "CourseCatalogController#courses(...)",
              "predicate": "{GET [/api/catalog/courses]}"
            }
          ]
        }
      }
    }
  }
}

Назви полів можуть бути трохи різними залежно від версії Boot, але ідея одна: ви побачите, що є розділ про MVC-маршрути (зазвичай прив’язано до dispatcherServlet), і всередині будуть правила з предикатами на кшталт «GET + шлях», а також посилання на метод-обробник.

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

Шматок у mappings Що це означає по суті Що ви там зазвичай шукаєте
dispatcherServlets Маршрути Spring MVC (ваші контролери й обробники статичних ресурсів) /api/catalog/**, / (якщо він через MVC), обробку статичних ресурсів
servlets Зареєстровані servlet’и наявність dispatcherServlet, інколи management servlet
servletFilters Фільтри, через які проходить запит корисно, якщо ви розбираєте, чому запит змінився, але це вже більш просунутий рівень
Блоки про Actuator endpoints Маршрути management endpoint'ів /actuator/health, /actuator/info, /actuator/mappings і хто їх обслуговує

На практиці в 90 % випадків вам достатньо першого рядка: dispatcherServlets. Тому що ви шукаєте ваші API-endpoint'и, а вони живуть саме там.

5. Що видно в catalog-service

Наш навчальний сервіс до цього моменту вже працює: він віддає HTML-лендінг (/), віддає read-only JSON-дані по каталогу і має Actuator. Тому в mappings ви зазвичай хочете підтвердити, що піднялися щонайменше такі прикладні маршрути:

/api/catalog/courses
/api/catalog/courses/{slug}
/api/catalog/featured
/api/catalog/tracks

Покажу приклад коректного контролера в стилі проєкту, щоб було зрозуміло, які речі мають збігатися в runtime-карті:

package com.example.catalogservice.catalog.web;

import java.util.List;

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

@RestController // <-- Робимо bean, щоб Spring зареєстрував handler methods.
@RequestMapping("/api/catalog") // <-- Спільний префікс теж бере участь у підсумковому mapping.
class CourseCatalogController {

    @GetMapping("/tracks") // <-- Підсумковий шлях буде: /api/catalog/tracks
    List<String> tracks() {
        // Для навчального проєкту повертаємо "заглушку", щоб було що побачити у відповіді.
        return List.of("SPRING", "DATA", "INFRA");
    }
}

Якщо такий контролер справді зареєстровано, то в /actuator/mappings ви побачите mapping з предикатом на кшталт GET [/api/catalog/tracks] і method handler, схожий на CourseCatalogController#tracks().

І це дуже практично: коли ви відкриваєте mappings, ви підтверджуєте не «я ніби написав метод», а «контейнер справді знає, що цей endpoint існує».

6. Сценарії, де mappings економить години

“404: endpoint не знайдено” — контролер не зареєструвався

Дуже класична історія: ви написали новий controller або новий метод, запустили застосунок, і… 404. Починається ритуальний танець: «може, порт не той, може, кеш, може, Postman дивиться не так». Перше, що варто зробити, — відкрити /actuator/mappings і знайти шлях. Якщо шляху там немає, то проблема не в «мережевих феях», а в реєстрації.

Одна з найбанальніших причин — забули анотацію @RestController (або хоча б @Controller). У підсумку Spring сприймає клас просто як звичайний Java-клас, і mapping'и не з’являються.

Порівняйте:

package com.example.catalogservice.catalog.web;

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

class BrokenController {
    // Важливо: тут НЕМАЄ @RestController/@Controller, тому Spring не вважатиме цей клас контролером.

    @GetMapping("/api/broken")
    String broken() {
        return "oops";
    }
}

Цей код компілюється, IDE радіє, але Spring не побачить його як контролер. У mappings його не буде, і це чесний сигнал: «маршрут не зареєстровано». Виправлення тут банальне (і нехай воно буде банальним — ми вчимося):

package com.example.catalogservice.catalog.web;

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

@RestController // <-- Тепер Spring зареєструє handler method, і він зʼявиться в /actuator/mappings.
class FixedController {

    @GetMapping("/api/fixed")
    String fixed() {
        return "ok";
    }
}

Після цього маршрут з’являється і в mappings, і в реальному виклику.

“405 Method Not Allowed” — HTTP-метод інший

Друга типова проблема: ви натискаєте “Send” у Postman, бачите 405 і думаєте: «але ж я точно зробив endpoint!». Так, зробили. Але не той, який ви викликаєте.

Наприклад, у catalog-service майже все GET. Якщо ви випадково надішлете POST на /api/catalog/courses, то endpoint може існувати, але бути прив’язаним до іншого методу. І ось тут mappings допомагає не домислювати, а перевірити.

У контролера, наприклад, так:

package com.example.catalogservice.catalog.web;

import java.util.List;

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

@RestController
class CourseReadController {

    @GetMapping("/api/catalog/courses") // <-- Mapping лише для GET (POST сюди не підходить).
    List<String> courses() {
        // Сенс прикладу: показати, що endpoint є, але він "прив'язаний" до HTTP-методу.
        return List.of("spring-boot", "spring-core");
    }
}

У mappings ви побачите саме GET [/api/catalog/courses]. А якщо ви надсилаєте POST, то координати не збігаються: шлях той самий, метод — ні. На рівні HTTP саме так і виникає 405.

“/ працює без контролера” — static resources теж у карті

Це улюблена пастка після теми статичних ресурсів. Ми додавали static/index.html, і тепер GET / віддає сторінку. Новачок іноді щиро вважає, що «все, що віддається, обов’язково має мати контролер». Але у Spring Boot є вбудований обробник статичних ресурсів, і welcome page — його частина.

Коли ви дивитеся mappings, ви часто побачите, що / або /** обслуговуються не вашим контролером, а ResourceHttpRequestHandler (або подібним обробником ресурсу). Це нормально і навіть корисно: mappings дозволяє відрізнити «це контролер» від «це ресурси». А ще це допомагає пояснити дивні ситуації на кшталт «я зробив контролер на /, але чомусь відповідає не він». Іноді статичний ресурс і ваш mapping починають конфліктувати, і ви бачите це прямо по карті.

Для навчального catalog-service це особливо приємно: landing page у нас мінімальна, але сервіс сприймається як справжній, і mappings показує, за рахунок якої інфраструктури це працює.

“Actuator endpoint недоступний” — перевірка exposure-політики

Якщо ви дотримуєтеся profile-стратегії (а ми дотримуємося), то набір доступних Actuator endpoint'ів відрізняється в local/dev і prod. Тому ситуація «вчора /actuator/mappings відкривався, а сьогодні ні» зазвичай означає не містику, а те, що ви запустилися з іншим профілем або змінили політику експонування.

mappings хороший тим, що він показує не лише ваші API, а й management-маршрути. Тобто ви можете перевірити: endpoint справді зареєстровано й експоновано, чи він узагалі не піднімався.

При цьому пам’ятайте важливе логічне коло: щоб подивитися mappings, його самого треба спершу експонувати. Тому якщо ви сховали його в prod, то в prod ви його й не побачите — і в межах нашої політики це, строго кажучи, правильно.

Маленький лайфхак: як шукати потрібне в mappings

Коли JSON великий, очі швидко втомлюються й уже не хочуть читати його вручну. Тому краще застосовувати «ледачі» техніки: відкрити в браузері та скористатися пошуком по сторінці (Ctrl+F) за вашим шляхом, або зробити запит через curl і пошукати підрядок. Це не «хакерство», а нормальна інженерна гігієна: у комп’ютера краще виходить шукати рядки, ніж у людини після третьої чашки кави.

Наприклад, для catalog-service ви зазвичай шукаєте /api/catalog або конкретний endpoint /api/catalog/courses. Важливий момент: шукайте не лише шлях, а й поруч перевіряйте HTTP-метод, тому що один і той самий шлях у теорії може мати кілька handler'ів за різними методами. У навчальному проєкті ми таким не зловживаємо, але світ загалом підступний.

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

Помилка № 1: сприймати mappings як «документацію API».
Цей endpoint показує, які маршрути зареєстровано, але не пояснює контракт, формат JSON, зміст параметрів і бізнес-правила. Якщо ви починаєте «документувати API» через mappings, ви отримаєте список адрес без змісту. Це корисно для діагностики, але не для клієнтів вашого API.

Помилка № 2: дивитися лише на URL і ігнорувати HTTP-метод.
405 Method Not Allowed зазвичай виглядає як загадка лише в перші два рази. Потім ви розумієте, що маршрут — це «метод + шлях», і починаєте перевіряти обидва. mappings якраз виводить предикати, де метод видно явно, тому ігнорувати його — це все одно що шукати квартиру, знаючи лише вулицю, але не номер будинку.

Помилка № 3: не відокремлювати прикладні маршрути від інфраструктурних.
У mappings буде багато того, що ви не писали: ресурсні обробники, Actuator endpoints, іноді якісь внутрішні речі MVC. Якщо ви починаєте аналіз зі слів «чому тут стільки дивних маршрутів», ви швидко поїдете вбік. Нормальна стратегія — спочатку знайти свої /api/catalog/**, і лише потім, якщо потрібно, дивитися інше.

Помилка № 4: відкривати mappings «про всяк випадок» усюди, включно з prod.
З точки зору безпеки й «інформаційної щедрості» це сумнівна ідея. mappings розкриває структуру застосунку: які endpoint'и існують, які класи їх обслуговують, яка інфраструктура підключена. У local/dev це корисно, а в production-середовищі це частіше зайва підказка для допитливих. У нашому курсі здорова позиція проста: розширена діагностика — локально, мінімум — у проді.

Помилка № 5: намагатися за mappings зрозуміти, «що не так у бізнес-логіці».
Якщо у вас повертається не той список курсів або фільтрація працює не так, mappings не допоможе. Він відповідає на питання «куди потрапив запит», а не «що всередині методу відбувається». Це як перевіряти карту метро, щоб зрозуміти, чому вам не сподобалася шаурма біля станції: карта корисна, але не з цієї причини.

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