rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())

rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片


介绍 在工作之余阅读缓存相关的书籍时,看到了http缓存相关的知识,HTTP 缓存机制是一个 web 性能优化的重要手段,无论是做前端还是做web后台,都可能会用得到它,应该是知识体系库中的一个基础环节,以前这一块学的不是很扎实,现在整理资料巩固巩固。
HTTP缓存可以说是浏览器缓存的其中一种,浏览器缓存也包含很多内容:HTTP 缓存、indexDB、cookie、localstorage 等等。这里我们只讨论 HTTP 缓存相关内容。
浏览器主要分为Last-Modified/Etag和Cache-Control/Expires
其中Cache-Control/Expires属于强缓存,Last-Modified/Etag属于协商(比较)缓存
cache-control rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
cache-control字段位于这里,像这样设置的cache-control是没有缓存的
在请求中使用Cache-Control 时,它可选的值有:
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
在响应中使用Cache-Control 时,它可选的值有:
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
在Cache-Control 中,这些值可以自由组合,多个值如果冲突时,也是有优先级的,而no-store优先级最高。
像cache-control这种强缓存是性能高的,它不需要请求服务器,就直接从本地加载文件了,后面的Last-Modified/Etag是需要请求服务器来验证文件的。
在实际使用中,浏览器会先验证强缓存,所以对于js、css、img文件的过期时间可以设置非常长,因为前端构建工具打包出来的文件是有利于缓存的,比如可以配置webpack打包后的js文件跟个hash值,就可以根据hash来判断文件是否发生变化。在打包出来的文件名上加上文件内容的hash是目前最常见的有效使用浏览器强缓存的方法,js文件如果有内容更新,hash就会更新,浏览器请求路径变化所以更新缓存,如果js内容不变,hash不变,直接用缓存,下面的etag也有类似的思想。
如果对于变化频繁的文件就不要加cache-control了,如果配置cache-control的过期时间比较长的话,由于cache-control优先级比Last-Modified/Etag高,所以会在过期时间内不会更新文件。
Expires 关于Expires:

  • Expires 是以前用来控制缓存的http头,Cache-Control是新版的API。
  • 现在首选 Cache-Control。
  • 如果在Cache-Control响应头设置了 “max-age” 或者 “s-max-age” 指令,那么 Expires 头会被忽略。
  • 响应头设置方式:Expires: Wed, 21 Oct 2015 07:28:00 GMT
  • Expires 响应头包含日期/时间, 即在此时候之后,响应过期。
