⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 https://my.oschina.net/dtkking/blog/89443 「dtubest」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

故事通常是这样开始的: 从前,有一个程序猿,他语重心长地对孙子说:“孩子,要是你以后写servlet,最好不要重写service方法啊” 孙子大为不解,程序猿又说:“听爷爷的,准没错,爷爷的爷爷就是这么说的……”

——为什么不应该重写service方法呢?

如果你也曾思考过这个问题,但暂时无解,这篇文章或许可以给你一点启发。

先来看一个具体的例子:

当时我正在osc看红薯的一篇大作,只见我右手F12熟练的打开了chrome的开发者工具,左手迅猛的按了几下F5,然后看到了这个结果。

聪明的你一定已经发现,除了第一个名为12_77118的请求返回状态为200,其他的都为304,那么200和304有什么区别呢?这个稍后解释。

一切从代码里面来,我们先抛开理论,看一个具体的code:

我编写了一个index.html,如下:

<html>
<body>
<h3>I'm a test page . </h3>
<h3>I'm a test page . </h3>
<h3>I'm a test page . </h3>
<h3>I'm a test page . </h3>
<h3>I'm a test page . </h3>
<h3>I'm a test page . </h3>
<h3>I'm a test page . </h3>
</body>
</html>

我们来访问这个页面看看。

Image(2)

这是我第一次访问这个页面(表示本地并没有对这个文件的缓存):

我们来看看http请求和响应的消息头:

《图:一》

为了作为对比,我们再F5刷新一次:

《图:二》

这次请求的头信息中多了一条If-Modified-Since,而且返回的响应中,状态变为了304,这是怎么回事?还记得红薯那篇文章页中的304么,你会发现,304多出现在对于静态资源的请求上面。

原来对于静态资源来说:

  1. 当浏览器第一次发起请求时(请求头中没有If-Modified-Since),server会在响应中告诉浏览器这个资源最后修改的时间(响应头中的Last-Modified)。(见图一)

  2. 浏览器也很聪明,当你再次(点击链接,或者F5,或者回车,但是不能是ctrl+F5)请求这个资源时,浏览器会询问server这个资源自上次告诉我的最后修改时间以来有没有被修改(请求头中If-Modified-Since)。(见图二)

  3. 如果资源没有被修改,server返回304状态码,并不会再次将资源发送给浏览器,浏览器则很知趣的使用本地的缓存文件。(见图二)

所以所有的静态资源如果没有发生变化,通常是不会传递多次的,不管什么浏览器或者server都应该遵守这种询问的约定。看起来很爽啊,很智能是不是?这种约定的机制就是 http缓存协商——这是约定优于配置的又一体现。

有了缓存协商的知识,理解为什么我们不应该重写service就很容易了。还是从代码出发,这次我们看一个复杂一点的例子:

在这个例子中,我们请求一个控制器(MeServlet),然后转向一个视图(index.html),为了简单起见,web.xml中将只有这个servlet的配置:

<web-app>
<servlet>
<servlet-name>me</servlet-name>
<servlet-class>com.me.web.MeServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>me</servlet-name>
<url-pattern>/test</url-pattern>
</servlet-mapping>
</web-app>

然后是MeServlet:

public class MeServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
/**
* 1. 处理具体的业务:
* -- 处理请求参数
* -- 检查缓存
* -- 处理具体数据
* -- 更新缓存
*/
doBizLogic(req, res);
/**
* 2. 根据处理的结果转向具体的视图:
* -- 这里假设就是 index.html
*/
getServletContext()
.getRequestDispatcher("/index.html").include(req, res);
}
public void doBizLogic(HttpServletRequest request, HttpServletResponse response) {
System.out.println("do biz.");
}
}

可以看到,每次F5刷新返回的状态码都是200,让我们看看具体的请求和响应头:

我们发现无论我们如何刷新页面,每次响应状态都是200,index.html的内容每次都被完整的发送给浏览器,这看起来很笨,为什么不像静态资源一样进行缓存协商呢?原因是缓存协商是基于http请求和响应头中的Modified信息的,如果没有这个信息,是无法进行缓存协商的。而对于动态内容而言,server无法帮我们决定内容是不是有改变,也无法替我们决定动态内容的最后修改时间。

所以它不会帮我们在响应中加上Last-Modified,我们必须自己来做这件事,我们小小地修改一下MeServlet:

