JavaRush /Курсы /Spring REST & MVC /Сигнатура метода контроллера

Сигнатура метода контроллера

Spring REST & MVC
5 уровень , 2 лекция
Открыта

1. Сигнатура как контракт API

Когда вы впервые смотрите на Spring MVC, очень хочется думать, что контракт endpoint’а — это только аннотация @GetMapping("/что-то"), а метод внутри — «обычный Java-код». Но в web-разработке метод контроллера — это не просто метод: это форма публичного договора между клиентом и сервером. И сигнатура (аргументы + возвращаемый тип) делает этот договор конкретным.

Если упрощать до рабочей модели, то аннотации говорят Spring: «какой запрос сюда подходит», а сигнатура говорит: «что мне нужно вытащить из запроса, чтобы этот метод вообще можно было вызвать» и «что нужно положить в ответ после выполнения». Поэтому сигнатура — это не косметика и не «Java-формальность», а вторая половина публичного интерфейса API, прямо рядом с URI.

Важно понять ещё одну вещь: Spring готовит аргументы до вызова метода. То есть вы не «получаете HttpServletRequest и парсите его руками» (как в старых легендах). Spring смотрит на параметры метода, на аннотации на них и пытается их «разрешить» из запроса. Если это невозможно, метод может даже не запуститься — и это нормально: API-граница должна быть строгой.

Сигнатура: вход и выход

Сигнатура метода контроллера в Spring MVC похожа на хорошо организованный заказ в кафе. Вы не просто говорите «сделайте мне что-нибудь вкусное», вы называете конкретику: что нужно принести (аргументы) и что вы хотите получить в итоге (возвращаемое значение). Если вы не уточнили заказ, официант (Spring) либо принесёт не то, либо вообще остановится и скажет: «Я не понял, что вы хотели».

На текущем уровне нам достаточно держать в голове простую схему: сначала mapping выбирает метод, затем Spring подготавливает аргументы, затем выполняется код метода, затем результат превращается в HTTP-ответ. Под «превращается» мы пока понимаем очень приземлённо: «то, что вернули из метода, уходит клиенту». Как именно это превращение устроено технически — отдельная тема, и сегодня мы в неё не ныряем.

Ниже — маленькая «карта соответствий», которая помогает перестать путаться. Это не полный справочник (его никто не выдержит), а минимум для сегодняшнего дня:

Что мы пишем в методе Откуда Spring это берёт Минимальный пример
метод без аргументов ниоткуда — запрос самодостаточный public String ping()
@PathVariable String taskId из пути, из сегмента {taskId} GET /tasks/{taskId}
возвращаемый String станет телом ответа "pong"
возвращаемый List<String> станет телом ответа (список) List.of("t1", "t2")

И да: пока мы сознательно не трогаем query-параметры, body, заголовки и прочие источники входа. Сегодня нам важен базовый навык: увидеть, как кусок URL попадает в аргумент метода.

2. Коллекция: GET /api/v1/tasks

Список ресурсов (коллекция) — это тот счастливый сценарий, в котором мы можем написать метод без входных аргументов и не чувствовать себя виноватыми. Если клиент делает GET /api/v1/tasks, то он не адресует конкретную задачу, а просит «коллекцию задач» целиком. На этом уровне нам не нужен taskId, потому что клиент его не присылал.

Важно почувствовать: отсутствие аргументов — это не «я забыл параметры», а отражение смысла endpoint’а. У списка задач есть контракт: «дай мне список». И раз запрос не содержит идентификатора конкретного ресурса, сигнатура тоже его не требует. А вот что вернуть — это уже наша ответственность как API.

Минимальный пример endpoint’а списка (в учебном виде, пока без сервисов и без настоящих моделей):

import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController // Говорим Spring, что это REST-контроллер (ответ пойдёт в тело HTTP-ответа)
class DemoController {

    @GetMapping("/api/v1/tasks") // Маппинг для запроса коллекции
    public List<String> listTaskIds() {
        // Для примера возвращаем «игрушечные» id задач
        return List.of("t1", "t2");
    }
}

Здесь важно не содержимое списка (оно пока игрушечное), а сам факт: метод не принимает ничего, потому что запрос не несёт входных данных в пути. Клиент попросил «коллекцию», и мы на этом уровне отвечаем «коллекцией». Да, позже у нас будут нормальные структуры, но принцип не изменится.

3. @PathVariable: taskId из URL

Когда мы переходим от коллекции к конкретному ресурсу, появляется ключевая идея REST-мышления: адресация. Конкретная задача живёт по адресу /api/v1/tasks/{taskId}, и клиент буквально говорит: «дай мне вот эту штуку по этому адресу». Это как квартира по номеру: подъезд/этаж/квартира — не фильтры и не пожелания, а точный адрес.

