一辈子的程序员?

爱你一生不变-芳芳!
posts - 27, comments - 15, trackbacks - 0, articles - 0
  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

2007年8月15日

    何去何从?有点迷茫,是沉寂在温柔之乡还是去迎接狂风暴雨!
看来自己还是没有自信!bs..........myself

posted @ 2007-08-15 14:32 boddi 阅读(578) | 评论 (3)编辑 收藏

2007年3月5日

2007最新骗局:

     建行一同志转送: 今天经过一栋大楼门口,门口有一提款机。有一个老伯,一直 

看著我走过他身边,突然叫住我。他说他不识字,拿一张提款卡要我帮他在大楼门口 

的自动提款机取钱。我回答我无法帮你取,叫警卫帮你。结果,他就回答我说不用了 

,继续找其他路人帮他取钱。朋友们要记住---取款机可是有摄影机耶。万一他说我抢 

劫或是偷他的提款卡,甚至他的卡片是偷来的,帮他领钱会在提款机留下影像,绝对 

会让你百口莫辩!我会警惕 ! 是因为已有同事上当,目前仍官司缠身。显然这是诈骗 

集团在找替身了! 请用力传出去~~~ 骗案真是层出不穷,一不小心就会踏入陷阱,真是 

令人防不胜防!

提醒各位朋友在外多小心! 

(2)一业主,家中突然断电,看到窗户外别人家里都有电,就出门查看自家电 

表箱,打开门就被刀子顶着了——持刀入室抢劫伤人家里突然断电,不要贸然就开门 

查看,有猫眼的多观察一会门外动静,没猫眼的也隔着门静听一段时间,没有异常响 

动再开门。 

(3)各位女同胞们注意了!这是最新骗局 

女同胞请注意 男同胞请叫自己的朋友注意 新出的情况,女性朋友要特别注意啦: 

一位上班的小姐在下班回家的路上看到一个小孩子一直哭,很可怜 ,然后就过去问那 

小朋友怎么了.小朋友就跟那个小姐说: 我迷路了,可以请你带我回家吗?然后拿一张 

纸条给她看, 说那是他家地址.然后她就笨笨的带小孩子去了.一般人都有同情心,然后 

带到那个所谓小孩子的家里以后,她一按铃,门铃像是有高压电,就失去知觉了.隔天醒 

来就被脱光光在一间空屋里,身边什么都没有了,她甚至连犯人长啥样子都没看见.所以 

,现在人犯案都是利用同情心啊,如果遇到类似这种的,千万别带他去,要带就带他到派 

出所去好了,走丢的小孩放到派出所一定没错啦,请通知身边所有女性,为了广大女士 

的安全,看完后麻烦给转发给所有人。 

(4)今天遇到讨饭新招,大家注意提防了~~ 

今天在家休息,有人按门铃,开门一看,是个50来岁的老妇女,手里拿了2包喜糖,我 

还以为是邻居来分喜糖的,结果一开口,听得出不是本地人,她说什么这2包糖给我们 

的,图个喜气,要换一点钱给她,后面还说了一大堆不知道什么,我也没听清楚,感 

觉就是不对,吓的马上关门,晕!这年头,还有这么讨钱的一刚。 

(5)转发:大家注意了!到自动取款机取钱时一定要倍加小心!!!!! 

昨晚在工行自动取款机取钱时,后面来了个老妇女,问我能不能取钱,还说 

什么取款机有个键可能坏了,旁边不知什么时候来了个小女孩,一直想我身边跻,我 

也没在意,小孩子淘气嘛,可是过分的是她竟然把手朝出钞口放,准备拿我的钱了, 

我感觉不对劲了,立即把她推到一边,等着把钱取出来。之后我想了一下,她们俩给 

我设了个套:老妇女负责和我瞎聊,吸引我的注意力,小女孩趁我不注意时抢走我的 

