文章目录

一、前言二、Last-Modify三、实现方案1. 实现 org.springframework.web.servlet.mvc.LastModified接口1.1. 简单演示1.2. 原理分析1.2.1 HandlerAdapter#getLastModified1.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;

// 逻辑处理

@Override

public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {

return new ModelAndView("/hello.html");

}

@Override

public 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 的具体实现如下。

@Override

public long getLastModified(HttpServletRequest request, Object handler) {

// 如果handler 是 LastModified 的 实现类。则直接调用 handler 的 getLastModified 方法

if (handler instanceof LastModified) {

return ((LastModified) handler).getLastModified(request);

}

// 否则返回-1

return -1L;

}

这里就很明确了。在这里会调用 BeanNameSayController#getLastModified 来获取最后修改时间。我们这里实现获取的是第一次请求时候保留的时间戳。

1.2.2 ServletWebRequest#checkNotModified(long)

经过上面的分析,我们可以得知 ha.getLastModified(request, mappedHandler.getHandler()); 调用返回的值是我们返回的第一次请求时候保留的时间戳。

我们来看看ServletWebRequest#checkNotModified(long) 怎么进行的校验

@Override

public boolean checkNotModified(long lastModifiedTimestamp) {

return checkNotModified(null, lastModifiedTimestamp);

}

@Override

public 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,并返回 notModified

if (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 方法,如下。

@Override

protected 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教程 如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正