В Spring MVC «номер квартиры» кладётся в аргумент метода через @PathVariable. В URL вы пишете плейсхолдер в фигурных скобках, а в методе — параметр с аннотацией. И Spring делает магию… точнее, делает очень конкретную работу: берёт значение из URL и подставляет его в аргумент.

Ментальная картинка может быть такой:

flowchart LR
    A["GET /api/v1/tasks/abc-123"] --> B["mapping: /api/v1/tasks/{taskId}"]
    B --> C["@PathVariable taskId = 'abc-123'"]
    C --> D["вызов метода контроллера"]

Минимальный пример detail endpoint’а:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@GetMapping("/api/v1/tasks/{taskId}") // В URL-шаблоне есть плейсхолдер {taskId}
public String getTask(@PathVariable String taskId) { // Spring возьмёт значение из URL и подставит сюда
    // Для наглядности возвращаем то, что пришло в path variable
    return "taskId=" + taskId;
}

Здесь очень полезно остановиться и проверить себя: taskId — это не «какая-то переменная в коде», а часть URL. Клиент прислал её в пути, Spring достал её из пути, и теперь вы в методе получаете готовое значение. Вы не парсите строку руками, не делаете substring, не пишете split("/"), и от этого мир становится чуточку добрее.

Имена: когда нужен @PathVariable("taskId")

Самая частая мелкая, но раздражающая проблема с @PathVariable — несостыковка имён. В URL-шаблоне вы написали {taskId}, а в параметре метода — String id. Для человека это «да какая разница, я же понимаю!», а для Spring это иногда выглядит как «у меня есть переменная taskId, а ты просишь id — ты кого вообще имеешь в виду?».

Правило, которое помогает жить спокойно: если имя параметра метода совпадает с именем в фигурных скобках, можно писать коротко. Если не совпадает — лучше указать имя явно в @PathVariable("..."). Это не «бюрократия», а просто явность контракта. Особенно полезно, когда вы переименовали параметр в IDE и забыли, что URL-то остался прежним.

Пример «совпали имена — всё просто»:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@GetMapping("/api/v1/tasks/{taskId}") // {taskId} в шаблоне совпадает с именем параметра
public String getTask(@PathVariable String taskId) { // Поэтому имя можно не уточнять явно
    // Возвращаем строку, чтобы увидеть значение в ответе
    return "taskId=" + taskId;
}

Пример «имена разные — пишем явно»:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@GetMapping("/api/v1/tasks/{taskId}") // Во внешнем контракте (URL) всё ещё taskId
public String getTask(@PathVariable("taskId") String id) { // Явно связываем {taskId} с параметром id
    // Внутри метода используем удобное нам имя переменной
    return "taskId=" + id;
}

Обратите внимание на тонкую пользу: метод теперь можно читать как предложение. «У нас есть endpoint /tasks/{taskId}; вытащи taskId и положи его в переменную id». То есть внешний контракт (слово taskId) остаётся стабильным, а внутреннее имя параметра вы можете выбирать хоть id, хоть taskIdentifier, хоть pleaseDontNameMeX (но последнее не надо, вы не злодей).

4. Возвращаемое значение и ответ

Возвращаемый тип метода контроллера — это тоже часть контракта. Не в смысле «клиент видит Java-тип» (нет), а в смысле того, какую форму данных вы отправляете наружу. В @RestController возвращаемое значение становится телом HTTP-ответа. Это означает простую вещь: выбирая String или List<...> или какой-то объект, вы фактически выбираете, как будет выглядеть ответ.

На сегодняшнем уровне достаточно понимать два тезиса. Первый: если метод успешно отработал и вернул значение, обычно клиент получит 200 OK и какое-то тело. Второй: форма тела ответа напрямую связана с тем, что вы вернули из метода. Поэтому к возвращаемому типу стоит относиться не как к «ну пусть будет Object», а как к публичному решению.

Пара мини-примеров поможет почувствовать разницу.

Если вернуть String, вы отдаёте простой текст:

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

@GetMapping("/api/v1/ping") // Простой health-check endpoint
public String ping() {
    // Возвращаем обычный текст (потом он станет телом HTTP-ответа)
    return "pong";
}

Если вернуть Map, вы отдаёте структурированные данные (обычно клиент увидит JSON-объект):

import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;

@GetMapping("/api/v1/demo") // Endpoint, который возвращает «объект»
public Map<String, Object> demo() {
    // Map удобно использовать как быстрый учебный «DTO»
    return Map.of(
            "status", "OK",
            "count", 2
    );
}

Если вернуть List<String>, вы отдаёте коллекцию (обычно это будет массив значений):

import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;

@GetMapping("/api/v1/demo-list") // Endpoint, который возвращает «массив»
public List<String> demoList() {
    // Список в ответе обычно сериализуется как JSON-массив
    return List.of("a", "b", "c");
}

Заметьте, мы сейчас намеренно не обсуждаем, «как именно оно превращается в JSON» и какие там настройки. Нам пока важно другое: возвращаемый тип — это не деталь реализации, а решение о форме ответа. И даже если вы сейчас отдаёте простые строки «для разогрева», сама привычка думать о форме ответа пригодится вам дальше на каждом шаге.