钱!如果我不防备的话,钱说不定就被抢走了,这样的话,我就进套了:(一则我立 

即去追小女孩,去追回我的钱,可是谁又会相信一个小女孩能抢我一个大人的钱呢? 

更可怕的是站在我后面的老妇女将会取光我卡中所有的钱,因为我的卡还在取款机里 

面;二则我不立即去追小女孩,等拿到卡再追,到那时小女孩就无影无踪了,钱也就 

没了啊:(她们真的很聪明,很可耻的!!!) 

这是我的亲身经历,希望大家以后取钱时一定要警惕起来,注意观察周围的所有人, 

并转告周围的家人、同事、朋友,让坏蛋分子没有可乘之机!!!!又出现骗局新招 

!! 

(6)我父母都退休在家。昨天上午,来一陌生中年人,说自己摩托车油开没了,加油 

站太远,摩托车又太重推不动,所以想问我父母要一个可乐瓶去买汽油,刚开口就说 

实在不行就出2、3元买一个空瓶好了。我母亲就拿了个空瓶给他,别说他还真从口袋 

里掏出钱来,不过是几张百元大钞,还让我父母找钱。我母亲顿生警觉,说算了,不 

过是一个空瓶而已。他10元钱买下来,只不过还是那张百元大钞。好在我母 

亲尚未龙钟,也不是那种爱贪小便宜的人 

女性朋友一定要认真看完,注意自我安全啊,现在万恶的社会。。。。朋友发给我一 

篇报道,现转给各位看看 ,出门在外,千万小心,小心千万。。。 

(7)一对新婚夫妇到巴黎度蜜月。在巴黎,妻子在一间时尚服装店试衣服 ,身为丈夫 

就在试衣间外等候。但等候多时却不见妻子走出来 ,紧张的丈夫要求店员帮忙到里头 

查看 ,却意外发现试衣间空无一人。丈夫以为妻子开玩笑作弄人 ,要他紧张.于是回到 

酒店等她回来。几小时后却不见妻子的踪影,才知事态严重。丈夫赶忙报警 ,并到巴黎 

所有服装店和医院询问妻子下落。三星期过去了,妻子犹如从人间蒸发,音讯全无,伤 

心的丈夫只能收拾包袱回到 ** 。由于无法从绝望中振作,丈夫无心工作,甚至独自生 

活 ,决定把自己放逐,流浪到各地方。几年后 ,他心血来潮到巴厘岛,在一破旧的屋子 

参观一畸形秀 ( freak show ) 。他见到一脏生锈的铁笼里,有一女人四肢全无,身躯, 

包括脸部,犹如破布般残破 ,充满疤痕。她在地上扭曲着 ,并发出有如野兽般的呻吟声 

。突然间男人惊恐地发出尖叫声。他从那毫无人样的女人脸上见到,他再熟悉不过,属 

于他新婚不久就告失踪的妻子脸上的红色胎记。 

(8)另一版本则发生在上海。几年前一女通知公安她的表妹在上海市集购物时无故失 

踪,可是遍寻不着 ,直到五年后一友人撞见这表妹在泰国曼谷街道上行乞。恐怖的是她 

不知何故没了双手双脚,身子被铁链绑在灯柱旁。 

(9)这是在某一对夫妇去香港游玩时发生的故事。一对夫妻不知不觉走入了全香港治 

安最坏的地区的一家精品店里 ? 妻子对店里的衣服样式十分喜欢 ,随后就进入试衣间 

试衣。可是,先生在外头等了又等,却不见妻子出来。由于实在是等太久了 ,所以先生 

开门 进去找她,可是试衣间里早已空空如也。他吃惊地向店员询问妻子到哪里去了,可 

是店员们却好象是串通好了一样,都说没有看见,并坚持根本没有象他妻子这样的人来 

过店里。因此他只好请当地的警察协助搜索这家精品店 ,可是却一无所获。后来他又 

一个人找了一段时间 ,直到他的签证到期。最后不得已他就在找不到妻子的情况下回 

国了。之后经过了一年 … 他向公司请了一段长假,再一次回到香港去找他的妻子。他 

带着妻子的相片走遍香港的大街小巷,但这次仍是一点线索也没有。终于假期就要结束 

了,他身心疲惫地开始考虑要回国的时候 ,有一天无意间经过了一间珍奇小屋。小屋的 

看板上写着 : 达磨(不倒翁) 虽然他对珍奇事物并不感兴趣 ,但由于连日疲劳他想 

让自己改变一下心情 , 加上看板上写着 达磨的文字也引起他的兴趣。最后他决定 

进去瞧瞧。但是他不该进去的!因为珍奇小屋里面展出一件令他惨不忍睹的东 … 小屋 

里的舞台上有一位手脚都被切断的全裸女性被当成花瓶一样摆在那里 ! 这位女性的舌 

头已经被拔掉了,不断发出奇怪的呻吟声。看到这么恶心的东西真令他恨不得马上拔腿 

就跑 ,但不知为什么他心里感受到一股奇怪的气氛 ,于是他又重新仔细看那女人的面 

孔 … 没错!这女人正是他一年前失踪的妻子。后来,他向当地的黑道支付庞大的赎金 

换取妻子的剩下的躯干。但一切都太迟了,他可怜的妻子早就已经疯了。现在她还住在 

国内某家医院,继续不断地发出奇怪的呻吟声 … 

(10)最近有人告诉我,他的朋友在晚上听到门口有婴儿在哭,不过当时已很晚了, 

而且她认为这件事很奇怪,于是她打电话给警察。警察告诉她∶ 「无论如何,绝对不 

要开 门。」这位女士表示那声音听起来象是婴儿爬到窗户附近哭,她担心婴儿会爬到 

街上,被车子碾过。警察告诉她∶我们已派人前往,无论如何不能开门。警方认为这 

是一个连续杀人犯,利用婴儿哭声的录音带,诱使女性以为有人在外面遗弃婴儿,她 

们出门察看。虽然尚未证实此事,但是警方已接到许多女性打电话来说,他们晚上独 

自在家时,听到门外有婴儿的哭声,请将这个消息传给其他人,不要因为听到婴儿的 

哭声而开门。 

请严肃看待这贴子!有这么离谱!小心为妙!!! 

以前听说大活人现场失踪,后来被卖到马戏团、被卖器官什么的,只当是天方夜谭。 

结果真的看到发生在真实中的恐怖故事。 

(11)事情是同事群发邮件告知的。她的朋友,简称小a吧,上周和两个女孩子,简称 

小b和小c,去逛罗湖商业城。罗湖商业城是深圳假货集散地,龙蛇混杂,紧靠深圳火 

车站和香港的罗湖口岸,人流量非常大。话说小c内急,就去上卫生间,小a和小b在洗 

手间外面等。等了很久很久,还是不见小c出来,两个人有点奇怪了。于是两个人进去 

催她。谁知道进去一看,人影全无。两个人倒竖一口冷气,打手机也没人接。一个大 

活人,难道就这么活不见人死不见尸的失踪了?于是赶紧报警。 警察来了,问情事情 

经过以后,说了一句令人无比毛骨悚然的话,“你们有没有看见其他可疑的人进去? 

”,两个人再三回忆,没有。因为不可能带着一个活生生的100多斤的人出来,而她们 

不注意。这时候小a突然想起来,其间有个清洁工打扮的人推着一辆清洁小车进去、接 

着又出来…… 警察告诉他们,这种事情已经不是第一次发生,现在深圳警方初步怀疑 

一个犯罪团伙,有组织地在管理疏松的低档商业城,利用人们,尤其是女性对清洁工 

没有防范意识的心理,进行有组织地绑架、贩卖人体器官犯罪。别忘了,罗湖商业城 

离香港和深圳火车站有多么地近。现在已经几天过去了,那个可怜的小c姑娘,仍是活 

不见人死不见尸。我的同事说,小a,也就是我同事的朋友,仍在等小c的消息。但是 

很可能,也许如果幸运的话,活着的小c会被扔在哪个角落,只是失去了她的肾,但是 

,更有可能的是,也许再过几天或者几个月、几年,小c的头颅和躯体、四肢会在深圳 

的城乡结合部的垃圾堆被人发现。如果看到这个发生在身边的活生生的恐怖事件,请 

转告身边的女性亲友,一定小心防范清洁工打扮的人,因为他/她很可能会趁你不注意 

把你敲晕,放进清洁车拉走,接下来等着你的是无比恐怖的活人分尸。 

posted @ 2007-03-05 17:11 boddi 阅读(396) | 评论 (2)编辑 收藏

2006年10月11日

远程方法调用入门指南(Java RMI Tutorial)


Java R MI Tutorial

远程方法调用入门指南

Copyright © 2005 Stephen Suen. All rights reserved.

Java 远程方法调用(Remote Method Invocation, RMI)使得运行在一个 Java 虚拟机(Java Virtual Machine, JVM)的对象可以调用运行另一个 JVM 之上的其他对象的方法,从而提供了程序间进行远程通讯的途径。RMI 是 J2EE 的很多分布式技术的基础,比如 RMI-IIOP 乃至 EJB。本文是 RMI 的一个入门指南,目的在于帮助读者快速建立对 Java RMI 的一个感性认识,以便进行更深层次的学习。事实上,如果你了解 RMI 的目的在于更好的理解和学习 EJB,那么本文就再合适不过了。通过本文所了解的 RMI 的知识和技巧,应该足够服务于这个目的了。

本文的最新版本将发布在程序员咖啡馆网站上(建设中)。欢迎订阅我们的邮件组,以获得关于本文的正式发布及更新信息。

全文在保证完整性,且保留全部版权声明(包括上述链接)的前提下可以在任意媒体转载——须保留此标注。


1. 简介

我们知道远程过程调用(Remote Procedure Call, RPC)可以用于一个进程调用另一个进程(很可能在另一个远程主机上)中的过程,从而提供了过程的分布能力。Java 的 RMI 则在 RPC 的基础上向前又迈进了一步,即提供分布式 对象间的通讯,允许我们获得在远程进程中的对象(称为远程对象)的引用(称为远程引用),进而通过引用调用远程对象的方法,就好像该对象是与你的客户端代码同样运行在本地进程中一样。RMI 使用了术语"方法"(Method)强调了这种进步,即在分布式基础上,充分支持面向对象的特性。

RMI 并不是 Java 中支持远程方法调用的唯一选择。在 RMI 基础上发展而来的 RMI-IIOP(Java Remote Method Invocation over the Internet Inter-ORB Protocol),不但继承了 RMI 的大部分优点,并且可以兼容于 CORBA。J2EE 和 EJB 都要求使用 RMI-IIOP 而不是 RMI。尽管如此,理解 RMI 将大大有助于 RMI-IIOP 的理解。所以,即便你的兴趣在 RMI-IIOP 或者 EJB,相信本文也会对你很有帮助。另外,如果你现在就对 API 感兴趣,那么可以告诉你,RMI 使用 java.rmi 包,而 RMI-IIOP 则既使用 java.rmi 也使用扩展的 javax.rmi 包。

本文的随后内容将仅针对 Java RMI。

2. 分布式对象

在学习 RMI 之前,我们需要了解一些基础知识。首先需要了解所谓的分布式对象(Distributed Object)。分布式对象是指一个对象可以被远程系统所调用。对于 Java 而言,即对象不仅可以被同一虚拟机中的其他客户程序(Client)调用,也可以被运行于其他虚拟机中的客户程序调用,甚至可以通过网络被其他远程主机之上的客户程序调用。

下面的图示说明了客户程序是如何调用分布式对象的:

从图上我们可以看到,分布式对象被调用的过程是这样的:

  1. 客户程序调用一个被称为 Stub (有时译作存根,为了不产生歧义,本文将使用其英文形式)的客户端代理对象。该代理对象负责对客户端隐藏网络通讯的细节。Stub 知道如何通过网络套接字(Socket)发送调用,包括如何将调用参数转换为适当的形式以便传输等。

  2. Stub 通过网络将调用传递到服务器端,也就是分布对象一端的一个被称为 Skeleton 的代理对象。同样,该代理对象负责对分布式对象隐藏网络通讯的细节。Skeleton 知道如何从网络套接字(Socket)中接受调用,包括如何将调用参数从网络传输形式转换为 Java 形式等。

  3. Skeleton 将调用传递给分布式对象。分布式对象执行相应的调用,之后将返回值传递给 Skeleton,进而传递到 Stub,最终返回给客户程序。

这个场景基于一个基本的法则,即行为的定义和行为的具体实现相分离。如图所示,客户端代理对象 Stub 和分布式对象都实现了相同的接口,该接口称为远程接口(Remote Interface)。正是该接口定义了行为,而分布式对象本身则提供具体的实现。对于 Java RMI 而言,我们用接口(interface)定义行为,用类(class)定义实现。

3. RMI 架构

RMI 的底层架构由三层构成:

  • 首先是 Stub/Skeleton 层。该层提供了客户程序和服务程序彼此交互的接口。

  • 然后是远程引用(Remote Reference)层。这一层相当于在其之上的 Stub/Skeleton 层和在其之下的传输协议层之前的中间件,负责处理远程对象引用的创建和管理。

  • 最后是传输协议(Transport Protocol) 层。该层提供了数据协议,用以通过线路传输客户程序和远程对象间的请求和应答。

这些层之间的交互可以参照下面的示意图:

和其它分布式对象机制一样,Java RMI 的客户程序使用客户端的 Stub 向远程对象请求方法调用;服务器对象则通过服务器端的 Skeleton 接受请求。我们深入进去,来看看其中的一些细节。

注意: 事实上,在 Java 1.2 之后,RMI 不再需要 Skeleton 对象,而是通过 Java 的反射机制(Reflection)来完成对服务器端的远程对象的调用。为了便于说明问题,本文以下内容仍然基于 Skeleton 来讲解。

当客户程序调用 Stub 时,Stub 负责将方法的参数转换为序列化(Serialized)形式,我们使用一个特殊的术语,即编列(Marshal)来指代这个过程。编列的目的是将这些参数转换为可移植的形式,从而可以通过网络传输到远程的服务对象一端。不幸的是,这个过程没有想象中那么简单。这里我们首先要理解一个经典的问题,即方法调用时,参数究竟是传值还是传引用呢?对于 Java RMI 来说,存在四种情况,我们将分别加以说明。

  • 对于基本的原始类型(整型,字符型等等),将被自动的序列化,以传值的方式编列。

  • 对于 Java 的对象,如果该对象是可序列化的(实现了 java.io.Serializable 接口),则通过 Java 序列化机制自动地加以序列化,以传值的方式编列。对象之中包含的原始类型以及所有被该对象引用,且没有声明为 transient 的对象也将自动的序列化。当然,这些被引用的对象也必须是可序列化的。

  • 绝大多数内建的 Java 对象都是可序列化的。 对于不可序列化的 Java 对象(java.io.File 最典型),或者对象中包含对不可序列化,且没有声明为 transient 的其它对象的引用。则编列过程将向客户程序抛出异常,而宣告失败。

  • 客户程序可以调用远程对象,没有理由禁止调用参数本身也是远程对象(实现了 java.rmi.Remote 接口的类的实例)。此时,RMI 采用一种模拟的传引用方式(当然不是传统意义的传引用,因为本地对内存的引用到了远程变得毫无意义),而不是将参数直接编列复制到远程。这种情况下,交互的双方发生的戏剧性变化值得我们注意。参数是远程对象,意味着该参数对象可以远程调用。当客户程序指定远程对象作为参数调用服务器端远程对象的方法时,RMI 的运行时机制将向服务器端的远程对象发送作为参数的远程对象的一个 Stub 对象。这样服务器端的远程对象就可以回调(Callback)这个 Stub 对象的方法,进而调用在客户端的远程对象的对应方法。通过这种方法,服务器端的远程对象就可以修改作为参数的客户端远程对象的内部状态,这正是传统意义的传引用所具备的特性。是不是有点晕?这里的关键是要明白,在分布式环境中,所谓服务器和客户端都是相对的。被请求的一方就是服务器,而发出请求的一方就是客户端。

在调用参数的编列过程成功后,客户端的远程引用层从 Stub 那里获得了编列后的参数以及对服务器端远程对象的远程引用(参见 java.rmi.server.RemoteRef API)。该层负责将客户程序的请求依据底层的 RMI 数据传输协议转换为传输层请求。在 RMI 中,有多种的可能的传输机制,比如点对点(Point-to-Point)以及广播(Multicast)等。不过,在当前的 JMI 版本中只支持点对点协议,即远程引用层将生成唯一的传输层请求,发往指定的唯一远程对象(参见 java.rmi.server.UnicastRemoteObject API)。

在服务器端,服务器端的远程引用层接收传输层请求,并将其转换为对远程对象的服务器端代理对象 Skeleton 的调用。Skeleton 对象负责将请求转换为对实际的远程对象的方法调用。这是通过与编列过程相对的反编列(Unmarshal)过程实现的。所有序列化的参数被转换为 Java 形式,其中作为参数的远程对象(实际上发送的是远程引用)被转换为服务器端本地的 Stub 对象。

如果方法调用有返回值或者抛出异常,则 Skeleton 负责编列返回值或者异常,通过服务器端的远程引用层,经传输层传递给客户端;相应地,客户端的远程引用层和 Stub 负责反编列并最终将结果返回给客户程序。

整个过程中,可能最让人迷惑的是远程引用层。这里只要明白,本地的 Stub 对象是如何产生的,就不难理解远程引用的意义所在了。远程引用中包含了其所指向的远程对象的信息,该远程引用将用于构造作为本地代理对象的 Stub 对象。构造后,Stub 对象内部将维护该远程引用。真正在网络上传输的实际上就是这个远程引用,而不是 Stub 对象。

4. RMI 对象服务

在 RMI 的基本架构之上,RMI 提供服务与分布式应用程序的一些对象服务,包括对象的命名/注册(Naming/Registry)服务,远程对象激活(Activation)服务以及分布式垃圾收集(Distributed Garbage Collection, DGC)。作为入门指南,本文将指介绍其中的命名/注册服务,因为它是实战 RMI 所必备的。其它内容请读者自行参考其它更加深入的资料。

在前一节中,如果你喜欢刨根问底,可能已经注意到,客户端要调用远程对象,是通过其代理对象 Stub 完成的,那么 Stub 最早是从哪里得来的呢?RMI 的命名/注册服务正是解决这一问题的。当服务器端想向客户端提供基于 RMI 的服务时,它需要将一个或多个远程对象注册到本地的 RMI 注册表中(参见java.rmi.registry.Registry API)。每个对象在注册时都被指定一个将来用于客户程序引用该对象的名称。客户程序通过命名服务(参见 java.rmi.Naming API),指定类似 URL 的对象名称就可以获得指向远程对象的远程引用。在 Naming 中的 lookup() 方法找到远程对象所在的主机后,它将检索该主机上的 RMI 注册表,并请求所需的远程对象。如果注册表发现被请求的远程对象,它将生成一个对该远程对象的远程引用,并将其返回给客户端,客户端则基于远程引用生成相应的 Stub 对象,并将引用传递给调用者。之后,双方就可以按照我们前面讲过的方式进行交互了。

注意: RMI 命名服务提供的 Naming 类并不是你的唯一选择。RMI 的注册表可以与其他命名服务绑定,比如 JNDI,这样你就可以通过 JNDI 来访问 RMI 的注册表了。

5. 实战 RMI

理论离不开实践,理解 RMI 的最好办法就是通过例子。开发 RMI 的分布式对象的大体过程包括如下几步:

  1. 定义远程接口。这一步是通过扩展 java.rmi.Remote 接口,并定义所需的业务方法实现的。

  2. 定义远程接口的实现类。即实现上一步所定义的接口,给出业务方法的具体实现逻辑。

  3. 编译远程接口和实现类,并通过 RMI 编译器 rmic 基于实现类生成所需的 Stub 和 Skeleton 类。

RMI 中各个组件之间的关系如下面这个示意图所示:

回忆我们上一节所讲的,Stub 和 Skeleton 负责代理客户和服务器之间的通讯。但我们并不需要自己生成它们,相反,RMI 的编译器 rmic 可以帮我们基于远程接口和实现类生成这些类。当客户端对象通过命名服务向服务器端的 RMI 注册表请求远程对象时,RMI 将自动构造对应远程对象的 Skeleton 实例对象,并通过 Skeleton 对象将远程引用返回给客户端。在客户端,该远程引用将用于构造 Stub 类的实例对象。之后,Stub 对象和 Skeleton 对象就可以代理客户对象和远程对象之间的交互了。

我们的例子展现了一个简单的应用场景。服务器端部署了一个计算引擎,负责接受来自客户端的计算任务,在服务器端执行计算任务,并将结果返回给客户端。客户端将发送并调用计算引擎的计算任务实际上是计算指定精度的 π 值。

重要: 本文的例子改编自 The Java™ Tutorial Trail:RMI。所有权利属于相应的所有人。

6. 定义远程接口

定义远程接口与非分布式应用中定义接口的方法没有太多的区别。只要遵守下面两个要求:

  • 远程接口必须直接或者间接的扩展自 java.rmi.Remote 接口。远程接口还可以在扩展该接口的基础上,同时扩展其它接口,只要被扩展的接口的所有方法与远程接口的所有方法一样满足下一个要求。

  • 在远程接口或者其超接口(Super-interface)中声明的方法必须满足下列对远程方法的要求:

    • 远程方法必须声明抛出 java.rmi.RemoteException 异常,或者该异常的超类(Superclass),比如 java.io.IOException 或者 java.lang.Exception 异常。在此基础上,远程方法可以声明抛出应用特定的其它异常。

    • 在远程方法声明中,作为参数或者返回值的远程对象,或者包含在其它非远程对象中的远程对象,必须声明为其对应的远程接口,而不是实际的实现类。

注意: 在 Java 1.2 之前,上面关于抛出异常的要求更严格,即必须抛出 java.rmi.RemoteExcption,不允许类似 java.io.IOException 这样的超类。现在之所以放宽了这一要求,是希望可以使定义既可以用于远程对象,也可以用于本地对象的接口变得容易一些(想想 EJB 中的本地接口和远程接口)。当然,这并没有使问题好多少,你还是必须声明异常。不过,一种观点认为这不是问题,强制声明异常可以使开发人员保持清醒的头脑,因为远程对象和本地对象在调用时传参的语意是不同的。本地对象是传引用,而远程对象主要是传值,这意味对参数内部状态的修改产生的结果是不同的。

对于第一个要求,java.rmi.Remote 接口实际上没有任何方法,而只是用作标记接口。RMI 的运行环境依赖该接口判断对象是否是远程对象。第二个要求则是因为分布式应用可能发生任何问题,比如网络问题等等。

例 1 列出了我们的远程接口定义。该接口只有一个方法:executeTask() 用以执行指定的计算任务,并返回相应的结果。注意,我们用后缀 Remote 表明接口是远程接口。

例 1. ComputeEngineRemote 远程接口

package rmitutorial;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface ComputeEngineRemote extends Remote {
    public Object executeTask(Task task) throws RemoteException;    
}

例 2 列出了计算任务接口的定义。该接口也只有一个方法:execute() 用以执行实际的计算逻辑,并返回结果。注意,该接口不是远程接口,所以没有扩展 java.rmi.Remote 接口;其方法也不必抛出 java.rmi.RemoteException 异常。但是,因为它将用作远程方法的参数,所以扩展了 java.io.Serializable 接口。

例 2. Task 接口

package rmitutorial;

import java.io.Serializable;

public interface Task extends Serializable {
    Object execute();
}

7. 实现远程接口

接下来,我们将实现前面定义的远程接口。例 3给出了实现的源代码。

例 3. ComputeEngine 实现

package rmitutorial;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class ComputeEngine extends UnicastRemoteObject 
        implements ComputeEngineRemote {
    
    public ComputeEngine() throws RemoteException {
        super();
    }
    
    public Object executeTask(Task task) throws RemoteException {
        return task.execute();
    }
}

ComputeEngine 实现了之前定义的远程接口,同时继承自 java.rmi.server.UnicastRemoteObject 超类。UnicastRemoteObject 类是一个便捷类,它实现了我们前面所讲的基于 TCP/IP 的点对点通讯机制。远程对象都必须从该类扩展(除非你想自己实现几乎所有 UnicastRemoteObject 的方法)。在我们的实现类的构造函数中,调用了超类的构造函数(当然,即使你不显式的调用这个构建函数,它也一样会被调用。这里这样做,只是为了突出强调这种调用而已)。该构造函数的最重要的意义就是调用 UnicastRemoteObject 类的 exportObject() 方法。导出(Export)对象是指使远程对象准备就绪,可以接受进来的调用的过程。而这个过程的最重要内容就是建立服务器套接字,监听特定的端口,等待客户端的调用请求。

8. 引导程序

为了让客户程序可以找到我们的远程对象,就需要将我们的远程对象注册到 RMI 的注册表。这个过程有时被称为"引导"过程(Bootstrap)。我们将为此编写一个独立的引导程序负责创建和注册远程对象。例 4 给出了引导程序的源代码。

例 4. 引导程序

package rmitutorial;

import java.rmi.Naming;
import java.rmi.RMISecurityManager;

public class Bootstrap {
    
    public static void main(String[] args) throws Exception {
        String name = "ComputeEngine";
        
        ComputeEngine engine = new ComputeEngine();
        System.out.println("ComputerEngine exported");
        
        Naming.rebind(name, engine);
        System.out.println("ComputeEngine bound");
    }
}

可以看到,我们首先创建了一个远程对象(同时导出了该对象),之后将该对象绑定到 RMI 注册表中。Namingrebind() 方法接受一个 URL 形式的名字作绑定之用。其完整格式如下:

protocol://host:port/object

其中,协议(Protocol)默认为 rmi;主机名默认为 localhost;端口默认为 1099。注意,JDK 中提供的默认 Naming 实现只支持 rmi 协议。在我们的引导程序里面只给出了对象绑定的名字,而其它部分均使用缺省值。

9. 客户端程序

例 5 给出了我们的客户端程序。该程序接受两个参数,分别是远程对象所在的主机地址和希望获得的 π 值的精度。

例 5. Client.java

package rmitutorial;

import java.math.BigDecimal;
import java.rmi.Naming;

public class Client {
    public static void main(String args[]) throws Exception {
            String name = "rmi://" + args[0] + "/ComputeEngine";
            ComputeEngineRemote engineRemote = 
                    (ComputeEngineRemote)Naming.lookup(name);
            Pi task = new Pi(Integer.parseInt(args[1]));
            BigDecimal pi = (BigDecimal)(engineRemote.executeTask(task));
            System.out.println(pi);
    }
}

例 6. Pi.java

package rmitutorial;

import java.math.*;

public class Pi implements Task {
    
    private static final BigDecimal ZERO =
            BigDecimal.valueOf(0);
    private static final BigDecimal  ONE =
            BigDecimal.valueOf(1);
    private static final BigDecimal FOUR =
            BigDecimal.valueOf(4);
    
    private static final int roundingMode =
            BigDecimal.ROUND_HALF_EVEN;
    
    private int digits;
    
    public Pi(int digits) {
        this.digits = digits;
    }
    
    public Object execute() {
        return computePi(digits);
    }
    
    public static BigDecimal computePi(int digits) {
        int scale = digits + 5;
        BigDecimal arctan1_5 = arctan(5, scale);
        BigDecimal arctan1_239 = arctan(239, scale);
        BigDecimal pi = arctan1_5.multiply(FOUR).subtract(
                arctan1_239).multiply(FOUR);
        return pi.setScale(digits,
                BigDecimal.ROUND_HALF_UP);
    }

    public static BigDecimal arctan(int inverseX,
            int scale) {
        BigDecimal result, numer, term;
        BigDecimal invX = BigDecimal.valueOf(inverseX);
        BigDecimal invX2 =
                BigDecimal.valueOf(inverseX * inverseX);
        
        numer = ONE.divide(invX, scale, roundingMode);
        
        result = numer;
        int i = 1;
        do {
            numer =
                    numer.divide(invX2, scale, roundingMode);
            int denom = 2 * i + 1;
            term =
                    numer.divide(BigDecimal.valueOf(denom),
                    scale, roundingMode);
            if ((i % 2) != 0) {
                result = result.subtract(term);
            } else {
                result = result.add(term);
            }
            i++;
        } while (term.compareTo(ZERO) != 0);
        return result;
    }
}

10. 编译示例程序

编译我们的示例程序和编译其它非分布式的应用没什么区别。只是编译之后,需要使用 RMI 编译器,即 rmic 生成所需 Stub 和 Skeleton 实现。使用 rmic 的方式是将我们的远程对象的实现类(不是远程接口)的全类名作为参数来运行 rmic 命令。参考下面的示例:

E:\classes\rmic rmitutorial.ComputeEngine

编译之后将生成 rmitutorial.ComputeEngine_Skelrmitutorial.ComputeEngine_Stub 两个类。

11. 运行示例程序

远程对象的引用通常是通过 RMI 的注册表服务以及 java.rmi.Naming 接口获得的。远程对象需要导出(注册)相应的远程引用到注册表服务,之后注册表服务就可以监听并服务于客户端对远程对象引用的请求。标准的 Sun Java SDK 提供了一个简单的 RMI 注册表服务程序,即 rmiregistry 用于监听特定的端口,等待远程对象的注册,以及客户端对这些远程对象引用的检索请求。

在运行我们的示例程序之前,首先要启动 RMI 的注册表服务。这个过程很简单,只要直接运行 rmiregistry 命令即可。缺省的情况下,该服务将监听 1099 端口。如果需要指定其它的监听端口,可以在命令行指定希望监听的端口(如果你指定了其它端口,需要修改示例程序以适应环境)。如果希望该程序在后台运行,在 Unix 上可以以如下方式运行(当然,可以缺省端口参数):

$ rmiregistry 1099 &

在 Windows 操作系统中可以这样运行:

C:\> start rmiregistry 1099

我们的 rmitutorial.Bootstrap 类将用于启动远程对象,并将其绑定在 RMI 注册表中。运行该类后,远程对象也将进入监听状态,等待来自客户端的方法调用请求。

$ java rmitutorial.Bootstrap
ComputeEngine exported
ComputeEngine bound

启动远程对象后,打开另一个命令行窗口,运行客户端。命令行的第一个参数为 RMI 注册表的地址,第二个参数为期望的 π 值精度。参考下面的示例:

$ java rmitutorial.Client localhost 50
3.14159265358979323846264338327950288419716939937511

12. 其它信息

在演示示例程序时,我们实际上是在同一主机上运行的服务器和客户端,并且无论是服务器和客户端所需的类都在相同的类路径上,可以同时被服务器和客户端所访问。这忽略了 Java RMI 的一个重要细节,即动态类装载。因为 RMI 的特性(包括其它几个特性)并不适用于 J2EE 的 RMI-IIOP 和 EJB 技术,所以,本文将不作详细介绍,请读者自行参考本文给出的参考资料。不过,为了让好奇的读者不至于过分失望,这里简单介绍一下动态类装载的基本思想。

RMI 运行时系统采用动态类装载机制来装载分布式应用所需的类。如果你可以直接访问应用所涉及的所有包括服务器端客户端在内的主机,并且可以把分布式应用所需的所有类都安装在每个主机的 CLASSPATH 中(上面的示例就是极端情况,所有的东西都在本地主机),那么你完全不必关心 RMI 类装载的细节。显然,既然是分布式应用,情况往往正相反。对于 RMI 应用,客户端需要装载客户端自身所需的类,将要调用的远程对象的远程接口类以及对应的 Stub 类;服务器端则要装载远程对象的实现类以及对应的 Skeleton 类(Java 1.2 之后不需要 Skeleton 类)。RMI 在处理远程调用涉及的远程引用,参数以及返回值时,可以将一个指定的 URL 编码到流中。交互的另一端可以通过 该 URL 获得处理这些对象所需的类文件。这一点类似于 Applet 中的 CODEBASE 的概念,交互的两端通过 HTTP 服务器发布各自控制的类,允许交互的另一端动态下载这些类。以我们的示例为例,客户端不必部署 ComputeEngine_Stub 的类文件,而可以通过服务器端的 HTTP 服务器获得类文件。同样,服务器端也不需要客户端实现的定制任务 Pi 的类文件。

注意,这种动态类装载将需要交互的两端加载定制的安全管理器(参见 java.rmi.RMISecurityManager API),以及对应的策略文件。

13. 参考资料

  • The Java™ Tutorial Trail:RMI

  • David Flanagan, Jim Farley, William Crawford and Kris Magnusson, 1999, ISBN 1-56592-483-5E, O'Reilly, Java™ Enterprise in a Nutshell

  • Ed Roman, Scott Ambler and Tyler Jewell 2002, ISBN 0-471-41711-4, John Wiley &Sons, Inc., Matering Enterprise JavaBeans™ , Second Edition

posted @ 2006-10-11 09:51 boddi 阅读(2115) | 评论 (1)编辑 收藏

2006年9月13日

正则表达式(转载)

关键词正则表达式    模式匹配    Javascript                                          

关键字:正则表达式  模式匹配 Javascript

摘要:收集一些常用的正则表达式。

正则表达式用于字符串处理,表单验证等场合,实用高效,但用到时总是不太把握,以致往往要上网查一番。我将一些常用的表达式收藏在这里,作备忘之用。本贴随时会更新。

匹配中文字符的正则表达式: [\u4e00-\u9fa5]

匹配双字节字符(包括汉字在内):[^\x00-\xff]

应用:计算字符串的长度(一个双字节字符长度计2,ASCII字符计1)

String.prototype.len=function(){return this.replace([^\x00-\xff]/g,"aa").length;}

匹配空行的正则表达式:\n[\s| ]*\r

匹配HTML标记的正则表达式:/<(.*)>.*<\/\1>|<(.*) \/>/

匹配首尾空格的正则表达式:(^\s*)|(\s*$)

String.prototype.trim = function()
{
    return this.replace(/(^\s*)|(\s*$)/g, "");
}

利用正则表达式分解和转换IP地址:

下面是利用正则表达式匹配IP地址,并将IP地址转换成对应数值的Javascript程序:

function IP2V(ip)
{
 re=/(\d+)\.(\d+)\.(\d+)\.(\d+)/g  //匹配IP地址的正则表达式
if(re.test(ip))
{
return RegExp.$1*Math.pow(255,3))+RegExp.$2*Math.pow(255,2))+RegExp.$3*255+RegExp.$4*1
}
else
{
 throw new Error("Not a valid IP address!")
}
}

