基于Solr的空间搜索 - 阿凡卢 - 博客园
Solr已经提供了3种filedType来进行空间搜索:
1) LatLonType(用于平面坐标,而不是大地坐标)
2) SpatialRecursivePrefixTreeFieldType(缩写为RPT)
3) BBoxField(用于边界索引查询)
本文重点介绍使用SpatialRecursivePrefixTreeFieldType,不仅可以用点,也可以用于多边形的查询。
1、配置Solr
首先看下数据:
Solr的schema.xml配置:
<field name="station_id" type="long" indexed="true" stored="true" required="true" multiValued="false" /> <field name="station_address" type="text_general" indexed="true" stored="true"/> <field name="station_position" type="location_rpt" indexed="true" stored="true"/> <uniqueKey>station_id</uniqueKey>
这里重点是station_position,它的type是location_rpt,它在Solr中的定义如下:
<!-- A specialized field for geospatial search. If indexed, this fieldType must not be multivalued. --> <fieldType name="location" class="solr.LatLonType" subFieldSuffix="_coordinate"/> <!-- An alternative geospatial field type new to Solr 4. It supports multiValued and polygon shapes. For more information about this and other Spatial fields new to Solr 4, see: http://wiki.apache.org/solr/SolrAdaptersForLuceneSpatial4 --> <fieldType name="location_rpt" class="solr.SpatialRecursivePrefixTreeFieldType" geo="true" distErrPct="0.025" maxDistErr="0.000009" units="degrees" /> <!-- Spatial rectangle (bounding box) field. It supports most spatial predicates, and has special relevancy modes: score=overlapRatio|area|area2D (local-param to the query). DocValues is required for relevancy. --> <fieldType name="bbox" class="solr.BBoxField" geo="true" units="degrees" numberType="_bbox_coord" /> <fieldType name="_bbox_coord" class="solr.TrieDoubleField" precisionStep="8" docValues="true" stored="false"/>
对solr.SpatialRecursivePrefixTreeFieldType的配置说明:
SpatialRecursivePrefixTreeFieldType
用于深度遍历前缀树的FieldType,主要用于获得基于Lucene中的RecursivePrefixTreeStrategy。
geo
默认为true,值为true的情况下坐标基于球面坐标系,采用Geohash的方式;值为false的情况下坐标基于2D平面的坐标系,采用Euclidean/Cartesian的方式。
distErrPct
定义非Point图形的精度,范围在0-0.5之间。该值决定了非Point的图形索引或查询时的level(如geohash模式时就是geohash编码的长度)。当为0时取maxLevels,即精度最大,精度越大将花费更多的空间和时间去建索引。
maxDistErr/maxLevels:maxDistErr
定义了索引数据的最高层maxLevels,上述定义为0.000009,根据GeohashUtils.lookupHashLenForWidthHeight(0.000009, 0.000009)算出编码长度为11位,精度在1米左右,直接决定了Point索引的term数。maxLevels优先级高于maxDistErr,即有maxLevels的话maxDistErr失效。详见SpatialPrefixTreeFactory.init()方法。不过一般使用maxDistErr。
units
单位是degrees。
worldBounds
世界坐标值:”minX minY maxX maxY”。 geo=true即geohash模式时,该值默认为”-180 -90 180 90”。geo=false即quad时,该值为Java double类型的正负边界,此时需要指定该值,设置成”-180 -90 180 90”。
2、建立索引
这里使用Solrj来建立索引:
//Index some base station data for test public void IndexBaseStation(){ BaseStationDb baseStationDb = new BaseStationDb(); List<BaseStation> stations = baseStationDb.getAllBaseStations(); Collection<SolrInputDocument> docList = new ArrayList<SolrInputDocument>(); for (BaseStation baseStation : stations) { //添加基站数据到Solr索引中 SolrInputDocument doc = new SolrInputDocument(); doc.addField("station_id", baseStation.getBaseStationId()); doc.addField("station_address", baseStation.getAddress()); String posString = baseStation.getLongitude()+" "+baseStation.getLatitude() ; doc.addField("station_position", posString); docList.add(doc); } try { server.add(docList); server.commit(); } catch (SolrServerException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } System.out.println("Index base station data done!"); }
这里使用“经度 纬度”这样的字符串格式将经纬度索引到station_position字段中。
3、查询
查询语法示例:
q={!geofilt pt=45.15,-93.85 sfield=poi_location_p d=5 score=distance}
q={!bbox pt=45.15,-93.85 sfield=poi_location_p d=5 score=distance}
q=poi_location_p:"Intersects(-74.093 41.042 -69.347 44.558)" //a bounding box (not in WKT)
q=poi_location_p:"Intersects(POLYGON((-10 30, -40 40, -10 -20, 40 20, 0 0, -10 30)))" //a WKT example
涉及到的字段说明:
字段 |
含义 |
q |
查询条件,如 q=poi_id:134567 |
fq |
过滤条件,如 fq=store_name:农业 |
fl |
返回字段,如fl=poi_id,store_name |
pt |
坐标点,如pt=54.729696,-98.525391 |
d |
搜索半径,如 d=10表示10km范围内 |
sfield |
指定坐标索引字段,如sfield=geo |
defType |
指定查询类型可以取 dismax和edismax,edismax支持boost函数相乘作用,dismax是通过累加方式计算最后的score. |
qf |
指定权重字段:qf=store_name^10+poi_location_p^5 |
score |
排序字段根据qf定义的字段defType定义的方式计算得到score排序输出 |
其中有几种常见的Solr支持的几何操作:
WITHIN:在内部
CONTAINS:包含关系
DISJOINT:不相交
Intersects:相交(存在交集)
1)点查询
测试代码:查询距离某个点pt距离为d的集合
SolrQuery params = new SolrQuery(); params.set("q", "*:*"); params.set("fq", "{!geofilt}"); //距离过滤函数 params.set("pt", "118.227985 39.410722"); //当前经纬度 params.set("sfield", "station_position"); //经纬度的字段 params.set("d", "50"); //就近 d km的所有数据 //params.set("score", "kilometers"); params.set("sort", "geodist() asc"); //根据距离排序:由近到远 params.set("start", "0"); //记录开始位置 params.set("rows", "100"); //查询的行数 params.set("fl", "*,_dist_:geodist(),score");//查询的结果中添加距离和score
solr搜索打分规制排序 - 白强 - ITeye技术网站
Solr内改变打分规则有几种形式:
1.配置solr的solrconfig.xml中edismax,来改变Boost打分规则
2.在solr的schema中增加一个字段,该字段专门用于排序
3.自写一个solr的评分规则。
一般简单的应用1和2就能满足。
举一个例子,电商类网站(比如淘宝)的商品搜索:
1.在商品名称上出现搜索关键字排序靠前,而内容的次之
2.对多皇冠的买家排序靠前等
3.对近期发布的商品排序靠前
4.对最近销售多商品靠前
综上获得一个综合排名
在solrconfig.xml的SearchHandler中如下配置
- <requestHandler name="standard" class="solr.StandardRequestHandler" default="true" >
- <lst name="defaults">
- <str name="echoParams">explicit</str>
- <str name="rows">10</str>
- <str name="hl">on</str>
- <str name="hl.fl">name,content</str>
- <str name="f.content.hl.fragsize">200</str>
- <str name="defType">edismax</str>
- <str name="bf">
- sum(recip(ms(NOW,pub_date),1,1,100),div(point,5632000),div(sale_count,1000000))
- </str>
- <str name="pf">
- content
- </str>
- <str name="qf">
- name^1.9
- </str>
- </lst>
- </requestHandler>
bf用函数计算某个字段的权重,如上例子中pub_date发布日期的权重,point比如诚信指数,sale_count销售数量
bf内字段必须是索引的,bf的函数查看solr api文档 http://wiki.apache.org/solr/FunctionQuery
pf查询字段,这样在schema不用制定默认字段
qf对默认查询增加权重比值,比如标题是content的1.9倍,值越大权重越大
这样查询就会计算如下的一个综合评分值了
Solr自定义排序:http://www.cnblogs.com/HD/p/4023960.html
加入一个查询Handler到solrconfig.xml的<config/>当中:
<requestHandler name="/browse" class="solr.SearchHandler" default="true" > <lst name="defaults"> <str name="defType">edismax</str> <str name="bf"> sum(linear(vip,1000,0),linear(sqrt(log(linear(point,1,2))),100,0),sqrt(log(ms(createTime)))) </str> <!--<str name="pf"> searchText </str> <str name="qf"> subject^1 content^0.8 </str>--> </lst> </requestHandler>
说明:
a)上面的default="true"意思为设置为默认的查询handler(记得把原standard中的default="true"删除掉)
b)见已经被注释的这段:
<!--<str name="pf"> searchText </str> <str name="qf"> subject^1 content^0.8 </str>-->
这是简单的不使用bf的排序加权方式,可以用于应付简单的排序,具体pf/qf的使用,可以上网上搜搜应用。这里演示的功能相对“复杂”,不适用它。
c)见这句公式:
sum(linear(vip,1000,0),linear(sqrt(log(linear(point,1,2))),100,0),sqrt(log(ms(createTime))))
公式中的函数定义和意思,可以参考:
官方文档:
http://wiki.apache.org/solr/FunctionQuery
中文说明:
http://mxsfengg.iteye.com/blog/352191
这里的函数意思是:
- 如果是vip信息=值+1000,非vip信息=值+0
- 点击率(point)的值范围为:50~500之间
- 发布时间(createTime)值范围为:50以内
以上三个值相加得出最统权重分从高到低排序
排序自定义函数参考:
http://mxsfengg.iteye.com/blog/352191
浏览器缓存机制 - 吴秦 - 博客园
浏览器缓存机制,其实主要就是HTTP协议定义的缓存机制(如: Expires; Cache-control等)。但是也有非HTTP协议定义的缓存机制,如使用HTML Meta 标签,Web开发者可以在HTML页面的<head>节点中加入<meta>标签,代码如下:
html code |
<META HTTP-EQUIV="Pragma" CONTENT="no-cache"> |
上述代码的作用是告诉浏览器当前页面不被缓存,每次访问都需要去服务器拉取。使用上很简单,但只有部分浏览器可以支持,而且所有缓存代理服务器都不支持,因为代理不解析HTML内容本身。
下面我主要介绍HTTP协议定义的缓存机制。
Expires策略
Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。
下面是宝宝PK项目中,浏览器拉取jquery.js web服务器的响应头:
注:Date头域表示消息发送的时间,时间的描述格式由rfc822定义。例如,Date: Mon,31 Dec 2001 04:25:57GMT。
Web服务器告诉浏览器在2012-11-28 03:30:01这个时间点之前,可以使用缓存文件。发送请求的时间是2012-11-28 03:25:01,即缓存5分钟。
不过Expires 是HTTP 1.0的东西,现在默认浏览器均默认使用HTTP 1.1,所以它的作用基本忽略。
Cache-control策略(重点关注)
Cache-Control与Expires的作用一致,都是指明当前资源的有效期,控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。只不过Cache-Control的选择更多,设置更细致,如果同时设置的话,其优先级高于Expires。
http协议头Cache-Control : |
值可以是public、private、no-cache、no- store、no-transform、must-revalidate、proxy-revalidate、max-age 各个消息中的指令含义如下:
|
还是上面那个请求,web服务器返回的Cache-Control头的值为max-age=300,即5分钟(和上面的Expires时间一致,这个不是必须的)。
Last-Modified/If-Modified-Since
Last-Modified/If-Modified-Since要配合Cache-Control使用。
l Last-Modified:标示这个响应资源的最后修改时间。web服务器在响应请求时,告诉浏览器资源的最后修改时间。
l If-Modified-Since:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Last-Modified声明,则再次向web服务器请求时带上头 If-Modified-Since,表示请求时间。web服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源又被改动过,则响应整片资源内容(写在响应消息包体内),HTTP 200;若最后修改时间较旧,说明资源无新修改,则响应HTTP 304 (无需包体,节省浏览),告知浏览器继续使用所保存的cache。
Etag/If-None-Match
Etag/If-None-Match也要配合Cache-Control使用。
l Etag:web服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器觉得)。Apache中,ETag的值,默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。
l If-None-Match:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Etage声明,则再次向web服务器请求时带上头If-None-Match (Etag的值)。web服务器收到请求后发现有头If-None-Match 则与被请求资源的相应校验串进行比对,决定返回200或304。
既生Last-Modified何生Etag?
你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag(实体标识)呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:
l Last-Modified标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间
l 如果某些文件会被定期生成,当有时内容并没有任何变化,但Last-Modified却改变了,导致文件没法使用缓存
l 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形
Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。
用户行为与缓存
浏览器缓存行为还有用户的行为有关!!!
用户操作 |
Expires/Cache-Control |
Last-Modified/Etag |
地址栏回车 |
有效 |
有效 |
页面链接跳转 |
有效 |
有效 |
新开窗口 |
有效 |
有效 |
前进、后退 |
有效 |
有效 |
F5刷新 |
无效 |
有效 |
Ctrl+F5刷新 |
无效 |
无效 |
总结
浏览器第一次请求:
浏览器再次请求时:
以下来自:http://www.infoq.com/cn/articles/etags
ETag如何帮助提升性能?
聪明的服务器开发者会把ETags和GET请求的“If-None-Match”头一起使用,这样可利用客户端(例如浏览器)的缓存。因为服务器首先产生ETag,服务器可在稍后使用它来判断页面是否已经被修改。本质上,客户端通过将该记号传回服务器要求服务器验证其(客户端)缓存。
其过程如下:
- 客户端请求一个页面(A)。
- 服务器返回页面A,并在给A加上一个ETag。
- 客户端展现该页面,并将页面连同ETag一起缓存。
- 客户再次请求页面A,并将上次请求时服务器返回的ETag一起传递给服务器。
- 服务器检查该ETag,并判断出该页面自上次客户端请求之后还未被修改,直接返回响应304(未修改——Not Modified)和一个空的响应体。
本文的其余部分将展示在基于Spring框架的Web应用中利用ETag的两种方法,该应用使用Spring MVC。首先我们将使用Servlet 2.3 Filter,利用展现视图(rendered view)的MD5校验和(checksum)以实现生成ETag的方法(一个“浅显的”ETag实现)。 第二种方法使用更为复杂的方法追踪view中所使用的model,以确定ETag有效性(一个“深入的”ETag实现)。尽管我们使用的是Spring MVC,但该技术可以应用于任何MVC风格的Web框架。
在我们继续之前,强调一下这里所展现的是提升动态产生页面性能的技术。已有的优化技术也应作为整体优化和应用性能特性调整分析的一部分来考虑。(见下)。
自顶向下的Web缓存
本文主要涉及对动态生成页面使用HTTP缓存技术。当考虑提升Web应用的性能的时候,应采取一个整体的、自顶向下的方法。为了这一目的,理解HTTP请求经过的各层是很重要的,应用哪些适当的技术取决于你所关注的热点。例如:
- 将Apache作为Servlet容器的前端,来处理如图片和javascript脚本这样的静态文件,而且还可以使用FileETag指令创建ETag响应头。
- 使用针对javascript文件的优化技术,如将多个文件合并到一个文件中以及压缩空格。
- 利用GZip和缓存控制头(Cache-Control headers)。
- 为确定你的Spring框架应用的痛处所在,可以考虑使用JamonPerformanceMonitorInterceptor。
- 确信你充分利用ORM工具的缓存机制,因此对象不需要从数据库中频繁的再生。花时间确定如何让查询缓存为你工作是值得的。
- 确保你最小化数据库中获取的数据量,尤其是大的列表。如果每个页面只请求大列表的一个小子集,那么大列表的数据应由其中某个页面一次获得。
- 使放入到HTTP session中的数据量最小。这样内存得到释放,而且当将应用集群的时候也会有所帮助。
- 使用数据库明细(database profiling)工具来查看在查询的时候使用了什么索引,在更新的时候整个表没有被上锁。
当然,应用性能优化的至理名言是:两次测量,一次剪裁(measure twice, cut once)。哦,等等,这是对木工而言的!没错,但是它在这里也很适用!
ETag Filter内容体
我们要考虑的第一种方法是创建一个Servlet Filter,它将基于页面(MVC中的“View”)的内容产生其ETag 记号。乍一看,使用这种方法所获得的任何性能提升看起来都是违反直觉的。我们仍然不得不产生页面,而且还增加了产生记号的计算时间。然而,这里的想法是减少带宽使用。在大的响应时间情形下,如你的主机和客户端分布在这个星球的两端,这很大程度上是有益的。我曾见过东京办公室使用纽约服务器上托管的应用,其响应时间达到了 350 ms。随着并发用户数的增长,这将变成巨大的瓶颈。
代码
我们用来产生记号的技术是基于从页面内容计算MD5哈希值。这通过在响应之上创建一个包装器来实现。该包装器使用字节数组来保存所产生的内容,在filter链处理完成之后我们利用数组的MD5哈希值计算记号。
doFilter方法的实现如下所示。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
ServletException {
HttpServletRequest servletRequest = (HttpServletRequest) req;
HttpServletResponse servletResponse = (HttpServletResponse) res;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ETagResponseWrapper wrappedResponse = new ETagResponseWrapper(servletResponse, baos);
chain.doFilter(servletRequest, wrappedResponse);
byte[] bytes = baos.toByteArray();
String token = '"' + ETagComputeUtils.getMd5Digest(bytes) + '"';
servletResponse.setHeader("ETag", token); // always store the ETag in the header
String previousToken = servletRequest.getHeader("If-None-Match");
if (previousToken != null && previousToken.equals(token)) { // compare previous token with current one
logger.debug("ETag match: returning 304 Not Modified");
servletResponse.sendError(HttpServletResponse.SC_NOT_MODIFIED);
// use the same date we sent when we created the ETag the first time through
servletResponse.setHeader("Last-Modified", servletRequest.getHeader("If-Modified-Since"));
} else { // first time through - set last modified time to now
Calendar cal = Calendar.getInstance();
cal.set(Calendar.MILLISECOND, 0);
Date lastModified = cal.getTime();
servletResponse.setDateHeader("Last-Modified", lastModified.getTime());
logger.debug("Writing body content");
servletResponse.setContentLength(bytes.length);
ServletOutputStream sos = servletResponse.getOutputStream();
sos.write(bytes);
sos.flush();
sos.close();
}
}
清单 1:ETagContentFilter.doFilter
你需注意到,我们还设置了Last-Modified头。这被认为是为服务器产生内容的正确形式,因为其迎合了不认识ETag头的客户端。
下面的例子使用了一个工具类EtagComputeUtils来产生对象所对应的字节数组,并处理MD5摘要逻辑。我使用了javax.security MessageDigest来计算MD5哈希码。
public static byte[] serialize(Object obj) throws IOException {
byte[] byteArray = null;
ByteArrayOutputStream baos = null;
ObjectOutputStream out = null;
try {
// These objects are closed in the finally.
baos = new ByteArrayOutputStream();
out = new ObjectOutputStream(baos);
out.writeObject(obj);
byteArray = baos.toByteArray();
} finally {
if (out != null) {
out.close();
}
}
return byteArray;
}
public static String getMd5Digest(byte[] bytes) {
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 cryptographic algorithm is not available.", e);
}
byte[] messageDigest = md.digest(bytes);
BigInteger number = new BigInteger(1, messageDigest);
// prepend a zero to get a "proper" MD5 hash value
StringBuffer sb = new StringBuffer('0');
sb.append(number.toString(16));
return sb.toString();
}
清单 2:ETagComputeUtils
直接在web.xml中配置filter。
<filter>
<filter-name>ETag Content Filter</filter-name>
<filter-class>org.springframework.samples.petclinic.web.ETagContentFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ETag Content Filter</filter-name>
<url-pattern>/*.htm</url-pattern>
</filter-mapping>
清单 3:web.xml中配置filter。
每个.htm文件将被EtagContentFilter过滤,如果页面自上次客户端请求后没有改变,它将返回一个空内容体的HTTP响应。
我们在这里展示的方法对特定类型的页面是有用的。但是,该方法有两个缺点:
- 我们是在页面已经被展现在服务器之后计算ETag的,但是在返回客户端之前。如果有Etag匹配,实际上并不需要再为model装进数据,因为要展现的页面不需要发送回客户端。
- 对于类似于在页脚显示日期时间这样的页面,即使内容实际上并没有改变,每个页面也将是不同的。
下一节,我们将着眼于另一种方法,其通过理解更多关于构造页面的底层数据来克服这些问题的某些限制。
ETag拦截器(Interceptor)
Spring MVC HTTP 请求处理途径中包括了在一个controller前插接拦截器(Interceptor)的能力,因而有机会处理请求。这儿是应用我们ETag比较逻辑的理想场所,因此如果我们发现构建一个页面的数据没有发生变化,我们可以避免进一步处理。
这儿的诀窍是你怎么知道构成页面的数据已经改变了?为了达到本文的目的,我创建了一个简单的ModifiedObjectTracker,它通过Hibernate事件侦听器清楚地知道插入、更新和删除操作。该追踪器为应用程序的每个view维护一个唯一的号码,以及一个关于哪些Hibernate实体影响每个view的映射。每当一个POJO被改变了,使用了该实体的view的计数器就加1。我们使用该计数值作为ETag,这样当客户端将ETag送回时我们就知道页面背后的一个或多个对象是否被修改了。
代码
我们就从ModifiedObjectTracker开始吧:
public interface ModifiedObjectTracker {
void notifyModified(> String entity);
}
够简单吧?这个实现还有一点更有趣的。任何时候一个实体改变了,我们就更新每个受其影响的view的计数器:
public void notifyModified(String entity) {
// entityViewMap is a map of entity -> list of view names
List views = getEntityViewMap().get(entity);
if (views == null) {
return; // no views are configured for this entity
}
synchronized (counts) {
for (String view : views) {
Integer count = counts.get(view);
counts.put(view, ++count);
}
}
}
一个“改变”就是插入、更新或者删除。这里给出的是侦听删除操作的处理器(配置为Hibernate 3 LocalSessionFactoryBean上的事件侦听器):
public class DeleteHandler extends DefaultDeleteEventListener {
private ModifiedObjectTracker tracker;
public void onDelete(DeleteEvent event) throws HibernateException {
getModifiedObjectTracker().notifyModified(event.getEntityName());
}
public ModifiedObjectTracker getModifiedObjectTracker() {
return tracker;
}
public void setModifiedObjectTracker(ModifiedObjectTracker tracker) {
this.tracker = tracker;
}
}
ModifiedObjectTracker通过Spring配置被注入到DeleteHandler中。还有一个SaveOrUpdateHandler来处理新建或更新POJO。
如果客户端发送回当前有效的ETag(意味着自上次请求之后我们的内容没有改变),我们将阻止更多的处理,以实现我们的性能提升。在Spring MVC里,我们可以使用HandlerInterceptorAdaptor并覆盖preHandle方法:
public final boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws
ServletException, IOException {
String method = request.getMethod();
if (!"GET".equals(method))
return true;
String previousToken = request.getHeader("If-None-Match");
String token = getTokenFactory().getToken(request);
// compare previous token with current one
if ((token != null) && (previousToken != null && previousToken.equals('"' + token + '"'))) {
response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
// re-use original last modified timestamp
response.setHeader("Last-Modified", request.getHeader("If-Modified-Since"))
return false; // no further processing required
}
// set header for the next time the client calls
if (token != null) {
response.setHeader("ETag", '"' + token + '"');
// first time through - set last modified time to now
Calendar cal = Calendar.getInstance();
cal.set(Calendar.MILLISECOND, 0);
Date lastModified = cal.getTime();
response.setDateHeader("Last-Modified", lastModified.getTime());
}
return true;
}
我们首先确信我们正在处理GET请求(与PUT一起的ETag可以用来检测不一致的更新,但其超出了本文的范围。)。如果该记号与上次我们发送的记号相匹配,我们返回一个“304未修改”响应并“短路”请求处理链的其余部分。否则,我们设置ETag响应头以便为下一次客户端请求做好准备。
你需注意到我们将产生记号逻辑抽出到一个接口中,这样可以插接不同的实现。该接口有一个方法:
public interface ETagTokenFactory {
String getToken(HttpServletRequest request);
}
为了把代码清单减至最小,SampleTokenFactory的简单实现还担当了ETagTokenFactory的角色。本例中,我们通过简单返回请求URI的更改计数值来产生记号:
public String getToken(HttpServletRequest request) {
String view = request.getRequestURI();
Integer count = counts.get(view);
if (count == null) {
return null;
}
return count.toString();
}
大功告成!
会话
这里,如果什么也没改变,我们的拦截器将阻止任何搜集数据或展现view的开销。现在,让我们看看HTTP头(借助于LiveHTTPHeaders),看看到底发生了什么。下载文件中包含了配置该拦截器的说明,因此owner.htm“能够使用ETag”:
我们发起的第一个请求说明该用户已经看过了这个页面:
----------------------------------------------------------
http://localhost:8080/petclinic/owner.htm?ownerId=10
GET /petclinic/owner.htm?ownerId=10 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8
X-lori-time-1: 1182364348062
If-Modified-Since: Wed, 20 Jun 2007 18:29:03 GMT
If-None-Match: "-1"
HTTP/1.x 304 Not Modified
Server: Apache-Coyote/1.1
Date: Wed, 20 Jun 2007 18:32:30 GMT
我们现在应该做点修改,看看ETag是否改变了。我们给这个物主增加一个宠物:
----------------------------------------------------------
http://localhost:8080/petclinic/addPet.htm?ownerId=10
GET /petclinic/addPet.htm?ownerId=10 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://localhost:8080/petclinic/owner.htm?ownerId=10
Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8
X-lori-time-1: 1182364356265
HTTP/1.x 200 OK
Server: Apache-Coyote/1.1
Pragma: No-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Cache-Control: no-cache, no-store
Content-Type: text/html;charset=ISO-8859-1
Content-Language: en-US
Content-Length: 2174
Date: Wed, 20 Jun 2007 18:32:57 GMT
----------------------------------------------------------
http://localhost:8080/petclinic/addPet.htm?ownerId=10
POST /petclinic/addPet.htm?ownerId=10 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://localhost:8080/petclinic/addPet.htm?ownerId=10
Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8
X-lori-time-1: 1182364402968
Content-Type: application/x-www-form-urlencoded
Content-Length: 40
name=Noddy&birthDate=1000-11-11&typeId=5
HTTP/1.x 302 Moved Temporarily
Server: Apache-Coyote/1.1
Pragma: No-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Cache-Control: no-cache, no-store
Location: http://localhost:8080/petclinic/owner.htm?ownerId=10
Content-Language: en-US
Content-Length: 0
Date: Wed, 20 Jun 2007 18:33:23 GMT
因为对addPet.htm我们没有配置任何已知ETag,也没有设置头信息。现在,我们再一次查看id为10的物主。注意ETag这时是1:
----------------------------------------------------------
http://localhost:8080/petclinic/owner.htm?ownerId=10
GET /petclinic/owner.htm?ownerId=10 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://localhost:8080/petclinic/addPet.htm?ownerId=10
Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8
X-lori-time-1: 1182364403109
If-Modified-Since: Wed, 20 Jun 2007 18:29:03 GMT
If-None-Match: "-1"
HTTP/1.x 200 OK
Server: Apache-Coyote/1.1
Etag: "1"
Last-Modified: Wed, 20 Jun 2007 18:33:36 GMT
Content-Type: text/html;charset=ISO-8859-1
Content-Language: en-US
Content-Length: 4317
Date: Wed, 20 Jun 2007 18:33:45 GMT
最后,我们再次查看id为10的物主。这次我们的ETag命中了,我们得到一个“304未修改”响应:
----------------------------------------------------------
http://localhost:8080/petclinic/owner.htm?ownerId=10
GET /petclinic/owner.htm?ownerId=10 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Cookie: JSESSIONID=13D2E0CB63897F4EDB56639E46D2BBD8
X-lori-time-1: 1182364493500
If-Modified-Since: Wed, 20 Jun 2007 18:33:36 GMT
If-None-Match: "1"
HTTP/1.x 304 Not Modified
Server: Apache-Coyote/1.1
Date: Wed, 20 Jun 2007 18:34:55 GMT
我们已经利用HTTP缓存节约了带宽和计算时间!
细粒度印记(The Fine Print):实践中,我们可以通过以更细粒度的跟踪对象变化来获得更大的功效,例如使用对象id。然而,这种使修改对象关联到view上的想法高度依赖应用程序的整体数据模型设计。这里的实现(ModifiedObjectTracker)是说明性的,有意为更多的探索提供想法。它并不是旨在生产环境中使用(比如它在簇中使用还不稳定)。一个可选的更深的考虑是使用数据库触发器来跟踪变化,让拦截器访问触发器所写入的表。
结论
我们已经看了两种使用ETag减少带宽和计算的方法。我希望本文已为你当下或将来基于Web的项目提供了精神食粮,并正确评价在底层利用ETag响应头的做法。
正如牛顿(Isaac Newton)的名言所说:“如果说我看得更远,那是因为我站在巨人的肩膀上。”REST风格应用的核心是简单、好的软件设计、不要重新发明轮子。我相信随着使用量和知名度的增长,针对基于Web应用的REST风格架构有益于主流应用开发的迁移,我期盼着它在我将来的项目中发挥更大的作用。
关于作者
Gavin Terrill 是BPS公司的首席技术执行官。Gavin已经有20多年的软件开发历史了,擅长企业Java应用程序,但仍拒绝扔掉他的TRS-80。闲暇时间Gavin喜欢航海、钓鱼、玩吉他、品红酒(不分先后顺序)。