在Spring Boot中实现HTTP缓存


缓存是HTTP协议的一个强大功能,但由于某些原因,它主要用于静态资源,如图像,CSS样式表或JavaScript文件,但是,HTTP缓存不仅限于这些,还可以将其用于动态计算的资源。
通过少量工作,您可以加快应用程序并改善整体用户体验。在本文中,您将学习如何使用内置的HTTP响应缓存机制来实现缓存SpringBoot控制器的结果

1.如何以及何时使用HTTP响应缓存?
您可以在应用程序的多个层上进行缓存。数据库具有其缓存存储,Web客户端也在其需要重用的信息。HTTP协议负责网络通信。缓存机制允许我们通过减少客户端和服务器之间传输的数据量来优化网络流量。
何时优化:当Web资源不经常更改或您确切知道何时更新时,就可以使用HTTP缓存进行优化。一旦确定了HTTP缓存的竞争者,就需要选择合适的方法来管理缓存的验证。HTTP协议定义了几个请求和响应标头,您可以使用它们来控制客户端何时清除缓存
选择适当的HTTP标头取决于您要优化的特定情况。但是无论用例如何,我们可以根据缓存的验证发生在哪里进行缓存管理选项的划分。

2.客户端缓存验证
当您知道请求的资源在给定的时间内不会更改时,服务器可以将此类信息作为响应标头发送到客户端。基于该信息,客户端决定是否应该再次获取资源或重用先前下载的资源。
有两种可能的选项可以描述客户端何时应该再次获取资源并删除存储的缓存值。所以让我们看看他们是如何运行的。

HTTP缓存在固定的时间内有效:如果要阻止客户端在指定时间内重新获取资源,则应该使用Cache-Control标头,可以在其中指定应该重新获取所获取数据的时间。
通过将标头的值设置为max-age = <seconds>,可以通知客户端多长时间不再需要再次获取资源。缓存值的有效性与请求的时间有关。

为了设置在Spring的控制器中的HTTP标头,就要在RESTContoller用ResponseEntity包装类

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

HTTP标头的值只是一个常规字符串,但是Cache-Control Spring为我们提供了一个特殊的构建器类,它可以防止我们犯下像拼写错误这样的小错误。


HTTP缓存有效到固定日期:有时您知道资源何时会发生变化。对于公布的数据而言,这是常见的情况,如天气预报或昨天交易时段计算的股市指标。资源的确切到期日期可以向客户端公开。应该使用Expires HTTP标头。应使用标准化数据格式之一格式化日期值。

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


幸运的是,Java附带了第一个这些格式的预定义格式化程序。可以在下面找到将标题设置为当天结束的示例。

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

请注意,HTTP日期格式需要有关时区的信息。这就是上面的例子使用ZonedDateTime的原因。如果您尝试使用LocalDateTime,则最终会在运行时出现以下错误消息:
java.time.temporal.UnsupportedTemporalTypeException:不支持的字段:OffsetSeconds

如果响应中存在Cache-ControlExpires标头,则客户端仅使用Cache-Control

3.服务器端缓存验证
在基于用户输入的动态生成的内容中,更常见的是服务器不知道何时将改变所请求的资源。在这种情况下,客户端可以使用先前获取的数据,但首先,它需要询问服务器该数据是否仍然有效。
自第一次握手以来资源是否被修改?如果跟踪Web资源的修改日期,则可以将此类日期作为响应的一部分公开给客户端。在下一个请求中,客户端将此日期发送回服务器,以便它可以验证自上一个请求以来资源是否已被修改。如果资源未更改,则服务器不必再次重新发送数据。相反,它使用304 HTTP代码响应,没有任何有效负载。
要公开资源的修改日期,您应该设置Last-Modified标头。Spring的ResponseEntity构建器有一个名为lastModified()[url=https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/ResponseEntity.HeadersBuilder.htmllastModified-long-]的特殊方法[/url],它可以帮助您以正确的格式分配值。你会在一分钟内看到这一点。
在发送完整响应之前,应检查客户端是否在请求中包含If-Modified-Since标头。客户端根据Last-Modified标头的值设置其值,该标头是与此特定资源的先前响应一起发送的。
如果If-Modified-Since标头的值与所请求资源的修改日期匹配,则可以节省一些带宽并使用空主体响应客户端。
Spring再次提供了一个辅助方法,简化了上述日期的比较。这个名为checkNotModified()的方法可以在WebRequest包装器类中找到,您可以将其作为输入添加到控制器的方法中。
让我们仔细看看完整的例子。

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