不过上面的程序如果不用正则表达式,而直接用split函数来分解可能更简单,程序如下:

var ip="10.100.20.168"
ip=ip.split(".")
alert("IP值是:"+(ip[0]*255*255*255+ip[1]*255*255+ip[2]*255+ip[3]*1))

匹配Email地址的正则表达式:\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*

匹配网址URL的正则表达式:http://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?

利用正则表达式去除字串中重复的字符的算法程序:[注:此程序不正确,原因见本贴回复]

var s="abacabefgeeii"
var s1=s.replace(/(.).*\1/g,"$1")
var re=new RegExp("["+s1+"]","g")
var s2=s.replace(re,"")
alert(s1+s2)  //结果为:abcefgi

我原来在CSDN上发贴寻求一个表达式来实现去除重复字符的方法,最终没有找到,这是我能想到的最简单的实现方法。思路是使用后向引用取出包括重复的字符,再以重复的字符建立第二个表达式,取到不重复的字符,两者串连。这个方法对于字符顺序有要求的字符串可能不适用。

得用正则表达式从URL地址中提取文件名的javascript程序,如下结果为page1

s="http://www.9499.net/page1.htm"
s=s.replace(/(.*\/){0,}([^\.]+).*/ig,"$2")
alert(s)

利用正则表达式限制网页表单里的文本框输入内容:

