Cache-Header in Spring MVC

1. Übersicht

In diesem Tutorial erfahren Sie mehr über das HTTP-Caching. Wir werden auch verschiedene Möglichkeiten untersuchen, um diesen Mechanismus zwischen einem Client und einer Spring MVC-Anwendung zu implementieren.

2. Einführung in das HTTP-Caching

Wenn wir eine Webseite in einem Browser öffnen, werden normalerweise viele Ressourcen vom Webserver heruntergeladen:

In diesem Beispiel muss ein Browser beispielsweise drei Ressourcen für eine / Anmeldeseite herunterladen . Es ist üblich, dass ein Browser für jede Webseite mehrere HTTP-Anforderungen stellt. Wenn wir solche Seiten sehr häufig anfordern, verursacht dies viel Netzwerkverkehr und die Bereitstellung dieser Seiten dauert länger .

Um die Netzwerklast zu verringern, können Browser mit dem HTTP-Protokoll einige dieser Ressourcen zwischenspeichern. Wenn diese Option aktiviert ist, können Browser eine Kopie einer Ressource im lokalen Cache speichern. Infolgedessen können Browser diese Seiten aus dem lokalen Speicher bereitstellen, anstatt sie über das Netzwerk anzufordern:

Ein Webserver kann den Browser anweisen, eine bestimmte Ressource zwischenzuspeichern, indem er der Antwort einen Cache-Control- Header hinzufügt .

Da die Ressourcen als lokale Kopie zwischengespeichert werden, besteht die Gefahr, dass veraltete Inhalte vom Browser bereitgestellt werden . Daher fügen Webserver normalerweise eine Ablaufzeit in den Cache-Control- Header ein.

In den folgenden Abschnitten fügen wir diesen Header in einer Antwort des Spring MVC-Controllers hinzu. Später werden wir auch Spring-APIs sehen, um die zwischengespeicherten Ressourcen basierend auf der Ablaufzeit zu validieren.

3. Cache-Kontrolle in der Antwort des Controllers

3.1. Verwenden von ResponseEntity

Der einfachste Weg, dies zu tun, ist die Verwendung der von Spring bereitgestellten Builder-Klasse CacheControl :

@GetMapping("/hello/{name}") @ResponseBody public ResponseEntity hello(@PathVariable String name) { CacheControl cacheControl = CacheControl.maxAge(60, TimeUnit.SECONDS) .noTransform() .mustRevalidate(); return ResponseEntity.ok() .cacheControl(cacheControl) .body("Hello " + name); }

Dadurch wird der Antwort ein Cache-Control- Header hinzugefügt :

@Test void whenHome_thenReturnCacheHeader() throws Exception { this.mockMvc.perform(MockMvcRequestBuilders.get("/hello/baeldung")) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.header() .string("Cache-Control","max-age=60, must-revalidate, no-transform")); }

3.2. Verwenden von HttpServletResponse

Häufig müssen die Controller den Ansichtsnamen von der Handler-Methode zurückgeben. Mit der ResponseEntity- Klasse können wir jedoch nicht den Ansichtsnamen zurückgeben und gleichzeitig den Anforderungshauptteil bearbeiten .

Alternativ können wir für solche Controller den Cache-Control- Header direkt in der HttpServletResponse festlegen :

@GetMapping(value = "/home/{name}") public String home(@PathVariable String name, final HttpServletResponse response) { response.addHeader("Cache-Control", "max-age=60, must-revalidate, no-transform"); return "home"; }

Dadurch wird der HTTP-Antwort ähnlich wie im letzten Abschnitt ein Cache-Control- Header hinzugefügt :

@Test void whenHome_thenReturnCacheHeader() throws Exception { this.mockMvc.perform(MockMvcRequestBuilders.get("/home/baeldung")) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.header() .string("Cache-Control","max-age=60, must-revalidate, no-transform")) .andExpect(MockMvcResultMatchers.view().name("home")); }

4. Cache-Kontrolle für statische Ressourcen

Im Allgemeinen stellt unsere Spring MVC-Anwendung viele statische Ressourcen wie HTML-, CSS- und JS-Dateien bereit. Da solche Dateien viel Netzwerkbandbreite beanspruchen, ist es für Browser wichtig, sie zwischenzuspeichern. Wir werden dies wieder mit dem Cache-Control- Header in der Antwort aktivieren .

Mit Spring können wir dieses Caching-Verhalten bei der Ressourcenzuordnung steuern:

@Override public void addResourceHandlers(final ResourceHandlerRegistry registry) { registry.addResourceHandler("/resources/**").addResourceLocations("/resources/") .setCacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS) .noTransform() .mustRevalidate()); }

Dadurch wird sichergestellt, dass alle unter / resources definierten Ressourcen mit einem Cache-Control- Header in der Antwort zurückgegeben werden .

5. Cache-Kontrolle in Interceptors