注意:因为过期标准的时间用的是本地时间,所以不靠谱,所以要使用Cache-Control代替Expires。
关于第三点:一般会把Cache-Control和Expires都设置上,因为 Expires 是 HTTP 1.0 定义的字段,而 Cache-Control 是 HTTP 1.1 的字段,万一客户端只支持 HTTP 1.0,那么 Cache-Control 有可能就会不工作,所以一般为了兼容会都写上。
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
Cache-Control设置时间长度、Expires 设置时间点
Last-Modify/If-Modify-Since 浏览器第一次请求一个资源的时候,服务器返回的 header中会加上Last-Modify,Last-modify是一个时间标识该资源的最后修改时间。
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
【rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())】当浏览器再次请求该资源时,发送的请求头中会包含If-Modify-Since,该值为缓存之前返回的Last-Modify。服务器收到If-Modify-Since后,根据资源的最后修改时间判断是否命中缓存。
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
如果命中缓存,则返回http304,并且不会返回资源内容,并且不会返回Last-Modify。由于对比的服务端时间,所以客户端与服务端时间差距不会导致问题。但是有时候通过最后修改时间来判断资源是否修改还是不太准确(资源变化了最后修改时间也可以一致)。于是出现了ETag/If-None-Match。
Etag/If-None-Match 经过上面的缓存后,若,则浏览器会将请求发送至服务器。服务器根据http头信息中的Last-Modify/If-Modify-Since或Etag/If-None-Match来判断是否命中协商缓存。如果命中,则http返回码为304,浏览器从缓存中加载资源。
与Last-Modify/If-Modify-Since不同的是,Etag/If-None-Match返回的是一个校验码(ETag: entity tag)。ETag可以保证每一个资源是唯一的,资源变化都会导致ETag变化。ETag值的变更则说明资源状态已经被修改。服务器根据浏览器上发送的If-None-Match值来判断是否命中缓存。
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
ETag是HTTP1.1中才加入的一个属性,用来帮助服务器控制Web端的缓存验证。它的原理是这样的,当浏览器请求服务器的某项资源(A)时, 服务器根据A算出一个哈希值(3f80f-1b6-3e1cb03b)并通过 ETag 返回给浏览器,浏览器把"3f80f-1b6-3e1cb03b" 和 A 同时缓存在本地,当下次再次向服务器请求A时,会通过类似 If-None-Match: “3f80f-1b6-3e1cb03b” 的请求头把ETag发送给服务器,服务器再次计算A的哈希值并和浏览器返回的值做比较,如果发现A发生了变化就把A返回给浏览器(200),如果发现A没有变化就给浏览器返回一个304未修改。这样通过控制浏览器端的缓存,可以节省服务器的带宽,因为服务器不需要每次都把全量数据返回给客户端。
HTTP中并没有指定如何生成Etag,可以自己定义一种好的方式来生成Etag
通过etag,实际上还是要向服务器发送一次请求,但是把资源文件缓存下来了,减少服务器带宽。
总结 Last-Modified与 Etag对比
你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag(实体标识)呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:
  • Last-Modified标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间。
  • 如果某些文件会被定期生成,当有时内容并没有任何变化,但Last-Modified却改变了,导致文件没法使用缓存。
  • 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形。
Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
浏览器第一次和第二次请求对比,这张图反映出优先级:ETag优先级 > Last-Modified优先级、cache-control优先级 > expires优先级。
用户操作与缓存
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
在spring boot中开启http缓存 本地测试环境:
  • Spring boot版本: 2.1.3.RELEASE
  • 项目前后端通过ajax交互,前端文件全部放在src/main/resources/static目录下,跟后台一起部署。
