Обработчики запросов в Spring Web MVC

Самое главное в любом web фреймворке — отображение: какой код обслуживает какой http endpoint((это ещё называют routing). В Web MVC для этого используются аннотации, связывающие методы классов с веб запросами.

Основы web и HTTP

Перед тем, как начинать писать методы, вызываемые через web, стоит определиться с терминологией и понять, как это всё работает. Протокол HTTP (и HTTP/2 тоже) используют URL для идентификации ресурсов. URL выглядит следующим образом:

<схема>:[//[<логин>:<пароль>@]<хост>[:<порт>]][/<URL‐путь>][?<параметры>][#<якорь>]

Всё, что написано до /<URL-путь> несомненно очень важно, однако обрабатывается до того, как в дело вступает наше приложение. Когда HTTP клиент подключается к HTTP серверу, он посылает путь в запросе. Кроме пути каждый запрос характеризуется http методом, указывающего, что же надо сделать с ресурсом, определяемым путём. Таким образом, к одному пути может быть аж до девяти обработчиков, по одному для каждого метода. Метод и путь — не единственная часть запроса, с запросом передаётся набор заголовков, параметры запроса и тело запроса. К сожалению, заголовков даже в стандарте определено слишком много, чтобы их здесь перечислять, а уж параметры и тело могут быть вообще какие угодно. Использование всего богатства HTTP я рассмотрю в следующих статьях, а пока сосредоточимся на простом — пути и методах.

Контроллеры Spring Wev MVC

В качестве основы я взял пустое Spring boot приложение с поддержкой Spring Web MVC. В это пустое приложение добавим единственный контроллер и тест к нему:

Всякий контроллер обязательно имеет аннотацию @Controller или @RestController, последняя расширяет @Controller и добавляет к нему @ResponseBody, тем самым говоря, что все методы класса возвращают именно те данные, которые надо переслать клиенту, игнорируя MVC часть (которую я тоже рассмотрю отдельно). Вторая аннотация в моём классе контроллера — @RequestMapping, в такой форме она задаёт общий путь для всех членов класса, каждый из которых может как обслуживать какие-то методы, вызываемые для общего пути класса, так и методы для путей под путём класса.

Собственно первый же метод, list(), использует этот путь по умолчанию. Аннотация @GetMapping говорит — метод list() должен быть вызван, когда кто-то вызывает метод GET на пути /notes. Имя пути, очевидно, берётся из параметра @RequestMapping на классе.

Второй метод, read(), показывает сразу две особенности маппинга. Во-первых @GetMapping  содержит путь и этот путь будет добавлен к пути, который задан у всего класса. Во-вторых, этот путь содержит переменную  {id}, что позволяет не задавать путь явно, если вы не знаете какую-либо часть пути или если он переменный. В случае метода read() он будет вызываться на все пути вида

  • /notes/1
  • /notes/test
  • /notes/parmigiano reggiano

и так далее.

Разумеется, если есть метод задать переменную часть пути, есть метод и прочитать её. И именно этот метод показан в параметрах read() —  @PathVariable String id, имя переменной в методе должно совпадать с именем переменной пути. Это позволяет иметь пути с несколькими переменными.

Наконец, именно в методе read()  показано, как можно из одной функции вернуть два ответа. Собственно я следую соглашениям HTTP — если по запрошенному пути что-то есть, возвращаем данные, если нет — возвращаем ошибку 404. А для того, чтобы в java вернуть ошибку, используются исключения. А Spring Web MVC умеет перехватывать исключения и конвертировать их в HTTP статусы. Для этого достаточно завести собственное исключение и навесить на него аннотацию @ResponseStatus

Метод create() показывает, что @ResponseStatus можно навешивать и на любой метод, чтобы он начал отвечать на всё заданным кодом ответа. Аннотация @PostMapping без спецификации пути, говорит нам, что метод create() обслуживает POST запросы по пути /notes. Кроме того, в этом методе показано как работать с другими частями HTTP запроса: параметрами, передающимися в URL и телом запроса. @RequestParam связывает параметр запроса и переменную по имени. По умолчанию параметры обязательны и, если его не будет, ваш код даже не будет вызван и Spring Web MVC вернёт ошибку самостоятельно. Однако в моём случае метод можно вызывать без параметра. Параметров метода с @RequestParam может быть сколько угодно. А вот параметр с @RequestBody может быть только один и с ним будет передано тело запроса. Кстати, хотя это и редко используется, тело запроса можно передавать с любым типом запроса.

Наконец метод update() показывает, как обрабатывать PUT запросы. В принципе в нём используются уже описанные вещи, поэтому подробно я его рассматривать не буду. Строго говоря, я вписал этот метод только для полноты поддержки CRUD (Create-Read-Update-Delete).

Последний метод, delete(), показывает альтернативный метод маппинга, с помощью @RequestMapping — в параметрах аннотации можно передать не только путь, но и HTTP метод, на который следует реагировать. Строго говоря, все ранее перечисленные аннотации являются всего лишь сокращёнными версиями @RequestMapping

Работа приложения

Как несложно было понять из кода — приложение позволяет сохранять строки в памяти, просматривать их и удалять. Давайте проверим его в работе:

Записи вроде бы создались, проверим их наличие:

Выглядит нормально. Попробуем заменить значение одной из записей и прочитать её:

Редактирование работает, самое время что-нибудь удалить:

Запись успешно удалена и попытка её прочитать возвращает ошибку 404. Обратите внимание, что Spring Web MVC любезно сформировал красивый JSON ответ с ошибкой для нас.

Код примера доступен на github.