http缓存介绍以及springweb中的使用方式
1 http缓存介绍
1.1 参考链接
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types
1.2 http版本
版本 | 特性 |
---|---|
0.9 | GET POST HEAD |
1.0 | PUT HEAD 无状态短连接 |
1.1 | 长连接、管道化、缓存处理、断点续传 |
2.0 | 二进制分帧、多路复用、头部压缩 |
1.3 Http缓存
请求头
Cache-control
Cache-Control: max-age=<seconds>
Cache-Control: max-stale[=<seconds>]
Cache-Control: min-fresh=<seconds>
Cache-control: no-cache
Cache-control: no-store
Cache-control: no-transform
Cache-control: only-if-cached
If-None-Match
If-None-Match: <etag_value>
If-None-Match: <etag_value>, <etag_value>, …
If-None-Match: *
对于 GET
和 HEAD
请求方法来说,当且仅当服务器上没有任何资源的 ETag
属性值与这个首部中列出的相匹配的时候,服务器端会才返回所请求的资源,响应码为 200
对于 GET
和 HEAD
方法来说,当验证失败的时候,服务器端必须返回响应码 304
(Not Modified
,未改变)
当浏览器的缓存已过期,在重新请求的时候,浏览器会先发送一个带有该请求头的标识If-None-Match
或If-Modified-Since
判断该资源有效,则直接返回304
(不会带有实体信息,标识资源依然有效),否则返回200正常返回资源
If-Match
If-Match: <etag_value>
If-Match: <etag_value>, <etag_value>, …
在请求方法为 GET
和 HEAD
的情况下,服务器仅在请求的资源满足此首部列出的 ETag值时才会返回资源
对于 GET
和 HEAD
方法,搭配 Range
首部使用,可以用来保证新请求的范围与之前请求的范围是对同一份资源的请求。如果 ETag
无法匹配,那么需要返回 416
(Range Not Satisfiable,范围请求无法满足) 响应。
If-Modified-Since
If-Modified-Since: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
当与 If-None-Match
一同使用的时候,If-None-Match
优先级更高(假如服务器支持的话)
-
If-Unmodified-Since
不是使用在缓存中~这里只是用于比较各个请求头之间的区别
If-Unmodified-Since: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
HTTP协议中的 If-Unmodified-Since
消息头用于请求之中,使得当前请求成为条件式请求:只有当资源在指定的时间之后没有进行过修改的情况下,服务器才会返回请求的资源,或是接受 POST
或其他 non-safe 方法的请求。如果所请求的资源在指定的时间之后发生了修改,那么会返回 412
(Precondition Failed) 错误
响应头
Cache-control
http/1.1
Cache-Control: no-store
无缓存
Cache-Control: no-cache
浏览器每次请求该资源(同时带上校验该资源的标识,服务器根据该标识是否有效来返回304还是资源),等同于Cache-Control: max-age=0
Cache-Control: private
私有缓存,local
Cache-Control: public
CDN之类的中间缓存
Cache-Control: max-age=N
max-age=<seconds>
、表示资源能够被缓存(保持新鲜)的最大时间。相对Expires
而言,max-age
是距离请求发起的时间的秒数
如果在Cache-Control
响应头设置了 max-age
或者 s-max-age
指令,那么 Expires
头会被忽略。否则会查找Expires
的属性设置
Cache-Control: must-revalidate
一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。
no-transform
不得对资源进行转换或转变。Content-Encoding
、Content-Range
、Content-Type
等HTTP头不能由代理修改
proxy-revalidate
与must-revalidate作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略。
s-maxage=<seconds>
覆盖max-age或者Expires头,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它。
**客户端可缓存说明**:
- Cache-Control: no-cache
- Cache-Control: max-age=N
指定 no-cache
或 max-age=0
表示客户端可以缓存资源,每次使用缓存资源前都必须重新验证其有效性。这意味着每次都会发起 HTTP 请求,但当缓存内容仍有效时可以跳过 HTTP 响应体的下载。
Pragma
http/1.0的标准
通常定义Pragma
以向后兼容基于HTTP/1.0的客户端。
Pragma: no-cache
效果同Cache-Control: no-cache
,通常定义Pragma
以向后兼容基于HTTP/1.0的客户端。
-
ETag
强校验器值得注意的是 其值必须带有 ""引号
ETag: W/"<etag_value>"
‘W/’(大小写敏感) 表示使用弱验证器
ETag: “<etag_value>”
Last-Modified
弱校验器
Last-Modified: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
<day-name>
“Mon”, “Tue”, “Wed”, “Thu”, “Fri”, “Sat” 或 “Sun” 之一 (区分大小写)。
<day>
两位数字表示的天数, 例如"04" or “23”。
<month>
“Jan”, “Feb”, “Mar”, “Apr”, “May”, “Jun”, “Jul”, “Aug”, “Sep”,“Oct”, “Nov”, “Dec” 之一(区分大小写)。
<year>
4位数字表示的年份, 例如 “1990” 或者"2016"。
<hour>
两位数字表示的小时数, 例如 “09” 或者 “23”。
<minute>
两位数字表示的分钟数,例如"04" 或者 “59”。
<second>
两位数字表示的秒数,例如 “04” 或者 “59”。
GMT
国际标准时间。HTTP中的时间均用国际标准时间表示,从来不使用当地时间。
Expires
独立的header
Expires: Wed, 21 Oct 2015 07:28:00 GMT
Content-Disposition
Content-Disposition
响应头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。
Content-Disposition: inline
内联形式,表示非下载,比如在线展示或者播放
Content-Disposition: attachment
附件
Content-Disposition: attachment; filename=“filename.jpg”
附件并以指定的名称
Vary
vary验证
Vary: *
所有的请求都被视为唯一并且非缓存的,使用Cache-Control: no-store
,来实现则更适用,这样用于说明不存储该对象更加清晰。
Vary: <header-name>, <header-name>, …
逗号分隔的一系列http头部名称,用于确定缓存是否可用。
HTTP 响应头决定了对于后续的请求头,如何判断是请求一个新的资源还是使用缓存的文件。用以验证某个请求头的值是否与缓存中的值一致
Age
Age
消息头里包含对象在缓存代理中存贮的时长,以秒为单位
Age
的值通常接近于0
。表示此对象刚刚从原始服务器获取不久;其他的值则是表示代理服务器当前的系统时间与此应答中的通用头 Date
的值之差
2 spring中如何正确使用http缓存?
2.1 spring web的HTTP Caching介绍
https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web.html#mvc-caching
根据http协议的规范,结合spring web的官方介绍。
我这里会使用 CacheControl
、Last-Modified
、ETag
、Content-Length
、ResponseEntity<T>
返回值作为处理缓存的重要一环。
说一句:当返回值为
ResponseEntity<AbstractResource>
时,spring也会尝试帮我们自动设置Content-Length
,暖暖的,很贴心,但是我都是自己设置。
至于Static Resources
、ETag ShallowEtagHeaderFilter
不会使用。
原因如下:
Static Resources
一般直接交给一些固定的服务器比如nginx代理即可。ShallowEtagHeaderFilter
,官方给了这么一句话:
save bandwidth but not CPU time
- 节约带宽是因为返回304才节约(浏览器就不会读流,因此节约),不会减少cpu消耗时间则是由于,返回的流始终被读取过了。而为什么我不用?因为
ShallowEtagHeaderFilter
会读取返回流,如下:
protected String generateETagHeaderValue(InputStream inputStream, boolean isWeak) throws IOException {
// length of W/ + " + 0 + 32bits md5 hash + "
StringBuilder builder = new StringBuilder(37);
if (isWeak) {
builder.append("W/");
}
builder.append("\"0");
//读流转为16进制md5作为etag
DigestUtils.appendMd5DigestAsHex(inputStream, builder);
builder.append('"');
return builder.toString();
}
//将content内容返回给rawResponse中
protected void copyBodyToResponse(boolean complete) throws IOException {
if (this.content.size() > 0) {
HttpServletResponse rawResponse = (HttpServletResponse) getResponse();
if ((complete || this.contentLength != null) && !rawResponse.isCommitted()) {
rawResponse.setContentLength(complete ? this.content.size() : this.contentLength);
this.contentLength = null;
}
this.content.writeTo(rawResponse.getOutputStream());
this.content.reset();
if (complete) {
super.flushBuffer();
}
}
}
ShallowEtagHeaderFilter
使用了ConditionalContentCachingResponseWrapper
来包装rawResponse
,内部会使用content
保存流的内容,这种生成md5
的时间以及返回copy
这本身就是一种浪费时间,一般缓存都是文件流,那么如果我这次文件是被修改过了的,那么会生成新的etag
,也就是返回200
流会被浏览器正常读取。中间就生成etag
的性能损耗以及重新写回rawResponse
的损耗;为何我不在业务层直接获取流的时候使用返回文件的lastModified
作为http缓存etag
或者Last-Modified
呢(一般情况下两者都会添加),也就是文件缓存精确到了秒级别,基本上是足够一般业务场景的,如果为了极致的业务场景,则在文件存储的时候,就可以保存文件的md5
,这样就无需在ShallowEtagHeaderFilter
上消耗cpu了,所以我的建议是不使用ShallowEtagHeaderFilter
。
2.2 spring缓存小尝试
设计缓存,只有当id等于在固定的(1,2)中才缓存,同时设置缓存时间为30秒,其他的不需要缓存。
说明,不要尝试在浏览器端一个url下不断的回车,多打开几个tab😉,你只会看到不断看到304,不要问我为什么,我有痛苦的回忆,我也不知道我为什么非得这么测试😢😢😥
@GetMapping("/string2/{id}")
public ResponseEntity<String> showBook2(@PathVariable Long id) {
System.out.println("in fun "+atomicInteger.get());
if (id == 1 || id == 2) {
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.SECONDS))
.eTag(String.valueOf(id)) // lastModified is also available//自动处理返回304 HttpEntityMethodProcessor#handleReturnValue方法完成自动转304
.body(String.valueOf(atomicInteger.incrementAndGet()));
}
return ResponseEntity
.ok()
.body(String.valueOf(atomicInteger.incrementAndGet()));
}
尝试结果:
尝试1:get url /string/1
结果:第一次返回200
尝试2:get url /string/2
结果:第一次返回200
尝试3:get url /string/3
结果:第一次返回200 正常没有缓存的直接返回
尝试4:get url /string/1
结果:第二次返回200 (from disk cache),说明浏览器直接从缓存中获取的值,而非发起了一个http请求,,也就是根本没有与服务器进行交互!
尝试5:get url /string/2
结果:第二次返回200 (from disk cache)
尝试6:get url /string/3
结果:第二次返回200 正常没有缓存的直接返回。
以下3次均在30秒后
尝试7:get url /string/1
结果:第三次返回304 说明缓存中的内容依然有效(HttpEntityMethodProcessor#handleReturnValue方法完成将200根据etag自动转304)
尝试8:get url /string/2
结果:第三次返回304 说明缓存中的内容依然有效(HttpEntityMethodProcessor#handleReturnValue方法完成将200根据etag自动转304)
尝试9:get url /string/3
结果:第二次返回200 正常没有缓存的直接返回
根据以上结果,有了一个大致的印象以及思维,浏览器http缓存以及spring web做了些什么。
2.3 spring缓存小尝试2
官方例子,根据WebRequest#checkNotModified
来校验request
的If-None-Match
请求头,实现返回304
@RequestMapping
public String myHandleMethod(WebRequest request, Model model) {
long eTag = ...
if (request.checkNotModified(eTag)) {
return null; //返回304,因此无需任何返回body
}
model.addAttribute(...);
return "myViewName";
}
2.4 自己实现一个可用的demo
目标
打算实现文件流在浏览器端的缓存,使用spring
的AbstractResource
作为ResponseEntity<T>
的泛型参数。
设计一个模型wrapper
业务层需要返回一个wrapper
,其包裹一个AbstractResource
为实际的资源。
import org.springframework.core.io.AbstractResource;
import org.springframework.http.HttpStatus;
public class AbstractResourceWrapper {
private final AbstractResource resource;
private HttpStatus httpStatus = HttpStatus.OK;
private long fileSize;//如不设置,只能自己尝试从resource中获取
private String fileType;//.xxx 文件返回类型
private String filename;//file simple name
private String etag; //需设置,缓存所需
private long lastModified;//一般设置为文件的最后修改日期,因此可以直接使用Last-Modified 如不设置,只能自己尝试从resource中获取
//...
}
设计一个接口,用于处理http缓存配置并返回ResponseEntity<AbstractResource>
public interface HttpCacheConfigurer{
//核心方法
ResponseEntity<AbstractResource> returnResourceResponseEntity(
boolean nocache, //是否缓存参数
boolean attachment, //是否附件
WebRequest webRequest, //spring mvc参数
AbstractResourceWrapper resourceWrapper//上面的wrapper
);
//处理content-type
default void commonContentType(ResponseEntity.BodyBuilder bodyBuilder, String fileType) {
if (!fileType.startsWith(".")) {
fileType = "." + fileType;
}
String mimeType = URLConnection.guessContentTypeFromName(fileType);
if (mimeType != null) {
bodyBuilder.contentType(MediaType.valueOf(mimeType));
} else {
bodyBuilder.contentType(MediaType.APPLICATION_OCTET_STREAM);
}
}
}
缓存参数配置
使用spring的@ConfigurationProperties(prefix = "xxx")
继承即可。
public class AbstractHttpCacheConfiguration {
Map<String, String> cacheControl = new HashMap<>();//缓存参数
public Map<String, String> getCacheControl() {
return cacheControl;
}
}
实现HttpCacheConfigurer
比如就叫MyHttpCacheConfigurer
,也可继续abstract class
核心方法如下
@Override
public ResponseEntity<AbstractResource> returnResourceResponseEntity(boolean nocache, boolean attachment, WebRequest webRequest, AbstractResourceWrapper resourceWrapper) {
ResponseEntity.BodyBuilder bodyBuilder = ResponseEntity.status(resourceWrapper.getHttpStatus());
if (nocache) {//禁用缓存
bodyBuilder = bodyBuilder.cacheControl(CacheControl.noStore());
} else {//缓存设置
this.withCacheControl(bodyBuilder, resourceWrapper.getFileSize());
}
if (webRequest.checkNotModified(resourceWrapper.getEtag())) {//说明资源依然有效,并重置缓存期限
try {
IoUtil.close(resourceWrapper.getResource().getInputStream());//主动关闭资源
} catch (Exception ignored) {
}
this.withResetCache(bodyBuilder, webRequest, resourceWrapper);
return bodyBuilder.body(null);
}
if (attachment) {//是否附件
bodyBuilder.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + resourceWrapper.getFilename());
}
if (resourceWrapper.getEtag() != null) {
bodyBuilder.eTag(resourceWrapper.getEtag());
}
if (resourceWrapper.getFileSize() > 0) {
bodyBuilder.contentLength(resourceWrapper.getFileSize());
}
if (resourceWrapper.getLastModified() > 0) {
bodyBuilder.lastModified(resourceWrapper.getLastModified());
}
this.withContentType(bodyBuilder, resourceWrapper.getFileType());
this.beforeReturnResourceResponseEntity(bodyBuilder, webRequest, resourceWrapper);
return bodyBuilder.body(resourceWrapper.getResource());
}
public abstract AbstractHttpCacheConfiguration getAbstractHttpCacheConfiguration();
//CacheControl配置
protected void withCacheControl(ResponseEntity.BodyBuilder bodyBuilder, long fileSize) {
//...根据自己预设的config缓存参数,设计
//当然还可以设计自己得其他得参数,比如根据资源大小选择是否缓存等等
if(config.noStore){
bodyBuilder.cacheControl(CacheControl.noStore());
return;
}
if(config.noCache){
bodyBuilder.cacheControl(CacheControl.noCache());缓存但浏览器会每次验证
return;
}
CacheControl cache = CacheControl.maxAge(config.useMaxAge, config.useUnit);
if(config.mustRevalidate){
cache.mustRevalidate();
}
bodyBuilder.cacheControl(cache);
}
protected abstract void withContentType(ResponseEntity.BodyBuilder bodyBuilder, String fileType){
//...
}
protected void withResetCache(ResponseEntity.BodyBuilder bodyBuilder, WebRequest webRequest, AbstractResourceWrapper resourceWrapper) {
//...
}
//只能自己尝试从resource中获取一些必要参数
protected void beforeReturnResourceResponseEntity(ResponseEntity.BodyBuilder bodyBuilder, WebRequest webRequest, AbstractResourceWrapper resourceWrapper) {//暂留一个钩子
AbstractResource resource = resourceWrapper.getResource();
if (resourceWrapper.getFileSize() <= 0) {
if (resource instanceof InputStreamResource) {//spring也是这么做的,InputStreamResource不读length
//nothing
} else {
try {
bodyBuilder.contentLength(resource.contentLength());
} catch (IOException ignored) {
}
}
}
if (resourceWrapper.getEtag() == null || resourceWrapper.getLastModified() <= 0) {
try {
long lastModified = resource.lastModified();
if (resourceWrapper.getLastModified() <= 0) {
bodyBuilder.lastModified(lastModified);
}
if (resourceWrapper.getEtag() == null) {
bodyBuilder.eTag(String.valueOf(lastModified));//用lastModified代替etag
}
} catch (IOException ignored) {
}
}
}
总体逻辑为根据业务层返回得AbstractResourceWrapper
经过HttpCacheConfigurer
的配置,返回一个ResponseEntity<AbstractResource>
作为controller
的返回值。
当然基于文件的,也可以支持Range
请求,但是这里就没写了,扩展起来多思考思考就知道的。
3 总结
本次着重在http协议中有关浏览器缓存的知识点Cache-Control
的介绍,以及spring web是如何处理http
缓存的,根据这些知识点就可以写出自己的一个简易缓存处理机制,并给予其一定的扩展性。
写这次博客是有一定的目的的说的🙃🙃~~因为是1024节日,
所以在此祝各位1024,更加1024.( ̄︶ ̄)↗
4 参考
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types
https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web.html#mvc-caching