Http缓存Last-Modified、ETag和Expires的Java终结解决之道

Last-Modified和Expires针对浏览器,而ETag则与客户端无关,所以可适合REST架构中。两者都应用在浏览器端的区别是:Expires日期到达前,浏览器不会再发出新的请求,除非用户按浏览器的刷新,所以,Last-Modified和Expires基本是降低浏览器向服务器发出请求的次数,而ETag更侧重客户端和服务器之间联系。

先谈Last-Modified和Expires,最新的Tomcat 7 将ExpireFilter加入其容器中,这样,Java WEB也可以象Apache的Mod_expire模块一样对Http头部进行统一设置了,不过它只对响应文档类型进行统一设置判断,如text/html或text/image 或/css等等,如果想对个别URL输出的jsp进行定制就不行,urlrewrite据说是可以,但是要把URL在其配置文件再配置一下,麻烦,一旦jsp改动影响面大,还有一个问题就是web.xml配置了Tomcat 7容器的ExpireFilter,与容器耦合,移植性差(移植到Resin就不行了)。

所以,我在jivejdon 4.2最新版本中,通过加入下面一段代码在服务器端对来自客户端的Last-Modified以及当前时间进行判断,如未过期,response.setStatus设为304,可以终止后面的各种Jsp界面计算,直接返回浏览器一个304的响应包,JSP页面也不会输出到客户端,将带宽节省给更加需要互动实时性的请求。

再谈谈ETag,ETag定义:RFC2616(也就是HTTP/1.1)中没有说明ETag该是什么格式的,只要确保用双引号括起来就行了,所以你可以用文件的hash,甚至是直接用Last-Modified,以下是服务器端返回的格式:
ETag: "50b1c1d4f775c61:df3" 客户端向服务端发出的请求:If-None-Match: W/"50b1c1d4f775c61:df3" 这样,在J2EE/JavaEE服务器端,我们判断如果ETag没改变也是返回状态304,起到类似Last-Modified和Expires效果。

与Last-Modified和Expires区别是:如果过了Expires日期,服务器肯定会再次发出JSP完整响应;或者用户强按浏览器的刷新按钮,服务器也必须响应,apache等静态页面输出也是这样,但是这时动态页面就发挥了作用,如果JSP涉及的业务领域模型还是没有更新,和原来一样,那么就不必再将动态页面输出了(浏览器客户端已有一份),从Etag中获取上次设置的领域模型对象修改日期,和现在内存中领域模型(In-memory Model)修改日期进行比较,如果修改日期一致,表示领域模型没有被更新过,那么返回响应包304,浏览器将继续用本地缓存的该页面,再次节省了带宽传输。

通过上述Expire和Etag两次缓存,可以大大降低服务器的响应负载,如果你的应用不是状态集中并发修改和实时输出,而是分散修改然后分发,如个人空间 个人博客(每个人只是修改它们自己的状态,不影响全局)或QQ类似个人工具,那么采取这样的方法效果非常明显,实际就是一种动态页面静态化技术,但比通常事先进行页面静态化要灵活强大。

InfoQ的那篇:http://www.infoq.com/articles/etags还用MD5计算放入其中,Md5计算稍微复杂点,负载大了点,有的人结合Hibernate或数据库触发器来判断数据库数据是否更新,以决定Etag的更新,这将表现层和持久层耦合在一起,由于JiveJdon采取的是MDD/DDD模型驱动架构,表现层的Etag更新是根据中间业务层的模型对象修改日期来决定,不涉及数据库层,而且起到服务器缓存的更新和http的Etag更新一致的效果,在松耦合设计和性能上取得综合平衡。

代码如下:


