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

11-03-27 banq
                   

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

[该贴被banq于2011-03-27 12:23修改过]

[该贴被admin于2011-03-28 08:14修改过]

[该贴被admin于2011-03-28 08:35修改过]

                   

19
banq
2011-03-27 13:41

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

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

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

相关帖子:

http://www.jdon.com/jivejdon/thread/26287#16644889

[该贴被banq于2011-03-27 13:44修改过]

zhagy
2011-08-26 21:21

banq

请问一下,你所说的在浏览器中直接访问是没问题的,可是如果是用ajax访问呢。

ajax要不就直接读缓存,和服务器根本没交互,要不就在url后面加个时间戳每次都是第一次访问。

请问ajax里面怎么解决你所说的问题呢

banq
2011-08-27 08:45

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

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

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

zhagy
2011-08-27 11:10

举个例子:

比如一个页面需要用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'来确定数据没有被修改过。

不过好像并不起作用

2Go 1 2 下一页