首先,我们获取所请求的资源并访问其修改日期。我们将日期转换为自格林威治标准时间1970年1月1日以来的毫秒数,因为这是Spring框架期望的格式。
然后,我们将日期与If-Modified-Since标头的值进行比较,并在正匹配上返回一个空。否则,服务器发送具有Last-Modified标头的适当值的完整响应主体。
凭借所有这些知识,您几乎可以涵盖所有常见的缓存设置选项。但是有一个更重要的机制你应该知道的是......

使用ETag进行资源版本控制
到目前为止,我们定义了有效期的精确度,精确度为1秒。但是如果你需要更好的精度而不仅仅是一秒呢?这就是ETag的用武之地。
可以将ETag定义为唯一的字符串值,该值在该时间点明确地标识资源。通常,服务器根据给定资源的属性计算ETag,或者,如果可用,则计算其最新修改日期。
客户端和服务器之间的通信流程与修改日期检查的情况几乎相同。只有标题的名称和值不同。
服务器在名为ETag的标题中设置ETag值。当客户端再次访问资源时,它应该在名为If-None-Match的头中发送其值。如果该值与资源的新计算的ETag匹配,则服务器可以使用空内容和HTTP代码304进行响应。
在Spring中,您可以实现ETag服务器流程,如下所示:

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

与前一个样本几乎相同,并且修改日期检查。我们只是使用不同的值进行比较(以及MD5算法来计算ETag)。请注意,WebRequest有一个重载的checkNotModified()方法来处理表示为字符串的ETag。
如果Last-ModifiedETag工作几乎相同,为什么我们需要两者吗?

Last-Modified vs ETag
正如我已经提到的,Last-Modified标头不太精确,因为它具有一秒的精度。为了获得更高的精度,请选择ETag
当您不跟踪资源的修改日期时,您也被迫使用ETag。服务器可以根据资源的属性计算其值。将其视为对象的哈希码。

如果资源具有其修改日期并且您可以使用一秒精度,请使用Last-Modified标头。为什么?因为ETag计算可能是一项昂贵的操作

顺便提一下,值得一提的是HTTP协议没有指定用于计算ETag的算法。选择算法时,您应该关注它的速度。
本文重点介绍缓存GET请求,但您应该知道服务器可以使用ETag来同步更新操作。

Spring ETag过滤器
因为ETag只是内容的字符串表示,所以服务器可以使用响应的字节表示来计算其值。意思是你可以实际将ETag分配给任何响应。
Spring框架为您提供了ETag响应过滤器实现,它可以为您完成。您所要做的就是在应用程序中配置过滤器。
在Spring应用程序中添加HTTP过滤器的最简单方法是通过配置类中的FilterRegistrationBean

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

在这种情况下,对addUrlPatterns()的调用是多余的,因为默认情况下所有路径都匹配。我把它放在这里证明你可以控制Spring应该添加ETag值的资源。
除了ETag生成之外,过滤器还会在可能的情况下响应HTTP 304和空体内容。
但要注意。
ETag计算可能很昂贵。对于某些应用程序启用此过滤器实际上可能会导致弊大于利。在使用之前考虑一下您的解决方案。

结论
现在您已了解如何使用HTTP缓存优化应用程序,哪种方法最适合您,因为应用程序有不同的需求。
您了解到客户端缓存验证是最有效的方法,因为不涉及数据传输。在适用时,您应该始终支持客户端缓存验证。
我们还讨论了服务器端验证并比较了Last-ModifiedETag标头。最后,您了解了如何在Spring应用程序中设置全局ETag过滤器。