|
利用搜索引擎技术进行对象持久化 利用表面上无关的技术来帮助解决一些典型问题 作者:Mikhail Garber 译者:sunjune
版权声明:任何获得Matrix授权的网站,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明 作者:Mikhail Garber;sunjune 原文地址:http://www.javaworld.com/javaworld/jw-01-2005/jw-0103-search.html 中文地址:http://www.matrix.org.cn/resource/article/43/43920_search_Lucene_Persitent.html 关键词: search,Lucene,Persitent
摘要: 在这篇文章中,Mikhail Garber 专注于利用一种新的手法来解决一个旧的问题,那就是如何对基本的JavaBeans进行持久化。他向你展现了一种不需要利用复杂数据库或者XML驱动却一样能达到目的的方法。同时,这种方法也并不只是简单的通过笨拙的属性文件和内存列表来进行工作,而是利用了通用搜索引擎技术将对象作为文档进行索引和检索。这篇文章将告诉你怎样一步一步的建立起这个系统,相信你将会对这种方法的有效性和鲁棒性感到惊诧。(1500字,2005-1-3)
Java开发人员时常会遇到对java类提供持久化机制的需求,而此类相关问题却又经常陷入一种困境,那就是利用属性文件进行持久化的方法功能不足,而基于数据库(和对象关联)的持久化则过度庞大复杂。
典型的解决办法是利用对象链或者XML绑定技术建立一个简单的数据存储。这种方法的吸引力在于,它不以比例来决定是否有能力处理数千个对象,另外特别是在检索方面,要有相当好的表现。 那么,如何建立一个数据存储,不但可以满足JavaBeans量大的需要,而且可以提供高效的检索能力,同时又不依赖于复杂的关系数据库和内存列表呢?
在这篇文章里,我将向你展示一个从另外一个角度来解决这个问题的方法:通过因特网搜索引擎将单个对象当作文档进行索引。我将示范如何使用流行的第三方索引/检索类库对标准的JavaBeans的各个属性进行索引以及如何从建立的数据存储中快速读取这些属性。通用的数据库接口包括查找,取回,存储和删除方法会被给出。 作为此方法的现实例子,我将给出一个仿unix认证系统(用户和组)类作为通常的java对象。
接口 本质上,我们要给出的是一个库——可以让其他程序员希望用于自己工程中。一个好的库开始于一个好的接口。在开始写“真正的”第一行代码之前,我们首先得设计一个好的接口。那么我们希望我们的库能做什么呢?
很显然,我们想要它去存储和取回java对象。我们需要明白的是,我们的对象实际上将被持久化在磁盘上从而在创建它们的程序结束之后我们依然可以取到它们。我们需要区分出各个不同的对象,因此各个对象流经库时必须有各自不同的ID。另外如果想使其方便,那么它也必须可以立刻分辨出一个对象是否与我们库中的对象一致。一个简单的实现方法是将所有的存储对象实现下面的接口:
public interface StorableInterface { public String getObjectId(); public void setObjectId(String id); }
现在,让我们想象一下我们实际的存储服务将会做些什么。我们利用下面的接口来定义它们:
public void store(StorableInterface object) throws StorageException; public StorableInterface retrieve(String objectId) throws StorageException; public StorableInterface[] find(String key, String value) throws StorageException; public StorableInterface[] find(Class clazz) throws StorageException; public StorableInterface[] find(String query) throws StorageException; public void delete(String objectId) throws StorageException; public void delete(String key, String value) throws StorageException;
正如你所看到的,我们提供了以下方法: · 将一个对象存储到磁盘。 · 利用给出的对象ID取得对象。 · 利用给出的属性名和此属性的值得到相匹配的所有对象构成的数组(所以 我们可以初始化查询"firstName" = "Joe")。 · 利用给出的类得到所有此类的对象。 · 得到匹配自由格式查询的所有对象。(我们有点说大话,截至目前,我们只讨论类似于"firstName = "Joe"和"lastName" = "Smith"的查询。如果以后我们扩展了更多复杂功能,那么自由格式查询将会实现。) · Delete an object by objectId. · 利用给出的对象ID删除此对象。 · Delete several objects matching a given property name and value. · 利用给出的属性名和此属性的值删除一系列相匹配的所有对象。
其中抛出的所有异常将会被我们的方法包装(也可能记录)之后再将其作为自定义的存储异常抛出。 这个接口既简单又强大,利用它将使很多应用系统获利。现在我们来看看如何利用搜索引擎技术来真正实现这些服务。
利用Lucene 本质上说,问题集中在大量的任意数据的合理存储和有效重取上。有一种专门解决这个问题的技术已经出现了,那就是搜索引擎。今天的计算机最重要的应用方面可能正是一个用户与google这样的Internet搜索引擎之间的交互。顷刻之间,数据云集在你的指尖。搜索技术的进步无疑已经取得了软件开发者的关注,越来越多的方法已经被用于日常软件的类网络搜索功能上了。
Jakarta Lucene是java程序今天能得到的一个成熟,成功和驰名的搜索引擎工具箱。根据它的站点上所说,“Lucene 是一个完全用java编写的高性能的全文搜索引擎。”我已经数次在不同的场合应用过Lucene并且一直惊叹其执行的速度和准确。Lucene 有专门的文章(和书)进行介绍,所以我们在这里不讨论其使用的细节。我们只就Lucene对任意文本进行索引之后进行查询和取回的实现进行一下介绍。
如果你想利用Lucene建立索引,你首先需要建立一个Write:
writer = new IndexWriter("where_out_index_is_stored", new StandardAnalyzer(), true);
接下来你需要建立一个org.apache.lucene.document.Document的实例,它是你稍后需要进行索引和检索的文档,可能是一个网页,一个文本,甚至可以是任何东西。
Document doc = new Document();
你通过添加一些Fields来索引你的文档,一个Field代表文档的一个用来被索引的属性。当创建Fields的时候需要提供它的名字和值。
Field field = new Field("name", "value", true, true, true); doc.add(field); writer.addDocument(doc); (注意:方法中最后的那些Boolean变量参数代表了Lucene怎样处理我们的field,但是属于我们讨论范围之外) 此时,我们已经准备好了利用Lucene进行我们文档的搜索了,下面的代码段举例说明了这个过程:
IndexSearcher searcher = new IndexSearcher("where_our_index_is_stored"); String queryString = "name:value"; // Will match any Docs where field name is "name" and // value is "value" Analyzer analyzer = new StandardAnalyzer(); Query query = (new QueryParser("some field name", analyzer)).parse(queryString); Hits hits = searcher.search(query); for(int i=0; i< hits.length(); i++){ Document doc = hits.doc(i); ... }
现在,程序员们可以检查和使用返回的文档对象了。Lucene 以高效的组织和专有的格式储存索引文件。如果你想编写一个Lucene查询,那么看看你的硬盘中在where_our_index_is_stored子目录下有真正的Lucene索引文件。 到现在为止,你已经知道了如何方便地利用Lucene进行任意文档的索引和检索。那么接下来就让我们来看看如何来利用Lucene进行简单而又强大的java对象持久化吧!
深入细节 使用我们库的程序员还需要得到一个简单的参考。我们已经定义了我们的服务,现在让我们定义一个factory ,利用它用户可以实现一个我们服务的例子。factory 可以产生一个拥有服务接口的对象,并且我们不需要知道这个接口是怎样实现的。
public class StorageFactory { private StorageFactory() { // Private constructor prevents object creation } public static StorageServiceInterface getStorageService(String key) throws StorageException { try { return new StorageServiceImpl(key); } catch(Exception ex) { throw new StorageException(ex.getMessage()); } } }
字符串key用于指明我们的索引被创建于文件系统的具体位置,从而区分开各个数据库。对象StorageServiceImpl则是我们所实现的StorageServiceInterface的一个实例。现在我们来看一下设计的细节。
我们将使用Lucene文档来代替java对象进行存储。一个文档简单的说其实就是多个属性名/值组合,可以用多个Filed来描述。现在我们需要有几个有效的方法可以将我们想要存储的java对象转化成Lucene文档。
下面是一个将需要存储的对象转化为文档和用Lucene对其建立索引的方法。这个方法利用映射将一个对象中的所有JavaBean属性提取出来。每个Field代表一个键/值对,键作为getter方法的名字,值则是代表对象特征的字符串。
private void addDocument(StorableInterface o) throws Exception{ Document doc = new Document(); doc.add(new Field("id", o.getObjectId(), true, true, true)); Class clazz = o.getClass(); Method[] methods = clazz.getMethods(); doc.add(new Field("Class", o.getClass().getName(), true, true, true)); for(int i=0; i < methods.length; i++) { if(methods[i].getName().startsWith("get")) { Object val = methods[i].invoke(o, null); if(val != null && !methods[i].getName().substring(3).equals("Class")) doc.add(new Field(methods[i].getName().substring(3), val.toString(), true, true, true)); } } writer.addDocument(doc); }
下面我们需要一个可以将Lucene文档转化为对象的方法: private StorableInterface documentToObject(Document doc) throws Exception {
String id = doc.getField("id").stringValue(); Class clazz = Class.forName(doc.getField("Class").stringValue()); Object o = clazz.newInstance(); Method[] methods = clazz.getMethods(); for(int i=0; i < methods.length; i++) { if(methods[i].getName().startsWith("set")) { Class argType = methods[i].getParameterTypes()[0]; Field field = doc.getField(methods[i].getName().substring(3)); String argValue = null; if(field != null) argValue = field.stringValue(); if(argValue != null) { Object arg = null; if(argType.equals(boolean.class)) { if(argValue.equals("true")) arg = new Boolean(true); else arg = new Boolean(false); } else { arg = argType.getConstructor(new Class[] { String.class }) .newInstance(new String[] {argValue} ); } methods[i].invoke(o, new Object[] {arg} ); } } } StorableInterface result = (StorableInterface)o; result.setObjectId(id); return result; }
下面这个是可以检索Lucene索引文件并且将检索结果转化为java对象返回的方法: private StorableInterface[] search(String queryString) throws Exception { searcher = new IndexSearcher(idx); Analyzer analyzer = new StandardAnalyzer(); Query query = (new QueryParser("content", analyzer)).parse(queryString); Hits hits = searcher.search(query); StorableInterface[] results = new StorableInterface[hits.length()]; for(int i=0; i < hits.length(); i++) { Document doc = hits.doc(i); results[i] = documentToObject(doc); } return results; }
有了以上这些代码,现在我们可以来完成我们的StorageServiceImpl 类了。在下面的代码中,为了简略我将省略掉意外处理。我们的retrieve()方法如下: public StorableInterface retrieve(String objectId) throws StorageException { StorableInterface[] results = this.search("id:" + objectId); if(results.length == 0) return null; return results[0]; } store()方法实现对象持久化:
public void store(StorableInterface object) throws StorageException { delete(object.getObjectId()); addDocument(object); }
我们的find()方法相当简洁:
public StorableInterface[] find(String query) throws StorageException { return search(query); }
public StorableInterface[] find(String key, String value) throws StorageException { return find(key + ":" + value); }
public StorableInterface[] find(Class clazz) throws StorageException { return find("Class:" + clazz.getName()); } 下面是通过objectId进行删除操作的delete()方法。
public void delete(String objectId) throws StorageException { reader.delete(new Term("id", objectId)); closeReader(); } 或者通过执行一个查询来删除一系列对象的操作: public void delete(String key, String value) throws StorageException { reader.delete(new Term(key, value)); closeReader(); } 以上包括了我们的 StorageServiceImpl 类中所有的方法。你可以在我们网站的资源页上下载到所有的代码,里面还包含了一个本程序的JUint测试。
实际应用 正如你所看到的,将Lucene搜索引擎应用到对象持久化中是很容易的。虽然这里的例子比较简单和原始,但是通过它你能欣喜的发现一条新路。我已经在我的数个项目中应用了这个方法,非常成功。 你可以很容易将基本的库进行扩展来适应你的特殊需要。例如,考虑一下如果我们想在你的对象持久化中加入认证系统该做些什么。假设你的对象类似于unix文件:他们“知道”他们属于哪些用户和组,他们也“知道”这些用户和组以及其他的用户和组是否有权利对他们读或者写。你可以方便的设计一个BaseProtectedObject 用来存储用户和组的权限,以及在JavaBean中设计一个布尔变量来表示许可与否。
现在StorageServiceInterface接口必须进行修改以使各个方法能适应用户和组的信息:
public interface ControlledStorageServiceInterface { public void store(String actorUserId, String actorGroupId, BaseProtectedObject object) throws AccessControlException, StorageException; public BaseProtectedObject retrieve(String actorUserId, String actorGroupId, String objectId) throws AccessControlException, StorageException; public List find(String actorUserId, String actorGroupId, String query) throws StorageException; public List find(String actorUserId, String actorGroupId, String key, String value) throws StorageException; public List find(String actorUserId, String actorGroupId, Class clazz) throws StorageException; public void delete(String actorUserId, String actorGroupId, String objectId) throws AccessControlException, StorageException; public void delete(String actorUserId, String actorGroupId, String key, String value) throws StorageException; }
同时,我们还需要设计一个异常,当它抛出时表明用户没有权限来进行他所进行的操作。 在我的开源项目MAOS(Meta 货物商店)中有按照此持久化机制的完整实现,此项目开源在SourceForge.net上。你可以在那里找到最新的实现版本,快去吧!
结论 在这篇文章中,我们利用搜索引擎技术实现了一个简单的java对象持久化机制。我希望通过这个例子能告诉你,跳出公认技术的笼罩。利用表面并不相关的技术来解决你的问题是可以获得巨大成功的。
作者简介Mikhail Garber是达拉斯的一个有着12年多企业软件开发经验的独立技术顾问。Garber擅长Java/J2EE, 数据库, 通讯, 以及开源解决方案。他的工作已经得到了 Mary Kay 化妆品公司, 波音公司, Verizon 无线电公司,联邦政府, 洛克希德马丁公司等机构的认可。
参考资料 · 下载本文相关代码 ·javaworld.com:javaworld.com ·Matrix-Java开发者社区:http://www.matrix.org.cn/ ·Lucene 搜索引擎库: http://jakarta.apache.org/lucene/docs/index.html ·MAOS 开源项目: http://sourceforge.net/projects/maos/
|