Перейти к основному содержимому

ETags для REST с Spring

· 7 мин. чтения

1. Обзор

В этой статье речь пойдет о работе с ETags в Spring , интеграционном тестировании REST API и сценариях потребления с помощью curl .

2. REST и ETag

Из официальной документации Spring по поддержке 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 , который будет сохранен для дальнейшего использования:

curl -H "Accept: application/json" -i http://localhost:8080/spring-boot-rest/foos/1
HTTP/1.1 200 OK
ETag: "f88dd058fe004909615a64f01be66a7"
Content-Type: application/json;charset=UTF-8
Content-Length: 52

Для следующего запроса клиент включит заголовок запроса If-None-Match со значением ETag из предыдущего шага. Если ресурс не изменился на сервере, ответ не будет содержать тела и код состояния 304 — не изменен :

curl -H "Accept: application/json" -H 'If-None-Match: "f88dd058fe004909615a64f01be66a7"'
-i http://localhost:8080/spring-boot-rest/foos/1
HTTP/1.1 304 Not Modified
ETag: "f88dd058fe004909615a64f01be66a7"

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

curl -H "Content-Type: application/json" -i 
-X PUT --data '{ "id":1, "name":"Transformers2"}'
http://localhost:8080/spring-boot-rest/foos/1
HTTP/1.1 200 OK
ETag: "d41d8cd98f00b204e9800998ecf8427e"
Content-Length: 0

Наконец, мы отправляем последний запрос, чтобы снова получить Foo. Имейте в виду, что мы обновили его с момента последнего запроса, поэтому предыдущее значение ETag больше не должно работать. Ответ будет содержать новые данные и новый ETag, который, опять же, можно сохранить для дальнейшего использования:

curl -H "Accept: application/json" -H 'If-None-Match: "f88dd058fe004909615a64f01be66a7"' -i 
http://localhost:8080/spring-boot-rest/foos/1
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 :

