HTTP cache with Spring examples

Caching is a powerful feature of the HTTP protocol but for some reason, it’s mainly considered for static resources like images, CSS stylesheets, or JavaScript files. However, HTTP caching isn’t limited to application’s assets as you can also use it for dynamically computed resources.

With a small amount of work, you can speed up your application and improve the overall user experience. In this article, you will learn how to use the built-in HTTP response cache mechanism for Spring controller’s results.

Advertisement

1. How and when to use HTTP response cache?

You can do caching on multiple layers of your application. Databases have their cache storages, application caches some data in the operation memory, a web client also reuses information on its side.

As you may know, the HTTP protocol is responsible for network communication. The caching mechanism allows us to optimize the network traffic by decreasing the amount of data being transported between the client and the server.

What you can (and should) optimize?

When a web resource doesn’t change very often or you exactly know when it is updated, then you have a perfect candidate for optimization using the HTTP cache.

Once you identify contenders for HTTP caching, you need to choose a suitable approach to manage the validation of the cache. The HTTP protocol defines several request and response headers which you can use to control when the client should clear the cache.

The choice of appropriate HTTP headers depends on a particular case that you want to optimize. But regardless of the use case, we can divide cache management options due to where the validation of the cache takes place. It can be verified by the client or by the server.

Let’s get this show on the road.

Long road

2. Client-side cache validation

When you know a requested resource isn’t going to change for a given amount of time, the server can send such information to the client as a response header. Based on that information, the client decides if it should fetch the resource again or reuse the one previously downloaded.

There are two possible options to describe when the client should fetch the resource again and remove the stored cache value. So let’s see them in action.

2.1. HTTP cache valid for the fixed amount of time

If you want to prevent the client from refetching a resource for a given amount of time, you should take a look at the Cache-Control header where you can specify for how long the fetched data should be reused.

By setting the value of the header to max-age=<seconds> you inform the client for how long in seconds the resource doesn’t have to be fetched again. The validity of the cached value is relative to the time of the request.

In order to set an HTTP header in Spring’s controller, instead of a regular payload object you should return the ResponseEntity wrapper class. Here’s an example:

@GetMapping("/{id}")
ResponseEntity<Product> getProduct(@PathVariable long id) {
   // …
   CacheControl cacheControl = CacheControl.maxAge(30, TimeUnit.MINUTES);
   return ResponseEntity.ok()
           .cacheControl(cacheControl)
           .body(product);
}

The value of a header is just a regular String but in case of Cache-Control Spring provides us with a special builder class which prevents us from making small mistakes like typos.

2.2. HTTP cache valid to the fixed date

Sometimes you know when a resource is going to change. It’s a common case for data published with some frequency like a weather forecast or stock market indicators calculated for yesterday’s trading session. The exact expiration date of a resource can be exposed to the client.

In order to do so, you should use the Expires HTTP header. The date value should be formatted using one of the standardized data formats.

Sun, 06 Nov 1994 08:49:37 GMT  ; RFC 822, updated by RFC 1123
Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
Sun Nov  6 08:49:37 1994       ; ANSI C's asctime() format

Luckily, Java comes with the predefined formatter for the first one these formats. Below you can find an example which sets the header to the end of the current day.

@GetMapping("/forecast")
ResponseEntity<Forecast> getTodaysForecast() {
   // ...
   ZonedDateTime expiresDate = ZonedDateTime.now().with(LocalTime.MAX);
   String expires = expiresDate.format(DateTimeFormatter.RFC_1123_DATE_TIME);
   return ResponseEntity.ok()
           .header(HttpHeaders.EXPIRES, expires)
           .body(weatherForecast);
}

Notice that the HTTP date format requires information about the time zone. That is why the above example uses ZonedDateTime. If you try to use LocalDateTime instead you will end up with the following error message at runtime:

java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: OffsetSeconds

If both Cache-Control and Expires headers are present in the response, the client uses only Cache-Control.

3. Server-side cache validation

In dynamically generated content based on users’ input, it’s much more common that the server doesn’t know when a requested resource is going to be changed. In that case, the client can use the previously fetched data but first, it needs to ask the server if that data is still valid.

3.1. Was a resource modified since date?

If you track the modification date of a web resource, you can expose such date to the client as a part of the response. In the next request, the client will send this date back to the server so that it can verify if the resource was modified since the previous request. If the resource is not changed, the server doesn’t have to resend the data again. Instead, it responds with 304 HTTP code without any payload.

Keep modification date

To expose the modification date of a resource you should set the Last-Modified header. Spring’s ResponseEntity builder has a special method called lastModified() which helps you assigning the value in the correct format. You’ll see this in a minute.

But before you send the full response, you should check if the client included the If-Modified-Since header in the request. The client sets its value based on the value of the Last-Modified header which was sent with the previous response for this particular resource.

If the value of the If-Modified-Since header matches the modification date of the requested resource you can save some bandwidth and respond to the client with an empty body.