5. Мини-сборка: tasks через сигнатуры

Чтобы картина была цельной, давайте соберём маленький DemoTaskController. Это всё ещё учебная мини‑сборка: она нужна только для того, чтобы увидеть контраст между endpoint’ом коллекции и endpoint’ом конкретного ресурса.

Ниже пример контроллера, где данные пока «условные», зато сигнатуры очень честно отражают смысл API. Такой код удобно держать как промежуточный скелет: читается за 15 секунд, и вы сразу понимаете, что у API есть endpoint списка и endpoint детали. А если вы понимаете это по коду, то и клиенту будет проще понять это по URI.

import java.util.List;
import org.springframework.web.bind.annotation.*;

@RestController // REST-контроллер: то, что вернём из методов, уйдёт в HTTP-ответ
@RequestMapping("/api/v1/tasks") // Базовый путь для всех endpoint'ов в этом контроллере
class DemoTaskController {

    @GetMapping // GET /api/v1/tasks — коллекция
    public List<String> getAllTasks() {
        // Коллекция: входных данных нет, поэтому сигнатура без аргументов
        return List.of("t1", "t2");
    }

    @GetMapping("/{taskId}") // GET /api/v1/tasks/{taskId} — конкретный ресурс
    public String getTaskById(@PathVariable String taskId) {
        // Detail: чтобы адресовать ресурс, нужен taskId из URL
        return "taskId=" + taskId;
    }
}

Обратите внимание, насколько «говорящими» получаются две сигнатуры. Первая: ничего не надо на вход, потому что это коллекция. Вторая: нужен taskId, потому что это отдельный ресурс. И это именно то REST-мышление, которое мы зафиксировали на прошлых днях: коллекция и отдельный ресурс — разные точки API.

6. Типичные ошибки при работе с @PathVariable

В @PathVariable почти нет «сложной магии», но есть маленькие грабли, на которые легко наступить, особенно когда вы только начинаете. Ирония в том, что ошибки выглядят как пустяк (буква не совпала, скобку забыли), а результат — как будто «Spring сломался» и «ничего не работает». На самом деле всё работает — просто контракт стал противоречивым, и фреймворк честно отказывается угадывать ваши намерения.

Ошибка №1: плейсхолдер в пути и параметр метода называются по-разному, но имя не указано явно.
Вы пишете mapping "/{taskId}", а в методе параметр называется id и помечен просто @PathVariable. В зависимости от условий это может привести к тому, что Spring не сможет понять, какое значение подставлять. Привычка, которая спасает: либо называйте параметр так же (taskId), либо всегда пишите @PathVariable("taskId") String id.

Ошибка №2: забыли фигурные скобки или случайно «съели» слэш.
Иногда пишут @GetMapping("/taskId") вместо @GetMapping("/{taskId}"). Для человека это «почти одно и то же», но для маршрутизации это два разных мира. В первом случае URL должен буквально быть /taskId, а во втором — там может быть любое значение. Если вдруг endpoint не вызывается, первое, что нужно проверить, — правильность шаблона в mapping.

Ошибка №3: пытаются сделать detail endpoint без @PathVariable, но с «хардкодом» внутри.
Классика учебных примеров: метод называется getTaskById, но внутри он всегда возвращает одну и ту же задачу, потому что taskId нигде не принимается. Это ломает смысл API: клиент адресует конкретный ресурс, а сервер делает вид, что не заметил адрес. Правильный минимализм — это принять taskId аргументом, даже если пока вы только печатаете его в ответ.

Ошибка №4: возвращаемый тип выбирается «на глаз», а не по смыслу ответа.
Новичок часто думает: «Ну я верну Object, а там разберёмся». В @RestController это плохая привычка: вы теряете ясность контракта, и метод становится нечитаемым. Гораздо полезнее выбрать конкретный тип: String для простого текста, List<String> для списка, Map<String, Object> для простого структурированного ответа. Даже в учебных примерах это тренирует дисциплину формы ответа.

Ошибка №5: сигнатура перегружается «на всякий случай».
Иногда в метод начинают добавлять параметры, которые прямо сейчас не нужны: «вдруг пригодится». В итоге метод становится похож на рюкзак туриста-новичка, который тащит и палатку, и гитару, и кастрюлю, и утюг. На текущем этапе держим сигнатуры короткими: для коллекции — без аргументов, для одного ресурса — один @PathVariable. Это делает и код, и контракт очевидными.

1
Задача
Spring REST & MVC, 5 уровень, 2 лекция
Недоступна
Идентификатор заказа из пути
Идентификатор заказа из пути
1
Задача
Spring REST & MVC, 5 уровень, 2 лекция
Недоступна
Сигнатуры для списка и одного элемента
Сигнатуры для списка и одного элемента
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