用正则表达式限制只能输入中文:onkeyup="value=value.replace(/[^\u4E00-\u9FA5]/g,'')" onbeforepaste="clipboardData.setData('text',clipboardData.getData('text').replace(/[^\u4E00-\u9FA5]/g,''))"

用正则表达式限制只能输入全角字符: onkeyup="value=value.replace(/[^\uFF00-\uFFFF]/g,'')" onbeforepaste="clipboardData.setData('text',clipboardData.getData('text').replace(/[^\uFF00-\uFFFF]/g,''))"

用正则表达式限制只能输入数字:onkeyup="value=value.replace(/[^\d]/g,'') "onbeforepaste="clipboardData.setData('text',clipboardData.getData('text').replace(/[^\d]/g,''))"

用正则表达式限制只能输入数字和英文:onkeyup="value=value.replace(/[\W]/g,'') "onbeforepaste="clipboardData.setData('text',clipboardData.getData('text').replace(/[^\d]/g,''))"

posted @ 2006-09-13 16:28 boddi 阅读(356) | 评论 (0)编辑 收藏

2006年9月11日

     摘要: javascript小技巧 事件源对象 event.srcElement.tagName event.srcElement.type 捕获释放 event.srcElement.setCapture();  event...  阅读全文

posted @ 2006-09-11 13:35 boddi 阅读(782) | 评论 (0)编辑 收藏

2006年9月9日



Java Excel是一开放源码项目,通过它Java开发人员可以读取Excel文件的内容、创建新的Excel文件、更新已经存在的Excel文件。使用该API非Windows操作系统也可以通过纯Java应用来处理Excel数据表。因为是使用Java编写的,所以我们在Web应用中可以通过JSP、Servlet来调用API实现对Excel数据表的访问。

现在发布的稳定版本是V2.0,提供以下功能:

从Excel 95、97、2000等格式的文件中读取数据;
读取Excel公式(可以读取Excel 97以后的公式);
生成Excel数据表(格式为Excel 97);
支持字体、数字、日期的格式化;
支持单元格的阴影操作,以及颜色操作;
修改已经存在的数据表;
现在还不支持以下功能,但不久就会提供了:

不能够读取图表信息;
可以读,但是不能生成公式,任何类型公式最后的计算值都可以读出;
应用示例

1 从Excel文件读取数据表

Java Excel API既可以从本地文件系统的一个文件(.xls),也可以从输入流中读取Excel数据表。读取Excel数据表的第一步是创建Workbook(术语:工作薄),下面的代码片段举例说明了应该如何操作:(完整代码见ExcelReading.java)


import java.io.*;
import jxl.*;
… … … …
try
{
//构建Workbook对象, 只读Workbook对象
//直接从本地文件创建Workbook
//从输入流创建Workbook
   InputStream is = new FileInputStream(sourcefile);
   jxl.Workbook rwb = Workbook.getWorkbook(is);
}
catch (Exception e)
{
e.printStackTrace();
}



一旦创建了Workbook,我们就可以通过它来访问Excel Sheet(术语:工作表)。参考下面的代码片段:


//获取第一张Sheet表
Sheet rs = rwb.getSheet(0);



我们既可能通过Sheet的名称来访问它,也可以通过下标来访问它。如果通过下标来访问的话,要注意的一点是下标从0开始,就像数组一样。

一旦得到了Sheet,我们就可以通过它来访问Excel Cell(术语:单元格)。参考下面的代码片段:


//获取第一行,第一列的值
Cell c00 = rs.getCell(0, 0);
String strc00 = c00.getContents();

//获取第一行,第二列的值
Cell c10 = rs.getCell(1, 0);
String strc10 = c10.getContents();

//获取第二行,第二列的值
Cell c11 = rs.getCell(1, 1);
String strc11 = c11.getContents();

System.out.println("Cell(0, 0)" + " value : " + strc00 + "; type : " + c00.getType());
System.out.println("Cell(1, 0)" + " value : " + strc10 + "; type : " + c10.getType());
System.out.println("Cell(1, 1)" + " value : " + strc11 + "; type : " + c11.getType());



如果仅仅是取得Cell的值,我们可以方便地通过getContents()方法,它可以将任何类型的Cell值都作为一个字符串返回。示例代码中Cell(0, 0)是文本型,Cell(1, 0)是数字型,Cell(1,1)是日期型,通过getContents(),三种类型的返回值都是字符型。

如果有需要知道Cell内容的确切类型,API也提供了一系列的方法。参考下面的代码片段:


String strc00 = null;
double strc10 = 0.00;
Date strc11 = null;

Cell c00 = rs.getCell(0, 0);
Cell c10 = rs.getCell(1, 0);
Cell c11 = rs.getCell(1, 1);

if(c00.getType() == CellType.LABEL)
{
LabelCell labelc00 = (LabelCell)c00;
strc00 = labelc00.getString();
}
if(c10.getType() == CellType.NUMBER)
{
NmberCell numc10 = (NumberCell)c10;
strc10 = numc10.getValue();
}
if(c11.getType() == CellType.DATE)
{
DateCell datec11 = (DateCell)c11;
strc11 = datec11.getDate();
}

System.out.println("Cell(0, 0)" + " value : " + strc00 + "; type : " + c00.getType());
System.out.println("Cell(1, 0)" + " value : " + strc10 + "; type : " + c10.getType());
System.out.println("Cell(1, 1)" + " value : " + strc11 + "; type : " + c11.getType());



在得到Cell对象后,通过getType()方法可以获得该单元格的类型,然后与API提供的基本类型相匹配,强制转换成相应的类型,最后调用相应的取值方法getXXX(),就可以得到确定类型的值。API提供了以下基本类型,与Excel的数据格式相对应,如下图所示:





每种类型的具体意义,请参见Java Excel API Document。

当你完成对Excel电子表格数据的处理后,一定要使用close()方法来关闭先前创建的对象,以释放读取数据表的过程中所占用的内存空间,在读取大量数据时显得尤为重要。参考如下代码片段:


//操作完成时,关闭对象,释放占用的内存空间
rwb.close();



Java Excel API提供了许多访问Excel数据表的方法,在这里我只简要地介绍几个常用的方法,其它的方法请参考附录中的Java Excel API Document。

Workbook类提供的方法

1. int getNumberOfSheets()
获得工作薄(Workbook)中工作表(Sheet)的个数,示例:


jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
int sheets = rwb.getNumberOfSheets();



2. Sheet[] getSheets()
返回工作薄(Workbook)中工作表(Sheet)对象数组,示例:


jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
Sheet[] sheets = rwb.getSheets();



3. String getVersion()
返回正在使用的API的版本号,好像是没什么太大的作用。


jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
String apiVersion = rwb.getVersion();



Sheet接口提供的方法

1) String getName()
获取Sheet的名称,示例:


jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
jxl.Sheet rs = rwb.getSheet(0);
String sheetName = rs.getName();



2) int getColumns()
获取Sheet表中所包含的总列数,示例:


jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
jxl.Sheet rs = rwb.getSheet(0);
int rsColumns = rs.getColumns();



3) Cell[] getColumn(int column)
获取某一列的所有单元格,返回的是单元格对象数组,示例:


jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
jxl.Sheet rs = rwb.getSheet(0);
Cell[] cell = rs.getColumn(0);



4) int getRows()
获取Sheet表中所包含的总行数,示例:


jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
jxl.Sheet rs = rwb.getSheet(0);
int rsRows = rs.getRows();



5) Cell[] getRow(int row)
获取某一行的所有单元格,返回的是单元格对象数组,示例子:


jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
jxl.Sheet rs = rwb.getSheet(0);
Cell[] cell = rs.getRow(0);



6) Cell getCell(int column, int row)
获取指定单元格的对象引用,需要注意的是它的两个参数,第一个是列数,第二个是行数,这与通常的行、列组合有些不同。


jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
jxl.Sheet rs = rwb.getSheet(0);
Cell cell = rs.getCell(0, 0);



2 生成新的Excel工作薄

下面的代码主要是向大家介绍如何生成简单的Excel工作表,在这里单元格的内容是不带任何修饰的(如:字体,颜色等等),所有的内容都作为字符串写入。(完整代码见ExcelWriting.java)

与读取Excel工作表相似,首先要使用Workbook类的工厂方法创建一个可写入的工作薄(Workbook)对象,这里要注意的是,只能通过API提供的工厂方法来创建Workbook,而不能使用WritableWorkbook的构造函数,因为类WritableWorkbook的构造函数为protected类型。示例代码片段如下:


import java.io.*;
import jxl.*;
import jxl.write.*;
… … … …
try
{
//构建Workbook对象, 只读Workbook对象
//Method 1:创建可写入的Excel工作薄
   jxl.write.WritableWorkbook wwb = Workbook.createWorkbook(new File(targetfile));

//Method 2:将WritableWorkbook直接写入到输出流
/*
   OutputStream os = new FileOutputStream(targetfile);
   jxl.write.WritableWorkbook wwb = Workbook.createWorkbook(os);
*/
}
catch (Exception e)
{
e.printStackTrace();
}



API提供了两种方式来处理可写入的输出流,一种是直接生成本地文件,如果文件名不带全路径的话,缺省的文件会定位在当前目录,如果文件名带有全路径的话,则生成的Excel文件则会定位在相应的目录;另外一种是将Excel对象直接写入到输出流,例如:用户通过浏览器来访问Web服务器,如果HTTP头设置正确的话,浏览器自动调用客户端的Excel应用程序,来显示动态生成的Excel电子表格。

接下来就是要创建工作表,创建工作表的方法与创建工作薄的方法几乎一样,同样是通过工厂模式方法获得相应的对象,该方法需要两个参数,一个是工作表的名称,另一个是工作表在工作薄中的位置,参考下面的代码片段:


//创建Excel工作表
jxl.write.WritableSheet ws = wwb.createSheet("Test Sheet 1", 0);



"这锅也支好了,材料也准备齐全了,可以开始下锅了!",现在要做的只是实例化API所提供的Excel基本数据类型,并将它们添加到工作表中就可以了,参考下面的代码片段:


//1.添加Label对象
jxl.write.Label labelC = new jxl.write.Label(0, 0, "This is a Label cell");
ws.addCell(labelC);

//添加带有字型Formatting的对象
jxl.write.WritableFont wf = new jxl.write.WritableFont(WritableFont.TIMES, 18, WritableFont.BOLD, true);
jxl.write.WritableCellFormat wcfF = new jxl.write.WritableCellFormat(wf);
jxl.write.Label labelCF = new jxl.write.Label(1, 0, "This is a Label Cell", wcfF);
ws.addCell(labelCF);

//添加带有字体颜色Formatting的对象
jxl.write.WritableFont wfc = new jxl.write.WritableFont(WritableFont.ARIAL, 10, WritableFont.NO_BOLD, false,
Underlinestyle.NO_UNDERLINE, jxl.format.Colour.RED);
jxl.write.WritableCellFormat wcfFC = new jxl.write.WritableCellFormat(wfc);
jxl.write.Label labelCFC = new jxl.write.Label(1, 0, "This is a Label Cell", wcfFC);
ws.addCell(labelCF);

//2.添加Number对象
jxl.write.Number labelN = new jxl.write.Number(0, 1, 3.1415926);
ws.addCell(labelN);

//添加带有formatting的Number对象
jxl.write.NumberFormat nf = new jxl.write.NumberFormat("#.##");
jxl.write.WritableCellFormat wcfN = new jxl.write.WritableCellFormat(nf);
jxl.write.Number labelNF = new jxl.write.Number(1, 1, 3.1415926, wcfN);
ws.addCell(labelNF);

//3.添加Boolean对象
jxl.write.Boolean labelB = new jxl.write.Boolean(0, 2, false);
ws.addCell(labelB);

//4.添加DateTime对象
jxl.write.DateTime labelDT = new jxl.write.DateTime(0, 3, new java.util.Date());
ws.addCell(labelDT);

//添加带有formatting的DateFormat对象
jxl.write.DateFormat df = new jxl.write.DateFormat("dd MM yyyy hh:mm:ss");
jxl.write.WritableCellFormat wcfDF = new jxl.write.WritableCellFormat(df);
jxl.write.DateTime labelDTF = new jxl.write.DateTime(1, 3, new java.util.Date(), wcfDF);
ws.addCell(labelDTF);



这里有两点大家要引起大家的注意。第一点,在构造单元格时,单元格在工作表中的位置就已经确定了。一旦创建后,单元格的位置是不能够变更的,尽管单元格的内容是可以改变的。第二点,单元格的定位是按照下面这样的规律(column, row),而且下标都是从0开始,例如,A1被存储在(0, 0),B1被存储在(1, 0)。

最后,不要忘记关闭打开的Excel工作薄对象,以释放占用的内存,参见下面的代码片段:


//写入Exel工作表
wwb.write();

//关闭Excel工作薄对象
wwb.close();