public class MeServlet extends HttpServlet {
@Override
protected long getLastModified(HttpServletRequest req) {
/**
* 这里你要自己决定动态内容的最后修改时间,例如你可以返回
* -- 数据缓存最后更新的时间
* -- 简单起见,我们假设最后的修改时间是 1000
*/
return 1000;
}

@Override
protected void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
/**
* 1. 处理具体的业务:
* -- 处理请求参数
* -- 检查缓存
* -- 处理具体数据
* -- 更新缓存
*/
doBizLogic(req, res);
/**
* 2. 根据处理的结果转向具体的视图:
* -- 这里假设就是 index.html
*/
getServletContext()
.getRequestDispatcher("/index.html").include(req, res);
}

public void doBizLogic(HttpServletRequest request, HttpServletResponse response) {
System.out.println("do biz.");
}
}

你会看到getLastModified这个方法是重写的,说明HttpServlet中已经有了这个方法,我们使用这个方法来告诉server在这个动态资源中,最后内容变化的时间是多少。最理想的情况是server会自己回调这个方法,那就太省心啦。

我们先访问的看看:发现依然每次都是200,server没有告诉浏览器最后的修改时间,缓存协商机制无法工作。

先别沮丧,忘了我们要解释什么问题吗——为什么不要重写service方法。也许你已经猜到了,如果你看看service方法的实现,现在你已经明白了,service方法自己实现了缓存协商的机制,如果我们重写它,反而将这中良好的机制给去掉了。

我们再修改一下,这次我们重写doGet,在doGet中完成完全相同的逻辑:

public class MeServlet extends HttpServlet {

@Override
protected long getLastModified(HttpServletRequest req) {
/**
* 这里你要自己决定动态内容的最后修改时间,例如你可以返回
* -- 数据缓存最后更新的时间
* -- 简单起见,我们假设最后的修改时间是 1000
*/
return 1000;
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
/**
* 1. 处理具体的业务:
* -- 处理请求参数
* -- 检查缓存
* -- 处理具体数据
* -- 更新缓存
*/
doBizLogic(req, res);
/**
* 2. 根据处理的结果转向具体的视图:
* -- 这里假设就是 index.html
*/
getServletContext()
.getRequestDispatcher("/index.html").include(req, res);
}
public void doBizLogic(HttpServletRequest request, HttpServletResponse response) {
System.out.println("do biz.");
}
}

这次在访问,

终于出现了久违的Last-Modified,再次回车请求页面,哈哈变成304了。

现在你也许已经清楚了,为什么不应该重写service方法,似乎是为了保留HttpServlet默认实现的缓存协商的机制;其实还有另外一个原因:就是禁用你没有在servlet中重写的方法,例如post、head等,这样就从一定程度上提高了安全性。

理论到此为止,现在让我们来看看缓存协商机制有什么实际的好处:

还是红薯的那边文章,我们现在全加载(ctrl+F5)一次看看,

我们看到总共发起了45个请求,请求的数据量为198.93KB,然后F5刷新一次:

这次只有36个请求,数据量只有23.62KB

我们看到这篇文章被9960个id访问, 而每一个id实际上可能访问这个页面多次(像我这样,实际的数据可能得问问红薯),然后我们看到很多304静态资源都是整站通用的:

如果你是osc的常客,并且不经常更换浏览器,不经常清理缓存,甚至其他人的头像都可以是通用的,为了简单起见,我们这里考虑每个id都只访问这个页面一次,并且假设所有的资源都已经缓存在用户本地,得出:

(198.93-23.62)×9960 = 1746086.6KB = 1705.1637M = 1.665G。

很惊人吧,这只是一个页面,别忘了,我们还假设所有的用户都只访问一次,你想想osc上面有多少篇博文,加起来……

流量是什么,是银子啊。

幸运的是,这些省银子的事情浏览器和server都已经帮我们做好了,那我们就不需要关心这个了吗??我们看到12_77118这个请求所占用的资源也不少,如果文章再长点,再长点的话……还会更大。

如果红薯愿意,也可以让这个请求实现缓存协商,可以进一步减少流量。

当然这里的计算并不是完全的精确,实际的情况复杂很多,但是这个计算的量级应该是对的,是值得参考的。

流量涉及的另一个问题就是带宽,以更小的贷款提供更高的并发是每个站长应该追求的。不过考虑到osc以新闻为主,一次性消费,所以……不过那时题外话了。

好了,如果你有耐心看到这里,我想你也许会对service有了新的理解,为什么我们不应该重写这个方法。

万事有例外,如果你需要实现一个前端控制器的话,就是另外一回事了,这留给大家自己思考。

文章目录