Spring Web MVC и данные HTTP запросов

Как я писал в предыдущей статье, две главные вещи, которые определяют HTTP запрос, это путь и метод. Однако, если есть главные вещи, должны быть и второстепенные и их есть. Давайте посмотрим на типичный HTTP запрос к какому-нибудь приложению:

Кроме метода и пути в обе стороны отправляются заголовки запроса, заголовки ответа и данные, как ответа, так и запроса. И всё это можно использовать в обработчиках запросов Web MVC.

HEAD и OPTION

И начать я бы хотел всё таки с дополнительных методов. В статье о обработчиках HTTP запросов я рассматривал четыре основных HTTP метода, которые изменяют или возвращают данные. Однако существует два метода, которые работают не с данными, а с самим HTTP и отображением запросов. В первую очередь это метод HEAD, который ведёт себя точно так же, как и GET, за одним исключением — тело запроса не возвращается, возвращаются только заголовки запроса. Это позволяет проверять, какой будет ответ сервера (например его размер), без получения собственно ответа. Spring автоматически включает поддержку HEAD для всех GET запросов. Для примера я сделаю простой контроллер с GET обработчиком и обращусь к нему с методами GET и HEAD:

Видно, что заголовки в первом и во втором случае практически не отличаются, добавился лишь Content-Length, указывающий, сколько будет передано данных в случае GET запроса, но сами данные в HEAD запросе не передаются.

Второй важный метод, работающий не с вашими данными, а с HTTP как таковым — OPTIONS. OPTIONS, вызванный для пути, сообщает, какие методы можно вызывать для этого пути и какие значения прочих заголовков могут приниматься. Обработчик метода OPTIONS так же создаётся автоматически.

Заголовок Allow говорит нам, что для /api/strings можно вызывать методы GET и HEAD.

Заголовки запроса

Раз уж речь зашла о залоговках, давайте научимся их читать из запроса. Для этого используется аннотация @RequestHeader, которая навешивается на параметр метода:

Кстати, если вы хотите получить все заголовки запроса, просто используйте параметр типа HttpHeaders с этой аннотацией.

Проверим, что без пароля доступа нет, а с паролем есть:

Принимаемые и отправляемые типы данных

Некоторые заголовки настолько важны, что для них сделана особая поддержка. Заголовок Content-Type указывает на тип данных, передающихся с запросом или с ответом. В Spring можно задать, какой тип данных готов обработать метод-обработчик запроса. Более того, можно завести несколько методов, которые будут отличаться только обрабатываемым типом данных.

Как пример, если вы хотите сделать сервис по созданию заметок, который принимает данные и в JSON и в XML, он может выглядеть как-то так:

Оба обработчика работают с методом POST и обрабатывают один и тот же путь. Разница только в типе передаываемых данных. В зависимости от содержимого заголовка Content-Type будет вызван тот или иной метод:

Что интересно, если указать не тот Content-Type или не указать его вообще, Spring автоматически вернёт ошибку:

Content-Type передаётся как с запросом, так и с ответом. Обычно Spring довольно неплохо сам определяет тип передаваемых данных и самостоятельно выставляет корректное значение Content-Type, но, при необходимости, можно задать его и вручную. А ещё можно задать несколько разных Content-Type, на случай, если мы хотим возвращать разные данные для одного вызова.

Например, в сервисе заметок, описанном выше, вы хотели бы строго следовать принципам REST и иметь один единственный уникальный путь к каждой заметке и при этом предоставлять как машинно читаемое API, так и HTML страницы для человека. Это можно реализовать с помощью двух методов, отдающих контент разных типов:

А чтобы выбрать соответствующий метод, необходимо передать заголовок Accept в запросе:

Если Accept не передавать или указывать в нём достаточно широкие варианты, по умолчанию будет выбран text/html. А вот если указать неподдерживаемый тип, будет возвращена ошибка.

Описанный выше трюк называется content negotiation и позволяет клиенту указывать, какой именно ответ он ожидает — тип данных, кодировка, язык и так далее. Однако описание всех возможностей content negotiation выходит за пределы данной статьи и ознакомиться с ним можно в других местах.

Cookies

Cookies представляют собой небольшие (до 4 килобайт) наборы данных, которые сервер может послать клиенту, а клиент будет обязан записать их и отправлять обратно серверу при каждом запросе. Cookie передаются с использованием HTTP заголовков, но, так как этот механизм тоже очень важен, в Spring Web MVC есть специальная поддержка для получения данных из Cookie.

Каждый Cookie идентифицируется тремя вещами — доменом, чтобы браузер знал, какому сайту его отправлять; путём, чтобы иметь возможность ставить разные Cookie для разных частей сайта и именем. Последнее и используется в Spring Web MVC для доступа к Cookie:

Так как мой код только читает Cookie, но не выставляет его значение, придётся сымитировать браузер:

HttpServletResponse

К сожалению, Spring Web MVC не предоставляет простого метода для установки Cookie. Поэтому нам придётся познакомиться с более низкими уровнями реализации HTTP в java. Весь Spring Web MVC реализован поверх API, которое построено вокруг двух объектов — запрос от клиента оборачивается в HttpSerlvetRequest, а ответ формируется из заполненного вашим кодом HttoServletResponse. Имея доступ к этим объектам, вы получаете полный контроль над всей HTTP сессией. Spring Web MVC позволяет обращаться к этим объектам напрямую, при этом отключается вся его автоматизация.

Код выше создаёт объект Cookie, назначает ему срок годности и путь, и добавляет его к ответу. Всё остальное, это задание стандартных заголовков и не менее стандартного кода ответа. Обратите внимание, что метод setCookie() ничего не возвращает в Spring Web MVC и формирование всего ответа сервера происходит только путём вызова методов HttpServletResponse.

Cookie будет выслан в ответе в особом заголовке. При этом метод в запросе не играет никакой роли — Cookie будет отправлен для любого метода (кроме OPTIONS).

Статья уже получилась довольно длинной, поэтому я разобью её на две. В следующей части мы рассмотрим управлением кэшированием, обработку формп и загрузку файлов. Код примера доступен на github.