ETags para REST com Spring

ETags para REST com Spring

1. Visão geral

Este artigo se concentrará emworking with ETags in Spring, teste de integração da API REST e cenários de consumo comcurl.

Leitura adicional:

Introdução ao Spring REST Docs

Este artigo apresenta o Spring REST Docs, um mecanismo controlado por teste para gerar documentação para serviços RESTful que é precisa e legível.

Read more

Um tipo de mídia personalizado para uma API REST do Spring

Uma introdução rápida ao uso de um tipo de mídia personalizado em uma API REST do Spring.

Read more

Paginação com Spring REST e tabela AngularJS

Uma visão abrangente de como implementar uma API simples com paginação com Spring e como consumi-la com AngularJS e UI Grid.

Read more

2. REST e ETags

Na documentação oficial do Spring sobre suporte ao ETag:

UmETag (tag de entidade) é um cabeçalho de resposta HTTP retornado por um servidor da web compatível com HTTP / 1.1 usado para determinar a mudança no conteúdo em um determinado URL.

Podemos usar ETags para duas coisas - cache e solicitações condicionais. OETag value can be thought of as a hash calculado a partir dos bytes do corpo da resposta. Como o serviço provavelmente usa uma função de hash criptográfico, mesmo a menor modificação do corpo altera drasticamente a saída e, portanto, o valor do ETag. Isso só é verdadeiro para ETags fortes - o protocolo também forneceweak Etag.

Using an If-* header turns a standard GET request into a conditional GET. Os dois cabeçalhosIf-* que estão usando com ETags são “https://tools.ietf.org/html/rfc2616#section-14.26[If-None-Match]” e “https: / /tools.ietf.org/html/rfc2616#section-14.24[If-Match] ”- cada um com sua própria semântica, conforme discutido posteriormente neste artigo.

3. Comunicação cliente-servidor comcurl

Podemos decompor uma comunicação simples entre cliente e servidor envolvendo ETags nas etapas:

Primeiro, o cliente faz uma chamada REST API -the Response includes the ETag header que será armazenada para uso posterior:

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. Se o recurso não mudou no servidor, a resposta não conterá corpo e um código de statusof 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"

Agora, antes de recuperar o Recurso novamente, vamos alterá-lo realizando uma atualização:

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

Finalmente, enviamos o último pedido para recuperar o Foo novamente. Lembre-se de que o atualizamos desde a última vez que o solicitamos, portanto, o valor ETag anterior não deve funcionar mais. A resposta conterá os novos dados e um novo ETag que, novamente, pode ser armazenado para uso posterior:

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

E aí está - ETags na largura de banda selvagem e econômica.

4. Suporte ETag na primavera

Sobre o suporte ao Spring: o uso do ETag no Spring é extremamente fácil de configurar e completamente transparente para o aplicativo. We can enable the support by adding a simple Filter emweb.xml:


   etagFilter
   org.springframework.web.filter.ShallowEtagHeaderFilter


   etagFilter
   /foos/*

Estamos mapeando o filtro no mesmo padrão de URI da própria API RESTful. O filtro em si é a implementação padrão da funcionalidade ETag desde o Spring 3.0.

The implementation is a shallow one - o aplicativo calcula a ETag com base na resposta, o que economiza largura de banda, mas não o desempenho do servidor.

Portanto, uma solicitação que se beneficiará do suporte à ETag ainda será processada como uma solicitação padrão, consumirá qualquer recurso que normalmente consumiria (conexões com o banco de dados, etc.) e somente antes de ter sua resposta retornada ao cliente o suporte à ETag será acionado. dentro.

Nesse ponto, a ETag será calculada fora do corpo da Resposta e definida no próprio Recurso; além disso, se o cabeçalhoIf-None-Match foi definido na solicitação, ele será tratado também.

Uma implementação mais profunda do mecanismo ETag poderia fornecer benefícios muito maiores - como atender a algumas solicitações do cache e não precisar executar a computação - mas a implementação definitivamente não seria tão simples nem tão plugável quanto a abordagem superficial descrito aqui.

4.1. Configuração baseada em Java

Vamos ver como a configuração baseada em Java ficaria emdeclaring a ShallowEtagHeaderFilter bean in our Spring context:

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

Lembre-se de que, se precisarmos fornecer mais configurações de filtro, podemos declarar uma instânciaFilterRegistrationBean:

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

Finalmente, se não estivermos usando Spring Boot, podemos configurar o filtro usando o métodoAbstractAnnotationConfigDispatcherServletInitializer'sgetServletFilters .

4.2. Usando o métodoeTag() de ResponseEntity

Este método foi introduzido no Spring framework 4.1 ewe can use it to control the ETag value that a single endpoint retrieves.

Por exemplo, imagine que estamos usando entidades versionadas comoOptimist Locking mechanism para acessar nossas informações de banco de dados.

Podemos usar a própria versão como ETag para indicar se a entidade foi modificada:

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

O serviço recuperará o estado304-Not Modified correspondente se o cabeçalho condicional da solicitação corresponder aos dados de cache.

5. Testando ETags

Vamos começar simples -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. Se a solicitação para recuperarResource do servidor usar o valorETag correto, o servidor não recuperará o recurso:

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

Passo a passo:

  • criamos e recuperamos um recurso,storing o valorETag

  • enviar uma nova solicitação de recuperação, desta vez com o cabeçalho “If-None-Match” especificando o valorETag previamente armazenado

  • nesta segunda solicitação, o servidor simplesmente retorna um304 Not Modified, já que o próprio Recurso de fato não foi modificado entre as duas operações de recuperação

Por fim, verificamos o caso em que o Recurso é alterado entre a primeira e a segunda solicitações de recuperação:

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

Passo a passo:

  • primeiro criamos e recuperamos umResource - e armazenamos o valorETag para uso posterior

  • então atualizamos o mesmoResource

  • enviar uma nova solicitação GET, desta vez com o cabeçalho “If-None-Match” especificando oETag que armazenamos anteriormente

  • nesta segunda solicitação, o servidor retornará um200 OK junto com o Recurso completo, uma vez que o valorETag não está mais correto, pois atualizamos o Recurso nesse meio tempo

Por fim, o último teste - que não vai funcionar porque a funcionalidade temnot 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);
}

Passo a passo:

  • criamos um recurso

  • em seguida, recupere-o usando o cabeçalho “If-Match” especificando um valorETag incorreto - esta é uma solicitação GET condicional

  • o servidor deve retornar um412 Precondition Failed

6. ETags são GRANDES

We have only used ETags for read operations. UmRFC exists tentando esclarecer como as implementações devem lidar com ETags em operações de gravação - isso não é padrão, mas é uma leitura interessante.

Existem, é claro, outros usos possíveis do mecanismo ETag, como para um mecanismo de bloqueio otimista, bem como lidar comrelated “Lost Update Problem”.

Existem também váriospotential pitfalls and caveats conhecidos que você deve conhecer ao usar ETags.

7. Conclusão

Este artigo apenas arranhou a superfície com o que é possível com Spring e ETags.

Para uma implementação completa de um serviço RESTful habilitado para ETag, junto com testes de integração verificando o comportamento da ETag, verifique oGitHub project.