实操:
配置Cache-Control 在spring boot中,发现没有经过http缓存相关的配置默认是这样子的,response带有不使用缓存的http头,导致每次加载都要从服务端获取数据,而有些静态资源文件是不必要每次从服务器去获取的,像常用的js库、css库什么的。
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
Spring boot配置cache-control和expires可以通过WebMvcConfigurationSupport来配置,也可通过修改配置文件的方式,这里使用配置类的方式。
这里需要注意的是,使用WebMvcConfigurationSupport会使WebMvcAutoConfiguration的自动配置不生效,因为WebMvcAutoConfiguration里面有个@ConditionalOnMissingBean(WebMvcConfigurationSupport.class),表现为默认的一些映射路径无法访问,如果想保留默认的配置,那就使用implements WebMvcConfigurer的方式来配置
@Configuration
public  class  WebConfigConfigurer  extends  WebMvcConfigurationSupport  {
        @Override
        public  void  addResourceHandlers(ResourceHandlerRegistry  registry)  {
                registry.addResourceHandler( "/static/**")
                                .addResourceLocations( "classpath:/static/")
                                .setCacheControl(CacheControl.maxAge( 604800,  TimeUnit.SECONDS));
        }
}
这里将src/main/resources/static目录映射到/static的url下,然后setCacheControl就可以了。
spring.resources.cache.cachecontrol.cache- private=  #  Indicate  that  the  response  message  is  intended  for  a  single  user  and  must  not  be  stored  by  a  shared  cache.
spring.resources.cache.cachecontrol.cache- public=  #  Indicate  that  any  cache  may  store  the  response.
spring.resources.cache.cachecontrol.max-age=  #  Maximum  time  the  response  should  be  cached,  in  seconds  if  no  duration  suffix  is  not  specified.
spring.resources.cache.cachecontrol.must-revalidate=  #  Indicate  that  once  it  has  become  stale,  a  cache  must  not  use  the  response  without  re-validating  it  with  the  server.
spring.resources.cache.cachecontrol.no-cache=  #  Indicate  that  the  cached  response  can  be  reused  only  if  re-validated  with  the  server.
spring.resources.cache.cachecontrol.no-store=  #  Indicate  to  not  cache  the  response  in  any  case.
spring.resources.cache.cachecontrol.no-transform=  #  Indicate  intermediaries  (caches  and  others)  that  they  should  not  transform  the  response  content.
spring.resources.cache.cachecontrol.proxy-revalidate=  #  Same  meaning  as  the  "must-revalidate"  directive,  except  that  it  does  not  apply  to  private  caches.
spring.resources.cache.cachecontrol.s-max-age=  #  Maximum  time  the  response  should  be  cached  by  shared  caches,  in  seconds  if  no  duration  suffix  is  not  specified.
spring.resources.cache.cachecontrol.stale- if-error=  #  Maximum  time  the  response  may  be  used  when  errors  are  encountered,  in  seconds  if  no  duration  suffix  is  not  specified.
spring.resources.cache.cachecontrol.stale- while-revalidate=  #  Maximum  time  the  response  can  be  served  after  it  becomes  stale,  in  seconds  if  no  duration  suffix  is  not  specified.
如果要使用配置的方式也很简单,上面这些是关于cache的配置,在spring boot官方文档最后的附录里面有。
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
再来测试,首先把缓存开启,它这个选项就是通过request headers里的CacheControl实现的。
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
然后刷新发现已经使用了http缓存。
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
发现已经不是之前默认的no-store了,而是变为我们自己设置的缓存过期时间了,说明已经在使用http的强缓存了!
小提示:这个addResourceHandler和addResourceLocations不要弄反了,addResourceHandler的参数是URL path patterns,addResourceLocations的参数是本地资源路径
@Override
public  void  addResourceHandlers(ResourceHandlerRegistry  registry)  {
        registry.addResourceHandler( "/**")
                        .addResourceLocations( "classpath:/static/")
                        .setCacheControl(CacheControl.maxAge( 0,  TimeUnit.SECONDS)
                                        .cachePublic());
}
像这种的,就是将src/main/resources/static目录映射到/的url下
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
如果遇到问题,那么就debug ResourceHandlerRegistry的AbstractHandlerMapping getHandlerMapping()方法,在urlMap变量中看到url与handler的映射关系
配置Last-Modified缓存 @Override
public  void  addResourceHandlers(ResourceHandlerRegistry  registry)  {
        registry.addResourceHandler( "/static/**")
                        .addResourceLocations( "classpath:/static/")
                        .setCacheControl(
                                        CacheControl.maxAge( 0,  TimeUnit.SECONDS)
                                        .cachePublic());
}
如果遇到问题,那么就debug ResourceHandlerRegistry的AbstractHandlerMapping getHandlerMapping()方法,在urlMap变量中看到url与handler的映射关系
配置Last-Modified缓存 @Override
public  void  addResourceHandlers(ResourceHandlerRegistry  registry)  {
        registry.addResourceHandler( "/static/**")
                        .addResourceLocations( "classpath:/static/")
                        .setCacheControl(
                                        CacheControl.maxAge( 0,  TimeUnit.SECONDS)
                                        .cachePublic());
}
先一步一步的来,把cacheControl过期时间设置0,并设置为public,根据之前的流程图,因为过期时间为0,判断已过期,进入etag判断,而这里没有设置etag,那就进入Last-Modified判断。
需要注意的就是no-cache与max-age=0的区别,no-cache是压根都不用缓存,直接请求服务器200,过期时间为0就会走Etag/Last-Modified缓存的判断
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
发现已经变成了304,设置成功了。
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
现在就是通过对比If-Modified-Since和Last-Modified,就是文件修改时间来判断缓存的。
配置ETag缓存 @Bean
public  FilterRegistrationBean  filterRegistrationBean  ()  {
        ShallowEtagHeaderFilter  eTagFilter  =  new  ShallowEtagHeaderFilter();
        //设置为weakETag,默认为false
        //  eTagFilter.setWriteWeakETag(true);
        FilterRegistrationBean  registration  =  new  FilterRegistrationBean();
        registration.setFilter(eTagFilter);
        registration.addUrlPatterns( "/static/*");
        return  registration;
}
如果想开启Etag可以使用这个过滤器,把static下文件的设置上etag
这个过滤器是通过生成MD5值来校验的,也可以自定义一个生成Etag的规则
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
第一次请求,服务器会返回一个Etag标签,200状态码
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
第二次请求,浏览器携带这个etag带去后台验证,然后返回304
rxjava|阿里二面(SpringBoot中如何设置HTTP缓存())
文章图片
修改这个文件,发现返回了一个新的Etag,再次请求也是304,说明缓存成功了。

    推荐阅读