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.
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.
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.
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.