Параметры запроса: @PathVariable, @RequestParam, @RequestBody

Клиент передаёт данные тремя путями: в пути URL, в query-строке и в теле запроса. Spring связывает каждый с параметром метода своей аннотацией.
Суть: @PathVariable — часть пути (/users/7), @RequestParam — query-параметр (?page=2), @RequestBody — JSON-тело запроса (для POST/PUT).

Чтобы построить полезное API, нужно уметь принимать ввод. Spring предлагает три аннотации, и выбор между ними не случаен — каждая отражает семантику REST.

@PathVariable — идентификатор ресурса

Когда данные однозначно идентифицируют ресурс, они идут в путь:

@GetMapping("/api/users/{id}")
public User byId(@PathVariable Long id) {
    return service.findById(id);
}

URL /api/users/7 — это «пользователь номер 7». Идентификатор в пути читается как часть адреса ресурса.

@RequestParam — фильтры и опции

Когда данные уточняют запрос (фильтр, сортировка, страница), они идут в query-строку после ?:

@GetMapping("/api/users")
public List<User> search(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(required = false) String name) {
    return service.search(name, page);
}

URL /api/users?page=2&name=Анна отфильтрует и пролистает список. Параметр defaultValue задаёт значение по умолчанию, required = false делает параметр необязательным.

@RequestBody — данные для создания/изменения

Когда клиент присылает целый объект (создать пользователя), он идёт в тело запроса как JSON:

@PostMapping("/api/users")
public User create(@RequestBody CreateUserRequest request) {
    return service.create(request);
}

Spring через Jackson десериализует JSON из тела в объект Java, сопоставляя поля по именам.

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

Для каждого параметра метода Spring выбирает HandlerMethodArgumentResolver — компонент, умеющий извлечь значение из нужного места запроса. @PathVariable берёт его из шаблона URL, @RequestParam — из query-строки, @RequestBody запускает Jackson на теле запроса.

  POST /api/users?notify=true
  тело: {"name":"Анна","age":30}
        |
        v
  +-- @RequestParam notify  <- query-строка ("true")
  +-- @RequestBody request  <- тело -> Jackson -> объект
  +-- @PathVariable (нет в этом URL)
        |
        v
  метод create(...) получает готовые аргументы

Смоделируем разбор входа на стороне «контроллера»:

# Разбор параметров запроса по трём источникам
import json
from urllib.parse import urlparse, parse_qs

def parse_request(path, query, body):
    parts = path.strip("/").split("/")
    path_vars = {}
    if len(parts) == 3 and parts[0] == "api" and parts[1] == "users":
        path_vars["id"] = parts[2]                  # @PathVariable

    params = parse_qs(query)                          # @RequestParam
    page = int(params.get("page", ["0"])[0])

    payload = json.loads(body) if body else None      # @RequestBody
    return {"id": path_vars.get("id"), "page": page, "body": payload}

print(parse_request("/api/users/7", "", ""))
print(parse_request("/api/users", "page=2", ""))
print(parse_request("/api/users", "", '{"name": "Анна"}'))

Нажмите «Попробуй сам ▶» — каждый источник данных разбирается отдельно, как делает Spring.

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

  • Несовпадение имён. Если имя параметра метода не совпадает с именем в URL, нужно указать его явно: @RequestParam("user_name").
  • @RequestBody на GET. GET-запросы по семантике не несут тела — данные передавайте через query-параметры.
  • Несколько @RequestBody. В одном методе может быть только одно тело запроса.

Best practices

  • Идентификатор ресурса — в путь, фильтры — в query, объекты — в тело.
  • Задавайте defaultValue для пагинации, чтобы клиент мог не передавать page.
  • Принимайте в тело отдельный DTO запроса, а не сущность БД (подробнее — в разделе про DTO).

Итог: три аннотации — три источника данных. @PathVariable идентифицирует ресурс, @RequestParam уточняет запрос, @RequestBody принимает объект. Выбор отражает смысл данных в REST.

Проверьте себя
1. Клиент создаёт пользователя через POST и присылает JSON с его полями. Какой аннотацией принять эти данные?
A@PathVariable
B@RequestParam
C@RequestBody
D@RequestHeader
2. Где правильно передать параметры пагинации page и size при чтении списка?
AВ теле GET-запроса через @RequestBody
BВ query-строке через @RequestParam (?page=2&size=20)
CВ пути URL через @PathVariable
DВ заголовках через @RequestHeader