Wir können Interceptors in unserer Spring MVC-Anwendung verwenden, um für jede Anforderung eine Vor- und Nachbearbeitung durchzuführen. Dies ist ein weiterer Platzhalter, in dem wir das Caching-Verhalten der Anwendung steuern können.

Anstatt einen benutzerdefinierten Interceptor zu implementieren, verwenden wir jetzt den von Spring bereitgestellten WebContentInterceptor :

@Override public void addInterceptors(InterceptorRegistry registry) { WebContentInterceptor interceptor = new WebContentInterceptor(); interceptor.addCacheMapping(CacheControl.maxAge(60, TimeUnit.SECONDS) .noTransform() .mustRevalidate(), "/login/*"); registry.addInterceptor(interceptor); }

Hier haben wir den WebContentInterceptor registriert und den Cache-Control- Header hinzugefügt, ähnlich wie in den letzten Abschnitten. Insbesondere können wir verschiedene Cache-Control- Header für verschiedene URL-Muster hinzufügen .

Im obigen Beispiel fügen wir für alle Anforderungen, die mit / login beginnen , diesen Header hinzu:

@Test void whenInterceptor_thenReturnCacheHeader() throws Exception { this.mockMvc.perform(MockMvcRequestBuilders.get("/login/baeldung")) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.header() .string("Cache-Control","max-age=60, must-revalidate, no-transform")); }

6. Cache-Validierung in Spring MVC

Bisher haben wir verschiedene Möglichkeiten besprochen, einen Cache-Control- Header in die Antwort aufzunehmen. Dies gibt an, dass die Clients oder Browser die Ressourcen basierend auf Konfigurationseigenschaften wie max-age zwischenspeichern sollen .

Im Allgemeinen ist es eine gute Idee, mit jeder Ressource eine Cache-Ablaufzeit hinzuzufügen . Infolgedessen können Browser vermeiden, abgelaufene Ressourcen aus dem Cache bereitzustellen.

Obwohl Browser immer nach Ablauf suchen sollten, muss die Ressource möglicherweise nicht jedes Mal neu abgerufen werden. Wenn ein Browser überprüfen kann, ob sich eine Ressource auf dem Server nicht geändert hat, kann er weiterhin die zwischengespeicherte Version bereitstellen. Zu diesem Zweck stellt uns HTTP zwei Antwortheader zur Verfügung:

  1. Etag - ein HTTP- Antwortheader , der einen eindeutigen Hashwert speichert, um festzustellen, ob sich eine zwischengespeicherte Ressource auf dem Server geändert hat - ein entsprechender If-None-Match- Anforderungsheader muss den letzten Etag-Wert enthalten
  2. LastModified – an HTTP response header that stores a unit of time when the resource was last updated – a corresponding If-Unmodified-Since request header must carry the last modified date

We can use either of these headers to check if an expired resource needs to be re-fetched. After validating the headers,the server can either re-send the resource or send a 304 HTTP code to signify no change. For the latter scenario, browsers can continue to use the cached resource.

The LastModified header can only store time intervals up to seconds precision. This can be a limitation in cases where a shorter expiry is required. For this reason, it's recommended to use Etag instead. Since Etag header stores a hash value, it's possible to create a unique hash up to more finer intervals like nanoseconds.

That said, let's check out what it looks like to use LastModified.

Spring provides some utility methods to check if the request contains an expiration header or not:

@GetMapping(value = "/productInfo/{name}") public ResponseEntity validate(@PathVariable String name, WebRequest request) { ZoneId zoneId = ZoneId.of("GMT"); long lastModifiedTimestamp = LocalDateTime.of(2020, 02, 4, 19, 57, 45) .atZone(zoneId).toInstant().toEpochMilli(); if (request.checkNotModified(lastModifiedTimestamp)) { return ResponseEntity.status(304).build(); } return ResponseEntity.ok().body("Hello " + name); }

Spring provides the checkNotModified() method to check if a resource has been modified since the last request:

@Test void whenValidate_thenReturnCacheHeader() throws Exception { HttpHeaders headers = new HttpHeaders(); headers.add(IF_UNMODIFIED_SINCE, "Tue, 04 Feb 2020 19:57:25 GMT"); this.mockMvc.perform(MockMvcRequestBuilders.get("/productInfo/baeldung").headers(headers)) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().is(304)); }

7. Conclusion

In diesem Artikel haben wir das HTTP-Caching mithilfe des Cache-Control- Antwortheaders in Spring MVC kennengelernt. Wir können den Header entweder mithilfe der ResponseEntity- Klasse oder durch Ressourcenzuordnung für statische Ressourcen in die Antwort des Controllers einfügen.

Wir können diesen Header auch für bestimmte URL-Muster mithilfe von Spring-Interceptors hinzufügen.

Wie immer ist der Code auf GitHub verfügbar.