Автор оригинала: Eugen Paraschiv.
1. Обзор
Эта статья будет посвящена работе с ETags весной , интеграционному тестированию REST API и сценариям потребления с curl .
Дальнейшее чтение:
Введение в документы весеннего ОТДЫХА
Пользовательский тип носителя для API Spring REST
Разбиение на страницы с весенним отдыхом и таблицей AngularJS
2. ОТДЫХ и ETags
Из официальной весенней документации по поддержке ETag:
Мы можем использовать ETags для двух вещей – кэширования и условных запросов. Значение ETag можно рассматривать как хэш , вычисленный из байтов тела ответа. Поскольку служба, скорее всего, использует криптографическую хэш-функцию, даже самая незначительная модификация тела резко изменит выходные данные и, следовательно, значение ETag. Это верно только для сильных ETAG – протокол также предоставляет слабый Etag .
Использование заголовка If-* превращает стандартный запрос GET в условный GET. Два заголовка If -* , которые используются с ETags,-это ” 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 весной
Переход К поддержке пружины: использование ETag в Spring чрезвычайно легко настроить и полностью прозрачно для приложения. Мы можем включить поддержку, добавив простой Фильтр в web.xml :
etagFilter org.springframework.web.filter.ShallowEtagHeaderFilter etagFilter /foos/*
Мы сопоставляем фильтр с тем же шаблоном URI, что и сам API RESTful. Сам фильтр является стандартной реализацией функциональности ETag с весны 3.0.
Реализация является неглубокой – приложение вычисляет ETag на основе ответа, что позволит сэкономить пропускную способность, но не производительность сервера.
Таким образом, запрос, который выиграет от поддержки ETag, по-прежнему будет обрабатываться как стандартный запрос, потреблять любой ресурс, который он обычно потребляет (подключения к базе данных и т. Д.), И только до того, как его ответ будет возвращен клиенту, начнет работать поддержка ETag.
В этот момент ETag будет вычислен из тела ответа и установлен на самом ресурсе; кроме того, если в запросе был установлен заголовок If-None-Match , он также будет обработан.
Более глубокая реализация механизма ETag потенциально может обеспечить гораздо большие преимущества – например, обслуживание некоторых запросов из кэша и отсутствие необходимости выполнять вычисления вообще, – но реализация, безусловно, не будет такой простой и подключаемой, как описанный здесь поверхностный подход.
4.1. Конфигурация на основе Java
Давайте посмотрим, как будет выглядеть конфигурация на основе Java, объявив ShallowEtagHeaderFilter bean в нашем весеннем контексте :
@Bean public ShallowEtagHeaderFilter shallowEtagHeaderFilter() { return new ShallowEtagHeaderFilter(); }
Имейте в виду, что если нам нужно предоставить дополнительные конфигурации фильтров, мы можем вместо этого объявить экземпляр FilterRegistrationBean :
@Bean public FilterRegistrationBeanshallowEtagHeaderFilter() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>( new ShallowEtagHeaderFilter()); filterRegistrationBean.addUrlPatterns("/foos/*"); filterRegistrationBean.setName("etagFilter"); return filterRegistrationBean; }
Наконец, если мы не используем Spring Boot, мы можем настроить фильтр с помощью метода AbstractAnnotationConfigDispatcherServletInitializer ‘s getServletFilters .
4.2. Использование метода ETag() Объекта ответа
Этот метод был введен в Spring framework 4.1, и мы можем использовать его для управления значением ETag, которое получает одна конечная точка .
Например, представьте, что мы используем версионные сущности в качестве механизма блокировки оптимистов для доступа к информации нашей базы данных.
Мы можем использовать саму версию в качестве ETag, чтобы указать, была ли изменена сущность:
@GetMapping(value = "/{id}/custom-etag") public ResponseEntityfindByIdWithCustomEtag(@PathVariable("id") final Long id) { // ...Foo foo = ... return ResponseEntity.ok() .eTag(Long.toString(foo.getVersion())) .body(foo); }
Служба получит соответствующее состояние 304-Не изменено , если условный заголовок запроса соответствует данным кэширования.
5. Тестирование ETags
Давайте начнем с простого – нам нужно убедиться, что ответ на простой запрос, извлекающий один ресурс, действительно вернет заголовок “ 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 Не измененный , поскольку сам ресурс действительно не был изменен между двумя операциями извлечения
Наконец, мы проверяем случай, когда ресурс изменяется между первым и вторым запросами на извлечение:
@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 больше не является правильным, поскольку мы тем временем обновили ресурс
Наконец, последний тест – который не будет работать, потому что функциональность еще не была реализована весной – это поддержка заголовка If-Match HTTP:
@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 Большие
Мы использовали теги только для операций чтения. An RFC существует попытка прояснить, как реализации должны иметь дело с ETAGS при операциях записи – это не стандартно, но интересно читать.
Конечно, существуют и другие возможные варианты использования механизма ETag, например, для оптимистичного механизма блокировки, а также для решения связанной с “Проблемой потерянного обновления” .
Существует также несколько известных потенциальных подводных камней и предостережений , о которых следует помнить при использовании ETags.
7. Заключение
Эта статья только поцарапала поверхность тем, что возможно с пружиной и ETags.
Для полной реализации службы RESTful с поддержкой ETag, а также интеграционных тестов, проверяющих поведение ETag, ознакомьтесь с проектом GitHub .