本文翻译:吴嘉俊,叩丁狼高级讲师

缓存是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

Spring中实现HTTP缓存相关推荐

  1. spring中对浏览器缓存的控制

    我们平常在页面发送一个url请求的时候,会通过网络去服务器获取这个资源,网速好的时候倒是没什么,但是网络差一点的话,资源获取的延时就会很长,用户体验就会大大降低.但是我们可以使用缓存来解决这个问题. ...

  2. redis 清空缓存_「镜头回放」简直了!spring中清除redis缓存导致应用挂死

    异常场景 springWeb应用一直运行正常,同事最近反应,每次版本更新完毕,刷新缓存,就会导致应用挂死.只有重启redis应用才恢复正常. 项目概况 springWeb项目,常用配置表做了redis ...

  3. Spring 中使用redis缓存方法记录

    背景 在平时项目中,可能会有某个条件的查询,会多次进到db里面去查,这样就会重复的查询相同的数据,但是我们的数据又不是需要更改及显示的,这时候就可以用到 方法的缓存了.例如在我们调用微信小程序时,需要 ...

  4. Spring源码 - 从缓存中获取单例Bean

    # Spring源码 - 从缓存中获取单例Bean Spring版本:Spring 5.3.13-release # 1.从缓存中获取单例Bean 单实例Bean在Spring的同一个容器中只会创建一 ...

  5. 在Spring、Hibernate中使用Ehcache缓存

    前一篇http://blog.csdn.net/ibm_hoojo/article/details/7739181介绍了Ehcache整合Spring缓存,使用页面.对象缓存:这里将介绍在Hibern ...

  6. spring_在Spring中使用多个动态缓存

    spring 在第三篇有关Spring(很长一段时间)中缓存管理器的文章中,我想通过展示如何配置多个动态创建缓存的缓存管理器来扩展前 两个. Spring具有CompositeCacheManager ...

  7. redis spring 切面缓存_今日份学习: Spring中使用AOP并实现redis缓存?

    笔记 在Spring中如何使用AOP? Spring是如何切换JDK动态代理和CGLIB的? spring.aop.proxy-target-class=true (在下方第二个链接中,原生doc中提 ...

  8. 第 4-4 课:Spring Boot 中使⽤ Cache 缓存的使⽤

    我们知道绝⼤多数的⽹站/系统,最先遇到的⼀个性能瓶颈就是数据库,使⽤缓存做数据库的前置缓存,可以 ⾮常有效地降低数据库的压⼒,从⽽提升整个系统的响应效率和并发量. 以往使⽤缓存时,通常创建好缓存⼯具类 ...

  9. Spring中Bean的生命周期以及三级缓存介绍

    Bean的生命周期以及三级缓存介绍 简述 测试代码编写 创建IOC容器(Bean创建) 1.refresh()方法 2.finishBeanFactoryInitialization(beanFact ...

  10. 在Spring Boot中使用数据缓存

    关注公众号[江南一点雨],专注于 Spring Boot+微服务以及前后端分离等全栈技术,定期视频教程分享,关注后回复 Java ,领取松哥为你精心准备的 Java 干货! 春节就要到了,在回家之前要 ...

最新文章

  1. python二十八:模块
  2. js动态改变下拉菜单内容示例 .
  3. Scala数组的mkString()方法
  4. linux apache gzip压缩,Linux入门教程:配置Apache开启gzip压缩传输,gzip压缩 LoadModul
  5. 在EditPlus中配置java快捷键
  6. keil4 c语言标准,求助!关于KEIL4和C语言
  7. 网络调试助手做什么用的
  8. matlab画区间柱状图,科学网—Matlab画柱状图 - 高淑敏的博文
  9. Day7 零基础python入门100天Udemy训练营-Hangman Game 继续学习import, if else, while loop, for loop
  10. PDF转jpg工具(含注册码)
  11. 用户数据报协议(UDP)
  12. 单片机IO口悬空,高阻态究竟是什么意思?
  13. unity大量较高尺寸的序列帧图片出包画面马赛克问题
  14. 以太网使用的CSMA/CD协议是以争用方式接入到共享信道的。这与传统的时分复用TDM相比优缺点如何?网络适配器的作用是什么?网络适配器工作在哪一层?假定1km长的CSMA/CD网络的数据率为1Gb/s
  15. 黄峥不再担任拼多多董事长;恒大首席经济学家任泽平离职 | 高管变动
  16. DGA:域名生成算法
  17. PingCAP Clinic 服务:贯穿云上云下的 TiDB 集群诊断服务
  18. iOS系统中判断设备类型
  19. linux nmea解析程序,GPS的NMEA数据解析
  20. 最新手机号码、固话号码正则表达式

热门文章

  1. 统计学习之第二天(可汗学院公开课:统计学)
  2. 包学会之浅入浅出Vue.js:开学篇(转)
  3. 港科百创 | “一清创新”完成新一轮融资,跻身准独角兽之列!
  4. Centos8关闭防火墙
  5. aec一pc_什么是AEC声学回声消除器?
  6. MC34063在扩展后的降压应用
  7. 2021高考志愿填报总结-yy
  8. XShell VIM 粘贴
  9. Linux redhat 5.7 安装 Teamviewer7
  10. 《数据结构C语言版》——绪论