Mechanizm Conditional requests czyli warunkowych zapytań służy do optymalizacji żądań HTTP. Bardzo często odpytujemy o dany zasób po mimo iż jego stan się nie zmienił. W odpowiedzi dostajemy cały zasób i musimy zaimplementować logikę porównywania czy pojawiła się jakaś zmiana w obiekcie. Jednakże standard HTTP dostarcza nam mechanizmy do realizacji tego zagadnienia, są to słabe oraz silne nagłówki cachowania.
Mechanizm ten powinien być wykonywany dla operacji odczytu (ang. read). W momencie wysłania warunkowego żądania sprawdzany jest żądany zasób, czy zmienił się w stosunku do ostatniego wywołania. Nie jest to sprawdzanie bajt po bajcie tylko odbywa się to przy wykorzystaniu validatorów. Rozróżniamy dwa typy validatorów:
- data ostatniej modyfikacji zawarta w nagłówku Last-Modified
- wartość typu String przechowująca numer wersji nazywana Entity Tag lub ETag
Samo porównywanie wartości może odbywać się na dwa sposoby:
- słaba walidacja kiedy mogą wystąpić “drobne” różnice
- silna walidacja kiedy każdy bajt musi się zgadzać
HTTP domyślnie używa silnej walidacji. Natomiast do wykonywanie warunkowych zapytań wykorzystujemy warunkowe nagłówki:
- If-None-Match dla ETagów
- If-Modified-Since dla Last-Modified
Przykładowe flow znajduje się poniżej. Pierwsze wykonanie to wysłanie żądania GET o dowolny zasób. W odpowiedzi dostajemy ten zasób wraz z dwoma nagłówkami ETag oraz Last-Modified. Jeden z nich jest nadmiarowy, do realizacji warunkowych żądań wystarczy jeden mechanizm, ja po prostu chciałem zaprezentować dwa od razu.
Teraz jeśli chcemy sprawdzić czy zasób się zmienił nie musimy ponownie odpytywać o cały zasób. Wystarczy wykorzystać odpowiednie nagłówki. Dla ETag należy dodać If-None-Match z wartością otrzymaną za pierwszym razem natomiast dla Last-Modified należy użyć If-Modified-Since z datą otrzymaną wcześniej. Jeśli zasób się nie zmienił powinniśmy otrzymać kod 304 Not Modified. W przypadku gdy zasób zmienił się od tego czasu otrzymamy kod 200 wraz z tym zasobem oraz nową wersją ETagu i z zaktualizowaną datą Last-Modified.
W Springu możemy zrealizować ETagi na dwa sposoby. Pierwszy z nich to wykorzystanie adnotacji @Version z pakietu javax.persistence, która umieszczona na polu przechowującym wersję będzie ją automatycznie aktualizować:
@Data
@Entity
@RequiredArgsConstructor
public class Todo {
@Id @GeneratedValue
private Long id;
@Version
private Long version;
private String content;
public Todo(String content) {
this.content = content;
}
Natomiast drugim sposobem jest dodanie beany ShallowEtagHeaderFilter:
@Bean
public Filter shallowEtagHeaderFilter() {
return new ShallowEtagHeaderFilter();
}
Jedyną różnicą pomiędzy tymi rozwiązaniami jest rodzaj generowanego ETagu. W przypadku @Version tag generowany jest od zera. W drugim rozwiązaniu tag wygląda na losowy ciąg na przykład: 03f91e84687eb66da6e5479c563dd6c9d. Aby sprawdzić poprawność działania można wykorzystać POSTMan’a lub jak najbardziej testy!
@Test
public void shouldReturnETag() throws Exception {
todoRepository.save(new Todo(“content”));
MockHttpServletResponse response = mvc.perform(get("/todos/1"))
.andExpect(header().string(ETAG, is(notNullValue())))
.andReturn().getResponse();
mvc.perform(get("/todos/1").header(IF\_NONE\_MATCH, response.getHeader(ETAG)))
.andExpect(status().isNotModified())
.andExpect(header().string(ETAG, is(notNullValue())))
.andReturn().getResponse();
}
Jeśli natomiast chcemy wykorzystać mechanizm If-Modified-Since musimy wykorzystać audyty JPA. Mechanizm audytów będzie automatycznie aktualizował pole które przechowuje datę ostatnie modyfikacji:
@Data
@Entity
@RequiredArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class Todo {
@Id @GeneratedValue
private Long id;
@JsonIgnore
@LastModifiedDate
private LocalDateTime lastModifiedDate;
private String content;
public Todo(String content) {
this.content = content;
}
}
Należy pamiętać także o dodaniu adnotacji @EnableJpaAuditing na naszej aplikacji. Test do sprawdzenia działania:
@Test
public void shouldReturnLastModified() throws Exception {
todoRepository.save(new Todo(“content”));
MockHttpServletResponse response = mvc.perform(get("/todos/1"))
.andExpect(header().string(LAST\_MODIFIED, is(notNullValue())))
.andReturn().getResponse();
mvc.perform(get("/todos/1").header(IF\_MODIFIED\_SINCE, response.getHeader(LAST\_MODIFIED)))
.andExpect(status().isNotModified())
.andExpect(header().string(LAST\_MODIFIED, is(notNullValue())))
.andReturn().getResponse();
}


