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

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

1. обзор

Эта статья будет посвященаworking with ETags in Spring, интеграционному тестированию REST API и сценариям потребления сcurl.

Дальнейшее чтение:

Введение в Spring REST Docs

В этой статье представлен Spring REST Docs, механизм, управляемый тестами, для создания документации для сервисов RESTful, которая является точной и удобочитаемой.

Read more

Пользовательский тип носителя для Spring REST API

Краткое введение в использование пользовательского типа мультимедиа в Spring REST API.

Read more

Нумерация страниц с помощью Spring REST и AngularJS таблицы

Подробно рассмотрим, как реализовать простой API с разбиением на страницы в Spring и как использовать его с AngularJS и UI Grid.

Read more

2. REST и ETags

Из официальной весенней документации по поддержке ETag:

ETag (тег объекта) - это заголовок ответа HTTP, возвращаемый веб-сервером, совместимым с HTTP / 1.1, который используется для определения изменения содержимого по заданному URL-адресу.

Мы можем использовать ETag для двух вещей - кеширования и условных запросов. ETag value can be thought of as a hash вычисляется из байтов тела ответа. Поскольку служба, скорее всего, использует криптографическую хеш-функцию, даже самая маленькая модификация тела резко изменит выходную информацию и, следовательно, значение ETag. Это верно только для сильных ETag - протокол также предоставляетweak Etag.

Using an If-* header turns a standard GET request into a conditional GET. Два заголовкаIf-*, которые используются с тегами ETag: «https://tools.ietf.org/html/rfc2616#section-14.26[If-None-Match]» и «https: / /tools.ietf.org/html/rfc2616#section-14.24[If-Match] »- каждый со своей собственной семантикой, как обсуждается далее в этой статье.

3. Связь клиент-сервер сcurl

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

Сначала Клиент выполняет вызов REST API -the Response includes the ETag header, который будет сохранен для дальнейшего использования:

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

For the next request, the Client will include the If-None-Match request header with the ETag value from the previous step. Если Ресурс не изменился на Сервере, Ответ не будет содержать тела и кода состоянияof 304 – Not Modified:

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 чрезвычайно просто в настройке и полностью прозрачно для приложения. We can enable the support by adding a simple Filter вweb.xml:


   etagFilter
   org.springframework.web.filter.ShallowEtagHeaderFilter


   etagFilter
   /foos/*

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

The implementation is a shallow one - приложение вычисляет ETag на основе ответа, что позволит сэкономить полосу пропускания, но не производительность сервера.

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

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

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

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

Давайте посмотрим, как будет выглядеть конфигурация на основе Java наdeclaring a ShallowEtagHeaderFilter bean in our Spring context:

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

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

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

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

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

Этот метод был представлен в Spring framework 4.1 иwe can use it to control the ETag value that a single endpoint retrieves.

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

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

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

    // ...Foo foo = ...

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

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

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

Начнем с простого -we need to verify that the response of a simple request retrieving a single Resource will actually return the “ETag” header:

@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"));
}

Next,we verify the happy path of the ETag behavior. Если в запросе на получениеResource с сервера используется правильное значение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);
}

Шаг за шагом:

  • мы создаем и получаем ресурс,storing значение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);
}

Шаг за шагом:

  • сначала мы создаем и получаемResource - и сохраняем значениеETag для дальнейшего использования

  • затем обновляем тот жеResource

  • отправить новый запрос GET, на этот раз с заголовком «If-None-Match», указывающимETag, которые мы ранее сохранили

  • по этому второму запросу сервер вернет200 OK вместе с полным ресурсом, поскольку значениеETag больше неверно, поскольку мы тем временем обновили ресурс

Наконец, последний тест - который не сработает, потому что у функции естьnot yet been implemented in Spring - этоthe support for the If-Match HTTP header:

@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 БОЛЬШИЕ

We have only used ETags for read operations.RFC exists пытается прояснить, как реализации должны поступать с ETags при операциях записи - это не стандартно, но представляет интерес.

Конечно, есть и другие возможные применения механизма ETag, например, для оптимистического механизма блокировки, а также для работы сrelated “Lost Update Problem”.

Есть также несколько известныхpotential pitfalls and caveats, о которых следует помнить при использовании ETags.

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

Эта статья лишь поверхностно коснулась того, что возможно с Spring и ETags.

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