REST-контроллеры: @RestController и маршрутизация

Контроллер — это входная дверь REST API. Аннотации связывают URL и HTTP-метод с конкретным методом Java, а Spring сам превращает результат в JSON.
Суть: @RestController = @Controller + @ResponseBody. Метод возвращает объект — Spring через Jackson сериализует его в JSON и кладёт в тело ответа.

REST — это стиль построения API, где ресурсы (пользователи, заказы) адресуются через URL, а действия выражаются HTTP-методами: GET — прочитать, POST — создать, PUT — заменить, DELETE — удалить. Spring Boot делает написание таких API почти декларативным: вы расставляете аннотации, остальное фреймворк берёт на себя.

Базовый контроллер

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

@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping
    public List<String> all() {
        return List.of("Анна", "Борис");
    }

    @GetMapping("/{id}")
    public String byId(@PathVariable Long id) {
        return "Пользователь #" + id;
    }
}

@RequestMapping("/api/users") на классе задаёт общий префикс. @GetMapping без аргумента означает «GET на /api/users», а @GetMapping("/{id}") — «GET на /api/users/7». Фигурные скобки в пути — это переменная, которую @PathVariable вытаскивает в параметр метода.

Как работает под капотом

При старте Spring сканирует контроллеры и строит карту маршрутов: для каждого метода запоминает URL-шаблон и HTTP-метод. Когда приходит запрос, DispatcherServlet ищет в этой карте подходящий метод (HandlerMapping), вызывает его, а возвращённый объект отдаёт сериализатору. Поскольку это @RestController, результат не ищется как шаблон страницы, а сериализуется в JSON через Jackson.

  GET /api/users/7
        |
        v
  DispatcherServlet
        |  ищет совпадение в карте маршрутов
        v
  /api/users/{id}  ->  UserController.byId(Long id)
        |  id = 7 (из @PathVariable)
        v
  возвращён объект  ->  Jackson  ->  JSON  ->  тело ответа

Смоделируем роутинг: по сути это поиск по таблице «(метод, шаблон) → функция»:

# Упрощённая карта маршрутов, как внутри DispatcherServlet
import re

routes = []

def route(method, pattern, handler):
    regex = re.sub(r"\{(\w+)\}", r"(?P<\1>[^/]+)", pattern)
    routes.append((method, re.compile("^" + regex + "$"), handler))

def all_users():
    return ["Анна", "Борис"]

def user_by_id(id):
    return "Пользователь #" + id

route("GET", "/api/users", lambda **kw: all_users())
route("GET", "/api/users/{id}", lambda **kw: user_by_id(kw["id"]))

def dispatch(method, path):
    for m, rx, handler in routes:
        match = rx.match(path)
        if m == method and match:
            return handler(**match.groupdict())
    return "404 Not Found"

print(dispatch("GET", "/api/users"))      # список
print(dispatch("GET", "/api/users/7"))    # Пользователь #7
print(dispatch("GET", "/api/orders"))     # 404

Запустите «Попробуй сам ▶». Spring делает примерно то же, но добавляет сериализацию, валидацию и обработку ошибок.

Частые ошибки

  • Забыть @RestController. С обычным @Controller Spring попытается найти HTML-шаблон с именем строки-результата вместо отдачи JSON.
  • Дублирование маршрутов. Два метода на один и тот же URL и метод приведут к ошибке неоднозначного маппинга при старте.
  • Глаголы в URL. /getUser — антипаттерн. Ресурс — существительное (/users), действие выражает HTTP-метод.

Best practices

  • Группируйте эндпоинты ресурса в один контроллер с общим @RequestMapping.
  • Используйте множественное число существительных: /api/users, /api/orders.
  • Держите контроллеры тонкими — вся логика в сервисе.

Итог: @RestController превращает методы класса в REST-эндпоинты. Spring строит карту маршрутов, находит метод по URL и HTTP-методу и сериализует результат в JSON. Дальше разберём, как принимать данные от клиента.

Закрепим главное

Контроллер — это переводчик между миром HTTP и миром Java-объектов, и важно не нагружать его ничем сверх этой роли. Каждый метод-эндпоинт должен читаться как короткая декларация: вот URL, вот HTTP-метод, вот какие данные принимаю, вот что возвращаю. Вся «настоящая работа» делегируется сервису. Такой контроллер легко читать, легко тестировать срезом @WebMvcTest и легко поддерживать.

Подумайте об именовании ресурсов заранее: хорошо спроектированное REST API выглядит предсказуемо, и опытный разработчик угадывает адреса, не глядя в документацию. Существительные во множественном числе, иерархия через вложенные пути (/api/users/7/orders), действия через HTTP-методы — это конвенции, которые экономят силы всей команде. Чем дисциплинированнее вы следуете им с первого дня, тем меньше технического долга накапливает API.

Проверьте себя
1. В чём разница между @RestController и обычным @Controller?
A@RestController работает быстрее
B@RestController = @Controller + @ResponseBody: результат сериализуется в JSON, а не ищется как шаблон
C@Controller нельзя использовать в Spring Boot 3
DРазницы нет, это синонимы
2. Как извлечь значение из URL вида /api/users/{id}?
A@RequestParam Long id
B@PathVariable Long id
C@RequestBody Long id
D@RequestHeader Long id