Spring 源码分析衍生篇十 :Last-Modified 缓存机制
文章目录
- 一、前言
- 二、Last-Modify
- 三、实现方案
- 1. 实现 org.springframework.web.servlet.mvc.LastModified接口
- 1.1. 简单演示
- 1.2. 原理分析
- 1.2.1 HandlerAdapter#getLastModified
- 1.2.2 ServletWebRequest#checkNotModified(long)
- 2. 使用WebRequest#checkNotModified(long)
- 2.1. 简单演示
- 2.2. 原理分析
- 四、Http其他缓存方法
一、前言
本文是 Spring源码分析:Spring源码分析二十一:Spring MVC③ DispatcherServlet的逻辑 的衍生文章。主要是因为本人菜鸡,在分析源码的过程中还有一些其他的内容不理解,故开设衍生篇来完善内容以学习。
Spring全集目录:Spring源码分析:全集整理
本系列目录如下:
- Spring源码分析十九:Spring MVC① 搭建
- Spring源码分析二十:Spring MVC② DispatcherServlet的初始化
- Spring源码分析二十一:Spring MVC③ DispatcherServlet的逻辑
衍生篇目录如下:
- Spring 源码分析衍生篇十 :Last-Modified 缓存机制
- Spring 源码分析衍生篇十一 :HandlerMapping
二、Last-Modify
以下内容来源于百度百科
在浏览器第一次请求某一个URL时,服务器端的返回状态会是200,内容是客户端请求的资源,同时有一个Last-Modified的属性标记此文件在服务器端最后被修改的时间。Last-Modified格式类似这样:Last-Modified : Fri , 12 May 2006 18:53:33 GMT
客户端第二次请求此URL时,根据HTTP协议的规定,浏览器会向服务器传送If-Modified-Since报头,询问该时间之后文件是否有被修改过:If-Modified-Since : Fri , 12 May 2006 18:53:33 GMT
如果服务器端的资源没有变化,则自动返回 HTTP 304(Not Changed.)状态码,内容为空,这样就节省了传输数据量。当服务器端代码发生改变或者重启服务器时,则重新发出资源,返回和第一次请求时类似。从而保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。
三、实现方案
1. 实现 org.springframework.web.servlet.mvc.LastModified接口
1.1. 简单演示
这种方案一般是用在 Controller 实现 org.springframework.web.servlet.mvc.Controller
接口的场景,如下:
@Component("/beanNameSay")
public class BeanNameSayController implements Controller, LastModified {private long lastModified;// 逻辑处理@Overridepublic ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {return new ModelAndView("/hello.html");}@Overridepublic long getLastModified(HttpServletRequest request) {// 如果 lastModified 刚初始化,则赋值为当前时间戳并返回。if (lastModified == 0L){lastModified = System.currentTimeMillis();}return lastModified;}
}
在一次请求成功后,第二次请求返回的状态码 304。
1.2. 原理分析
在 Spring源码分析二十一:Spring MVC③ DispatcherServlet的逻辑 一文中,我们其中提到了Last-Modified 缓存机制。本文我们集中关注这一点
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {...// 获取请求方式String method = request.getMethod();boolean isGet = "GET".equals(method);// 如果是 get请求或者 head 请求则进入该分支if (isGet || "HEAD".equals(method)) {// 调用 HandlerAdapter#getLastModified 方法 来获取最后修改时间long lastModified = ha.getLastModified(request, mappedHandler.getHandler());// 判断到目前为止是否有过修改,没有则直接return。实现缓存的功能if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {return;}}...}
我们这里需要关注的方法无非就是两个 HandlerAdapter#getLastModified
和 ServletWebRequest#checkNotModified(long)
。我们一个一个来看
1.2.1 HandlerAdapter#getLastModified
关于 HandlerAdapter#getLastModified
不同的 HandlerAdapter
有不同的实现方式,由于我们这里使用的是 BeanNameUrlHandlerMapping
处理器映射方式。所以这里匹配的HandlerAdapter 方法是 SimpleControllerHandlerAdapter#getLastModified
SimpleControllerHandlerAdapter#getLastModified
的具体实现如下。
@Overridepublic long getLastModified(HttpServletRequest request, Object handler) {// 如果handler 是 LastModified 的 实现类。则直接调用 handler 的 getLastModified 方法if (handler instanceof LastModified) {return ((LastModified) handler).getLastModified(request);}// 否则返回-1return -1L;}
这里就很明确了。在这里会调用 BeanNameSayController#getLastModified
来获取最后修改时间。我们这里实现获取的是第一次请求时候保留的时间戳。
1.2.2 ServletWebRequest#checkNotModified(long)
经过上面的分析,我们可以得知 ha.getLastModified(request, mappedHandler.getHandler());
调用返回的值是我们返回的第一次请求时候保留的时间戳。
我们来看看ServletWebRequest#checkNotModified(long)
怎么进行的校验
@Overridepublic boolean checkNotModified(long lastModifiedTimestamp) {return checkNotModified(null, lastModifiedTimestamp);}@Overridepublic boolean checkNotModified(@Nullable String etag, long lastModifiedTimestamp) {HttpServletResponse response = getResponse();// notModified 为true 标志没有被修改,默认false// 如果 notModified 已经true || 返回状态码已经不是200直接返回 notModified if (this.notModified || (response != null && HttpStatus.OK.value() != response.getStatus())) {return this.notModified;}// Evaluate conditions in order of precedence.// See https://tools.ietf.org/html/rfc7232#section-6// 解析校验 If-Unmodified-Since 请求头。这个请求头和 If-Modified-Since 请求头相反if (validateIfUnmodifiedSince(lastModifiedTimestamp)) {// 设置状态码 304,并返回 notModifiedif (this.notModified && response != null) {response.setStatus(HttpStatus.PRECONDITION_FAILED.value());}return this.notModified;}// 校验 If-None-Match 请求头。这是针对 Etag 缓存。boolean validated = validateIfNoneMatch(etag);if (!validated) {// 校验 If-Modified-Since 请求头validateIfModifiedSince(lastModifiedTimestamp);}// Update response// 更新 Response。包括状态码等信息if (response != null) {boolean isHttpGetOrHead = SAFE_METHODS.contains(getRequest().getMethod());if (this.notModified) {response.setStatus(isHttpGetOrHead ?HttpStatus.NOT_MODIFIED.value() : HttpStatus.PRECONDITION_FAILED.value());}if (isHttpGetOrHead) {if (lastModifiedTimestamp > 0 && parseDateValue(response.getHeader(HttpHeaders.LAST_MODIFIED)) == -1) {response.setDateHeader(HttpHeaders.LAST_MODIFIED, lastModifiedTimestamp);}if (StringUtils.hasLength(etag) && response.getHeader(HttpHeaders.ETAG) == null) {response.setHeader(HttpHeaders.ETAG, padEtagIfNecessary(etag));}}}return this.notModified;}// 解析校验 If-Modified-Since 请求头private boolean validateIfModifiedSince(long lastModifiedTimestamp) {if (lastModifiedTimestamp < 0) {return false;}long ifModifiedSince = parseDateHeader(HttpHeaders.IF_MODIFIED_SINCE);if (ifModifiedSince == -1) {return false;}// We will perform this validation...this.notModified = ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000);return true;}
这里我们就可以知道是什么情况了
- 浏览器第一次请求,一切正常。
SimpleControllerHandlerAdapter#getLastModified
保存到当前请求的时间戳,并将该时间戳 通过Last-Modified
响应头返回给浏览器。 - 浏览器第二次请求,会使用
If-Modified-Since
请求头带上上次请求获取到的Last-Modified
。在DispatcherServlet
的处理过程中会调用HandlerAdapter#getLastModified
来获取第一步保存的时间戳lastModified
,这个时间戳是上次调用时候的时间戳。 WebRequest#checkNotModified(long)
方法校验了下面三个请求头来确定请求是否被修改:
-If-Unmodified-Since
:与If-Modified-Since
相反,只要它没有被最后给定的日期之后修改。如果请求在给定日期之后被修改,则该响应将是412(先决条件失败)错误。
-If-None-Match
: 针对 ETag 缓存。有服务器没有ETag与给定资源匹配的情况下,服务器才会返回具有状态的请求资源。对于其他方法,仅当最终现有资源ETag不符合任何列出的值时才会处理该请求。
-If-Modified-Since
:只有当它已经给定的日期之后被最后修改。如果请求没有被修改,那么响应将是304没有任何主体的;Last-Modified头将包含最后一次修改的日期。不同于If-Unmodified-Since,If-Modified-Since只能与GET或HEAD一起使用。
2. 使用WebRequest#checkNotModified(long)
对于我们通过 @RequestMapping("say")
注解方式来修饰的请求,是无法通过实现 org.springframework.web.servlet.mvc.LastModified
接口来实现该功能的。具体原因我们稍后再讲。
这里我们只能通过直接调用 WebRequest#checkNotModified(long)
的方式实现
2.1. 简单演示
@RestController
@RequestMapping("say")
public class SayController{private long lastModified;@RequestMapping("hello")public String hello(WebRequest webRequest) {if(webRequest.checkNotModified(lastModified)){return null;}lastModified = System.currentTimeMillis();return "hello";}
}
2.2. 原理分析
我们来看一看为什么 直接实现 org.springframework.web.servlet.mvc.LastModified
不可以。还是之前 DispatcherServlet#doDispatch
的地方。
可以看到唯一不同的地方时 HandlerAdapter
不同,这里的HandlerAdapter
类型是 RequestMappingHandlerAdapter
类型, 而 RequestMappingHandlerAdapter#getLastModified
方法会调用 RequestMappingHandlerAdapter#getLastModifiedInternal
方法,如下。
@Overrideprotected long getLastModifiedInternal(HttpServletRequest request, HandlerMethod handlerMethod) {return -1;}
可以看到直接返回的-1,也就是我们这种方式根本没办法修改 lastModified。
所以我们通过上面 hello 的写法,在调用的时候通过 WebRequest#checkNotModified(long)
方法直接进行判断。WebRequest#checkNotModified(long)
方法的逻辑这里不再赘述。
四、Http其他缓存方法
推荐阅读:
Last-Modify、ETag、Expires和Cache-Control
以上:内容部分参考
Last-Modify、ETag、Expires和Cache-Control
腾讯Http教程
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正
Spring 源码分析衍生篇十 :Last-Modified 缓存机制相关推荐
- Spring 源码分析衍生篇十三 :事务扩展机制 TransactionSynchronization
文章目录 一.前言 二.TransactionSynchronization 1. TransactionSynchronization 1.1 TransactionSynchronization ...
- Spring 源码分析衍生篇三 : lookup-method 和 replaced-method
文章目录 一.前言 二.基本使用 1. 作用 三.原理实现 1. 预处理 1.1 AbstractBeanDefinition#prepareMethodOverrides 1.2 Autowired ...
- Spring源码分析前篇
穷举法:把生活所见所闻全部归纳到我们所学的知识体系中来,加以思考总结变成自己的东西.(举例子) 类比法:用自己熟悉的方法(利用自己已有的知识体系),去对比学习新的知识. 学习最好的方法:就是重复 Sp ...
- Spring源码分析系列-循环依赖和三级缓存
目录 循环依赖 多级缓存 一级缓存 二级缓存 当循环依赖遇上AOP 三级缓存 Spring三级缓存源码实现 总结 循环依赖 BeanFactory作为bean工厂管理我们的单例bean,那么肯定需 ...
- spring源码分析之spring-core总结篇
1.spring-core概览 spring-core是spring框架的基石,它为spring框架提供了基础的支持. spring-core从源码上看,分为6个package,分别是asm,cgli ...
- spring源码分析之BeanDefinition相关
目录 前言: BeanDefinition的家族系列 1.BeanDefintion的UML类图 2.BeanDefintion家族类详解 2.1.通用接口 2.2.BeanDefintion接口 2 ...
- Spring源码分析系列——bean创建过程分析(三)——工厂方法创建bean
前言 spring创建bean的方式 测试代码准备 createBeanInstance()方法分析 instantiateUsingFactoryMethod()方法分析 总结 spring创建be ...
- Spring源码分析(三)
Spring源码分析 第三章 手写Ioc和Aop 文章目录 Spring源码分析 前言 一.模拟业务场景 (一) 功能介绍 (二) 关键功能代码 (三) 问题分析 二.使用ioc和aop重构 (一) ...
- Spring 源码分析 (一)——迈向 Spring 之路
一切都是从 Bean 开始的 在 1996 年,Java 还只是一个新兴的.初出茅庐的编程语言.人们之所以关注她仅仅是因为,可以使用 Java 的 Applet 来开发 Web 应用.但这些开发者很快 ...
最新文章
- python系统问题
- 产品经理如何做好数据埋点
- python不想学了-嫌Python太慢但又不想学C/C++?来了解下JIT技术
- Kafka创建查看topic,生产消费指定topic消息
- oracle recover redo,oracle redo log日志(当前或非当前日志)损坏之后的db恢复
- 个人技术博客--团队Git规范(参考西瓜学长)
- 用 VR 检查代码,码农们的必备神器!
- Java语言的基础知识6
- 《Android Property
- 现代信号处理 张贤达_清华信号处理著名学者张贤达去世,享年74岁
- Excel中,条件格式的跟多应用-「数字条」「图标集」
- 【Python】闭包Closure
- 怎么在Guitar Pro乐谱中加入哇音
- AIDA64 Business Edition 4.00.2700绿色单文件破解版下载
- 程序员做饭指南,GitHub教程来了
- 飞思卡尔单片机学习记录(一)
- 改造家里的开关成为智能开关,保留原有开关控制,零火版,基础入门(一)
- linux脚本 输出双引号,Linux Shell中三种引号的用法及区别
- 通过对TCPWindowSize的调整对网络流量的性能优化
- Hive——多行转一行及一行转多行