Again, Spring comes with a helper method which simplifies the comparison of aforementioned dates. This method called checkNotModified() can be found in the WebRequest wrapper class which you can add to controller’s method as an input.

Sounds complicated?

Let’s take a closer look in the full example.

@GetMapping("/{id}")
ResponseEntity<Product> getProduct(@PathVariable long id, WebRequest request) {
   Product product = repository.find(id);
   long modificationDate = product.getModificationDate()
           .toInstant().toEpochMilli();

   if (request.checkNotModified(modificationDate)) {
       return null;
   }

   return ResponseEntity.ok()
           .lastModified(modificationDate)
           .body(product);
}

First, we fetch the requested resource and access its modification date. We convert the date to the number of milliseconds since January 1, 1970 GMT because that’s the format the Spring framework expects.

Then, we compare the date with the value of the If-Modified-Since header and return an empty body on the positive match. Otherwise, the server sends the full response body with an appropriate value of the Last-Modified header.

With all that knowledge you can cover almost all common caching candidates. But there is one more important mechanism that you should be aware of which is …

3.2. Resource versioning with ETag

Up until now, we defined the precision of the expiration date with an accuracy of one second.

But what if you need a better precision than just a second?

That’s where the ETag comes in.

The ETag can be defined as a unique string value which unambiguously identifies a resource at the point in time. Usually, the server calculates the ETag based on properties of a given resource or, if available, its latest modification date.

The communication flow between the client and the server is almost the same as in case of the modification date checking. Only headers’ names and values are different.

The server sets the ETag value in the header called (surprisingly) ETag. When the client access the resource again, it should send its value in the header named If-None-Match. If that value matches with newly calculated ETag for the resource, the server can respond with an empty body and HTTP code 304.

In Spring, you can implement the ETag server flow as presented below:

@GetMapping("/{id}")
ResponseEntity<Product> getProduct(@PathVariable long id, WebRequest request) {
   Product product = repository.find(id);
   String modificationDate = product.getModificationDate().toString();
   String eTag = DigestUtils.md5DigestAsHex(modificationDate.getBytes());

   if (request.checkNotModified(eTag)) {
       return null;
   }

   return ResponseEntity.ok()
           .eTag(eTag)
           .body(product);
}

Does it look similar?

Yes, the sample is almost the same as the previous one with the modification date checking. We just use a different value for the comparison (and the MD5 algorithm to calculate the ETag). Notice the WebRequest has an overloaded checkNotModified() method to deal with ETags represented as strings.

If Last-Modified and ETag work almost the same why do we need both?

Arm wrestling

3.3. Last-Modified vs ETag

As I’ve already mentioned, the Last-Modified header is less precise as it has an accuracy of one second. For greater precision choose the ETag.

When you don’t track the modification date of a resource, you’re also forced to use the ETag. The server can calculate its value based on properties of a resource. Think about it as a hash code of an object.

If a resource has its modification date and one-second precision is fine for you, go with the Last-Modified header. Why? Because ETag calculation may be an expensive operation.

By the way, it’s worth mentioning that the HTTP protocol doesn’t specify the algorithm which you should use to calculate ETag. When choosing the algorithm you should focus on its speed.

This article focuses on caching GET requests but you should know the server can use the ETag to synchronize update operations. But that’s an idea for another article.

3.4. Spring ETag filter

Because the ETag is just a string representation of a content, the server can calculate its value using the byte representation of a response. What is mean is that you can actually assign the ETag to any response.

And guess what?

The Spring framework provides you with the ETag response filter implementation which does that for you. All you have to do is to configure the filter in your application.

The simplest way to add an HTTP filter in a Spring application is via the FilterRegistrationBean in your configuration class.

@Bean
public FilterRegistrationBean filterRegistrationBean () {
   ShallowEtagHeaderFilter eTagFilter = new ShallowEtagHeaderFilter();
   FilterRegistrationBean registration = new FilterRegistrationBean();
   registration.setFilter(eTagFilter);
   registration.addUrlPatterns("/*");
   return registration;
}

In this case, the call to addUrlPatterns() is redundant as by default all paths are matched. I put it here to demonstrate that you can control to which resources Spring should add the ETag value.

Beside the ETag generation, the filter also responds with HTTP 304 and an empty body when it’s possible.

But beware.

The ETag calculation can be expensive. For some applications enabling this filter may actually cause more harm than good. Think over your solution before using it.

Conclusion

The article seems pretty long but we covered a lot of useful material. Now you know how to optimize your application using the HTTP cache and which approach is the best for you as applications have different needs.

You learned the client-side cache validation is the most effective approach as no data transmission is involved. You should always favor client-side cache validation when it’s applicable.

We also discussed the server-side validation and compared Last-Modified and ETag headers. Finally, you saw how to set a global ETag filter in a Spring application.

I hope you find the article useful. If you like it, please share or write your comments down below. Also, let me know if I can improve or extende the content. I’d love to know your thoughts.

Facebooktwittergoogle_plusredditlinkedinmail

Articles you may like

Advertisement