1. Обзор
Эта статья будет посвящена ETags — поддержке Spring, интеграционному тестированию RESTful API и сценариям потребления с использованием curl. Это девятая из серии статей о настройке защищенного веб-сервиса RESTful с использованием Spring 3.1 и Spring Security 3.1 с настройкой на основе Java.
Серия «Отдых с весной»:
- Часть 1. Начальная загрузка веб-приложения с помощью Spring 3.1 и конфигурации на основе Java
- P art 2 — Создание веб-службы RESTful с помощью Spring 3.1 и конфигурации на основе Java
- P art 3 — Защита веб-службы RESTful с помощью Spring Security 3.1
- Часть 4. Обнаружение веб-службы RESTful
- Часть 5. Обнаружение службы REST с помощью Spring
- Часть 6. Базовая и дайджест-проверка подлинности для службы RESTful с Spring Security 3.1
- Часть 7. REST Pagination весной
- Часть 8. Аутентификация на RESTful-сервисе с помощью Spring Security
2. ОТДЫХ и ETags
Из официальной весенней документации по поддержке ETag:
ETag (тег сущности) — это заголовок ответа HTTP, возвращаемый веб-сервером, совместимым с HTTP / 1.1, который используется для определения изменения содержимого по заданному URL-адресу.ETag используются для двух вещей — кеширования и условных запросов. Значение ETag может быть, однако, в виде хэша, вычисленного из байтов тела ответа. Поскольку криптографическая хеш-функция, скорее всего, используется, даже самая маленькая модификация тела резко изменит выход и, следовательно, значение ETag. Это верно только для сильных ETag — протокол также обеспечивает слабый Etag .
Использование заголовка If- * превращает стандартный запрос GET в условный GET . Два заголовка If- * , которые используются с ETag — это « If-None-Match » и « If-Match » — каждый со своей семантикой, как будет обсуждаться далее в этой статье.
3. Связь клиент-сервер с curl
Простое взаимодействие клиент-сервер с использованием ETag можно разбить на этапы:
— во- первых , клиент выполняет вызов REST API; — ответ содержит заголовок ETag, который необходимо сохранить для дальнейшего использования:
|
1
|
curl -H 'Accept: application/json' -i http://localhost:8080/rest-sec/api/resources/1 |
|
1
2
3
4
|
HTTP/1.1 200 OKETag: 'f88dd058fe004909615a64f01be66a7'Content-Type: application/json;charset=UTF-8Content-Length: 52 |
— следующий запрос, который Клиент отправляет в RESTful API, включает заголовок запроса If-None-Match со значением ETag из предыдущего шага; если Ресурс не изменился на Сервере, Ответ не будет содержать тела и кода состояния 304 — Не изменено :
|
1
2
|
curl -H 'Accept: application/json' -H 'If-None-Match: 'f88dd058fe004909615a64f01be66a7'' -i http://localhost:8080/rest-sec/api/resources/1 |
|
1
2
|
HTTP/1.1 304 Not ModifiedETag: 'f88dd058fe004909615a64f01be66a7' |
— теперь , прежде чем снова получить Ресурс, мы изменим его, выполнив обновление:
|
1
2
3
|
curl --user admin@fake.com:adminpass -H 'Content-Type: application/json' -i -X PUT --data '{ 'id':1, 'name':'newRoleName2', 'description':'theNewDescription' }'http://localhost:8080/rest-sec/api/resources/1 |
|
1
2
3
|
HTTP/1.1 200 OKETag: 'd41d8cd98f00b204e9800998ecf8427e'<strong>Content-Length: 0</strong> |
— наконец , мы отправляем последний запрос на получение привилегии снова; имейте в виду, что он был обновлен с момента последнего извлечения, поэтому предыдущее значение ETag больше не должно работать — ответ будет содержать новые данные и новый ETag, который, опять же, может быть сохранен для дальнейшего использования:
|
1
2
|
curl -H 'Accept: application/json' -H 'If-None-Match: 'f88dd058fe004909615a64f01be66a7'' -ihttp://localhost:8080/rest-sec/api/resources/1 |
|
1
2
3
4
|
HTTP/1.1 200 OKETag: '03cb37ca667706c68c0aad4cb04c3a211'Content-Type: application/json;charset=UTF-8Content-Length: 56 |
И вот что у вас есть — ETags в дикой природе и экономии трафика.
4. Поддержка ETag весной
Относительно поддержки Spring — использовать ETag в Spring чрезвычайно просто в настройке и полностью прозрачно для приложения. Поддержка включается добавлением простого фильтра в файл web.xml :
|
1
2
3
4
5
6
7
8
|
<filter> <filter-name>etagFilter</filter-name> <filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class></filter><filter-mapping> <filter-name>etagFilter</filter-name> <url-pattern>/api/*</url-pattern></filter-mapping> |
Фильтр сопоставлен с тем же шаблоном URI, что и сам RESTful API. Сам фильтр является стандартной реализацией функциональности ETag начиная с Spring 3.0.
Реализация является мелкой — ETag рассчитывается на основе ответа, который сохранит пропускную способность, но не производительность сервера . Таким образом, запрос, который получит пользу от поддержки ETag, будет по-прежнему обрабатываться как стандартный запрос, потреблять любой ресурс, который он обычно потребляет (соединения с базой данных и т. Д.), И только до того, как ответ будет возвращен клиенту, поддержка ETag будет отключена. в.
В этот момент ETag будет вычислен из тела ответа и установлен на самом ресурсе; также, если заголовок If-None-Match был установлен в Запросе, он также будет обработан.
Более глубокая реализация механизма ETag может потенциально обеспечить гораздо большие преимущества — например, обслуживать некоторые запросы из кэша и вообще не выполнять вычисления — но реализация, безусловно, будет не такой простой и не такой подключаемой, как поверхностный подход. описано здесь.
5. Тестирование ETags
Давайте начнем с простого — нам нужно убедиться, что ответ простого запроса, извлекающего один ресурс, действительно вернет заголовок « ETag» :
|
01
02
03
04
05
06
07
08
09
10
11
|
@Testpublic void givenResourceExists_whenRetrievingResource_thenEtagIsAlsoReturned() { // Given Resource existingResource = getApi().create(new Resource()); String uriOfResource = baseUri + '/' + existingResource.getId(); // When Response findOneResponse = RestAssured.given(). header('Accept', 'application/json').get(uriOfResource); // Then assertNotNull(findOneResponse.getHeader(HttpHeaders.ETAG));} |
Далее мы проверяем правильный путь поведения ETag — если Запрос на получение Ресурса с сервера использует правильное значение ETag , то Ресурс больше не возвращается.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
@Testpublic void givenResourceWasRetrieved_whenRetrievingAgainWithEtag_thenNotModifiedReturned() { // Given T existingResource = getApi().create(createNewEntity()); String uriOfResource = baseUri + '/' + existingResource.getId(); Response findOneResponse = RestAssured.given(). header('Accept', 'application/json').get(uriOfResource); String etagValue = findOneResponse.getHeader(HttpHeaders.ETAG); // When Response secondFindOneResponse= RestAssured.given(). header('Accept', 'application/json').headers('If-None-Match', etagValue) .get(uriOfResource); // Then assertTrue(secondFindOneResponse.getStatusCode() == 304);} |
Шаг за шагом:
- Ресурс сначала создается, а затем извлекается — значение ETag сохраняется для дальнейшего использования
- отправляется новый запрос на получение, на этот раз с заголовком « If-None-Match », в котором указано ранее сохраненное значение ETag
- в этом втором запросе сервер просто возвращает 304 Not Modified , поскольку сам ресурс действительно не изменяется между двумя операциями поиска
Наконец , мы проверяем случай, когда Ресурс изменяется между первым и вторым поисковыми запросами:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
@Testpublic void givenResourceWasRetrieved_whenRetrievingAgainWithEtag_thenNotModifiedReturned() { // Given T existingResource = getApi().create(createNewEntity()); String uriOfResource = baseUri + '/' + existingResource.getId(); Response findOneResponse = RestAssured.given(). header('Accept', 'application/json').get(uriOfResource); String etagValue = findOneResponse.getHeader(HttpHeaders.ETAG); existingResource.setName(randomAlphabetic(6)) getApi().update(existingResource.setName(randomString)); // When Response secondFindOneResponse= RestAssured.given(). header('Accept', 'application/json').headers('If-None-Match', etagValue) .get(uriOfResource); // Then assertTrue(secondFindOneResponse.getStatusCode() == 200);} |
Шаг за шагом:
- Ресурс сначала создается, а затем извлекается — значение ETag сохраняется для дальнейшего использования
- тот же ресурс затем обновляется
- отправляется новый запрос на получение, на этот раз с заголовком « If-None-Match », в котором указано ранее сохраненное значение ETag
- при этом втором запросе сервер вернет 200 OK вместе с полным ресурсом, поскольку значение ETag больше не является правильным, так как ресурс был обновлен за это время
Затем мы проверяем поведение « If-Match » — ShallowEtagHeaderFilter не имеет встроенной поддержки HTTP-заголовка If-Match (отслеживаемого по этой проблеме JIRA ), поэтому следующий тест должен завершиться неудачей:
|
01
02
03
04
05
06
07
08
09
10
11
|
@Testpublic void givenResourceExists_whenRetrievedWithIfMatchIncorrectEtag_then412IsReceived() { // Given T existingResource = getApi().create(createNewEntity()); // When String uriOfResource = baseUri + '/' + existingResource.getId(); Response findOneResponse = RestAssured.given().header('Accept', 'application/json'). headers('If-Match', randomAlphabetic(8)).get(uriOfResource); // Then assertTrue(findOneResponse.getStatusCode() == 412);} |
Шаг за шагом:
- Ресурс впервые создан
- Ресурс затем извлекается с заголовком « If-Match », указывающим неверное значение ETag — это условный запрос GET
- сервер должен вернуть 412 Сбой предварительного условия
6. ETags БОЛЬШИЕ
Мы использовали только ETag для операций чтения — существует RFC, пытающийся выяснить, как реализации должны работать с ETag в операциях записи — это не стандартное, но интересное чтение.
Конечно, есть и другие возможные варианты использования механизма ETag, например, для механизма оптимистической блокировки, использующего Spring 3.1, а также для решения связанной с этим «проблемы утраченного обновления» .
Есть также несколько известных потенциальных ловушек и предостережений, о которых следует помнить при использовании ETag.
7. Заключение
Эта статья только о том, что возможно с Spring и ETags. Для полной реализации службы RESTful с поддержкой ETag, а также интеграционных тестов, проверяющих поведение ETag, ознакомьтесь с проектом github .
Ссылка: ETags for REST with Spring от нашего партнера JCG Евгения Параскива в блоге baeldung .