Статьи

ETags для отдыха с весны

1. Обзор

Эта статья будет посвящена ETags — поддержке Spring, интеграционному тестированию RESTful API и сценариям потребления с использованием curl. Это девятая из серии статей о настройке защищенного веб-сервиса RESTful с использованием Spring 3.1 и Spring Security 3.1 с настройкой на основе Java.

Серия «Отдых с весной»:

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
curl --user [email protected]: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 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 .