这可能与读取Excel文件的操作有少少不同,在关闭Excel对象之前,你必须要先调用write()方法,因为先前的操作都是存储在缓存中的,所以要通过该方法将操作的内容保存在文件中。如果你先关闭了Excel对象,那么只能得到一张空的工作薄了。

3 拷贝、更新Excel工作薄

接下来简要介绍一下如何更新一个已经存在的工作薄,主要是下面二步操作,第一步是构造只读的Excel工作薄,第二步是利用已经创建的Excel工作薄创建新的可写入的Excel工作薄,参考下面的代码片段:(完整代码见ExcelModifying.java)


//创建只读的Excel工作薄的对象
jxl.Workbook rw = jxl.Workbook.getWorkbook(new File(sourcefile));

//创建可写入的Excel工作薄对象
jxl.write.WritableWorkbook  wwb = Workbook.createWorkbook(new File(targetfile), rw);
          
//读取第一张工作表
jxl.write.WritableSheet ws = wwb.getSheet(0);

//获得第一个单元格对象
jxl.write.WritableCell wc = ws.getWritableCell(0, 0);
          
//判断单元格的类型, 做出相应的转化
if(wc.getType() == CellType.LABEL)
{
Label l = (Label)wc;
   l.setString("The value has been modified.");
}

//写入Excel对象
wwb.write();

//关闭可写入的Excel对象
wwb.close();

//关闭只读的Excel对象
rw.close();



之所以使用这种方式构建Excel对象,完全是因为效率的原因,因为上面的示例才是API的主要应用。为了提高性能,在读取工作表时,与数据相关的一些输出信息,所有的格式信息,如:字体、颜色等等,是不被处理的,因为我们的目的是获得行数据的值,既使没有了修饰,也不会对行数据的值产生什么影响。唯一的不利之处就是,在内存中会同时保存两个同样的工作表,这样当工作表体积比较大时,会占用相当大的内存,但现在好像内存的大小并不是什么关键因素了。

一旦获得了可写入的工作表对象,我们就可以对单元格对象进行更新的操作了,在这里我们不必调用API提供的add()方法,因为单元格已经于工作表当中,所以我们只需要调用相应的setXXX()方法,就可以完成更新的操作了。

尽单元格原有的格式化修饰是不能去掉的,我们还是可以将新的单元格修饰加上去,以使单元格的内容以不同的形式表现。

新生成的工作表对象是可写入的,我们除了更新原有的单元格外,还可以添加新的单元格到工作表中,这与示例2的操作是完全一样的。

最后,不要忘记调用write()方法,将更新的内容写入到文件中,然后关闭工作薄对象,这里有两个工作薄对象要关闭,一个是只读的,另外一个是可写入的。

以上摘自IBM网站

posted @ 2006-09-09 13:14 boddi 阅读(630) | 评论 (3)编辑 收藏

使用JXL读取Excel表格,拷贝、更新Excel工作薄
xymiser 原创  (参与分:41669,专家分:2761)   发表:2006-01-18 22:11   版本:1.0   阅读:1666

/**
* <p>读取Excel表格,拷贝、更新Excel工作薄 </p>
* <p>Description: 可以读取Excel文件的内容,更新Excel工作薄
* </p>
* <p>Copyright: Copyright (c) Corparation 2005</p>
* <p>程序开发环境为eclipse</p>
* @author Walker
* @version 1.0
*/
package cn.com.yitong.xls;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Vector;

import cn.com.yitong.ChartImg;
import cn.com.yitong.VireObj;
import cn.com.yitong.platform.log.YTLogger;

import jxl.CellType;
import jxl.Workbook;
import jxl.format.CellFormat;
import jxl.format.Colour;
import jxl.format.UnderlineStyle;
import jxl.write.Formula;
import jxl.write.Label;
import jxl.write.Number;
import jxl.write.WritableCell;
import jxl.write.WritableCellFormat;
import jxl.write.WritableFont;
import jxl.write.WritableImage;
import jxl.write.WritableSheet;
import jxl.write.WritableWorkbook;
import jxl.write.WriteException;
import jxl.write.biff.RowsExceededException;

public class XLSDemo
{
    private static final int TITLE_LENGTH = 7;
    private static final int SHEET_WIDTH = 32;
    private static final int SHEET_HEIGHT = 116;
    
    /**
     * 创建Excel
     */
    private void makeXls()
    {
        Workbook workbook = null;
        try
        {
            // 构建Workbook对象, 只读Workbook对象
            // 直接从本地文件创建Workbook, 从输入流创建Workbook
            InputStream ins = new FileInputStream("D:/Workspace/testproj/source.xls");
            workbook = Workbook.getWorkbook(ins);

            // 利用已经创建的Excel工作薄创建新的可写入的Excel工作薄
            File outFile = new File("D:/Workspace/testproj/test.xls");
            WritableWorkbook wwb = Workbook.createWorkbook(outFile, workbook);
            // 读取第一张工作表
            WritableSheet dataSheet = wwb.getSheet(0);
            //  设置冻结单元格
            dataSheet.getSettings().setVerticalFreeze(7);
            dataSheet.getSettings().setHorizontalFreeze(2);
            
            // 测试模拟数据
            Vector vecData = new Vector();
            for(int i = 0; i < 50; i ++)
            {
                VireObj obj = new VireObj();
                obj.setOrgNo("00" + i + "0");
                obj.setOrgName("机构" + (i + 1));
                obj.setOpenAcc((int)(100 * Math.random()));
                obj.setDestoryAcc((int)(10 * Math.random()));
                obj.setTotalAcc((int)(500 * Math.random()));
                obj.setMonthInCount((int)(500 * Math.random()));
                obj.setMonthInMoney(500 * Math.random());
                obj.setMonthOutCount((int)(500 * Math.random()));
                obj.setMonthOutMoney(500 * Math.random());
                
                vecData.add(obj);
            }            
            // 插入数据
            insertData(wwb, dataSheet, vecData);            
            // 插入模拟图像数据
            Vector vecImg = new Vector();
            for(int i = 0; i < 3; i ++)
            {
                ChartImg img = new ChartImg();
                img.setImgTitle("图像" + (i + 1));
                img.setImgName("D:/Workspace/testproj/images/barchart.png");
                vecImg.add(img);
            }
            // 插入图表
            insertImgsheet(wwb, vecImg);
            //写入Excel对象
            wwb.write();
            wwb.close();
        } catch (Exception e)
        {
            YTLogger.logDebug(e);
        } finally
        {
            // 操作完成时,关闭对象,释放占用的内存空间
            workbook.close();
        }
    }
    
    /**
     * 插入数据
     * @param wwb WritableWorkbook : 工作簿
     * @param dataSheet WritableSheet : 工作表
     * @throws RowsExceededException
     * @throws WriteException
     */
    private void insertData(WritableWorkbook wwb, WritableSheet dataSheet, Vector vecData) throws RowsExceededException, WriteException
    {
        // 获得标题单元格对象        
        modiStrCell(dataSheet, 2, 0, "工商银行江苏省分行 个人网上银行业务种类/开销户明细报表(2005-12)", null);
        // 修改数据单元格数据
        for(int i = 0; i < vecData.size(); i ++)
        {
            VireObj obj = (VireObj)vecData.get(i);
            modiStrCell(dataSheet, 0, TITLE_LENGTH + i, obj.getOrgNo(), null);
            modiStrCell(dataSheet, 1, TITLE_LENGTH + i, obj.getOrgName(), null);
            modiNumCell(dataSheet, 2, TITLE_LENGTH + i, obj.getOpenAcc(), null);
            modiNumCell(dataSheet, 3, TITLE_LENGTH + i, obj.getDestoryAcc(), null);
            modiNumCell(dataSheet, 4, TITLE_LENGTH + i, obj.getTotalAcc(), null);
            modiNumCell(dataSheet, 5, TITLE_LENGTH + i, obj.getMonthInCount(), null);
            modiNumCell(dataSheet, 6, TITLE_LENGTH + i, obj.getTotalInMoney(), null);
            modiNumCell(dataSheet, 7, TITLE_LENGTH + i, obj.getMonthOutCount(), null);
            modiNumCell(dataSheet, 8, TITLE_LENGTH + i, obj.getMonthOutMoney(), null);
        }    
        // 删除空行
        for (int j = vecData.size() + TITLE_LENGTH; j < SHEET_HEIGHT; j++)
        {
            dataSheet.removeRow(vecData.size() + TITLE_LENGTH);
        }        
        // 插入公式
        for(int i = 2; i < SHEET_WIDTH; i ++)
        {
            modiFormulaCell(dataSheet, i, vecData.size() + TITLE_LENGTH, 8, vecData.size() + TITLE_LENGTH, null);
        }        
    }

    /**
     * 修改字符单元格的值
     * @param dataSheet WritableSheet : 工作表
     * @param col int : 列
     * @param row int : 行
     * @param str String : 字符
     * @param format CellFormat : 单元格的样式
     * @throws RowsExceededException
     * @throws WriteException
     */
    private void modiStrCell(WritableSheet dataSheet, int col, int row, String str, CellFormat format) throws RowsExceededException, WriteException
    {
        // 获得单元格对象
        WritableCell cell = dataSheet.getWritableCell(col, row);
        // 判断单元格的类型, 做出相应的转化
        if (cell.getType() == CellType.EMPTY)
        {
            Label lbl = new Label(col, row, str);
            if(null != format)
            {
                lbl.setCellFormat(format);
            } else
            {
                lbl.setCellFormat(cell.getCellFormat());
            }
            dataSheet.addCell(lbl);
        } else if (cell.getType() == CellType.LABEL)
        {
            Label lbl = (Label)cell;
            lbl.setString(str);
        } else if (cell.getType() == CellType.NUMBER)
        {
            // 数字单元格修改
            Number n1 = (Number)cell;
            n1.setValue(42.05);
        }
    }
    
    /**
     * 修改数字单元格的值
     * @param dataSheet WritableSheet : 工作表
     * @param col int : 列
     * @param row int : 行
     * @param num double : 数值
     * @param format CellFormat : 单元格的样式
     * @throws RowsExceededException
     * @throws WriteException
     */
    private void modiNumCell(WritableSheet dataSheet, int col, int row, double num, CellFormat format) throws RowsExceededException, WriteException
    {
        // 获得单元格对象
        WritableCell cell = dataSheet.getWritableCell(col, row);
        // 判断单元格的类型, 做出相应的转化
        if (cell.getType() == CellType.EMPTY)
        {
            Number lbl = new Number(col, row, num);
            if(null != format)
            {
                lbl.setCellFormat(format);
            } else
            {
                lbl.setCellFormat(cell.getCellFormat());
            }
            dataSheet.addCell(lbl);
        } else if (cell.getType() == CellType.NUMBER)
        {
            // 数字单元格修改
            Number lbl = (Number)cell;
            lbl.setValue(num);
        } else if (cell.getType() == CellType.LABEL)
        {
            Label lbl = (Label)cell;
            lbl.setString(String.valueOf(num));
        }
    }
    
    /**
     * 修改公式单元格的值
     * @param dataSheet WritableSheet : 工作表
     * @param col int : 列
     * @param row int : 行
     * @param startPos int : 开始位置
     * @param endPos int : 结束位置
     * @param format
     * @throws RowsExceededException
     * @throws WriteException
     */
    private void modiFormulaCell(WritableSheet dataSheet, int col, int row, int startPos, int endPos, CellFormat format) throws RowsExceededException, WriteException
    {
        String f = getFormula(col, row, startPos, endPos);
        // 插入公式(只支持插入,不支持修改)
        WritableCell cell = dataSheet.getWritableCell(col, row);
        if (cell.getType() == CellType.EMPTY)
        {                    
            // 公式单元格
            Formula lbl = new Formula(col, row, f);
            if(null != format)
            {
                lbl.setCellFormat(format);
            } else
            {
                lbl.setCellFormat(cell.getCellFormat());
            }
            dataSheet.addCell(lbl);
        } else if (cell.getType() == CellType.STRING_FORMULA)
        {
            YTLogger.logWarn("Formula modify not supported!");
        }
    }
    
    /**
     * 得到公式
     * @param col int : 列
     * @param row int : 行
     * @param startPos int : 开始位置
     * @param endPos int : 结束位置
     * @return String
     * @throws RowsExceededException
     * @throws WriteException
     */
    private String getFormula(int col, int row, int startPos, int endPos)
            throws RowsExceededException, WriteException
    {
        char base = 'A';
        char c1 = base;
        StringBuffer formula = new StringBuffer(128);
        // 组装公式
        formula.append("SUM(");
        if (col <= 25)
        {
            c1 = (char) (col % 26 + base);
            formula.append(c1).append(startPos).append(":")
                   .append(c1).append(endPos).append(")");
        } else if (col > 25)
        {
            char c2 = (char) ((col - 26) / 26 + base);
            c1 = (char) ((col - 26) % 26 + base);
            formula.append(c2).append(c1).append(startPos).append(":")
                   .append(c2).append(c1).append(endPos).append(")");
        }

        return formula.toString();
    }
    