<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>/foos/*</url-pattern>
</filter-mapping>

Мы сопоставляем фильтр с тем же шаблоном URI, что и сам RESTful API. Сам фильтр является стандартной реализацией функциональности ETag, начиная с Spring 3.0.

Реализация неглубокая — приложение вычисляет ETag на основе ответа, что сэкономит пропускную способность, но не производительность сервера.

Таким образом, запрос, который выиграет от поддержки ETag, будет по-прежнему обрабатываться как стандартный запрос, потреблять любой ресурс, который он обычно потребляет (подключения к базе данных и т. д.), и только до того, как его ответ будет возвращен обратно клиенту, поддержка ETag отключится. в.

В этот момент ETag будет вычисляться из тела ответа и устанавливаться на самом ресурсе; Кроме того, если в запросе был установлен заголовок If-None-Match , он также будет обработан.

Более глубокая реализация механизма ETag потенциально могла бы обеспечить гораздо большие преимущества, такие как обслуживание некоторых запросов из кеша и вообще отсутствие необходимости выполнять вычисления, но реализация определенно не была бы такой простой и не такой подключаемой, как неглубокий подход. описано здесь.

4.1. Конфигурация на основе Java

Давайте посмотрим, как будет выглядеть конфигурация на основе Java, объявив bean-компонент ShallowEtagHeaderFilter в нашем контексте Spring :

@Bean
public ShallowEtagHeaderFilter shallowEtagHeaderFilter() {
return new ShallowEtagHeaderFilter();
}

Имейте в виду, что если нам нужно предоставить дополнительные конфигурации фильтра, мы можем вместо этого объявить экземпляр FilterRegistrationBean :

@Bean
public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
FilterRegistrationBean<ShallowEtagHeaderFilter> filterRegistrationBean
= new FilterRegistrationBean<>( new ShallowEtagHeaderFilter());
filterRegistrationBean.addUrlPatterns("/foos/*");
filterRegistrationBean.setName("etagFilter");
return filterRegistrationBean;
}

Наконец, если мы не используем Spring Boot, мы можем настроить фильтр с помощью метода AbstractAnnotationConfigDispatcherServletInitializer getServletFilters . ``

4.2. Использование метода eTag() объекта ResponseEntity

Этот метод был представлен в Spring framework 4.1, и мы можем использовать его для управления значением ETag, которое получает одна конечная точка .

Например, представьте, что мы используем версионные сущности в качестве механизма Optimist Locking для доступа к информации нашей базы данных.

Мы можем использовать саму версию в качестве ETag, чтобы указать, была ли изменена сущность:

@GetMapping(value = "/{id}/custom-etag")
public ResponseEntity<Foo>
findByIdWithCustomEtag(@PathVariable("id") final Long id) {

// ...Foo foo = ...

return ResponseEntity.ok()
.eTag(Long.toString(foo.getVersion()))
.body(foo);
}

Служба получит соответствующее состояние 304-Not Modified , если условный заголовок запроса соответствует данным кэширования.

5. Тестирование ETag

Начнем с простого — нам нужно убедиться, что ответ на простой запрос, извлекающий один ресурс, действительно вернет заголовок « ETag» :

@Test
public void givenResourceExists_whenRetrievingResource_thenEtagIsAlsoReturned() {
// Given
String uriOfResource = createAsUri();

// When
Response findOneResponse = RestAssured.given().
header("Accept", "application/json").get(uriOfResource);

// Then
assertNotNull(findOneResponse.getHeader("ETag"));
}

Затем мы проверяем счастливый путь поведения ETag. Если запрос на получение ресурса с сервера использует правильное значение ETag , сервер не получает ресурс:

@Test
public void givenResourceWasRetrieved_whenRetrievingAgainWithEtag_thenNotModifiedReturned() {
// Given
String uriOfResource = createAsUri();
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 , поскольку сам ресурс действительно не был изменен между двумя операциями поиска.

Наконец, мы проверяем случай, когда Ресурс изменяется между первым и вторым поисковыми запросами:

@Test
public void
givenResourceWasRetrievedThenModified_whenRetrievingAgainWithEtag_thenResourceIsReturned() {
// Given
String uriOfResource = createAsUri();
Response findOneResponse = RestAssured.given().
header("Accept", "application/json").get(uriOfResource);
String etagValue = findOneResponse.getHeader(HttpHeaders.ETAG);

existingResource.setName(randomAlphabetic(6));
update(existingResource);

// When
Response secondFindOneResponse= RestAssured.given().
header("Accept", "application/json").headers("If-None-Match", etagValue)
.get(uriOfResource);

// Then
assertTrue(secondFindOneResponse.getStatusCode() == 200);
}

Шаг за шагом:

  • мы сначала создаем и извлекаем ресурс — и сохраняем значение ETag для дальнейшего использования
  • затем мы обновляем тот же ресурс
  • отправьте новый запрос GET, на этот раз с заголовком « If-None-Match », указывающим ETag , который мы ранее сохранили
  • по этому второму запросу сервер вернет 200 OK вместе с полным ресурсом, поскольку значение ETag больше не является правильным, поскольку мы тем временем обновили ресурс.

Наконец, последний тест, который не будет работать, потому что функциональность еще не реализована в Spring , — это поддержка HTTP-заголовка If-Match :

@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 Precondition Failed

6. ETags большие

Мы использовали ETag только для операций чтения. Существует RFC, пытающийся прояснить, как реализации должны работать с ETags при операциях записи — это не стандартно, но интересно читать.

Конечно, есть и другие возможные применения механизма ETag, например, для механизма оптимистичной блокировки, а также для решения связанной с этим «проблемы с потерянными обновлениями» .

Есть также несколько известных потенциальных ловушек и предостережений , о которых следует помнить при использовании ETag.

7. Заключение

В этой статье только поверхностно рассмотрены возможности Spring и ETags.

Для полной реализации службы RESTful с поддержкой ETag, а также для интеграционных тестов, проверяющих поведение ETag, ознакомьтесь с проектом GitHub .