springweb中使用http缓存的小姿势

1 http缓存介绍

1.1 参考链接

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types

1.2 http版本

版本特性
0.9GET POST HEAD
1.0PUT 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: *

对于 GETHEAD 请求方法来说,当且仅当服务器上没有任何资源的 ETag属性值与这个首部中列出的相匹配的时候,服务器端会才返回所请求的资源,响应码为 200
对于 GETHEAD 方法来说,当验证失败的时候,服务器端必须返回响应码 304Not Modified,未改变)
当浏览器的缓存已过期,在重新请求的时候,浏览器会先发送一个带有该请求头的标识If-None-MatchIf-Modified-Since判断该资源有效,则直接返回304(不会带有实体信息,标识资源依然有效),否则返回200正常返回资源

  • If-Match

If-Match: <etag_value>
If-Match: <etag_value>, <etag_value>, …

在请求方法为 GETHEAD 的情况下,服务器仅在请求的资源满足此首部列出的 ETag值时才会返回资源
对于 GETHEAD 方法,搭配 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-EncodingContent-RangeContent-Type等HTTP头不能由代理修改

proxy-revalidate

与must-revalidate作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略。

s-maxage=<seconds>

覆盖max-age或者Expires头,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它。

**客户端可缓存说明**:

- Cache-Control: no-cache
- Cache-Control: max-age=N

指定 no-cachemax-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的官方介绍。
我这里会使用 CacheControlLast-ModifiedETagContent-LengthResponseEntity<T> 返回值作为处理缓存的重要一环。

说一句:当返回值为ResponseEntity<AbstractResource>时,spring也会尝试帮我们自动设置Content-Length,暖暖的,很贴心,但是我都是自己设置。

至于Static ResourcesETag 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来校验requestIf-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

目标

打算实现文件流在浏览器端的缓存,使用springAbstractResource作为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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值