    /**
     * 插入图表工作表
     * @param wwb WritableWorkbook : 工作簿
     * @param vecImg Vector : 图像链表
     * @throws RowsExceededException
     * @throws WriteException
     */
    private void insertImgsheet(WritableWorkbook wwb, Vector vecImg)
            throws RowsExceededException, WriteException
    {
        // 插入图像
        WritableSheet imgSheet;
        if((wwb.getSheets()).length < 2)
        {
            imgSheet = wwb.createSheet("图表", 1);
        } else
        {
            imgSheet = wwb.getSheet(1);
        }
        
        for (int i = 0; i < vecImg.size(); i++)
        {
            ChartImg chart = (ChartImg) vecImg.get(i);
            // 插入图像标题
            Label lbl = new Label(0, 2 + 20 * i, chart.getImgTitle());
            WritableFont font = new WritableFont(WritableFont.ARIAL,
                    WritableFont.DEFAULT_POINT_SIZE, WritableFont.NO_BOLD, false,
                    UnderlineStyle.NO_UNDERLINE, Colour.DARK_BLUE2);
            WritableCellFormat background = new WritableCellFormat(font);
            background.setWrap(true);
            background.setBackground(Colour.GRAY_25);
            imgSheet.mergeCells(0, 2 + 20 * i, 9, 2 + 20 * i);
            lbl.setCellFormat(background);
            imgSheet.addCell(lbl);
            // 插入图像单元格
            insertImgCell(imgSheet, 2, 4 + 20 * i, 8, 15, chart.getImgName());
        }
    }

    /**
     * 插入图像到单元格(图像格式只支持png)
     * @param dataSheet WritableSheet : 工作表
     * @param col int : 列
     * @param row int : 行
     * @param width int : 宽
     * @param height int : 高
     * @param imgName String : 图像的全路径
     * @throws RowsExceededException
     * @throws WriteException
     */
    private void insertImgCell(WritableSheet dataSheet, int col, int row, int width,
            int height, String imgName) throws RowsExceededException, WriteException
    {
        File imgFile = new File(imgName);
        WritableImage img = new WritableImage(col, row, width, height, imgFile);
        dataSheet.addImage(img);
    }
    
    /**
     * 测试
     * @param args
     */
    public static void main(String[] args)
    {
        XLSDemo demo = new XLSDemo();
        demo.makeXls();
    }
}

posted @ 2006-09-09 12:23 boddi 阅读(2959) | 评论 (0)编辑 收藏

jxl不错,简单易用

import jxl.*;
import jxl.write.*;
import java.io.*;
import java.io.File.*;
import java.util.*;



public class excel
{
public static void main(String[] args)
{

String targetfile = "c:/out.xls";//输出的excel文件名
String worksheet = "List";//输出的excel文件工作表名
String[] title = {"ID","NAME","DESCRIB"};//excel工作表的标题


WritableWorkbook workbook;
try
{
//创建可写入的Excel工作薄,运行生成的文件在tomcat/bin下
//workbook = Workbook.createWorkbook(new File("output.xls"));
System.out.println("begin");

OutputStream os=new FileOutputStream(targetfile);
workbook=Workbook.createWorkbook(os);

WritableSheet sheet = workbook.createSheet(worksheet, 0); //添加第一个工作表
//WritableSheet sheet1 = workbook.createSheet("MySheet1", 1); //可添加第二个工作
/*
jxl.write.Label label = new jxl.write.Label(0, 2, "A label record"); //put a label in cell A3, Label(column,row)
sheet.addCell(label);
*/

jxl.write.Label label;
for (int i=0; i<title.length; i++)
{
//Label(列号,行号 ,内容 )
label = new jxl.write.Label(i, 0, title[i]); //put the title in row1
sheet.addCell(label);
}




//下列添加的对字体等的设置均调试通过,可作参考用


//添加数字
jxl.write.Number number = new jxl.write.Number(3, 4, 3.14159); //put the number 3.14159 in cell D5
sheet.addCell(number);

//添加带有字型Formatting的对象
jxl.write.WritableFont wf = new jxl.write.WritableFont(WritableFont.TIMES,10,WritableFont.BOLD,true);
jxl.write.WritableCellFormat wcfF = new jxl.write.WritableCellFormat(wf);
jxl.write.Label labelCF = new jxl.write.Label(4,4,"文本",wcfF);
sheet.addCell(labelCF);

//添加带有字体颜色,带背景颜色 Formatting的对象
jxl.write.WritableFont wfc = new jxl.write.WritableFont(WritableFont.ARIAL,10,WritableFont.BOLD,false,jxl.format.UnderlineStyle.NO_UNDERLINE,jxl.format.Colour.RED);
jxl.write.WritableCellFormat wcfFC = new jxl.write.WritableCellFormat(wfc);
wcfFC.setBackground(jxl.format.Colour.BLUE);
jxl.write.Label labelCFC = new jxl.write.Label(1,5,"带颜色",wcfFC);
sheet.addCell(labelCFC);

//添加带有formatting的Number对象
jxl.write.NumberFormat nf = new jxl.write.NumberFormat("#.##");
jxl.write.WritableCellFormat wcfN = new jxl.write.WritableCellFormat(nf);
jxl.write.Number labelNF = new jxl.write.Number(1,1,3.1415926,wcfN);
sheet.addCell(labelNF);

//3.添加Boolean对象
jxl.write.Boolean labelB = new jxl.write.Boolean(0,2,false);
sheet.addCell(labelB);

//4.添加DateTime对象
jxl.write.DateTime labelDT = new jxl.write.DateTime(0,3,new java.util.Date());
sheet.addCell(labelDT);

//添加带有formatting的DateFormat对象
jxl.write.DateFormat df = new jxl.write.DateFormat("ddMMyyyyhh:mm:ss");
jxl.write.WritableCellFormat wcfDF = new jxl.write.WritableCellFormat(df);
jxl.write.DateTime labelDTF = new jxl.write.DateTime(1,3,new java.util.Date(),wcfDF);
sheet.addCell(labelDTF);

//和宾单元格
//sheet.mergeCells(int col1,int row1,int col2,int row2);//左上角到右下角
sheet.mergeCells(4,5,8,10);//左上角到右下角
wfc = new jxl.write.WritableFont(WritableFont.ARIAL,40,WritableFont.BOLD,false,jxl.format.UnderlineStyle.NO_UNDERLINE,jxl.format.Colour.GREEN);
jxl.write.WritableCellFormat wchB = new jxl.write.WritableCellFormat(wfc);
wchB.setAlignment(jxl.format.Alignment.CENTRE);
labelCFC = new jxl.write.Label(4,5,"单元合并",wchB);
sheet.addCell(labelCFC); //


//设置边框
jxl.write.WritableCellFormat wcsB = new jxl.write.WritableCellFormat();
wcsB.setBorder(jxl.format.Border.ALL,jxl.format.BorderLineStyle.THICK);
labelCFC = new jxl.write.Label(0,6,"边框设置",wcsB);
sheet.addCell(labelCFC);
workbook.write();
workbook.close();
}catch(Exception e)
{
e.printStackTrace();
}
System.out.println("end");
Runtime r=Runtime.getRuntime();
Process p=null;
//String cmd[]={"notepad","exec.java"};
String cmd[]={"C:\\Program Files\\Microsoft Office\\Office\\EXCEL.EXE","out.xls"};
try{
p=r.exec(cmd);
}
catch(Exception e){
System.out.println("error executing: "+cmd[0]);
}


}
}

posted @ 2006-09-09 11:20 boddi 阅读(7001) | 评论 (5)编辑 收藏

2006年9月7日

Java中合并XML文档的设计与实现
作者: 凌宗虎 李先国
出处: 计算机与信息技术
责任编辑: 方舟
[ 2005-06-09 08:39 ]

  摘 要:介绍了XML应用中合并XML文档的方法与应用,在基于XML的应用中,有着广泛的应用前景。

  关键词:XML文档 解析器 元素

  在XML应用中,最常用也最实用的莫过于XML文件的读写。由于XML语义比较严格,起始标记必须配对,所以合并XML文档并不像合并普通文件那样简单。在JAVA中,如何合并XML文档,下面介绍一种方法。

  设计思想

  应用javax.xml.parsers包中的解析器解析得到两个XML文件的根元素,再采用递归的方式逐一复制被合并文件的元素。


  实现过程

  为了读写XML文件,需要导入如下JAVA包,"//"后为注释说明,笔者的环境是JDK 1.3.1,在JDK 1.4.0中测试也通过。

Import java.io. *; //Java基础包,包含各种IO操作
Import java.util. *; //Java基础包,包含各种标准数据结构操作
Import javax.xml.parsers. *; //XML解析器接口
Import org.w3c.dom. *; //XML的DOM实现
import org.apache.crimson.tree.XmlDocument;//写XML文件要用到
Import javax.xml.transform. *;
Import javax.xml.transform.dom. *;
Import javax.xml.transform.stream. *;

  下面介绍合并XML文档的过程。先说明一下各个方法的作用。方法is Merging()有两个参数(分别是目标XML文件名和被合并的XML文件名),调用JAVA的解析器,获得两个要合并的XML文档的Document结构和根元素,并调用方法duplicate()和方法write To()。当然,在XML文档的合并过程中,可以加入另外的一些判断条件,比如,当被合并XML文档不存在时,将如何处理,等等。

Private Boolean is Merging (String mainFileName, String sub Filename) throws Exception {
 Boolean isOver = false;
 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
 Document Builder db = null;
 Try {
  Db = dbf.newDocumentBuilder ();
 } Catch (ParserConfigurationException pce) {
  System.err.println(pce); //出现异常时,输出异常信息
 }
 Document doc_main = null,doc_vice = null;
 //获取两个XML文件的Document。
 Try {
  Doc_main = db.parse (mainFileName);
  Doc_vice = db.parse (sub Filename);
 } Catch (DOM Exception dom) {
  System.err.println (dom.getMessage ());
 } Catch (Exception ioe) {
  System.err.println (ioe);
 }
 //获取两个文件的根元素。
 Element root_main = doc_main.getDocumentElement ();
 Element root_vice = doc_vice.getDocumentElement ();
 //下面添加被合并文件根节点下的每个元素
 Novelist message Items = root_vice.getChildNodes ();
 Int item_number = messageItems.getLength ();
 //如果去掉根节点下的第一个元素,比如<所属管理系统> ,那么i从3开始。否则i从1开始。
 For (int i=1; i < item_number; i=i+2 ) {
  //调用dupliate(),依次复制被合并XML文档中根节点下的元素。
  Element messageItem = (Element) messageItems.item (i);
  IsOver = dupliate (doc_main, root_main, messageItem);
 }
 //调用 write To(),将合并得到的Document写入目标XML文档。
 Boolean isWritten = write To (doc_main, mainFileName);
 Return isOver && isWritten;
}

  方法dupliate ()有三个参数(分别是目标XML文档的Document,目标XML文档中要添加节点的父节点和被合并XML文档的复制节点),采用递归的形式,将一个XML文档中的元素复制到另一个XML文档中。

Private Boolean dupliate (Document doc_dup, Element father, Element son) throws Exception {
 Boolean is done = false;
 String son_name = son.getNodeName ();
 Element sub ITEM = doc_dup.createElement (son_name);
 //复制节点的属性
 If (son.hasAttributes ()){
  NamedNodeMap attributes = son.getAttributes ();
  For (int i=0; i < attributes.getLength () ; i ++){
   String attribute_name = attributes. Item (i). GetNodeName ();
   String attribute_value = attributes. Item (i). GetNodeValue ();
   SubITEM.setAttribute (attribute_name, attribute_value);
  }
 }
 Father.appendChild (sub ITEM);
 //复制节点的值
 Text value son = (Text) son.getFirstChild ();
 String nodevalue_root = "";
 If (value_son! = null && value_son.getLength () > 0) nodevalue_root = (String) value_son.getNodeValue ();
 Text valuenode_root = null;
 If ((nodevalue_root! = null)&&(nodevalue_root.length () > 0)) valuenode_root = doc_dup.createTextNode (nodevalue_root);
 If (valuenode_root! = null && valuenode_root.getLength () > 0) subITEM.appendChild (valuenode_root);
 //复制子结点
 Novelist sub_messageItems = son.getChildNodes ();
 int sub_item_number = sub_messageItems.getLength();
 if (sub_item_number < 2){
  //如果没有子节点,则返回
  Is done = true;
 }
 Else {
  For (int j = 1; j < sub_item_number; j=j+2) {
   //如果有子节点,则递归调用本方法
   Element sub_messageItem = (Element) sub_messageItems.item (j);
   Is done = dupliate (doc_dup, subITEM, sub_messageItem);
  }
 }
 Return is done;
}

  方法writeTo()有两个参数(分别是目标XML文档的Document和文件名),将所得目标XML文档写入文件。

Private Boolean write To (Document doc, String fileName) throws Exception {
 Boolean isOver = false;
 DOM Source doms = new DOM Source (doc);
 File f = new File (fileName);
 Stream Result sr = new Stream Result (f);
 Try
 {
  Transformer Factory tf=TransformerFactory.newInstance ();
  Transformer t=tf.newTransformer ();
  Properties properties = t.getOutputProperties ();
  Properties.setProperty (OutputKeys.ENCODING,"GB2312");
  T.setOutputProperties (properties);
  T.transform (doms, sr);
  IsOver = true;
 }
 Catch (TransformerConfigurationException tce)
 {
  Tce.printStackTrace ();
 }
 Catch (Transformer Exception te)
 {
  Te.printStackTrace ();
 }
 Return isOver;
}

  最后使用测试函数进行测试。对于两个已经存在的XML文件(比如,存在文件D:/a.xml和D:/b.xml,要将b.xml合并到a.xml中),可以测试如下:

