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 .