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 OK ETag: 'f88dd058fe004909615a64f01be66a7' Content-Type: application /json ;charset=UTF-8 Content-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 Modified ETag: 'f88dd058fe004909615a64f01be66a7' |
— теперь , прежде чем снова получить Ресурс, мы изменим его, выполнив обновление:
1
2
3
|
-X PUT --data '{ ' id ':1, ' name ':' newRoleName2 ', ' description ':' theNewDescription ' }' http: //localhost :8080 /rest-sec/api/resources/1 |
1
2
3
|
HTTP /1 .1 200 OK ETag: 'd41d8cd98f00b204e9800998ecf8427e' <strong>Content-Length: 0< /strong > |
— наконец , мы отправляем последний запрос на получение привилегии снова; имейте в виду, что он был обновлен с момента последнего извлечения, поэтому предыдущее значение ETag больше не должно работать — ответ будет содержать новые данные и новый ETag, который, опять же, может быть сохранен для дальнейшего использования:
1
2
|
curl -H 'Accept: application/json' -H 'If-None-Match: ' f88dd058fe004909615a64f01be66a7 '' -i http: //localhost :8080 /rest-sec/api/resources/1 |
1
2
3
4
|
HTTP /1 .1 200 OK ETag: '03cb37ca667706c68c0aad4cb04c3a211' Content-Type: application /json ;charset=UTF-8 Content-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
|
@Test public 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
|
@Test public 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
|
@Test public 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
|
@Test public 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 .