1. Request lifecycle: практическая польза
Теперь уже видно, что web starter поднимает не только порт, но и целую MVC‑инфраструктуру. Следующий естественный вопрос очень приземлённый: как конкретный HTTP‑запрос проходит через эту инфраструктуру и в итоге оказывается в методе controller-а?
Когда вы только начинаете писать web‑код, очень хочется верить в простую сказку: «я написал @GetMapping, значит запрос туда попадёт». Иногда так и бывает. А иногда — нет. И вот тут у новичка часто включается режим «компьютер сломан», «Spring странный», «я не создан для backend». На самом деле чаще всего сломалось что-то приземлённое: путь не совпал, метод не тот, параметр не распарсился, сервер не поднялся, контроллер вообще не стал бином.
Поэтому наша цель сегодня — собрать понятную цепочку обработки: кто первый встречает HTTP‑запрос, кто решает «какой метод вызвать», кто достаёт slug из URL, и почему publishedOnly=false внезапно превращается в boolean без вашего ручного Boolean.parseBoolean(...). Это не глубокие «внутренности Spring ради внутреннихостей», а самый практический навык: уметь объяснить, почему запрос попал (или не попал) в controller.
Чтобы было проще держать это в голове, представьте, что запрос — это посылка, а Spring MVC — сортировочный центр. Посылка приезжает в город (порт), потом попадает на центральный терминал (DispatcherServlet), там по адресу решают, в какой отдел её отправить (HandlerMapping), затем сотрудник оформляет выдачу (HandlerAdapter), а на выходе посылку упаковывают в нужный формат (HttpMessageConverter). Да, звучит как логистика. Но это приятнее, чем «магия».
2. Запрос: Tomcat → HttpServletRequest
Самое важное, что нужно принять: HTTP‑запрос сначала попадает не в Spring, а в web‑сервер/контейнер. В нашем случае (после подключения spring-boot-starter-webmvc) Spring Boot поднимает embedded 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
\______/ \___________/ \______________________________/ \______________________/
scheme host:port path query string
HttpServletRequest содержит (упрощённо) примерно такую информацию: HTTP‑метод (GET), путь (/api/catalog/...), query string (publishedOnly=true&limit=10), заголовки (Accept, User-Agent, и т.д.), а также другие детали запроса.
Можно представить это в виде небольшой таблицы:
| Что нужно понять | Где оно живёт в запросе | Как Spring обычно достаёт |
|---|---|---|
| HTTP‑метод | GET / ... | по @GetMapping, @PostMapping и т.д. |
| Путь | /api/catalog/courses/{slug} | по шаблону маршрута |
| Path variable | кусочек пути | @PathVariable |
| Query parameter | после ? | @RequestParam |
| Тело запроса | (не сегодня) | (не сегодня) |
И вот теперь ключевой момент: Tomcat должен понять, какому сервлету отдавать запрос. И в Spring MVC есть один сервлет, который «встречает почти всё».
3. DispatcherServlet — главный «диспетчер» Spring MVC
В Spring MVC центральная фигура — это DispatcherServlet. Его роль проще всего описать так: это один вход в MVC‑мир, который дальше разруливает, какой controller и какой метод должны обработать запрос.
Очень полезно запомнить паттерн, который здесь используется: Front Controller. Вместо того чтобы регистрировать в контейнере «тысячу маленьких сервлетов на каждый путь», Spring регистрирует один большой «фронтальный» сервлет, который уже внутри себя делает маршрутизацию и вызывает ваш код.
Если нарисовать путь запроса совсем схематично, получится так:
flowchart TD
A[Клиент: браузер / Postman / curl] -->|HTTP request| B[Embedded Tomcat]
B --> C[DispatcherServlet]
C --> D[HandlerMapping: ищем подходящий метод]
D --> E[HandlerAdapter: готовим вызов]
E --> F[Controller method]
F --> G[ReturnValueHandler + HttpMessageConverter]
G -->|HTTP response| A
Важно понимать: DispatcherServlet не «сам по себе». Он живёт внутри Spring Boot приложения как часть инфраструктуры, которую Boot поднимает для вас. То есть вы его обычно не создаёте руками. Но вы можете увидеть его присутствие в логах старта — например, строки про инициализацию сервлета dispatcherServlet. Для новичка это хороший сигнал: «ага, MVC инфраструктура реально поднялась».
И ещё один очень важный момент: controller мы не вызываем вручную. Никаких new CourseCatalogController() и controller.findAll() в main(). Controller — это бин в 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 и controller поднялись
@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 вызывает этот метод не потому, что вы его где-то явно зарегистрировали, а потому что controller попал в 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 вызвал ваш controller‑метод и получил результат, включается следующий слой: Spring должен решить, как превратить возвращаемое значение в HTTP‑ответ. В нашем сегодняшнем контексте (и вообще в этом курсе) мы обычно используем @RestController, а значит возвращаемое значение рассматривается как тело ответа, а не как имя HTML‑страницы.
Дальше в игру входит HttpMessageConverter. Его задача — «упаковать» результат в нужный формат: текст, JSON и т.д. Сильно упрощая:
- если вы возвращаете String, то часто это становится текстом в ответе;
- если вы возвращаете объект или коллекцию, Boot (через Jackson) может превратить это в JSON;
- выбор формата зависит от типа возвращаемого значения и заголовков запроса (например, Accept).
Пока вам достаточно помнить одно: controller‑метод возвращает обычные Java‑данные, а писать JSON руками не нужно. Мы уже видели это в прошлой лекции, а в следующей — применим по-настоящему к доменной модели каталога.
7. Быстрая диагностика: запрос не дошёл до метода
Когда что-то пошло не так, полезно не гадать, а быстро определить, на каком участке маршрута случилась проблема. Для новичка это прям золотой навык: вы экономите часы «перепишу всё заново» и переходите в режим «я понимаю, что проверять».
Вот типичная логика диагностики, если вы вбили URL и не получили ожидаемого:
Если браузер пишет что-то вроде “Connection refused” или “Не удаётся установить соединение”, то запрос вообще не дошёл до приложения. Либо приложение не запущено, либо порт другой, либо вы стучитесь не туда. Это уровень «Tomcat даже не принял запрос».
Если вы получили 404 Not Found, это часто означает: DispatcherServlet работает, приложение живое, но не нашлось подходящего mapping. То есть ваш controller либо не зарегистрировался (не стал бином), либо путь/слэш/опечатка не совпали, либо вы забыли аннотацию, либо класс лежит вне component scan.
Если вы получили 405 Method Not Allowed, это обычно означает: путь существует (какой-то mapping похожий есть), но HTTP‑метод не подходит. Например, вы сделали POST, а у вас только @GetMapping.
Если вы получили 400 Bad Request, это часто про то, что mapping нашёлся, но не получилось связать параметры. Например, вы ожидали boolean, а пришло publishedOnly=banana, или обязательный query param не передан, или path variable не смогли извлечь.
Всё это — не «ошибки Spring как технологии», а вполне логичные последствия того, как устроен request lifecycle. И да, эти коды иногда выглядят пугающе, но на самом деле они просто говорят: «на каком шаге цепочка не сошлась».
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: ожидание, что controller кто-то должен вызвать вручную.
После консольных приложений хочется написать «главный код», который вызовет ваш controller‑метод. Но в web‑мире controller — это обработчик событий. Его «вызывает» не ваш main(), а Spring MVC в момент HTTP‑запроса. Если вы поймали себя на мысли «а где мне вызвать этот метод?» — значит вы ещё не до конца приняли event‑driven природу web‑приложений.
Ошибка №5: конфликт маршрутов (ambiguous mapping).
Иногда студент копирует несколько примеров из лекции и случайно делает два метода на один и тот же путь и HTTP‑метод. Тогда при старте Spring честно скажет: «я не могу выбрать, какой из них правильный». Это не баг и не придирка — это защита от непредсказуемого поведения. На первых порах держите правило: один путь + один HTTP‑метод → один метод‑обработчик.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