Public static void main (String [] args) throws Exception {
 Boolean is done = is Merging ("D:/a.xml","D:/b.xml");
 If (is Done) System.out.println ("XML files have been merged.");
 Else System.out.println ("XML files have NOT been merged.");
}

  总结

  本文介绍了如何利用JAVA中的XML解析器,合并两个XML文档。当然,在合并的过程中,还可以加入其他的约束条件,比如要求过滤掉特定的元素等。另外,复制元素的插入位置也可以加以限制。

posted @ 2006-09-07 15:25 boddi 阅读(982) | 评论 (0)编辑 收藏

2006年9月6日

在应用中加入全文检索功能
    ——基于Java的全文索引引擎Lucene简介

作者: 车东 Email: chedongATbigfoot.com/chedongATchedong.com

写于:2002/08 最后更新: 02/22/2006 14:42:55
Feed Back >> (Read this before you ask question)

版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明
http://www.chedong.com/tech/lucene.html

关键词:Lucene java full-text search engine Chinese word segment

内容摘要:

Lucene是一个基于Java的全文索引工具包。

  1. 基于Java的全文索引引擎Lucene简介:关于作者和Lucene的历史
  2. 全文检索的实现:Luene全文索引和数据库索引的比较
  3. 中文切分词机制简介:基于词库和自动切分词算法的比较
  4. 具体的安装和使用简介:系统结构介绍和演示
  5. Hacking Lucene:简化的查询分析器,删除的实现,定制的排序,应用接口的扩展
  6. 从Lucene我们还可以学到什么

基于Java的全文索引/检索引擎——Lucene

Lucene不是一个完整的全文索引应用,而是是一个用Java写的全文索引引擎工具包,它可以方便的嵌入到各种应用中实现针对应用的全文索引/检索功能。

Lucene的作者:Lucene的贡献者Doug Cutting是一位资深全文索引/检索专家,曾经是V-Twin搜索引擎(Apple的Copland操作系统的成就之一)的主要开发者,后在Excite担任高级系统架构设计师,目前从事于一些INTERNET底层架构的研究。他贡献出的Lucene的目标是为各种中小型应用程序加入全文检索功能。

Lucene的发展历程:早先发布在作者自己的www.lucene.com,后来发布在SourceForge,2001年年底成为APACHE基金会jakarta的一个子项目:http://jakarta.apache.org/lucene/

已经有很多Java项目都使用了Lucene作为其后台的全文索引引擎,比较著名的有:

  • J ive:WEB论坛系统;
  • Eyebrows:邮件列表HTML归档/浏览/查询系统,本文的主要参考文档“TheLucene search engine: Powerful, flexible, and free”作者就是EyeBrows系统的主要开发者之一,而EyeBrows已经成为目前APACHE项目的主要邮件列表归档系统。
  • Cocoon:基于XML的web发布框架,全文检索部分使用了Lucene
  • Eclipse:基于Java的开放开发平台,帮助部分的全文索引使用了Lucene

对于中文用户来说,最关心的问题是其是否支持中文的全文检索。但通过后面对于Lucene的结构的介绍,你会了解到由于Lucene良好架构设计,对中文的支持只需对其语言词法分析接口进行扩展就能实现对中文检索的支持。

全文检索的实现机制

Lucene的API接口设计的比较通用,输入输出结构都很像数据库的表==>记录==>字段,所以很多传统的应用的文件、数据库等都可以比较方便的映射到Lucene的存储结构/接口中。总体上看:可以先把Lucene当成一个支持全文索引的数据库系统

比较一下Lucene和数据库:

Lucene 数据库
索引数据源:doc(field1,field2...) doc(field1,field2...)
\ indexer /
_____________
| Lucene Index|
--------------
/ searcher \
结果输出:Hits(doc(field1,field2) doc(field1...))
 索引数据源:record(field1,field2...) record(field1..)
\ SQL: insert/
_____________
| DB Index |
-------------
/ SQL: select \
结果输出:results(record(field1,field2..) record(field1...))
Document:一个需要进行索引的“单元”
一个Document由多个字段组成
Record:记录,包含多个字段
Field:字段 Field:字段
Hits:查询结果集,由匹配的Document组成 RecordSet:查询结果集,由多个Record组成

全文检索 ≠ like "%keyword%"

通常比较厚的书籍后面常常附关键词索引表(比如:北京:12, 34页,上海:3,77页……),它能够帮助读者比较快地找到相关内容的页码。而数据库索引能够大大提高查询的速度原理也是一样,想像一下通过书后面的索引查找的速度要比一页一页地翻内容高多少倍……而索引之所以效率高,另外一个原因是它是排好序的。对于检索系统来说核心是一个排序问题

由于数据库索引不是为全文索引设计的,因此,使用like "%keyword%"时,数据库索引是不起作用的,在使用like查询时,搜索过程又变成类似于一页页翻书的遍历过程了,所以对于含有模糊查询的数据库服务来说,LIKE对性能的危害是极大的。如果是需要对多个关键词进行模糊匹配:like"%keyword1%" and like "%keyword2%" ...其效率也就可想而知了。

所以建立一个高效检索系统的关键是建立一个类似于科技索引一样的反向索引机制,将数据源(比如多篇文章)排序顺序存储的同时,有另外一个排好序的关键词列表,用于存储关键词==>文章映射关系,利用这样的映射关系索引:[关键词==>出现关键词的文章编号,出现次数(甚至包括位置:起始偏移量,结束偏移量),出现频率],检索过程就是把模糊查询变成多个可以利用索引的精确查询的逻辑组合的过程。从而大大提高了多关键词查询的效率,所以,全文检索问题归结到最后是一个排序问题。

由此可以看出模糊查询相对数据库的精确查询是一个非常不确定的问题,这也是大部分数据库对全文检索支持有限的原因。Lucene最核心的特征是通过特殊的索引结构实现了传统数据库不擅长的全文索引机制,并提供了扩展接口,以方便针对不同应用的定制。

可以通过一下表格对比一下数据库的模糊查询:

  Lucene全文索引引擎 数据库
索引 将数据源中的数据都通过全文索引一一建立反向索引 对于LIKE查询来说,数据传统的索引是根本用不上的。数据需要逐个便利记录进行GREP式的模糊匹配,比有索引的搜索速度要有多个数量级的下降。
匹配效果 通过词元(term)进行匹配,通过语言分析接口的实现,可以实现对中文等非英语的支持。 使用:like "%net%" 会把netherlands也匹配出来,
多个关键词的模糊匹配:使用like "%com%net%":就不能匹配词序颠倒的xxx.net..xxx.com
匹配度 有匹配度算法,将匹配程度(相似度)比较高的结果排在前面。 没有匹配程度的控制:比如有记录中net出现5词和出现1次的,结果是一样的。
结果输出 通过特别的算法,将最匹配度最高的头100条结果输出,结果集是缓冲式的小批量读取的。 返回所有的结果集,在匹配条目非常多的时候(比如上万条)需要大量的内存存放这些临时结果集。
可定制性 通过不同的语言分析接口实现,可以方便的定制出符合应用需要的索引规则(包括对中文的支持) 没有接口或接口复杂,无法定制
结论 高负载的模糊查询应用,需要负责的模糊查询的规则,索引的资料量比较大 使用率低,模糊匹配规则简单或者需要模糊查询的资料量少

全文检索和数据库应用最大的不同在于:让 最相关的 头100条结果满足98%以上用户的需求

Lucene的创新之处:

大部分的搜索(数据库)引擎都是用B树结构来维护索引,索引的更新会导致大量的IO操作,Lucene在实现中,对此稍微有所改进:不是维护一个索引文件,而是在扩展索引的时候不断创建新的索引文件,然后定期的把这些新的小索引文件合并到原先的大索引中(针对不同的更新策略,批次的大小可以调整),这样在不影响检索的效率的前提下,提高了索引的效率。

Lucene和其他一些全文检索系统/应用的比较:

  Lucene 其他开源全文检索系统
增量索引和批量索引 可以进行增量的索引(Append),可以对于大量数据进行批量索引,并且接口设计用于优化批量索引和小批量的增量索引。 很多系统只支持批量的索引,有时数据源有一点增加也需要重建索引。
数据源 Lucene没有定义具体的数据源,而是一个文档的结构,因此可以非常灵活的适应各种应用(只要前端有合适的转换器把数据源转换成相应结构), 很多系统只针对网页,缺乏其他格式文档的灵活性。
索引内容抓取 Lucene的文档是由多个字段组成的,甚至可以控制那些字段需要进行索引,那些字段不需要索引,近一步索引的字段也分为需要分词和不需要分词的类型:
   需要进行分词的索引,比如:标题,文章内容字段
   不需要进行分词的索引,比如:作者/日期字段
缺乏通用性,往往将文档整个索引了
语言分析 通过语言分析器的不同扩展实现:
可以过滤掉不需要的词:an the of 等,
西文语法分析:将jumps jumped jumper都归结成jump进行索引/检索
非英文支持:对亚洲语言,阿拉伯语言的索引支持
缺乏通用接口实现
查询分析 通过查询分析接口的实现,可以定制自己的查询语法规则:
比如: 多个关键词之间的 + - and or关系等
 
并发访问 能够支持多用户的使用  

 

关于亚洲语言的的切分词问题(Word Segment)

对于中文来说,全文索引首先还要解决一个语言分析的问题,对于英文来说,语句中单词之间是天然通过空格分开的,但亚洲语言的中日韩文语句中的字是一个字挨一个,所有,首先要把语句中按“词”进行索引的话,这个词如何切分出来就是一个很大的问题。

首先,肯定不能用单个字符作(si-gram)为索引单元,否则查“上海”时,不能让含有“海上”也匹配。

但一句话:“北京天安门”,计算机如何按照中文的语言习惯进行切分呢?
“北京 天安门” 还是“北 京 天安门”?让计算机能够按照语言习惯进行切分,往往需要机器有一个比较丰富的词库才能够比较准确的识别出语句中的单词。

另外一个解决的办法是采用自动切分算法:将单词按照2元语法(bigram)方式切分出来,比如:
"北京天安门" ==> "北京 京天 天安 安门"。

这样,在查询的时候,无论是查询"北京" 还是查询"天安门",将查询词组按同样的规则进行切分:"北京","天安安门",多个关键词之间按与"and"的关系组合,同样能够正确地映射到相应的索引中。这种方式对于其他亚洲语言:韩文,日文都是通用的。

基于自动切分的最大优点是没有词表维护成本,实现简单,缺点是索引效率低,但对于中小型应用来说,基于2元语法的切分还是够用的。基于2元切分后的索引一般大小和源文件差不多,而对于英文,索引文件一般只有原文件的30%-40%不同,


自动切分 词表切分
实现 实现非常简单 实现复杂
查询 增加了查询分析的复杂程度, 适于实现比较复杂的查询语法规则
存储效率 索引冗余大,索引几乎和原文一样大 索引效率高,为原文大小的30%左右
维护成本 无词表维护成本 词表维护成本非常高:中日韩等语言需要分别维护。
还需要包括词频统计等内容
适用领域 嵌入式系统:运行环境资源有限
分布式系统:无词表同步问题
多语言环境:无词表维护成本
对查询和存储效率要求高的专业搜索引擎

目前比较大的搜索引擎的语言分析算法一般是基于以上2个机制的结合。关于中文的语言分析算法,大家可以在Google查关键词"wordsegment search"能找到更多相关的资料。

安装和使用

下载:http://jakarta.apache.org/lucene/

注意:Lucene中的一些比较复杂的词法分析是用JavaCC生成的(JavaCC:JavaCompilerCompiler,纯Java的词法分析生成器),所以如果从源代码编译或需要修改其中的QueryParser、定制自己的词法分析器,还需要从https://javacc.dev.java.net/下载javacc。

lucene的组成结构:对于外部应用来说索引模块(index)和检索模块(search)是主要的外部应用入口

org.apache.Lucene.search/ 搜索入口
org.apache.Lucene.index/ 索引入口
org.apache.Lucene.analysis/ 语言分析器
org.apache.Lucene.queryParser/ 查询分析器
org.apache.Lucene.document/ 存储结构
org.apache.Lucene.store/  底层IO/存储结构
org.apache.Lucene.util/ 一些公用的数据结构

简单的例子演示一下Lucene的使用方法:

