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源码分析:全集整理


本系列目录如下:

  1. Spring源码分析十九:Spring MVC① 搭建
  2. Spring源码分析二十:Spring MVC② DispatcherServlet的初始化
  3. Spring源码分析二十一:Spring MVC③ DispatcherServlet的逻辑

衍生篇目录如下:

  1. Spring 源码分析衍生篇十 :Last-Modified 缓存机制
  2. Spring 源码分析衍生篇十一 :HandlerMapping

二、Last-Modify

以下内容来源于百度百科

  1. 在浏览器第一次请求某一个URL时,服务器端的返回状态会是200,内容是客户端请求的资源,同时有一个Last-Modified的属性标记此文件在服务器端最后被修改的时间。Last-Modified格式类似这样:Last-Modified : Fri , 12 May 2006 18:53:33 GMT

  2. 客户端第二次请求此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。
Spring 源码分析衍生篇十 :Last-Modified 缓存机制_第1张图片

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#getLastModifiedServletWebRequest#checkNotModified(long) 。我们一个一个来看

1.2.1 HandlerAdapter#getLastModified

关于 HandlerAdapter#getLastModified 不同的 HandlerAdapter 有不同的实现方式,由于我们这里使用的是 BeanNameUrlHandlerMapping 处理器映射方式。所以这里匹配的HandlerAdapter 方法是 SimpleControllerHandlerAdapter#getLastModified

Spring 源码分析衍生篇十 :Last-Modified 缓存机制_第2张图片

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 来获取最后修改时间。我们这里实现获取的是第一次请求时候保留的时间戳。

Spring 源码分析衍生篇十 :Last-Modified 缓存机制_第3张图片

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;
	}

这里我们就可以知道是什么情况了

  1. 浏览器第一次请求,一切正常。SimpleControllerHandlerAdapter#getLastModified 保存到当前请求的时间戳,并将该时间戳 通过 Last-Modified 响应头返回给浏览器。
  2. 浏览器第二次请求,会使用 If-Modified-Since 请求头带上上次请求获取到的 Last-Modified。在DispatcherServlet 的处理过程中会调用 HandlerAdapter#getLastModified 来获取第一步保存的时间戳 lastModified,这个时间戳是上次调用时候的时间戳。
  3. 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";
    }
}

Spring 源码分析衍生篇十 :Last-Modified 缓存机制_第4张图片

2.2. 原理分析

我们来看一看为什么 直接实现 org.springframework.web.servlet.mvc.LastModified 不可以。还是之前 DispatcherServlet#doDispatch 的地方。

Spring 源码分析衍生篇十 :Last-Modified 缓存机制_第5张图片
可以看到唯一不同的地方时 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教程
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

你可能感兴趣的:(#,源码分析衍生篇)