public static boolean checkHeaderCache(long adddays, long modelLastModifiedDate, HttpServletRequest request, HttpServletResponse response) {
// com.jdon.jivejdon.presentation.filter.ExpiresFilter
request.setAttribute(
"myExpire", adddays);

// convert seconds to ms.
long adddaysM = adddays * 1000;
long header = request.getDateHeader(
"If-Modified-Since");
long now = System.currentTimeMillis();
if (header > 0 && adddaysM > 0) {
if (modelLastModifiedDate > header) {
// adddays = 0; // reset
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
if (header + adddaysM > now) {
// during the period happend modified
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return false;
}
}

// if over expire data, see the Etags;
// ETags if ETags no any modified
String previousToken = request.getHeader(
"If-None-Match");
if (previousToken != null && previousToken.equals(Long.toString(modelLastModifiedDate))) {
// not modified
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return false;
}
// if th model has modified , setup the new modified date
response.setHeader(
"ETag", Long.toString(modelLastModifiedDate));
setRespHeaderCache(adddays, request, response);
return true;

}

public static boolean setRespHeaderCache(long adddays, HttpServletRequest request, HttpServletResponse response) {
request.setAttribute(
"myExpire", adddays);

long adddaysM = adddays * 1000;
String maxAgeDirective =
"max-age=" + adddays;
response.setHeader(
"Cache-Control", maxAgeDirective);
response.setStatus(HttpServletResponse.SC_OK);
response.addDateHeader(
"Last-Modified", System.currentTimeMillis());
response.addDateHeader(
"Expires", System.currentTimeMillis() + adddaysM);
return true;
}


[该贴被banq于2011-03-27 12:23修改过]
[该贴被admin于2011-03-28 08:14修改过]
[该贴被admin于2011-03-28 08:35修改过]

通过客户端http缓存 服务器端缓存这些技术综合在一起,可以大大增强每台服务器抗高负载能力,把带宽留给更需要实时更新的模型或页面。

这种方式比静态化页面的好处是:精确性,静态页面.html比如Apache中mod_expire设置,如设置expire为十分钟,如果过了十分钟,其实页面内容还是没有变,也要加载静态页面到客户端,而结合Etag的动态页面,不但起到静态页面一样的效果(其实经过Apache处理的静态页面html也是动态页面),而且可以做到精确制导,就象多国部队轰炸利比亚一样,精确打击。这是灵活性的魅力。

所以,不要再迷信页面静态化了

相关帖子:
http://www.jdon.com/jivejdon/thread/26287#16644889
[该贴被banq于2011-03-27 13:44修改过]

banq
请问一下,你所说的在浏览器中直接访问是没问题的,可是如果是用ajax访问呢。
ajax要不就直接读缓存,和服务器根本没交互,要不就在url后面加个时间戳每次都是第一次访问。
请问ajax里面怎么解决你所说的问题呢

2011年08月26日 21:21 "@zhagy"的内容
ajax要不就直接读缓存,和服务器根本没交互 ...

方式AJAX调用的URL都不要缓存啊,也就是在服务器端不用http缓存,比如可以从URL扩展名上区分:.shtml/.do是动态不需要缓存,而.html或无后缀名的都是有缓存的。

具体可参考jivejdon实现方法。jivejdon中间隔检查私信checkmsg.shtml是不设定缓存,与一般动态页面一致。

举个例子:
比如一个页面需要用ajax访问一个服务器上的xml文件,这个xml文件有可能会发生变化。
当客户端第一次访问完成后,这个xml会被浏览器缓存到客户端。但当第二次访问时,如果ajax设置读取缓存,ajax就直接读客户端缓存,这样如果xml发生变化就发现不了。
如果ajax不设置读取缓存,那就会在每个url访问后面加上时间戳,或者设置xmlHTTP.setRequestHeader("If-Modified-Since","0"),不过这样还能得到正确的结果吗。
在jquery里面有

ifModified
Boolean(默认: false) 仅在服务器数据改变时获取新数据。使用 HTTP 包 Last-Modified 头信息判断。在jQuery 1.4中,他也会检查服务器指定的'etag'来确定数据没有被修改过。

不过好像并不起作用

2011年08月27日 11:10 "@zhagy"的内容
访问一个服务器上的xml文件,这个xml文件有可能会发生变化。
当客户端第一次访问完成后,这个xml会被浏览器缓存到客户端 ...

你不要让一个XML文件直接被访问,因为XML属于静态文件,类似Html CSS,如果XML文件经常变化,那么用动态输出,比如jsp或servlet,输出的头部规定为XML格式,而不是普通的text/html格式即可。

谢谢bangq牛人的文章,但是我这里有几个问题:
1,“。。。可以终止后面的各种Jsp界面计算,直接返回浏览器一个304的响应包,JSP页面也不会输出到客户端。。。”这段话,是指JSP页面中有动态的分页内容(比如客户端点下一页情况下),也可以用你这个方法使用缓存吗?

2,如果可以,那“modelLastModifiedDate”参数是怎么确定的?如果根据JSP文件的更新日期的话,客户端点下一页这种时候怎么排除不要读取缓存?

能否给个确定参数“modelLastModifiedDate”的例子?