索引过程:从命令行读取文件名(多个),将文件分路径(path字段)和内容(body字段)2个字段进行存储,并对内容进行全文索引:索引的单位是Document对象,每个Document对象包含多个字段Field对象,针对不同的字段属性和数据输出的需求,对字段还可以选择不同的索引/存储字段规则,列表如下:
方法切词索引存储用途
Field.Text(String name, String value)YesYesYes切分词索引并存储,比如:标题,内容字段
Field.Text(String name, Reader value)YesYesNo切分词索引不存储,比如:META信息,
不用于返回显示,但需要进行检索内容
Field.Keyword(String name, String value)NoYesYes不切分索引并存储,比如:日期字段
Field.UnIndexed(String name, String value)NoNoYes不索引,只存储,比如:文件路径
Field.UnStored(String name, String value)YesYesNo只全文索引,不存储
public class IndexFiles { 
//使用方法:: IndexFiles [索引输出目录] [索引的文件列表] ...
public static void main(String[] args) throws Exception {
String indexPath = args[0];
IndexWriter writer;
//用指定的语言分析器构造一个新的写索引器(第3个参数表示是否为追加索引)
writer = new IndexWriter(indexPath, new SimpleAnalyzer(), false);

for (int i=1; i<args.length; i++) {
System.out.println("Indexing file " + args[i]);
InputStream is = new FileInputStream(args[i]);

//构造包含2个字段Field的Document对象
//一个是路径path字段,不索引,只存储
//一个是内容body字段,进行全文索引,并存储
Document doc = new Document();
doc.add(Field.UnIndexed("path", args[i]));
doc.add(Field.Text("body", (Reader) new InputStreamReader(is)));
//将文档写入索引
writer.addDocument(doc);
is.close();
};
//关闭写索引器
writer.close();
}
}
 

索引过程中可以看到:

  • 语言分析器提供了抽象的接口,因此语言分析(Analyser)是可以定制的,虽然lucene缺省提供了2个比较通用的分析器SimpleAnalyser和StandardAnalyser,这2个分析器缺省都不支持中文,所以要加入对中文语言的切分规则,需要修改这2个分析器。
  • Lucene并没有规定数据源的格式,而只提供了一个通用的结构(Document对象)来接受索引的输入,因此输入的数据源可以是:数据库,WORD文档,PDF文档,HTML文档……只要能够设计相应的解析转换器将数据源构造成成Docuement对象即可进行索引。
  • 对于大批量的数据索引,还可以通过调整IndexerWrite的文件合并频率属性(mergeFactor)来提高批量索引的效率。

检索过程和结果显示:

搜索结果返回的是Hits对象,可以通过它再访问Document==>Field中的内容。

假设根据body字段进行全文检索,可以将查询结果的path字段和相应查询的匹配度(score)打印出来,

public class Search { 
public static void main(String[] args) throws Exception {
String indexPath = args[0], queryString = args[1];
//指向索引目录的搜索器
Searcher searcher = new IndexSearcher(indexPath);
//查询解析器:使用和索引同样的语言分析器
Query query = QueryParser.parse(queryString, "body",
new SimpleAnalyzer());
//搜索结果使用Hits存储
Hits hits = searcher.search(query);
//通过hits可以访问到相应字段的数据和查询的匹配度
for (int i=0; i<hits.length(); i++) {
System.out.println(hits.doc(i).get("path") + "; Score: " +
hits.score(i));
};
}
}
在整个检索过程中,语言分析器,查询分析器,甚至搜索器(Searcher)都是提供了抽象的接口,可以根据需要进行定制。

Hacking Lucene

简化的查询分析器

个人感觉lucene成为JAKARTA项目后,画在了太多的时间用于调试日趋复杂QueryParser,而其中大部分是大多数用户并不很熟悉的,目前LUCENE支持的语法:

Query ::= ( Clause )*
Clause ::= ["+", "-"] [<TERM> ":"] ( <TERM> | "(" Query ")")

中间的逻辑包括:and or + - &&||等符号,而且还有"短语查询"和针对西文的前缀/模糊查询等,个人感觉对于一般应用来说,这些功能有一些华而不实,其实能够实现目前类似于Google的查询语句分析功能其实对于大多数用户来说已经够了。所以,Lucene早期版本的QueryParser仍是比较好的选择。

添加修改删除指定记录(Document)

Lucene提供了索引的扩展机制,因此索引的动态扩展应该是没有问题的,而指定记录的修改也似乎只能通过记录的删除,然后重新加入实现。如何删除指定的记录呢?删除的方法也很简单,只是需要在索引时根据数据源中的记录ID专门另建索引,然后利用IndexReader.delete(Termterm)方法通过这个记录ID删除相应的Document。

根据某个字段值的排序功能

lucene缺省是按照自己的相关度算法(score)进行结果排序的,但能够根据其他字段进行结果排序是一个在LUCENE的开发邮件列表中经常提到的问题,很多原先基于数据库应用都需要除了基于匹配度(score)以外的排序功能。而从全文检索的原理我们可以了解到,任何不基于索引的搜索过程效率都会导致效率非常的低,如果基于其他字段的排序需要在搜索过程中访问存储字段,速度回大大降低,因此非常是不可取的。

但这里也有一个折中的解决方法:在搜索过程中能够影响排序结果的只有索引中已经存储的docID和score这2个参数,所以,基于score以外的排序,其实可以通过将数据源预先排好序,然后根据docID进行排序来实现。这样就避免了在LUCENE搜索结果外对结果再次进行排序和在搜索过程中访问不在索引中的某个字段值。

这里需要修改的是IndexSearcher中的HitCollector过程:

...
 scorer.score(new HitCollector() {
private float minScore = 0.0f;
public final void collect(int doc, float score) {
if (score > 0.0f && // ignore zeroed buckets
(bits==null || bits.get(doc))) { // skip docs not in bits
totalHits[0]++;
if (score >= minScore) {
/* 原先:Lucene将docID和相应的匹配度score例入结果命中列表中:
* hq.put(new ScoreDoc(doc, score)); // update hit queue
* 如果用doc 或 1/doc 代替 score,就实现了根据docID顺排或逆排
* 假设数据源索引时已经按照某个字段排好了序,而结果根据docID排序也就实现了
* 针对某个字段的排序,甚至可以实现更复杂的score和docID的拟合。
*/
hq.put(new ScoreDoc(doc, (float) 1/doc ));
if (hq.size() > nDocs) { // if hit queue overfull
hq.pop(); // remove lowest in hit queue
minScore = ((ScoreDoc)hq.top()).score; // reset minScore
}
}
}
}
}, reader.maxDoc());

更通用的输入输出接口

虽然lucene没有定义一个确定的输入文档格式,但越来越多的人想到使用一个标准的中间格式作为Lucene的数据导入接口,然后其他数据,比如PDF只需要通过解析器转换成标准的中间格式就可以进行数据索引了。这个中间格式主要以XML为主,类似实现已经不下4,5个:

数据源: WORD       PDF     HTML    DB       other
\ | | | /
XML中间格式
|
Lucene INDEX

目前还没有针对MSWord文档的解析器,因为Word文档和基于ASCII的RTF文档不同,需要使用COM对象机制解析。这个是我在Google上查的相关资料:http://www.intrinsyc.com/products/enterprise_applications.asp
另外一个办法就是把Word文档转换成text:http://www.winfield.demon.nl/index.html


索引过程优化

索引一般分2种情况,一种是小批量的索引扩展,一种是大批量的索引重建。在索引过程中,并不是每次新的DOC加入进去索引都重新进行一次索引文件的写入操作(文件I/O是一件非常消耗资源的事情)。

Lucene先在内存中进行索引操作,并根据一定的批量进行文件的写入。这个批次的间隔越大,文件的写入次数越少,但占用内存会很多。反之占用内存少,但文件IO操作频繁,索引速度会很慢。在IndexWriter中有一个MERGE_FACTOR参数可以帮助你在构造索引器后根据应用环境的情况充分利用内存减少文件的操作。根据我的使用经验:缺省Indexer是每20条记录索引后写入一次,每将MERGE_FACTOR增加50倍,索引速度可以提高1倍左右。

搜索过程优化

lucene支持内存索引:这样的搜索比基于文件的I/O有数量级的速度提升。
http://www.onjava.com/lpt/a/3273
而尽可能减少IndexSearcher的创建和对搜索结果的前台的缓存也是必要的。

Lucene面向全文检索的优化在于首次索引检索后,并不把所有的记录(Document)具体内容读取出来,而起只将所有结果中匹配度最高的头100条结果(TopDocs)的ID放到结果集缓存中并返回,这里可以比较一下数据库检索:如果是一个10,000条的数据库检索结果集,数据库是一定要把所有记录内容都取得以后再开始返回给应用结果集的。所以即使检索匹配总数很多,Lucene的结果集占用的内存空间也不会很多。对于一般的模糊检索应用是用不到这么多的结果的,头100条已经可以满足90%以上的检索需求。

如果首批缓存结果数用完后还要读取更后面的结果时Searcher会再次检索并生成一个上次的搜索缓存数大1倍的缓存,并再重新向后抓取。所以如果构造一个Searcher去查1-120条结果,Searcher其实是进行了2次搜索过程:头100条取完后,缓存结果用完,Searcher重新检索再构造一个200条的结果缓存,依此类推,400条缓存,800条缓存。由于每次Searcher对象消失后,这些缓存也访问那不到了,你有可能想将结果记录缓存下来,缓存数尽量保证在100以下以充分利用首次的结果缓存,不让Lucene浪费多次检索,而且可以分级进行结果缓存。

Lucene的另外一个特点是在收集结果的过程中将匹配度低的结果自动过滤掉了。这也是和数据库应用需要将搜索的结果全部返回不同之处。

我的一些尝试

  • 支持中文的Tokenizer:这里有2个版本,一个是通过JavaCC生成的,对CJK部分按一个字符一个TOKEN索引,另外一个是从SimpleTokenizer改写的,对英文支持数字和字母TOKEN,对中文按迭代索引。
  • 基于XML数据源的索引器:XMLIndexer,因此所有数据源只要能够按照DTD转换成指定的XML,就可以用XMLIndxer进行索引了。
  • 根据某个字段排序:按记录索引顺序排序结果的搜索器:IndexOrderSearcher,因此如果需要让搜索结果根据某个字段排序,可以让数据源先按某个字段排好序(比如:PriceField),这样索引后,然后在利用这个按记录的ID顺序检索的搜索器,结果就是相当于是那个字段排序的结果了。

从Lucene学到更多

Luene的确是一个面对对象设计的典范

  • 所有的问题都通过一个额外抽象层来方便以后的扩展和重用:你可以通过重新实现来达到自己的目的,而对其他模块而不需要;
  • 简单的应用入口Searcher, Indexer,并调用底层一系列组件协同的完成搜索任务;
  • 所有的对象的任务都非常专一:比如搜索过程:QueryParser分析将查询语句转换成一系列的精确查询的组合(Query),通过底层的索引读取结构IndexReader进行索引的读取,并用相应的打分器给搜索结果进行打分/排序等。所有的功能模块原子化程度非常高,因此可以通过重新实现而不需要修改其他模块。 
  • 除了灵活的应用接口设计,Lucene还提供了一些适合大多数应用的语言分析器实现(SimpleAnalyser,StandardAnalyser),这也是新用户能够很快上手的重要原因之一。

这些优点都是非常值得在以后的开发中学习借鉴的。作为一个通用工具包,Lunece的确给予了需要将全文检索功能嵌入到应用中的开发者很多的便利。

此外,通过对Lucene的学习和使用,我也更深刻地理解了为什么很多数据库优化设计中要求,比如:

  • 尽可能对字段进行索引来提高查询速度,但过多的索引会对数据库表的更新操作变慢,而对结果过多的排序条件,实际上往往也是性能的杀手之一。
  • 很多商业数据库对大批量的数据插入操作会提供一些优化参数,这个作用和索引器的merge_factor的作用是类似的,
  • 20%/80%原则:查的结果多并不等于质量好,尤其对于返回结果集很大,如何优化这头几十条结果的质量往往才是最重要的。
  • 尽可能让应用从数据库中获得比较小的结果集,因为即使对于大型数据库,对结果集的随机访问也是一个非常消耗资源的操作。

参考资料:

Apache: Lucene Project
http://jakarta.apache.org/lucene/
Lucene开发/用户邮件列表归档
Lucene-dev@jakarta.apache.org
Lucene-user@jakarta.apache.org

The Lucene search engine: Powerful, flexible, and free
http://www.javaworld.com/javaworld/jw-09-2000/jw-0915-Lucene_p.html

Lucene Tutorial
http://www.darksleep.com/puff/lucene/lucene.html

Notes on distributed searching with Lucene
http://home.clara.net/markharwood/lucene/

中文语言的切分词
http://www.google.com/search?sourceid=navclient&hl=zh-CN&q=chinese+word+segment

搜索引擎工具介绍
http://searchtools.com/

Lucene作者Cutting的几篇论文和专利
http://lucene.sourceforge.net/publications.html 

Lucene的.NET实现:dotLucene
http://sourceforge.net/projects/dotlucene/

Lucene作者Cutting的另外一个项目:基于Java的搜索引擎Nutch
http://www.nutch.org/   http://sourceforge.net/projects/nutch/

关于基于词表和N-Gram的切分词比较
http://china.nikkeibp.co.jp/cgi-bin/china/news/int/int200302100112.html

2005-01-08 Cutting在Pisa大学做的关于Lucene的讲座:非常详细的Lucene架构解说

posted @ 2006-09-06 13:34 boddi 阅读(419) | 评论 (0)编辑 收藏