翻译:叩丁狼教育吴嘉俊
缓存是HTTP协议中一个非常重要的特征。但是由于某些原因,在HTTP协议中,缓存常常只用来做图片,CSS样式表或者JS等静态文件缓存。其实,HTTP缓存不仅仅可以用来做静态资源的缓存,同样对动态的请求也同样有用。
仅仅只需要一些简单的工作,你就可以提高应用的响应速度,提高用户体验。在这篇文章中,你将会学到如何在Spring中使用内置的HTTP响应缓存机制来缓存controller的结果。
如何使用HTTP响应缓存?
你可以在应用中的各个层次做缓存。比如数据库可以用内存缓存存储,在应用层可以缓存业务数据,在web层当然也可以缓存和重用数据。
客户端和服务器依赖HTTP协议进行交流。缓存机制允许我们通过减少在客户端和服务器之间传输的数据量来优化网络传输压力。
那么我们能做哪些优化呢?
当一个资源不会频繁更新,并且我们非常准确的知道数据什么时候更新的时候,HTTP缓存就是一个不错的优化性能的方案。
一旦确定使用HTTP缓存策略,你就必须要选择一种合适的验证缓存的机制。HTTP协议提供了多种请求头和响应头参数,可以用来控制缓存。
选择使用哪种HTTP头取决于你想怎么优化缓存。但是,不管你怎么使用,我们都可以根据缓存使用在什么地方,来划分缓存的管理选项,可以在客户端验证,也可以再服务器端验证。
我们来看看具体的使用。
客户端缓存验证
当你知道一个请求的资源在一个指定的时间内不会发生改变,服务器端可以将这个时间信息通过响应头发送给客户端。根据这个缓存时效信息,客户端可以选择是否重新冲服务端请求信息,或者重用之前已经下载的信息。
有两个可选的头参数可以控制什么时候客户端需要重新从服务器端获取数据,什么时候删除缓存。我们在实战中来看看。
HTTP缓存固定时间
如果你想阻止客户端在一个指定时间内不要去请求服务器,你可以使用Cache-Control头信息,通过设置获取到的数据可以在多长时间之内重复使用。
或者你可以通过设置max-age=[seconds]信息来告诉客户端,在多少秒之内,资源可以缓存使用。缓存的有效性与请求的时间有关。
为了在Spring的控制器中控制HTTP头信息,我们需要返回ResponseEntity包装类型。下面是一个例子:
@GetMapping("/{id}")
ResponseEntity<Product> getProduct(@PathVariable long id) {
// …
CacheControl cacheControl = CacheControl.maxAge(30, TimeUnit.MINUTES);
return ResponseEntity.ok()
.cacheControl(cacheControl)
.body(product);
}
在头中的信息应该是一个普通的字符串,但是在Spring中,专门为Cache-Control提供了一个builder类来辅助设置这个响应头信息。
HTTP缓存到指定的日期
在一些情况下,我们是知道一个资源什么时候会被更新,这种方式在定时发布数据的情况下是非常有用的,比如定时发布天气预报,或者定时发布昨天的股市行情信息等。在这种情况下,更适合告诉客户端具体的资源缓存到的日期/时间。
为了达到这个目的,我们可以使用Expires头信息。缓存到期的日期/时间需要按照以下标准格式设置:
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中提前定义好了第一种日期格式(RFC 1123)。下面给出一个例子,展示如何将缓存失效时间设定到今天的结束。
@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: Unsupported field: OffsetSeconds
如果Cache-Control和Expires两个头信息同时设置了,Cache-Control的优先级更高。
服务端缓存验证
在另一种情况,服务器端根据用户的输入动态生成内容,在这种情况下,服务器一般是不知道这个资源在客户端需要缓存多长时间。在这种情况下,缓存的方式就变成了:客户端首先需要请求服务器,刚才的数据是否为合法的缓存数据,如果是,可以继续使用之前的缓存数据,如果不是,则重新获取。
资源是否仍然有效?
如果你能够跟踪某个资源的修改时间,那么你可以这样处理服务器缓存:将资源的修改时间作为相应头信息返回给客户端,之后,每一次请求,客户端都会将这个时间作为请求头的一部分发回给服务端,服务端检查从上次请求截止到现在,资源是否被修改过,如果资源在这段时间没有被修改过,则不需要重新返回数据,只需要返回一个304状态码即可。
想要返回一个资源的修改时间,需要设置Last-Modified头信息。Spring提供的ResponseEntity提供了一个lastModified()方法能够使用特定格式的值来填充Last-Modified头信息。这个方法待会会在代码中展示。
但是当你在返回用户完整的响应之前,服务器端需要检查客户端的请求头中是否包含了If-Modified-Since头信息。如果客户端之前对这个资源请求的响应中,包含了Last-Modified这个响应头,并且这个资源的缓存没有被重置,那么在之后针对该资源的请求中就会自动设置这个值。
如果If-Modified-Since头信息包含的时间匹配资源的修改时间,即该资源在这个时间段内,并没有被修改过,则你可以直接返回一个空的响应。
同样,Spring提供了一个非常简单的方法来辅助判断这个修改时间。这个方法就是WebRequest包装对象中的checkNotModified(),这个WebRequest对象可以放在控制器方法的参数列表中,Spring会自动注入进来。
我们来完整的看一个实例代码,了解整个过程:
@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;// code
}
return ResponseEntity.ok()
.lastModified(modificationDate)
.body(product);
}
首先,我们通过请求的id获取Product,将Product的更新时间转换成1970年1月1日 GMT的毫秒数,这个格式是Spring用来对比Last-Modified的格式。
接着,我们对比request头中的If-Modified-Since,如果时间匹配,则直接返回一个空响应体。如果时间不匹配,则返回一个包含有最新Last-Modified头的完整的响应体。[注:在code这块地方,也可以通过return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build(); 返回一个304响应]。
上面的这些知识,已经足够你应付大部分缓存的情况了。但是,还有一种非常重要的机制,你需要注意:
使用ETag标记资源版本
在上面的例子中,缓存失效的最小失效单位为1秒钟。那么,假如你需要一个比秒更精确的缓存控制精度呢?这就是ETag处理的问题。
ETag定义了一个唯一的字符串值,这个字符串值可以唯一的标记在一个确切的时间点的一个资源。通常情况下,ETag在服务端,使用要标记的资源相关联的属性来计算,或者,也可以使用更精确的资源修改时间。
使用ETag的情况下,服务器和客户端之间的通信流程和上面使用修改时间的方式是基本一致的,只是报头信息的名字和值不一样。
在服务器端,就是使用ETag这个名字设置响应头即可。当客户端再次请求这个资源的嘶吼,他会将这个ETag值放到请求头If-None-Match属性中,如果这个值和服务器端该资源最新的ETag匹配,则服务器端只需要返回一个空响应体的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);
}
是不是和Last-Modified的例子很像?
是的,这个例子几乎和上面那个修改时间检查的例子完全一样。我们只是使用了另外的一个值交给checkNotModified去比较(我们将修改时间使用MD5加密来生成对应的ETag值)。注意,checkNotModified方法针对ETags有重载方法,这里是传入一个字符串,Last-Modified的例子中是传入一个long值。
既然Last-Modified和ETag几乎相同,为什么这两种策略会同时存在呢?
Last-Modified VS ETag
在上面的文字中我已经提到过,Last-Modified策略在精度上是有一定限制的,因为最小精度是秒。如果需要更高的缓存控制精度,就需要选择ETag。
如果资源不能使用修改时间来标记,那么也需要使用ETag。服务端可以通过资源的其他属性来计算ETag值,比如对象的hashcode值(甚至是对象的序列化版本号)。
如果一个资源有自己的修改时间,并且1秒钟的精度足够使用,那么我建议使用Last-Modified头。因为ETag的计算成本较高。
当然,在HTTP协议中,并没有强行要求ETag的计算算法,所以,当我们在选择ETag算法的时候,请注意考虑算法的速度。
这篇文章的主题是GET请求下的缓存,但是你可以了解一下,ETag在处理同步update操作的时候,是非常有用的,当然,这可以另开一篇文章了。
Spring ETag Filter
因为ETag只是根据响应内容表述的一段字符串而已,所以,服务器完全可以不依赖于响应体中具体的内容来获取ETag值,可以通过整个响应体的值来计算ETag值。这意味着,我们完全可以使用统一的方法为任何一个响应添加ETag值。
猜猜,万能的Spring会做什么?
是的,Spring框架提供了一个ETag响应过滤器实现,允许你直接通过过滤器,对响应直接计算并设置ETag值。所有你需要做的,仅仅只是配置一个过滤器而已。
最简单的配置方式,就是通过FilterRegistrationBean配置即可:
@Bean
public FilterRegistrationBean filterRegistrationBean () {
ShallowEtagHeaderFilter eTagFilter = new ShallowEtagHeaderFilter();
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(eTagFilter);
registration.addUrlPatterns("/*");
return registration;
}
在这种情况下,我们也可以通过addUrlPatterns()方法来匹配你需要的URL模式。我这里仅仅只是一个演示,所以我把所有的请求都使用ETag进行验证了。
除了正常生成ETag,这个过滤器还能在缓存有效的情况下正确的返回HTTP304状态码和空响应体。但是再次强调一点,ETag的计算是非常昂贵的,所以,在有些应用中,使用这个过滤器甚至会带来更大的性能损耗,所以,请一定要思考清楚。
原文地址:https://www.javacodegeeks.com/2018/10/http-cache-spring-examples.html
想获取更多技术视频,请前往叩丁狼官网:http://www.wolfcode.cn/openClassWeb_listDetail.html
网友评论