← назад к разделу

Когда браузер или другой сервис обращается к вашему приложению по 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 и проверка контроллеров.