Когда браузер или другой сервис обращается к вашему приложению по HTTP, кто-то должен принять этот запрос, понять, какой код за него отвечает, и вернуть ответ. Этим и занимается Spring MVC. Разберём с нуля: как запрос проходит через приложение, как написать контроллер для REST API, как проверить присланные данные и как аккуратно отдавать ошибки.
Зачем вообще нужен Spring MVC
Представьте, что HTTP-запросы вы обрабатываете вручную. Тогда в каждом методе пришлось бы самому разбирать URL, доставать параметры из строки запроса, читать тело и превращать JSON в объект, а потом обратно объект в JSON. Кода много, и он одинаковый в каждом месте.
Spring MVC берёт эту рутину на себя. Вы пишете обычный метод и помечаете его аннотацией — «этот метод отвечает на GET /orders/5». Всё остальное (разбор URL, чтение тела, преобразование JSON) фреймворк делает сам. MVC расшифровывается как Model-View-Controller, но для REST API нас интересует в основном Controller — класс с методами-обработчиками.
Это синхронный веб-стек: каждый запрос обрабатывается своим потоком от начала до конца. Есть и реактивная альтернатива — WebFlux — но она нужна реже, чем кажется, и для типичного backend-сервиса хватает обычного MVC.
Как запрос доходит до вашего метода
Раньше, без единой точки входа, каждый кусок приложения сам решал, какие запросы он ловит — получалась путаница. В Spring MVC есть один «диспетчер», через который проходят все запросы.
DispatcherServlet — это единственный вход в приложение (его называют front controller, «главный контроллер»). Любой HTTP-запрос сначала попадает в него, а он уже решает, куда направить дальше. Аналогия: ресепшен в большом офисе — посетители идут не сразу к нужному человеку, а сначала к стойке, и она подсказывает, в какой кабинет.
Упрощённо путь запроса такой:
HTTP-запрос
↓
[Tomcat — встроенный веб-сервер]
↓
[DispatcherServlet — главный вход]
↓
ищет метод, который отвечает за этот URL
↓
[вызывает ваш метод контроллера]
↓
превращает результат в JSON
↓
HTTP-ответ
Хорошая новость: всё это настраивается автоматически. Вам не нужно регистрировать DispatcherServlet руками — Spring Boot делает это при старте. Вы просто пишете контроллеры, а диспетчер сам находит нужный метод по URL.
REST-контроллер
REST API — это набор адресов, на которые клиент шлёт запросы и получает данные (обычно в JSON). Чтобы метод стал таким обработчиком, его кладут в класс с аннотацией @RestController.
@RestController
@RequestMapping("/orders")
public class OrderController {
@GetMapping("/{id}")
public OrderResponse get(@PathVariable Long id) {
return orderService.findById(id);
}
@PostMapping
public OrderResponse create(@RequestBody CreateOrderRequest request) {
return orderService.create(request);
}
}
Что здесь происходит:
@RestControllerговорит: это контроллер, и то, что возвращают его методы, нужно отдавать как тело ответа (JSON), а не искать HTML-страницу.@RequestMapping("/orders")на классе — общий префикс адреса. Все методы внутри будут начинаться с/orders.@GetMapping,@PostMapping— на какой HTTP-метод и путь отвечает метод. Есть ещё@PutMapping,@DeleteMapping,@PatchMapping— по одному на каждый вид запроса.
Метод get отвечает на GET /orders/5, метод create — на POST /orders. Возвращённый объект Spring сам превратит в JSON.
Как достать данные из запроса
Данные приходят в запросе тремя разными способами, и для каждого своя аннотация.
// /orders/5 — часть самого адреса
@GetMapping("/{id}")
public OrderResponse get(@PathVariable Long id) { ... }
// /orders?status=NEW&limit=20 — параметры после знака ?
@GetMapping
public List<OrderResponse> list(
@RequestParam String status,
@RequestParam(defaultValue = "10") int limit) { ... }
// тело POST-запроса (JSON) превращается в объект
@PostMapping
public OrderResponse create(@RequestBody CreateOrderRequest request) { ... }
@PathVariable— значение прямо из адреса (5в/orders/5).@RequestParam— параметр из строки запроса (status=NEWв/orders?status=NEW). Можно задать значение по умолчанию.@RequestBody— тело запроса. Spring сам прочитает JSON и соберёт из него объект.
Самое частое, что путают новички: @PathVariable достаёт кусок из самого пути, а @RequestParam — то, что идёт после ?.
Что вернуть из метода
Чаще всего достаточно вернуть обычный объект — Spring превратит его в JSON и отдаст со статусом 200 OK. Но иногда нужно управлять статусом ответа или заголовками: например, после создания записи принято возвращать 201 Created. Для этого есть ResponseEntity.
@PostMapping
public ResponseEntity<OrderResponse> create(@RequestBody CreateOrderRequest request) {
OrderResponse created = orderService.create(request);
return ResponseEntity
.status(HttpStatus.CREATED) // 201 вместо 200
.body(created);
}
Правило простое: если хватает статуса 200 — возвращайте объект напрямую, так короче. Нужен другой статус или заголовки — оборачивайте в ResponseEntity.
Проверка входных данных
Клиент может прислать что угодно: пустое имя, отрицательное количество, дату из прошлого. Если не проверять, кривые данные уйдут вглубь приложения и сломают что-нибудь там, где разобраться уже трудно. Проверять лучше сразу на входе.
Для этого есть Bean Validation — стандартный механизм, где правила задаются прямо на полях аннотациями. В Spring Boot он подключается зависимостью spring-boot-starter-validation.
public record CreateOrderRequest(
@NotBlank String customerName, // не пустая строка
@NotEmpty List<OrderLineRequest> lines, // список не пустой
@NotNull @Future LocalDateTime deliveryDate // дата в будущем
) {}
Самые ходовые аннотации: @NotNull (не null), @NotBlank (строка не пустая), @NotEmpty (коллекция не пустая), @Size (длина в диапазоне), @Min/@Max и @Positive (числовые ограничения), @Email, @Future/@Past (даты).
Чтобы Spring реально применил эти правила, на параметре ставят @Valid:
@PostMapping
public OrderResponse create(@Valid @RequestBody CreateOrderRequest request) { ... }
Без @Valid аннотации на полях ничего не делают — это частая причина «почему валидация не сработала». Если данные не прошли проверку, Spring сам отклонит запрос ещё до входа в тело метода.
Обработка ошибок в одном месте
Что-то всегда идёт не так: заказ не найден, данные не прошли проверку, упала база. Если в каждом методе писать try/catch и руками собирать ответ об ошибке, код раздувается и ошибки выглядят по-разному в разных местах. Лучше собрать всю обработку в одном классе.
Для этого есть @RestControllerAdvice — класс, который ловит исключения из всех контроллеров сразу. Внутри — методы с @ExceptionHandler, по одному на тип ошибки.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
public ResponseEntity<String> handleNotFound(OrderNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleUnknown(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Что-то пошло не так");
}
}
Теперь, где бы в контроллере ни выбросили OrderNotFoundException, клиент получит аккуратный ответ 404. А последний метод — «ловушка на всё остальное»: любая неожиданная ошибка превратится в 500, а не в кашу из стектрейса.
Единый формат ошибки
Чтобы ошибки от всего сервиса выглядели одинаково, в Spring есть готовый формат ответа — ProblemDetail. Это просто объект с понятными полями, который тоже отдаётся как JSON.
@ExceptionHandler(OrderNotFoundException.class)
public ProblemDetail handleNotFound(OrderNotFoundException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
}
Ответ получится таким:
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "Заказ 5 не найден"
}
Свой формат ошибок изобретать не стоит — когда у всех ответов одинаковая структура, клиентам (веб, мобильное приложение) проще их разбирать.
Документация API через OpenAPI
Когда контроллеров становится много, клиентам нужна понятная справка: какие есть адреса, что они принимают и возвращают. Писать её руками и поддерживать в актуальном виде тяжело — она быстро расходится с кодом.
Поэтому документацию генерируют автоматически. Библиотека springdoc-openapi подключается одной зависимостью и сама собирает описание API по вашим контроллерам.
dependencies {
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.x.x")
}
После этого по адресу /swagger-ui.html открывается готовая страница, где можно посмотреть все эндпоинты и даже подёргать их прямо из браузера. Поскольку справка строится по коду, она всегда соответствует тому, что есть на самом деле.
Коротко
- Spring MVC снимает рутину обработки HTTP: разбор URL, чтение параметров, преобразование JSON в объект и обратно.
DispatcherServlet— единый вход в приложение: все запросы идут через него, а он находит нужный метод по URL. Настраивается автоматически.@RestController+@GetMapping/@PostMapping/... — так метод становится обработчиком конкретного адреса; возвращённый объект превращается в JSON.- Данные из запроса достают тремя аннотациями:
@PathVariable(часть пути),@RequestParam(после?),@RequestBody(тело запроса). - Хватает статуса
200— возвращайте объект напрямую; нужен другой статус или заголовки —ResponseEntity. - Входные данные проверяют аннотациями Bean Validation на полях плюс
@Validна параметре; без@Validправила не сработают. - Обработку ошибок собирают в одном
@RestControllerAdvice; единый формат ответа —ProblemDetail. - Документацию API не пишут руками — её генерирует springdoc-openapi (страница
/swagger-ui.html).
Что почитать дальше
- Spring WebFlux — реактивная альтернатива и когда она действительно нужна.
@Transactionalглубоко — как сделать операции в контроллере транзакционными правильно.- Spring Security — аутентификация и авторизация внутри MVC.
- Spring Testing —
@WebMvcTest,MockMvcи проверка контроллеров.