每日一得

不求多得,只求一得 about java,hibernate,spring,design,database,Ror,ruby,快速开发
最近关心的内容:SSH,seam,flex,敏捷,TDD
本站的官方站点是:颠覆软件

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  220 随笔 :: 9 文章 :: 421 评论 :: 0 Trackbacks

2006年6月15日 #

     摘要: 你每天经常访问哪些站点?

你的收藏夹里有多少收藏的站点?  阅读全文
posted @ 2008-08-09 14:42 Alex 阅读(2349) | 评论 (2)编辑 收藏

     摘要: 上次参加open party朋友拿了一本《开源技术选型手册》,翻了一下,整理成脑图(图的效果还是比文字好哦)  阅读全文
posted @ 2008-08-06 10:30 Alex 阅读(2462) | 评论 (5)编辑 收藏

     摘要: 从svn版本上checkout出来的新的版本,dwr部分的xml配置文件是红色提示错误,一开始以为是jsp的普通验证错误,后发现不是那么回事。  阅读全文
posted @ 2008-08-04 15:00 Alex 阅读(1531) | 评论 (0)编辑 收藏

1.很多人喜欢用代码开发工具的debug功能来跟踪问题,虽然最终可以解决问题,但是从方向或者思想上不建议这样做,我依然记得最初我的技术经理对我说过 “代码不是调出来的”。

2.不是调出来的那是怎么出来的呢? “写出来的。” 呵呵,别扭,但是想想看,是否有点道理?

3.从另一个层面,我们需要加强代码的规范的写法,这就好比设计,先尽量将设计(在敏捷的年代似乎谈设计不太入流,那么你可以认为那是一种思考吧)做的到位一点

4.回到一个大的命题:战略如果错误,执行的越快死的越快;同理,思路或者方向如果有问题,做的越带劲,你越难受。

5.让我们朝这个目标挺进:代码一次编写就通过! 我见过一些这样的高人,只要你努力,是可以做到的。

posted @ 2008-08-03 20:56 Alex 阅读(2414) | 评论 (23)编辑 收藏

     摘要: 又一阵热风吹过来了,犹如当初的asp,犹如近段时间的SOA  阅读全文
posted @ 2008-08-03 01:02 Alex 阅读(1970) | 评论 (2)编辑 收藏

     摘要:   阅读全文
posted @ 2008-07-03 14:13 Alex 阅读(1568) | 评论 (0)编辑 收藏

     摘要:   阅读全文
posted @ 2008-07-03 14:12 Alex 阅读(2927) | 评论 (1)编辑 收藏

     摘要:   阅读全文
posted @ 2008-07-03 14:11 Alex 阅读(3482) | 评论 (5)编辑 收藏

//本站点内容来自于 颠覆软件

1.机器的ip经常改变,到公司一个ip,到客户那一个ip,到家又是一个ip,不知怎么搞的,今天到家oracle的dbconsole就启动不了了.

2.用命令行的方式启动 emctl.bat start dbconsole,结果还提示ORACLE_SID 还没有设置,晕,这个怎么没有了,set 一个吧 set ORACLE_SID=dggmcc (我的数据库实例)

3.还是有错,提示找不到192.168.0.52.这个地址,应该是ip和当初装数据库的时候IP不一样的缘故,将hosts设置一下 192.168.0.52   IDEA

4.OK

posted @ 2008-07-03 14:10 Alex 阅读(999) | 评论 (0)编辑 收藏

1.架构的分类:业务架构、数据库架构、软件设计架构,正在流行的所谓soa架构也算吧

2. 涉及到的主要内容:

  • 需求分析
  • 领域分析
  • UML建模
  • 文档设计管理
  • 设计模式
  • 基于javaEE5的设计(jsf+EJB3)[个人感觉jboss seam是最好的代表了]

3.今天一同事说在网上看到统计数据说基于soa的架构做的项目比传统的做法成功率确实高,可能性是存在的,不过也不排除这些统计数据是某些大厂赞助的结果 :)

4.今年打算有机会的话培训一下软件设计师的课程,内容摘要如下:(来自国信培训)

一、面向对象分析设计与UML

Ø 面向对象分析与设计OOAD

Ø UML建模与ICONIX进程

Ø 领域建模与用例建模

Ø 需求分析与UseCase

Ø 健壮性分析

Ø 时序图

Ø 对象图、状态图、活动图与协作图

Ø 类图、包图与组件图

Ø 部署图

Ø UMLRUP

Ø 设计进度和设计粒度的控制

二、设计模式

Ø GOF设计模式

Ø 创建型模式

Ø 结构型模式

Ø 行为型模式

Ø 模式扩展

Ø 开源项目中的设计模式

Ø 模式与交流

三、软件设计管理

Ø 软件设计文档

Ø 软件流程改进策略

Ø 软件设计风险管理

四、相关软件设计案例集

posted @ 2008-07-03 14:08 Alex 阅读(1544) | 评论 (0)编辑 收藏

//本站点内容来自于www.foxlog.org


1.中国还没有真正伟大的软件公司,就当自己做一下白日梦了

2.需要一个协同软件,比如oracle协作套件,或者IBM的Lotus,目的,提供邮件、文档、工作空间、门户、网络会议、移动办公等,让公司的所有资源在一个统一的平台上实施;当然,出于成本考虑,我也可以选择google的企业套件,最直接的方式了,而且效果不错,不过没有网络会议和门户功能,可以考虑使用webEx等 (近期,流行的一个观点是在协作平台里引入web2.0的相关技术)

3.需要一个学习平台,比如oracle ilearing.用于员工的入职须知,技能培训,为学习型组织提供一个最有力的帮助,最大的问题可能不是学习平台本身,而是学习内容的规划和设计,必要的时候需要购买 .  学习平台最终的目的就是一是培训员工,另一个就是知识共享。现在很多公司有这么一个毛病,公司运营了很多年,但是随着员工的逐步更换和流失,很多以前精华的东西都不见了,如果主意了知识的共享和积累(通过软件平台),这个问题就是另一种情况了.

4.需要一个 oa平台,主要基于公司的业务流程,目前大多数oa是基于工作流的机制来实现。事实上,oa的发展历程已经比较长了,也相对比较成熟,但是究竟能发挥多大作用或者该如何发挥最大的作用还依然是一个问题。

5.建立一个项目管理系统平台,所有项目都在上面监控,它应该是动态的,可以看到每一天的进度,最好能从上面能对项目的各个组员的工作作出评估,作出即时的沟通和修正。

6.软件开发过程系统:包含架构设计管理、开发动态控制管理、版本控制、测试管理 .这些内容的主要目的是保证开发的规范话、文档化

7.会议系统管理. 每个公司都需要开会,但是开会的层次差别太大了,存在的问题主要有:会议目标不明确,会议过程没有效率,会议结果不明确,会议结束后没有后期跟踪管理.关于这方面可以推荐一篇文章 九段秘书

8.以上好像偏重于技术的层面比较多,实际上一个公司的老总最看重的其实必然是市场,所以一个适合自己公司的ERP或者CRM系统也许是必要的,注意,我这里说的是也许。

9.最后,所有的软件仅仅提供了一个平台,它是“死”的,或者说其本身并不会发生什么作用,更关键的是看不见的“软件”——企业文化,一切的效率与创新都来自于公司的文化氛围,在这个基础上,软件才会有推波助澜的作用。

posted @ 2008-07-03 14:07 Alex 阅读(1040) | 评论 (3)编辑 收藏

     摘要: 很多东西是拿来被利用的,被 Fuck的,不是让你消磨时间的.  阅读全文
posted @ 2007-09-01 12:42 Alex 阅读(2510) | 评论 (5)编辑 收藏

     摘要: 项目,是一个范围很广的概念,三峡大坝是一个项目,IT企业的一次开发任务也是一个项目,个人某个时间段的活动安排仍然可以看作是一个项目.

  阅读全文
posted @ 2007-08-19 13:15 Alex 阅读(1780) | 评论 (7)编辑 收藏

     摘要: 经常用到update语句,不过很少用到其他表的数据来更新当前表,这次用到了,总结一下

  阅读全文
posted @ 2007-08-19 12:49 Alex 阅读(2221) | 评论 (1)编辑 收藏

key words: plsql,导出数据,大数据量

最近的账务的项目中需要导出Excel报表,数据量比较大,解决方案如下 :

1.通过plsql在服务器端查询出符合要求的纪录,然后直接写到文本文件中

2.在web端通过流的方式读文本文件,通过POI写到Excel,将生成的Excel流写到respoonse实现下载

这样做的好处是不必一次性将满足条件的纪录全部取出来,而是通过流的方式.

这个方式原来在处理Blob类型的字段时有直接的getStream来支持的,这次普通的数据只好通过文件的方式来实现.

贴一段sample的代码


sql 代码
 
  1. create or replace procedure alex_table_to_txt(filepath varchar2,filename varchar2) is  
  2. --var  
  3. v1 alex2.name%type;  
  4. v2 alex2.address%type;  
  5. v3 alex2.groupname%type;  
  6.   
  7. output varchar2(200);  
  8.   
  9. --file var  
  10. file_handle UTL_FILE.FILE_TYPE;  
  11.   
  12. cursor readtable is select t.name,t.address,t.groupname from alex2 t;  
  13. begin  
  14. file_handle:=utl_file.fopen(filepath,filename,'w',6000);  
  15. open readtable;  
  16.   
  17. loop  
  18. fetch readtable into v1,v2,v3;  
  19. exit when readtable%notfound;  
  20. output:=v1 || ',' || v2 || ',' || v3 ;  
  21. utl_file.put_line(file_handle,output);  
  22.   
  23. end loop;  
  24.   
  25. close readtable;  
  26. utl_file.fclose(file_handle);  
  27. end alex_table_to_txt;  
  28. /  

 

欢迎讨论你的方案

posted @ 2007-08-18 12:46 Alex 阅读(4230) | 评论 (1)编辑 收藏

     摘要: 一般来说,我们碰到问题都是无非google,看相关文档,请教专家,问圈子里的朋友,等等。我这里说的主要是个人自己的解决思路。

  阅读全文
posted @ 2007-08-18 12:17 Alex 阅读(1336) | 评论 (2)编辑 收藏

好久没有来这里了,blogjava的朋友们你们还好吗。

我的 blog搬家了,以后将主要在这里维护: www.foxlog.org 

需要和我交换连接的朋友请联系我 mail to me :  idea.wang@gmail.com,或者在我 blog留言
http://www.foxlog.org/%E7%BB%99%E6%88%91%E7%95%99%E8%A8%80
posted @ 2007-07-27 15:21 Alex 阅读(643) | 评论 (0)编辑 收藏

1.查看系统Swap空间使用

[root@jumper usr]# free
             total       used       free     shared    buffers     cached
Mem:        513980     493640      20340          0     143808     271780
-/+ buffers/cache:      78052     435928
Swap:      1052248      21256    1030992

2.在空间合适处创建swap文件

[root@jumper usr]# mkdir swap
[root@jumper usr]# cd swap
[root@jumper swap]# dd if=/dev/zero of=swapfile bs=1024 count=10000
10000+0 records in
10000+0 records out
[root@jumper swap]# ls -al
total 10024
drwxr-xr-x    2 root     root         4096  7月 28 14:58 .
drwxr-xr-x   19 root     root         4096  7月 28 14:57 ..
-rw-r--r--    1 root     root     10240000  7月 28 14:58 swapfile


[root@jumper swap]# mkswap swapfile
Setting up swapspace version 1, size = 9996 KiB


3.激活swap文件
[root@jumper swap]# swapon swapfile
[root@jumper swap]# ls -l
total 10016
-rw-r--r--    1 root     root     10240000  7月 28 14:58 swapfile
[root@jumper swap]# free
             total       used       free     shared    buffers     cached
Mem:        513980     505052       8928          0     143900     282288
-/+ buffers/cache:      78864     435116
Swap:      1062240      21256    1040984
[root@jumper swap]#  

 

 

Swap,即交换区,除了安装Linux的时候,有多少人关心过它呢?其实,Swap的调整对Linux服务器,特别是Web服务器的性能至关重要。通过调整Swap,有时可以越过系统性能瓶颈,节省系统升级费用。 

本文内容包括: 


Swap基本原理 

突破128M Swap限制 

Swap配置对性能的影响 

Swap性能监视 

有关Swap操作的系统命令 
Swap基本原理 

Swap的原理是一个较复杂的问题,需要大量的篇幅来说明。在这里只作简单的介绍,在以后的文章中将和大家详细讨论Swap实现的细节。 

众所周知,现代操作系统都实现了"虚拟内存"这一技术,不但在功能上突破了物理内存的限制,使程序可以操纵大于实际物理内存的空间,更重要的是,"虚拟内存"是隔离每个进程的安全保护网,使每个进程都不受其它程序的干扰。 

Swap 空间的作用可简单描述为:当系统的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间被临时保存到Swap空间中,等到那些程序要运行时,再从Swap中恢复保存的数据到内存中。这样,系统总是在物理内存不够时,才进行Swap交换。 

计算机用户会经常遇这种现象。例如,在使用Windows系统时,可以同时运行多个程序,当你切换到一个很长时间没有理会的程序时,会听到硬盘"哗哗"直响。这是因为这个程序的内存被那些频繁运行的程序给"偷走"了,放到了Swap区中。因此,一旦此程序被放置到前端,它就会从Swap区取回自己的数据,将其放进内存,然后接着运行。 

需要说明一点,并不是所有从物理内存中交换出来的数据都会被放到Swap中(如果这样的话,Swap就会不堪重负),有相当一部分数据被直接交换到文件系统。例如,有的程序会打开一些文件,对文件进行读写(其实每个程序都至少要打开一个文件,那就是运行程序本身),当需要将这些程序的内存空间交换出去时,就没有必要将文件部分的数据放到 Swap空间中了,而可以直接将其放到文件里去。如果是读文件操作,那么内存数据被直接释放,不需要交换出来,因为下次需要时,可直接从文件系统恢复;如果是写文件,只需要将变化的数据保存到文件中,以便恢复。但是那些用malloc和new函数生成的对象的数据则不同,它们需要Swap空间,因为它们在文件系统中没有相应的"储备"文件,因此被称作"匿名"(Anonymous)内存数据。这类数据还包括堆栈中的一些状态和变量数据等。所以说,Swap 空间是"匿名"数据的交换空间。 

突破128M Swap限制 

经常看到有些Linux(国内汉化版)安装手册上有这样的说明:Swap空间不能超过128M。为什么会有这种说法?在说明"128M"这个数字的来历之前,先给问题一个回答:现在根本不存在128M的限制!现在的限制是2G! 

Swap 空间是分页的,每一页的大小和内存页的大小一样,方便Swap空间和内存之间的数据交换。旧版本的Linux实现Swap空间时,用Swap空间的第一页作为所有Swap空间页的一个"位映射"(Bit map)。这就是说第一页的每一位,都对应着一页Swap空间。如果这一位是1,表示此页Swap可用;如果是0,表示此页是坏块,不能使用。这么说来,第一个Swap映射位应该是0,因为,第一页Swap是映射页。另外,最后10个映射位也被占用,用来表示Swap的版本(原来的版本是Swap_space ,现在的版本是swapspace2)。那么,如果说一页的大小为s,这种Swap的实现方法共能管理"8 * ( s - 10 ) - 1"个Swap页。对于i386系统来说s=4096,则空间大小共为133890048,如果认为 1 MB=2^20 Byte的话,大小正好为128M。 

之所以这样来实现Swap空间的管理,是要防止Swap空间中有坏块。如果系统检查到Swap中有坏块,则在相应的位映射上标记上0,表示此页不可用。这样在使用Swap时,不至于用到坏块,而使系统产生错误。 

现在的系统设计者认为: 

1.现在硬盘质量很好,坏块很少。 

2.就算有,也不多,只需要将坏块罗列出来,而不需要为每一页建立映射。 

3.如果有很多坏块,就不应该将此硬盘作为Swap空间使用。 

于是,现在的Linux取消了位映射的方法,也就取消了128M的限制。直接用地址访问,限制为2G。 

Swap配置对性能的影响 

分配太多的Swap空间会浪费磁盘空间,而Swap空间太少,则系统会发生错误。 

如果系统的物理内存用光了,系统就会跑得很慢,但仍能运行;如果Swap空间用光了,那么系统就会发生错误。例如,Web服务器能根据不同的请求数量衍生出多个服务进程(或线程),如果Swap空间用完,则服务进程无法启动,通常会出现"application is out of memory"的错误,严重时会造成服务进程的死锁。因此Swap空间的分配是很重要的。 

通常情况下,Swap空间应大于或等于物理内存的大小,最小不应小于64M,通常Swap空间的大小应是物理内存的2-2.5倍。但根据不同的应用,应有不同的配置:如果是小的桌面系统,则只需要较小的Swap空间,而大的服务器系统则视情况不同需要不同大小的Swap空间。特别是数据库服务器和Web服务器,随着访问量的增加,对Swap空间的要求也会增加,具体配置参见各服务器产品的说明。 

另外,Swap分区的数量对性能也有很大的影响。因为Swap交换的操作是磁盘IO的操作,如果有多个 Swap交换区,Swap空间的分配会以轮流的方式操作于所有的Swap,这样会大大均衡IO的负载,加快Swap交换的速度。如果只有一个交换区,所有的交换操作会使交换区变得很忙,使系统大多数时间处于等待状态,效率很低。用性能监视工具就会发现,此时的CPU并不很忙,而系统却慢。这说明,瓶颈在 IO上,依靠提高CPU的速度是解决不了问题的。 
系统性能监视 

Swap空间的分配固然很重要,而系统运行时的性能监控却更加有价值。通过性能监视工具,可以检查系统的各项性能指标,找到系统性能的瓶颈。本文只介绍一下在Solaris下和Swap相关的一些命令和用途。 

最常用的是Vmstat命令(在大多数Unix平台下都有这样一些命令),此命令可以查看大多数性能指标。 

例如: 
# vmstat 3 
procs memory swap io system cpu 
r b w swpd free buff cache si so bi bo in cs us sy id 
0 0 0 0 93880 3304 19372 0 0 10 2 131 10 0 0 99 
0 0 0 0 93880 3304 19372 0 0 0 0 109 8 0 0 100 
0 0 0 0 93880 3304 19372 0 0 0 0 112 6 0 0 100 
............ 

命令说明: 
vmstat 后面的参数指定了性能指标捕获的时间间隔。3表示每三秒钟捕获一次。第一行数据不用看,没有价值,它仅反映开机以来的平均性能。从第二行开始,反映每三秒钟之内的系统性能指标。这些性能指标中和Swap有关的包括以下几项: 


procs下的w 
它表示当前(三秒钟之内)需要释放内存、交换出去的进程数量。 

memory下的swpd 
它表示使用的Swap空间的大小。 

Swap下的si,so 
si表示当前(三秒钟之内)每秒交换回内存(Swap in)的总量,单位为kbytes;so表示当前(三秒钟之内)每秒交换出内存(Swap out)的总量,单位为kbytes。 
以上的指标数量越大,表示系统越忙。这些指标所表现的系统繁忙程度,与系统具体的配置有关。系统管理员应该在平时系统正常运行时,记下这些指标的数值,在系统发生问题的时候,再进行比较,就会很快发现问题,并制定本系统正常运行的标准指标值,以供性能监控使用。 

另外,使用Swapon-s也能简单地查看当前Swap资源的使用情况。例如: 
# swapon -s 
Filename Type Size Used Priority 
/dev/hda9 partition 361420 0 3 

能够方便地看出Swap空间的已用和未用资源的大小。 

应该使Swap负载保持在30%以下,这样才能保证系统的良好性能。 

有关Swap操作的系统命令 


增加Swap空间,分以下几步: 
1)成为超级用户 
$su - root 

2)创建Swap文件 
# dd if=/dev/zero of=swapfile bs=1024 count=65536 

创建一个有连续空间的交换文件。 

3)激活Swap文件 
#/usr/sbin/swapon swapfile 

swapfile指的是上一步创建的交换文件。 4)现在新加的Swap文件已经起作用了,但系统重新启动以后,并不会记住前几步的操作。因此要在/etc/fstab文件中记录文件的名字,和Swap类型,如: 
/path/swapfile none Swap sw,pri=3 0 0 

5)检验Swap文件是否加上 
/usr/sbin/swapon -s 


删除多余的Swap空间。 
1)成为超级用户 

2)使用Swapoff命令收回Swap空间。 
#/usr/sbin/swapoff swapfile 

3)编辑/etc/fstab文件,去掉此Swap文件的实体。 

4)从文件系统中回收此文件。 
#rm swapfile 

5)当然,如果此Swap空间不是一个文件,而是一个分区,则需创建一个新的文件系统,再挂接到原来的文件系统上。

posted @ 2007-06-18 14:27 Alex 阅读(995) | 评论 (0)编辑 收藏

     摘要:   阅读全文
posted @ 2007-06-13 16:43 Alex 阅读(3727) | 评论 (4)编辑 收藏


come from here

如果将需求分析阶段的工作归结为编写需求规格说明书,这种简化的做法往往是导致项目后期层出不穷问题的罪魁祸首。建议采用以下步骤形成软件需求:获取用户需求→分析用户需求→编写需求文档→评审需求文档→管理需求。下面我们先来讨论前两个步骤(获取用户需求、分析用户需求)的做法。

获取用户需求

  这是该阶段的一个最重要的任务。以下为获取用户需求需要执行的活动(如图1所示)。

  ● 了解客户方的所有用户类型以及潜在的类型。然后,根据他们的要求来确定系统的整体目标和系统的工作范围。

  ● 对用户进行访谈和调研。交流的方式可以是会议、电话、电子邮件、小组讨论、模拟演示等不同形式。需要注意的是,每一次交流一定要有记录,对于交流的结果还可以进行分类,便于后续的分析活动。例如,可以将需求细分为功能需求、非功能需求(如响应时间、平均无故障工作时间、自动恢复时间等)、环境限制、设计约束等类型。

  ● 需求分析人员对收集到的用户需求做进一步的分析和整理。下面是几条常见的准则:

  ⑴对于用户提出的每个需求都要知道“为什么”,并判断用户提出的需求是否有充足的理由;

  

  图1 获取用户需求的活动

  ⑵将那种以“如何实现”的表述方式转换为“实现什么”的方式,因为需求分析阶段关注的目标是“做什么”,而不是“怎么做”;

  ⑶分析由用户需求衍生出的隐含需求,并识别用户没有明确提出来的隐含需求(有可能是实现用户需求的前提条件),这一点往往容易忽略掉,经常因为对隐含需求考虑得不够充分而引起需求变更。

  ● 需求分析人员将调研的用户需求以适当的方式呈交给用户方和开发方的相关人员。大家共同确认需求分析人员所提交的结果是否真实地反映了用户的意图。需求分析人员在这个任务中需要执行下述活动:

  ⑴明确标识出那些未确定的需求项(在需求分析初期往往有很多这样的待定项);

  ⑵使需求符合系统的整体目标;

  ⑶保证需求项之间的一致性,解决需求项之间可能存在的冲突。

分析用户需求

  在很多情形下,分析用户需求是与获取用户需求并行的,主要通过建立模型的方式来描述用户的需求,为客户、用户、开发方等不同参与方提供一个交流的渠道。这些模型是对需求的抽象,以可视化的方式提供一个易于沟通的桥梁。用户需求的分析与获取用户需求有着相似的步骤,区别在于分析用户需求时使用模型来描述,以获取用户更明确的需求。分析用户需求需要执行下列活动:

  ● 以图形表示的方式描述系统的整体结构,包括系统的边界与接口;

  ● 通过原型、页面流或其它方式向用户提供可视化的界面,用户可以对需求做出自己的评价;

  ● 系统可行性分析,需求实现的技术可行性、环境分析、费用分析、时间分析等;

  ● 以模型描述系统的功能项、数据实体、外部实体、实体之间的关系、实体之间的状态转换等方面的内容。

  

  图2 DFD示意图

  用于需求建模的方法有很多种,最常用的包括数据流图(DFD)、实体关系图(ERD)和用例图(Use Case)三种方式。DFD作为结构化系统分析与设计的主要方法,已经得到了广泛的应用,DFD尤其适用于MIS系统的表述。DFD使用四种基本元素来描述系统的行为,过程、实体、数据流和数据存储。DFD方法直观易懂,使用者可以方便地得到系统的逻辑模型和物理模型,但是从DFD图中无法判断活动的时序关系。图2描述的是某个项目的DFD示意图。

  ERD方法用于描述系统实体间的对应关系,需求分析阶段使用ERD描述系统中实体的逻辑关系,在设计阶段则使用ERD描述物理表之间的关系。需求分析阶段使用ERD来描述现实世界中的对象。ERD只关注系统中数据间的关系,而缺乏对系统功能的描述。如果将ERD与DFD两种方法相结合,则可以更准确地描述系统的需求。

  在面向对象分析的方法中通常使用Use Case来获取软件的需求。Use Case通过描述“系统”和“活动者”之间的交互来描述系统的行为。通过分解系统目标,Use Case描述活动者为了实现这些目标而执行的所有步骤。Use Case方法最主要的优点,在于它是用户导向的,用户可以根据自己所对应的Use Case来不断细化自己的需求。此外,使用Use Case还可以方便地得到系统功能的测试用例。

编写需求文档

  需求文档可以使用自然语言或形式化语言来描述,还可以添加图形的表述方式和模型表征的方式。需求文档应该包括用户的所有需求(功能性需求和非功能性需求)。

评审需求文档

  需求文档完成后,需要经过正式评审,以便作为下一阶段工作的基础。一般的评审分为用户评审和同行评审两类。用户和开发方对于软件项目内容的描述,是以需求规格说明书作为基础的;用户验收的标准则是依据需求规格说明书中的内容来制订,所以评审需求文档时用户的意见是第一位的。而同行评审的目的,是在软件项目初期发现那些潜在的缺陷或错误,避免这些错误和缺陷遗漏到项目的后续阶段。

管理需求

  

  图1 需求变更流程

  需求的变更是不可避免的,如何以可控的方式管理软件的需求,对于项目的顺利进行有着重要的意义。如果匆匆忙忙地完成用户调研与分析,则往往意味着不稳定的需求。所以需求管理要保证需求分析各个活动都得到了充分的执行。对于需求变更的管理,则主要使用需求变更流程和需求跟踪矩阵的管理方式。需求变更流程和需求跟踪矩阵分别如图1和图2所示。

  

  图2 需求跟踪矩阵

  常见问题及建议

  Q、客户与最终用户的区别是什么?

  A、可以借助图3来说明它们之间的区别。

  

  图3 需求获取渠道示意图

  软件需求来自系统工程与客户两个方面,其中客户是主要的需求提供者(系统工程需求也来自于客户)。客户需要搜集其最终用户的需求并考虑自身的需求,然后再提供给开发方。假如客户并未去认真搜集最终用户的需求,开发方便需要做到这一点,因为系统最终要满足最终用户的需求。

  Q、如何进行用户访谈?

  A、首先,一定要事先确定访谈的目的和提纲。其次,因为用户往往并不知道应该提供哪些方面的需求,所以需要开发人员引导。

  Q、用户访谈内容是什么?

  A、首先,请用户描述他们如何完成自己当前的工作,并与用户一起抽象出一个工作流程或工作模型。然后,在得到用户的认可后,向用户解释自己是怎样来实现这些功能的,并说明哪些环节可以用自动化方式实现等。

  Q、采用哪一种方式做需求分析最好?

  A、不同的需求分析有不同的特点。还没有哪一种方法可以完全替代别的方法,否则,现在就不会存在不同的需求建模方式了。一般来说,可以使用DFD+ERD来描述那些功能层次比较清晰的需求;而USE CASE则适于描述功能结构复杂的需求。做需求分析的目的是为了建立需求的模型,不同的子系统有可能使用不同的建模方法。

  Q、怎样做原型,原型的目的是什么?

  A、通常使用原型分析方法来帮助开发方进一步获取用户需求或让用户确认需求。开发方往往先向用户提供一个可视界面作为原型,并在界面上布置必要的元素以演示用户所需要的功能。可以使用第四代语言(例如Visual Basic、Delphi等)来快速生成用户界面,也可以使用FrontPage等网页制作工具来生成用户可视的页面流。

  原型的目的往往是获取需求。但有时也使用原型的方式来验证关键技术或技术难点。对于技术原型,界面则往往被忽略掉。

posted @ 2007-06-12 17:47 Alex 阅读(967) | 评论 (0)编辑 收藏

echo "begin to start oracle"
lsnrctl start
sqlplus /nolog 
<<EOF
connect /as sysdba
startup
exit
exit
echo "oracle have started"
posted @ 2007-06-12 09:13 Alex 阅读(1129) | 评论 (0)编辑 收藏

已经打印了,晚上看一下,感觉不错。

download

posted @ 2007-06-11 18:24 Alex 阅读(650) | 评论 (0)编辑 收藏

     摘要:   阅读全文
posted @ 2007-06-04 00:18 Alex 阅读(2060) | 评论 (0)编辑 收藏

     摘要:   阅读全文
posted @ 2007-06-02 11:50 Alex 阅读(947) | 评论 (0)编辑 收藏

     摘要:   阅读全文
posted @ 2007-05-29 11:32 Alex 阅读(1475) | 评论 (5)编辑 收藏

     摘要:   阅读全文
posted @ 2007-05-24 09:52 Alex 阅读(1189) | 评论 (2)编辑 收藏

     摘要:   阅读全文
posted @ 2007-05-14 09:05 Alex 阅读(13331) | 评论 (8)编辑 收藏

     摘要:   阅读全文
posted @ 2007-05-13 17:15 Alex 阅读(1910) | 评论 (3)编辑 收藏

     摘要:   阅读全文
posted @ 2007-05-13 16:41 Alex 阅读(5618) | 评论 (13)编辑 收藏

key words: jstl,struts,log4j

1.jstl
jstl的配置参考这篇文章:
http://foolmouse.cnblogs.com/archive/2006/04/20/380695.html

在iAS904服务器上的jstl的版本只能用1.0 的
"standard.jar和jstl.jar文件拷贝到\WEB-INF\lib\
"

2.struts
struts的配置主要是把 相关jar文件(struts.jar,struts-legacy.jar)拷贝到\WEB-INF\lib
,另外,struts需要用到一些apache的commons的包(commons-beanutils.jar,commons-collections-2.1.1.jar,commons-digester.jar)

3.log4j
log4j经常有莫名其妙的问题,有时候能出来log有时候又不能出来log,最后把log4j.xml统一改为log4j.properties,暂时看好像有效果。

4。web.xml配置

<taglib>
        
<taglib-uri>http://java.sun.com/jsp/jstl/core</taglib-uri>
        
<taglib-location>/WEB-INF/c-1_0.tld</taglib-location>
    
</taglib>
    
<taglib>
        
<taglib-uri>/WEB-INF/struts-bean.tld</taglib-uri>
        
<taglib-location>/WEB-INF/struts-bean.tld</taglib-location>
    
</taglib>
    
<taglib>
        
<taglib-uri>/WEB-INF/struts-html.tld</taglib-uri>
        
<taglib-location>/WEB-INF/struts-html.tld</taglib-location>
    
</taglib>
    
<taglib>
        
<taglib-uri>/WEB-INF/struts-logic.tld</taglib-uri>
        
<taglib-location>/WEB-INF/struts-logic.tld</taglib-location>
    
</taglib>


今天终于先在iAS里部署完了struts,下一步把hibernate放进去,上次部署过一次,没有成功,据说是和toplink有点冲突。 知道的兄弟分享下oc4j中部署hibernate
posted @ 2007-05-08 19:11 Alex 阅读(1331) | 评论 (0)编辑 收藏

     摘要:   阅读全文
posted @ 2007-04-11 16:09 Alex 阅读(7768) | 评论 (8)编辑 收藏

     摘要:   阅读全文
posted @ 2007-03-30 08:34 Alex 阅读(798) | 评论 (0)编辑 收藏

key words: contentType,meta

come from here


 

在经典同时看到两个关于ContentType的问题

http://bbs.blueidea.com/thread-2729935-1-1.html

http://bbs.blueidea.com/thread-2729945-1-1.html

所以查了下资料,copy了一份详细的ContentType的列表

 

不同的ContentType 会影响客户端所看到的效果.

默认的ContentType为 text/html  也就是网页格式.

代码如:

<% response.ContentType ="text/html" %> 
<!--#i nclude virtual="/ContentType.html" -->

显示的为网页,而

<% response.ContentType ="text/plain" %> 
<!--#i nclude virtual="/sscript/ContentType.html" -->

则会显示html原代码.

以下为一些常用的 ContentType

GIF images
<% response.ContentType ="image/gif" %> 
<!--#i nclude virtual="/myimage.gif" -->
JPEG images
<% response.ContentType ="image/jpeg" %> 
<!--#i nclude virtual="/myimage.jpeg" -->
TIFF images
<% response.ContentType ="image/tiff" %> 
<!--#i nclude virtual="/myimage.tiff" -->
MICROSOFT WORD document
<% response.ContentType ="application/msword" %> 
<!--#i nclude virtual="/myfile.doc" -->
RTF document
<% response.ContentType ="application/rtf" %> 
<!--#i nclude virtual="/myfile.rtf" -->
MICROSOFT EXCEL document
<% response.ContentType ="application/x-excel" %> 
<!--#i nclude virtual="/myfile.xls" -->
MICROSOFT POWERPOINT document
<% response.ContentType ="application/ms-powerpoint" %> 
<!--#i nclude virtual="/myfile.pff" -->
PDF document
<% response.ContentType ="application/pdf" %> 
<!--#i nclude virtual="/myfile.pdf" -->
ZIP document
<% response.ContentType ="application/zip" %> 
<!--#i nclude virtual="/myfile.zip" -->

 

下面是更详细的ContentType

application/andrew-insetez
application/mac-binhex40hqx
application/mac-compactprocpt
application/mathml+xmlmathml
application/msworddoc
application/octet-streambin dms lha lzh exe class so dll
application/odaoda
application/oggogg
application/pdfpdf
application/postscriptai eps ps
application/rdf+xmlrdf
application/smilsmi smil
application/srgsgram
application/srgs+xmlgrxml
application/vnd.mifmif
application/vnd.mozilla.xul+xmlxul
application/vnd.ms-excelxls
application/vnd.ms-powerpointppt
application/vnd.wap.wbxmlwbxml
application/vnd.wap.wmlc.wmlc wmlc
application/vnd.wap.wmlscriptc.wmlsc wmlsc
application/voicexml+xmlvxml
application/x-bcpiobcpio
application/x-cdlinkvcd
application/x-chess-pgnpgn
application/x-cpiocpio
application/x-cshcsh
application/x-directordcr dir dxr
application/x-dvidvi
application/x-futuresplashspl
application/x-gtargtar
application/x-hdfhdf
application/x-httpd-php.php .php4 .php3 .phtml
application/x-httpd-php-source.phps
application/x-javascriptjs
application/x-koanskp skd skt skm
application/x-latexlatex
application/x-netcdfnc cdf
application/x-pkcs7-crl.crl
application/x-shsh
application/x-sharshar
application/x-shockwave-flashswf
application/x-stuffitsit
application/x-sv4cpiosv4cpio
application/x-sv4crcsv4crc
application/x-tar.tgz tar
application/x-tcltcl
application/x-textex
application/x-texinfotexinfo texi
application/x-trofft tr roff
application/x-troff-manman
application/x-troff-meme
application/x-troff-msms
application/x-ustarustar
application/x-wais-sourcesrc
application/x-x509-ca-cert.crt
application/xhtml+xmlxhtml xht
application/xmlxml xsl
application/xml-dtddtd
application/xslt+xmlxslt
application/zipzip
audio/basicau snd
audio/midimid midi kar
audio/mpegmpga mp2 mp3
audio/x-aiffaif aiff aifc
audio/x-mpegurlm3u
audio/x-pn-realaudioram rm
audio/x-pn-realaudio-pluginrpm
audio/x-realaudiora
audio/x-wavwav
chemical/x-pdbpdb
chemical/x-xyzxyz
image/bmpbmp
image/cgmcgm
image/gifgif
image/iefief
image/jpegjpeg jpg jpe
image/pngpng
image/svg+xmlsvg
image/tifftiff tif
image/vnd.djvudjvu djv
image/vnd.wap.wbmp.wbmp wbmp
image/x-cmu-rasterras
image/x-iconico
image/x-portable-anymappnm
image/x-portable-bitmappbm
image/x-portable-graymappgm
image/x-portable-pixmapppm
image/x-rgbrgb
image/x-xbitmapxbm
image/x-xpixmapxpm
image/x-xwindowdumpxwd
model/igesigs iges
model/meshmsh mesh silo
model/vrmlwrl vrml
text/calendarics ifb
text/csscss
text/html.shtml html htm
text/plainasc txt
text/richtextrtx
text/rtfrtf
text/sgmlsgml sgm
text/tab-separated-valuestsv
text/vnd.wap.wml.wml wml
text/vnd.wap.wmlscript.wmls wmls
text/x-setextetx
video/mpegmpeg mpg mpe
video/quicktimeqt mov
video/vnd.mpegurlmxu
video/x-msvideoavi
video/x-sgi-moviemovie
x-conference/x-cooltalkice
posted @ 2007-03-29 09:25 Alex 阅读(1937) | 评论 (0)编辑 收藏

     摘要:   阅读全文
posted @ 2007-03-28 16:55 Alex 阅读(4549) | 评论 (0)编辑 收藏

     摘要:   阅读全文
posted @ 2007-03-26 22:18 Alex 阅读(36075) | 评论 (7)编辑 收藏

     摘要:   阅读全文
posted @ 2007-03-20 16:23 Alex 阅读(3362) | 评论 (1)编辑 收藏

key words : jsp防盗链 header

最近碰到盗链的问题,即复制一个url地址,在另一个地方也能访问。

index.jsp页面
<html>
  
<head><title>Simple jsp page</title></head>
  
<body>Place your content here

  here is index jsp
    get header info
  
<href="a.jsp">a.jsp</a>
  
</body>
</html>

a.jsp页面
<html>
  
<head><title>Simple jsp page</title></head>
  
<body>Place your content here

  here is a. jsp
    get header info
  
<%=request.getHeader("Referer")%>
  
<%if(null == request.getHeader("Referer") || request.getHeader("Referer").indexOf("yourdomain.com"< 0){%>
     做人要厚道
  
<%}else{%>
  合法访问
  
<%}%>
  
</body>
</html>


即从内部访问可以,直接粘贴地址在另一个浏览器里访问禁止
posted @ 2007-03-13 10:01 Alex 阅读(5911) | 评论 (8)编辑 收藏

     摘要:   阅读全文
posted @ 2007-03-12 00:40 Alex 阅读(12373) | 评论 (18)编辑 收藏

     摘要:   阅读全文
posted @ 2007-02-25 00:20 Alex 阅读(397) | 评论 (0)编辑 收藏

     摘要:   阅读全文
posted @ 2007-02-24 23:37 Alex 阅读(1881) | 评论 (2)编辑 收藏

key words: pd, powerdesigner,外建

在PD中建立外键碰到一个问题,如: forum(id,name)表,forum_thread(id,forum_id)表,需要将forum的id设为forum_thread的外建,指向forum_id,默认的在 PD中是将主键设为外建.

需要如下设置才可以:

foreign_key.png
posted @ 2007-01-31 10:42 Alex 阅读(1296) | 评论 (0)编辑 收藏

key words: 上传文件

目里面有上传文件的需求,我想了一下不外乎下面两种处理方法:

1. 在数据库表中建立一个blob字段存放用户上传文件.
2. 在服务器上建立一个文件夹保存用户上传文件,数据库表中只存放该文件的url地址.

我本人现在比较倾向于第2种方案, 主要原因是担心方案1的效率(我用的是mysql数据库)。 但是处理过程中除了维护数据库中表的字段还要维护上传的文件,稍微麻烦一点。

大家在项目里面又是怎么做呢? 给我点建议!谢谢


讨论内容见: javaeye

robin更建议第二种方案,放在数据库中主要的问题是 AppServer吃不消,开销比较大.

上面是摘录,不过我们这里用的是Oracle9i AS,在Oracle的协作套间里一般文档或者上传的文件都是保存在数据库里,还把这个特性作为Oracle 与别的协作套件之间不同的卖点。

大家以为如何? 我直觉是Oracle的DB和oc4j的AppServer对付这个似乎没有开销上的担心,但是没有实际检测过。

换句话说,文件的管理是放在文件夹里方便还是数据库里方便? 有点为难

update (2007-5-13):
还有一种方案结合了数据库和IO,我认为比较可行,就是文件存在数据库,但是下载的时候第一次从数据库下载,然后第一次这个文件保存在一个临时文件夹下面,以后每次下载的时候总是先检查此临时文件夹,如果已经存在则直接下载,如果没有则从数据库重复这个动作。当然,保存在临时文件夹下的文件的命名需要唯一,这个应该没有问题。
posted @ 2007-01-31 09:06 Alex 阅读(1942) | 评论 (4)编辑 收藏


这个年代的偶像很多,值得学习的也很多,,如果让我说,我觉得在现在这个急功近利的所谓2.0时代,阿甘精神更值得我们学习。

什么是阿甘精神?
1。奔跑。 不停的奔跑,不要停下来
2。不断地重复,如果你做的不够好,那就说明你还重复的不够多。
3。执着。 不要相信权威,按自己的意志去做。

现在,聪明的人很多,但是小聪明似乎更多,包括我。

你认可阿甘精神么? 如果是,顶一下。

刚看到关于阿甘精神的另一个有意思的摘录:



"
阿甘就是看到一个目标就走过去了,别的人是,看见一个目标,先订一个作战计划,然后匍匐前进,往左闪,往右躲,再弄个掩体…一辈子就看他闪转腾挪活得那叫一个花哨,最后哪儿也没到达。
"

很形象哦。

祝各位在2007年工作进步。

posted @ 2007-01-30 08:59 Alex 阅读(1208) | 评论 (2)编辑 收藏

key words: js,javascript,检查上传文件大小

有时需要在客户端获得待上传得文件大小,google了一下,发现下面这个用法.

<html>  
<input type="file" name="file1" onchange="ShowSize(this.value)">  
<script language="JavaScript">  
<!--  
function ShowSize(files)  
{  
  
var fso,f;  
  fso
=new ActiveXObject("Scripting.FileSystemObject");  
  f
=fso.GetFile(files);
  
var mySize = f.size/1024;
  alert(mySize
+" K ");  
}
  
//-->  
</script>  
</html>


BTW: 奇怪,COS上传组件里怎么没有获得上传文件大小的方法?  MultipartRequest里是没有,有知道的兄弟通知下哦
posted @ 2007-01-25 17:16 Alex 阅读(10393) | 评论 (4)编辑 收藏

     摘要: key words : hibernate spring daocome from : http://lpacec.javaeye.com/blog/46220   1.  package infoweb.dao;         2.         3. import java.util.List;         4. import java.util.Iterator;         5...  阅读全文
posted @ 2007-01-25 09:24 Alex 阅读(896) | 评论 (0)编辑 收藏

key words: REST webservice

转自廖宇雷的Weblog

REST 是由 Roy Fielding 在他的论文《Architectural Styles and the Design of Network-based Software Architectures》中提出的一个术语。

REST 是英文 Representational State Transfer 的缩写,有中文翻译为“具象状态传输”(参考:《SIP/IMS网络中的Representational State Transfer (REST)和数据分布》)。

—————————————

前面的内容比较枯燥,我说说我自己的理解。

但是 REST 到底是什么呢?论文我看不懂,不过找到一篇更简单易懂的东西:《Building Web Services the REST Way》

根据这篇文章,我整理了一下我自己对 REST 的理解:

REST 首先只是一种架构样式,不是一种标准。这点和 Ajax 类似,两者都是利用现有的成熟技术。

在 REST 的定义中,一个 Web 应用总是使用固定的 URI 向外部世界呈现(或者说暴露)一个资源

URI 是英文 Uniform Resource Identifier 的缩写,中文翻译“通用资源标志符”。

“通用资源标志符”是指唯一标识一个资源(xhtml 文件、图片、css 样式表)的字符串。当然了,RFC 中定义的 URI 复杂得多,不过我们此处将 URI 想象成一个人的身份证号码就行了(你不能有两个同时有效的身份证号码,一个号码也不可能同时对应两个人)。而我们天天挂在嘴边的 URL 地址就是 URI 的一种表现形式(个人理解,有错请纠正)。

知道什么是 URI 后,我们来看一个实际例子:

http://www.example.com/photo/logo 指向 example.com 网站(可以视为一个 Web 应用)中类型为 photo,名字为 logo 的资源。我们用浏览器访问这个 URI,看到的将可能是一个 xhtml 文档,其中用 <img src=”……” /> 来显示实际的照片。

http://www.example.com/photo/logo 很容易让你想到 URL 重写。事实上,这个地址很可能会在服务器内部处理为 http://www.example.com/photo.php?name=logo 这样的地址。photo.php 是服务器端的一个动态脚本文件,根据 name 参数生成 xhtml 文档返回给浏览器。

现在假设我们要获取这张照片的 XML 文档。XML 文档中包含照片的文件名、文件大小、拍摄日期等等信息。也就是说我们要获取“同一个资源的不同表现形式的数据”。对于这个要求,我们可以很容易的用另一个 URL 地址达到:http://www.example.com/xml/logo。

但是,这就违背了“URI 唯一标识一个资源”的定义。如果我们要获取同一个资源的多种表现形式,那么就要使用更多的 URL,从而给一个资源指定了多个不同的 URI。

而在 REST 中,不管是获取照片的 xhtml 文档还是 XML 文档,或者照片文件本身,都是用同一个 URI,就是 http://www.example.com/photo/logo。

那这是怎么办到的呢?Ruby On Rails 中是通过分辨 HTTP Request Header 信息来分辨客户端是想要取得资源的哪一种表现形式的数据。

当我们用浏览器访问一个网址时,浏览器会构造一个 HTTP 请求。这个请求有一个头信息,其中包括了本次请求接受何种类型的数据。通常浏览器发送的 HTTP 请求头中,Accept 的值都是 */*,也就说接受服务器返回的任何类型的数据。

看到这里,聪明的家伙应该知道了。只要我们指定一个特定的 Accept 参数,那么服务器就可以通过判断该参数来决定返回什么类型的数据。所以在一个采用 REST 架构的应用中,要获取同一个资源的不同表现形式的数据,只需要使用不同的 HTTP 请求头信息就行了。

如果考虑为 Web 应用增加 Web Services,这种技术的价值就体现出来了。比如我写了一个 Delphi 程序,现在只需要构造一个包含 Accept: text/xml 的 HTTP 请求头,然后将请求发送到 http://www.example.com/photo/logo 就可以了。返回的结果就是一个 XML 文档,而不是 xhtml 文档。

因为我们的 HTTP 请求头信息有不同的状态,从而可以获得不同的数据,所以叫做“具象状态传输” :)

—————————————

除了上面的用法,REST 还有进一步的扩展。

我们在 Web 应用中处理来自客户端的请求时,通常只考虑 GET 和 POST 这两种 HTTP 请求方法。实际上,HTTP 还有 HEAD、PUT、DELETE 等请求方法。而在 REST 架构中,用不同的 HTTP 请求方法来处理对资源的 CRUD(创建、读取、更新和删除)操作:

  • POST: 创建
  • GET: 读取
  • PUT: 更新
  • DELETE: 删除

经过这样的一番扩展,我们对一个资源的 CRUD 操作就可以通过同一个 URI 完成了:

http://www.example.com/photo/logo(读取)
仍然保持为 [GET] http://www.example.com/photo/logo

http://www.example.com/photo/logo/create(创建)
改为 [POST] http://www.example.com/photo/logo

http://www.example.com/photo/logo/update(更新)
改为 [PUT] http://www.example.com/photo/logo

http://www.example.com/photo/logo/delete(删除)
改为 [DELETE] http://www.example.com/photo/logo

从而进一步规范了资源标识的使用。

通过 REST 架构,Web 应用程序可以用一致的接口(URI)暴露资源给外部世界,并提供对资源的操作服务。这对于以资源为中心的 Web 应用来说非常重要。例如照片共享网站、用户社区等。

—————————————

Ruby On Rails 1.2 版对 REST 有很好的支持,但要在 PHP 中应用 REST 还需要解决不少问题:

  • 如何在服务端判断 PUT、DELETE 请求方法;
  • 如何获取用 PUT、DELETE 请求方法中传递的数据;
  • 如何获取 HTTP 请求头信息中的 Accept 参数值;
  • 如何在浏览器端发起 PUT 和 DELETE 请求。

不过我仔细看了 PHP 文档,我觉得上面几个问题都是可以解决的。

服务端综合使用 $_SERVER[’HTTP_ACCEPT’]、$_SERVER[’REQUEST_URI’]、$_SERVER[’REQUEST_METHOD’]、$_SERVER[’QUERY_STRING’] 这些变量应该可以搞定前面三个问题。而第四个问题则可以用 JavaScript 的 XMLHttpRequest 对象来实现。

不过我想 REST 的真正价值在于 Web Services,而不是通过浏览器操作的应用程序。

—————————————

参考:

posted @ 2007-01-23 08:30 Alex 阅读(431) | 评论 (0)编辑 收藏

key words : web快速开发

web快速开发是一个值得期待的东西,最早我接触过codecharge,是一个商业软件,功能据说很强大,我试用过,支持php,asp,jsp等多语言,但是感觉对java的支持不是很顺手,接着就是asp,这个好也不好,原因就不说了,因为我对java关注。

2. 接着就是dorado ,刚看到的,感觉也不错,有asp的风格,不过更适合j2ee的环境 ,这里是他们的在线演示demo,有兴趣的可以看一下

3. 最后,刚google了一下,Oracle也有一个基于Oracle数据库的快速开发产品  : APEX,基于浏览器做DB 开发,原来的名字叫做HTML DB,名字很直观,据说Oracle的metalink就是用这个实现的,应该功能不赖。

4.对了,再补充一个现在很热的,Ror,听说javaeye的网站要重新用ruby1.2开发,试用过一点,感觉确实令人震惊,值得关注。

java的技术其实是看着很热闹,比较大的东西也不少,但是真正关注web这一块的其实不多,无论是struts还是webwork,离我们所说的真正的“快速开发”的距离很远,而以上一些工具的出现也许会逐步改善j2ee在这一领域的现状。

我之所以对这个目前有点兴趣,确实有一种强烈的反差在折磨着我,因为我相信在j2ee的web开发里确实存在本来要开发1周多的工作量很可能用一个好的工具几个小时就搞定了,这是一件多么荒唐搞笑的事情?如果dorado在它的产品中真的能做到这样那我们的java开发那么坑吃坑吃的折腾个什么劲啊? 也许真的有奇迹,我宁愿相信有这样的奇迹。

大家有什么好的想法可以讨论一下。
posted @ 2007-01-20 22:56 Alex 阅读(3761) | 评论 (7)编辑 收藏

key words: powerdesign,power , design

引言:
发现powerdesign的文档真的是不多,不过powerdesign还是很有用的,见到此类文档就收藏下吧.

转自edeed

 附上一些使用pd11的心得:

1、安装PD v11.0版

2、由pdm生成建表脚本时,字段超过15字符就发生错误(oracle)
原因未知,解决办法是打开PDM后,会出现Database的菜单栏,进入Database - Edit Current DBMS -script-objects-column-maxlen,把value值调大(原为30),比如改成60。出现表或者其它对象的长度也有这种错误的话都可以选择对应的objects照此种方法更改!
或者使用下面的这种方法:
生成建表脚本时会弹出Database generation提示框:把options - check model的小勾给去掉,就是不进行检查(不推荐)!
或者可以修改C:\Program Files\Sybase\PowerDesigner Trial 11\Resource Files\DBMS\oracl9i2.xdb文件
修改好后,再cdm转为pdm时,选择“Copy the DBMS definition in model”把把这个资源文件拷贝到模型中。

3、生成的建表脚本中如何把对象的双引号去掉?
打开cdm的情况下,进入Tools-Model Options-Naming Convention,把Name和Code的标签的Charcter case选项设置成Uppercase或者Lowercase,只要不是Mixed Case就行!
或者选择Database->Edit current database->Script->Sql->Format,有一项CaseSensitivityUsingQuote,它的 comment为“Determines if the case sensitivity for identifiers is managed using double quotes”,表示是否适用双引号来规定标识符的大小写,可以看到右边的values默认值为“YES”,改为“No”即可!
或者在打开pdm的情况下,进入Tools-Model Options-Naming Convention,把Name和Code的标签的Charcter case选项设置成Uppercase就可以!

4、建立一个表后,为何检测出现Existence of index的警告
A table should contain at least one column, one index, one key, and one reference.
可以不检查 Existence of index 这项,也就没有这个警告错误了!
意思是说没有给表建立索引,而一个表一般至少要有一个索引,这是一个警告,不用管也没有关系!

5、创建一个表在修改字段的时候,一修改name的内容,code也跟着变化,如何让code不随着name变化
Name和Code 的右侧都有一个按钮“=”,如果需要不同步的话,把这个按钮弹起来就可以了。
Tools->General Options->Dialog->Name to Code Mirroring (去掉)

6、由CDM生成PDM时,自动生成的外键的重命名
PDM Generation Options->Detail->FK index names默认是%REFR%_FK,改为FK_%REFRCODE%,其中%REFRCODE%指的就是CDM中Relationship的code!另外自动生成的父字段的规则是PDM Generation Options->Detail->FK column name template中设置的,默认是%.3:PARENT%_%COLUMN%,可以改为Par%COLUMN%表示是父字段!

7、如何防止一对一的关系生成两个引用(外键)
要定义关系的支配方向,占支配地位的实体(有D标志)变为父表。
在cdm中双击一对一关系->Detail->Dominant role选择支配关系

8、修改报表模板中一些术语的定义
即文件:C:\Program Files\Sybase\PowerDesigner Trial 11\Resource Files\Report Languages\Chinese.xrl
Tools-Resources-Report Languages-选择Chinese-单击Properties或双击目标
修改某些对象的名称:Object Attributes\Physical Data Model\Column\
        ForeignKey:外键
        Mandatory:为空
        Primary:主键
        Table:表
用查找替换,把“表格”替换成“表”
修改显示的内容为别的:Values Mapping\Lists\Standard,添加TRUE的转化列为是,FALSE的转化列为空
另外Report-Title Page里可以设置标题信息
posted @ 2007-01-17 09:55 Alex 阅读(3145) | 评论 (3)编辑 收藏

key words : beanShell ,动态脚本

原来第一次是在osworkflow中听说过beanShell,最近又碰到,感觉还是挺有用的,比如对于我来说我就想有一些class文件拿过来直接调用看看,但有不想搭建一个麻烦的环境,而beanshell就可以满足我这个需求。

假设我欲调用的java为MyShit
package com.app;

public class MyShit
{
    
static{
        System.out.println(
"this is in static blog");
    }

    
public static void main(String[] args){
        System.out.println(
"this is in main method : hello shit");
    }

    
public void shit(){
        System.out.println(
"hello shit!!!");
    }


    
public static void haha(){
        System.out.println(
"this is static method haha");
    }

}


javac ...
java ..

在dos中设置classpath,指向该class文件

set classpath=%classpath%;c:\beanshell

编写一个 bsh的bat文件,便于dos直接调用beanshell脚本
 
java bsh.Interpreter %1


编写一个test.bsh脚本
//mytest
import com.app.*;

print(
"hello,it's a beanShell test");

List list  
= new ArrayList();
list.add(
"111");
list.add(
"222");
list.add(
"333");

print(
"the list = " + list);

Date date 
= new Date();
print(
"the date = " + date);

//method test

add( a, b ) 
{
    
return a + b;
}


foo 
= add(12);            // 3
print("foo = " + foo);
foo 
= add("Oh"" baby");   // "Oh baby"
print("foo = " + foo);

//about object
foo() {
    print(
"foo");
    x
=5;

    bar() 
{
        print(
"foo's method bar()");
    }


    
return this;
}


myfoo 
= foo();    // prints "foo"
print( myfoo.x ); // prints "5"
myfoo.bar();      // prints "bar"

MyShit shit 
= new MyShit();
shit.shit();
shit.main(
null);
print(
"do you shit");

//MyShit.haha();


在dos下运行 bsh test.bsh就OK了

另外,beanshell作为动态脚本语言可以结合 spring2.0的新特性,你新写的业务类可以直接修改而不用重新发布,怎么样,是不是比较方便,详细操作请参考Springframework 2.0 与 ZK 混合开发实例

试一试!

详细使用请参考官方文档
posted @ 2007-01-09 20:01 Alex 阅读(6500) | 评论 (7)编辑 收藏

key words: 连接池 数据库

发现一个有意思的现象,好多人在开发过程中不知道是出于训练自己编码还是为了重新发明轮子,总之明明已经存在的功能他非要自己实现一遍。

今天又碰到一个。

jdbc的连接,好多人也喜欢自己实现一个连接池,但是对于app server来说本身一般都有支持连接池的,为什么不用呢?
写了一通又长又臭的代码,自己以为牛B的很,其实算个球啊。

不要浪费,不要自己发明轮子,你不是最牛的,一定还有比你更牛的!

对于weblogic或者oracle以及ibm的产品来说,大家还是比较习惯于用容器的jndi,但是即使对于tomcat,我个人也建议用tomcat自带的。

附: jndi连接数据库

public final static synchronized Connection getConnection(String inputJNDI) throws
      DBMException 
{
    Connection conn 
= null;
    String strConnJNDI 
= null;
    
try {
      Context ctx 
= new InitialContext();
      _log.debug(ctx);

      
if (null != inputJNDI) {
        
if(inputJNDI.length() > 0)
          strConnJNDI 
= inputJNDI;
        
else
          strConnJNDI 
= dbJndiName;
      }

      
else {
        strConnJNDI 
= dbJndiName;
      }

      DataSource ds 
= (DataSource) ctx.lookup(strConnJNDI);
      _log.debug(ds);
      conn 
= ds.getConnection();
    }

    
catch (Exception ex) {
      _log.error(
"It's error to get connection", ex);
      
throw new DBMException("Error to get connection");
    }

    _log.debug(conn);
    
return conn;
  }


以此类推,类似于xml解析等的工作也没有必要自己一步一步地用dom或者什么乱七八糟的sax自己去搞一遍,搞了半天就使为了得到其中的一个value,何苦来着?    如果你不是为了做研究,那么,效率第一,安全第一.

随便说说,也许有人不同意我的观点,没关系,尽管谈谈,哈哈  :)
posted @ 2007-01-04 20:58 Alex 阅读(1927) | 评论 (8)编辑 收藏

key words: 2007 plans

新的一年开始了,制定一个计划吧

说说你有什么计划

如图:

2007-plans.png
posted @ 2007-01-03 23:31 Alex 阅读(1482) | 评论 (12)编辑 收藏

key words: tomcat,数据源配置,datasource

把一个应用从4.1移到5.5数据源死活不出来,用probe察看了一下居然datasource配置错误.

4.x的配置如下:

<Context path="/cpms" docBase="cpms"   debug="99" privileged="true">
            
<Resource name="jdbc/report" auth="Container" type="javax.sql.DataSource"/>
            
<ResourceParams name="jdbc/report"> 
                
<parameter>
                    
<name>username</name>
                    
<value>root</value>
                
</parameter>
                
<parameter>
                    
<name>password</name>
                    
<value>XXX</value>
                
</parameter>
                
<parameter>
                    
<name>driverClassName</name> 
                    
<value>org.gjt.mm.mysql.Driver</value>
                
</parameter>
                
<parameter>
                    
<name>url</name> 
                    
<value>jdbc:mysql://localhost/app</value>
                
</parameter> 
                
<parameter>
          
<name>RemoveAbandoned</name>
          
<value>true</value>
        
</parameter>
        
<parameter>
          
<name>LogAbandoned</name>
          
<value>true</value>
        
</parameter>
        
<parameter>
          
<name>RemoveAbandonedTimeout</name>
          
<value>60</value>
        
</parameter>
              
</ResourceParams> 
    
</Context>  


5.x的配置如下:

<Context path="/app" docBase="cpms"
        debug
="5" reloadable="true" crossContext="true">



  
<Resource name="jdbc/report" auth="Container" type="javax.sql.DataSource"
               maxActive
="100" maxIdle="30" maxWait="10000"
               username
="develop" password="XX" driverClassName="com.mysql.jdbc.Driver"
               url
="jdbc:mysql://localhost:3306/app?autoReconnect=true"/>
</Context>


没有具体看原因.知道的兄弟可以发表高见

tomcat的管理和监控推荐用probe,自带的那个admin管理太滥了。

posted @ 2006-12-22 12:04 Alex 阅读(1702) | 评论 (1)编辑 收藏

key words: sql server,单用户,master恢复

 作者:jankie
日期:2006-12-1

一、MASTER数据库备份与恢复
   1、数据库master正常情况下对master进行(完全)备份;
若要恢复:
   2、停止SQL Server服务;
   3、以单用户实例的方式启动SQL Server,启动时不要把窗口关闭;(具体看二)
   4、然后再对MASTE数据库进行恢复即可;

二、MSSQL单用户实例的启动方法:
  如何在单用户模式下启动SQL Server的命名实例(命令提示符)   
  在单用户模式下从命令提示符启动SQL Server 的命名实例     
  从命令提示符输入:     
  sqlservr.exe -c  -m   -s   {instancename}   

例1:
  1、sqlserver.exe -c -m 回车(默认实例)
  2、sqlserver.exe -c -m -s benet (实例名为benet)
   
  说明在启动 sqlservr.exe之前,必须在命令窗口中切换到适当的目录
如:c:\program files\microsoft sql server\mssql\bin目录下
posted @ 2006-12-21 15:08 Alex 阅读(479) | 评论 (0)编辑 收藏

key words: dll,ocx,注册控件

最近项目中用到了收银,需要在顾显上显示金额.


1.将.dll、.ocx等考到system32下。
2.用命令注册regsvr32.exe  .dll。(eg:regsvr32.exe  OWC11.dll)
3.在注册表中查看:利用名称或与其唯一对应的clsid("clsid:0002E55D-0000-0000-C000-   000000000046")可查找到相关信息
posted @ 2006-12-20 15:13 Alex 阅读(675) | 评论 (0)编辑 收藏

     摘要: key words: bea world2006 ,workshop studio, 快速开发

bea world 2006 北京已经过去好几天了,早就想记录一下一直忙 :)
  阅读全文
posted @ 2006-12-20 11:39 Alex 阅读(2634) | 评论 (10)编辑 收藏

key words: commons log,log4j,apache log

前言: 对于log4j虽然在用,但是也存在一个疑问,怎么有的用apache的commons logging有的直接用log4j,下面的这篇文章解释了我的疑问.

转自 here

Apache组织开发了一套用于支持Logging的Log4J,Java 1.4版本也引入了一套内置的 Logging框架,如果开发者想在这两套Logging系统之间自由的切换,该怎么办呢?答案就是,使用Commons Logging。 Commons Logging定义了一套抽象的Logging接口,用户可以通过配置,使这些接口指向任何一个已存在的Logging系统。

•使用抽象Logging接口
问题:
你在编写一个可以重复使用的库,需要写入Log信息,但你不想使你的Logging功能绑定在Apache Log4J或者JDK 1.4 Logging框架上。
解决方案:

public static void main(String[] args) {//自己替换[]

  System.setProperty("org.apache.commons.logging.Log",
      "org.apache.commons.logging.impl.Jdk14Logger");
  Log log = LogFactory.getLog("com.discursive.jccook.SomeApp");

  if (log.isTraceEnabled()) {
    log.trace("This is a trace message");
  }

  if (log.isDebugEnabled()) {
    log.debug("This is a debug message");
  }

  log.info("This is an informational message");
  log.warn("This is a warning");
  log.error("This is an error");
  log.fatal("This is fatal");

}


LogFactory.getLog方法会根据底层环境返回一个适当的Log实现。如果用户想指定一个具体的Logging系统实现,可以设置org.apache.commons.logging.Log系统属性。例如:
System.setProperty("org.apache.commons.logging.Log",
"org.apache.commons.logging.impl.Log4JLogger");
这样就会使用Log4J作为Logging系统。
org.apache.commons.logging.Log可以设定为:
•org.apache.commons.logging.impl.Log4JLogger  使用Log4J
•org.apache.commons.logging.impl.Jdk14Logger  使用JDK 1.4 Logging框架
•org.apache.commons.logging.impl.SimpleLog  使用Commons Logging内置的简单Log实现
其他:
总结一下,Commons Logging会按照下列顺序来指定具体的Log实现。
•如果定义了org.apache.commons.logging.Log系统参数,实用指定的Logging实现。
•如果在CLASSPATH里发现了Log4J,使用Log4J。
•如果使用的是JDK1.4,使用JDK1.4内置的Logging框架。
•如果都没有找到,则使用Commons Logging内置的简单Log实现。
posted @ 2006-12-18 15:38 Alex 阅读(692) | 评论 (0)编辑 收藏

key words : 职业规划  高薪

原文见robbin的pdf,我整理了一下脑图

职业规划与高薪之路.png
posted @ 2006-12-17 13:25 Alex 阅读(3432) | 评论 (6)编辑 收藏

注意:这篇文章是由无人工介入的自动的机器翻译系统翻译完成。这些文章是微软为不懂英语的用户提供的, 以使他们能够理解这些文章的内容。微软不保证机器翻译的正确度,也不对由于内容的误译或者客户对它的使用所引起的任何直接的, 或间接的可能的问题负责。

概要

Microsoft SQL Server 2000 支持同一计算机上同时运行的多个 SQLServer 数据库引擎实例。 以下是两种类型的 SQLServer 数据库引擎实例: 默认和命名。 只能有, 任何计算机上运行一个默认实例并且由默认实例运行计算机上的名称标识它。 通常指定以下列格式: 计算机名和实例名称
computer_name\instance_name
要 MicrosoftSQLServer2000JDBC, 连接命名实例通过 SQL 服务器 2000 MicrosoftTextDriver 必须指定端口号与命名实例, 代替的命名实例名称相关联如前面。

更多信息

要查找 SQLServer 实例端口号, 请按照下列步骤:
1.在 Microsoft SQL Server 2000 服务器, 启动 SQL Server 网络实用工具。
2.要从 实例 下拉菜单实例依次, 常规 选项卡。
3.TCP/IP, 依次 属性 。 注意, 在 属性 对话框中出现端口号为此实例。

当您连接到 SQLServer 通过 JDBC 连接 URL 中使用只要有该值,。 下面是典型连接 URL 的示例:

jdbc:microsoft:sqlserver: / 1433; yourServerName / 用户 = yourUser ; 密码 = yourPwd :
在本示例, 使用默认端口是 1433。 将此默认替换端口号为您命名实例。
posted @ 2006-12-17 00:35 Alex 阅读(1216) | 评论 (0)编辑 收藏

key words: jsp,jstl,1.0,1.1,fn函数

原来一直用struts,最近项目里有人用jstl,我也就顺便拿来用,感觉还是不错。

过程中碰到一些小的问题总结如下:

一。版本问题
jstl存在1.0和1.1的差异问题,用EL建议需要在1.1的版本下,1.1的URI的标志为:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%>

注意,1.0版本没有/jsp/.

如果用的1.0会出现如下异常
org.apache.jasper.JasperException: /public/left_tree.jsp(100,24) According to TLD or attribute directive in tag file, attribute items does not accept any expressions
    org.apache.jasper.servlet.JspServletWrapper.handleJspException(JspServletWrapper.java:
510)
    org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:
375)
    org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:
314)
    org.apache.jasper.servlet.JspServlet.service(JspServlet.java:
264)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:
802)
    org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:
75)
    org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:
77)
    com.wellsoon.zfzw.webapp.common.VabAccessFilter.doFilter(VabAccessFilter.java:
43)


root cause 



二。打开EL

<%@ page contentType="text/html;charset=gb2312" language="java" isELIgnored="false" %>

前提是容器支持2.0,即使支持默认也未必打开,最安全的方式就是显示打开 isELIgnored="false"

三.fn的用法
对于Vo里的一个List作length的计算或判断很方便
<table width="160" border="0" cellspacing="0" cellpadding="4">
                        
<c:forEach items="${menuInfos}" var="m">
                            
<c:if test="${fn:length(m.subMenus)>0}">
                          
<tr>
                            
<td>
                                
<script language="JavaScript">
                                      
var tObj = new treeClass("A1")
                                      tObj.start();
                                     tObj.m_start(
"<c:out value="${m.menuModule}"/>",0);
                                    
<c:forEach items="${m.subMenus}" var="sub">
                                      tObj.add_Sub(
"<c:out value="${sub.menuName}"/>","<%=path%>
<c:out value=
"${sub.url}"/>","mymain");
                                    
</c:forEach>
                                     tObj.m_end();
                                      tObj.end();
                                      tObj.print();
                                 
</script>
                            
</td>
                        
</tr>
                            
</c:if>
                        
</c:forEach>

                    
</table>
 
四.tld的声明可以在web.xml作显式声明也可以用http作直接URL声明
推荐用本地的方式.

posted @ 2006-12-11 19:21 Alex 阅读(2779) | 评论 (2)编辑 收藏

key words: MiddleGen,hibernate,many to many,多对多

如果你有如下表结构

user(user_id,user_name)
role(role_id,role_name)

user_role(id,user_id,role_id)

那么默认MiddleGen生成的是两个一对多,但我们更多的情况是用many to many

需要修改middlegen的build.xml文件

<many2many>
            
<tablea generate="true" name="user"/>
            
<jointable name="user_role" generate="false"/>
            
<tableb generate="true" name="role"/>
         
</many2many>

posted @ 2006-12-07 20:26 Alex 阅读(892) | 评论 (0)编辑 收藏

越来越发现其实掌握 hibernate并不容易,Spring用起来其实简单多了,但是在用hibernate的时候真的是需要一定的时间积累,对一个项目组来说如果采用hibernate最好有一个对hibernate比较清楚的人否则碰到问题就会成为项目的风险。
我想告诉各位的是,掌握hibernate可能比你预期的难多了,当你轻松的告诉我,hibernate很简单的时候该是你自己多反省了. (只有一种情况例外,你是一个牛人)

好了,一个引子废话那么多,其实今天只是想先说一说hibernate里的Fetch的作用.

大家都知道,在hibernate里为了性能考虑,引进了lazy的概念,这里我们以Parent和Child为模型来说明,

public class Parent implements Serializable {

    
/** identifier field */
    
private Long id;

    
/** persistent field */
    
private List childs;

    
//skip all getter/setter method

  
}  



public class Child implements Serializable {

    
/** identifier field */
    
private Long id;

    
/** persistent field */
    
private net.foxlog.model.Parent parent;

    //skip all getter/setter method

}

在我们查询Parent对象的时候,默认只有Parent的内容,并不包含childs的信息,如果在Parent.hbm.xml里设置lazy="false"的话才同时取出关联的所有childs内容.

问题是我既想要hibernate默认的性能又想要临时的灵活性该怎么办?  这就是fetch的功能。我们可以把fetch与lazy="true"的关系类比为事务当中的编程式事务与声明式事务,不太准确,但是大概是这个意思。

总值,fetch就是在代码这一层给你一个主动抓取得机会.

Parent parent = (Parent)hibernateTemplate.execute(new HibernateCallback() {
            
public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Query q 
= session.createQuery(
                        
"from Parent as parent "+
                                
" left outer join fetch parent.childs " +
                                
" where parent.id = :id"
                );
                q.setParameter(
"id",new Long(15));
                
return (Parent)q.uniqueResult();
            }

        });

        Assert.assertTrue(parent.getChilds().size() 
> 0);


你可以在lazy="true"的情况下把fetch去掉,就会报异常. 当然,如果lazy="false"就不需要fetch了


有一个问题,使用Fetch会有重复记录的现象发生,我们可以理解为Fetch实际上不是为Parent服务的,而是为Child服务的.所以直接取Parent会有不匹配的问题.



参考一下下面的这篇文章
Hibernate集合初始化

======================================================================

update:以上有些结论错误,实际上在hibernate3.2.1版本下测试,可以不出现重复记录,

public void testNPlusOne() throws Exception{
        List list 
= (List)hibernateTemplate.execute(new HibernateCallback() {
            
public Object doInHibernate(Session session) throws HibernateException, SQLException {
                Query q 
= session.createQuery(
                        
"select distinct p from net.foxlog.model.Parent p inner join fetch p.childs"
                );
                
return q.list();
            }

        });

        
//((Parent)(list.get(0))).getChilds();
        System.out.println("list size = " + list.size());
        
for(int i=0;i<list.size();i++){
            Parent p 
= (Parent)list.get(i);
            System.out.println(
"===parent = " + p);
            System.out.println(
"===parent's child's length = " + p.getChilds().size());
        }

    }


打印结果如下:
Hibernate: select distinct parent0_.id as id2_0_, childs1_.id as id0_1_, childs1_.parent_id as parent2_0_1_, childs1_.parent_id as parent2_0__, childs1_.id as id0__ from parent parent0_ inner join child childs1_ on parent0_.id=childs1_.parent_id
list size 
= 3
===parent = net.foxlog.model.Parent@1401d28[id=14]
===parent's child's length = 1
===parent = net.foxlog.model.Parent@14e0e90[id=15]
===parent's child's length = 2
===parent = net.foxlog.model.Parent@62610b[id=17]
===parent's child's length = 3

另外,如果用open session in view模式的话一般不用fetch,但首先推荐fetch,如果非用的话因为有N+1的现象,所以可以结合batch模式来改善下性能.


posted @ 2006-12-01 13:01 Alex 阅读(19336) | 评论 (7)编辑 收藏

LDAPChina序言:这篇文章是互联网上最流行的一篇介绍LDAP基础知识的文章,本文作者Michael DonnellyLDAPMAN.org的站长。由于本文译文较长,我们将其分为两部分,第一部分介绍LDAP概论,第二部分介绍LDAP基础知识。感谢网友Brimmer翻译此文。

如 果你在计算机行业工作,那么对LDAP可能早有耳闻了。想深入地了解LDAP吗?那么可以好好地读一下这篇文章。这篇介绍性的文章是一系列介绍如何在企业 中设计、实现和集成LDAP环境的文章的头一篇。主要是先让你熟悉一下LDAP的基本概念,那些比较困难的细节问题将放到以后讨论。在这篇文章中我们将要 介绍:

什么是LDAP?
什么时候该用LDAP存储数据?

现 在LDAP技术不仅发展得很快而且也是激动人心的。在企业范围内实现LDAP可以让运行在几乎所有计算机平台上的所有的应用程序从LDAP目录中获取信 息。LDAP目录中可以存储各种类型的数据:电子邮件地址、邮件路由信息、人力资源数据、公用密匙、联系人列表,等等。通过把LDAP目录作为系统集成中 的一个重要环节,可以简化员工在企业内部查询信息的步骤,甚至连主要的数据源都可以放在任何地方。如果Oracle、Sybase、Informix或 Microsoft SQL数据库中已经存储了类似的数据,那么LDAP和这些数据库到底有什么不同呢?是什么让它更具优势?请继续读下去吧!


什么是LDAP?

LDAP 的英文全称是Lightweight Directory Access Protocol,一般都简称为LDAP。它是基于X.500标准的,但是简单多了并且可以根据需要定制。与X.500不同,LDAP支持TCP/IP, 这对访问Internet是必须的。LDAP的核心规范在RFC中都有定义,所有与LDAP相关的RFC都可以在LDAPChina.com RFC专栏中找到。

怎么使用LDAP这个术语呢?

在 日常交谈中,你可能会听到有些人这么说:“我们要把那些东西存在LDAP中吗?”,或者“从LDAP数据库中取出那些数据!”,又或者“我们怎么把 LDAP和关系型数据库集成在一起?”。严格地说,LDAP根本不是数据库而是用来访问存储在信息目录(也就是LDAP目录)中的信息的协议。更为确切和正式的说法应该是象这样的:“通过使用LDAP,可以在信息目录的正确位置读取(或存储)数据”。但是,也没有必要吹毛求疵,尽管表达得不够准确,我们也都知道对方在说什么。

LDAP目录是数据库吗?

就 象Sybase、Oracle、Informix或Microsoft的数据库管理系统(DBMS)是用于处理查询和更新关系型数据库那样,LDAP服务 器也是用来处理查询和更新LDAP目录的。换句话来说LDAP目录也是一种类型的数据库,但是不是关系型数据库。不象被设计成每分钟需要处理成百上千条数 据变化的数据库,例如:在电子商务中经常用到的在线交易处理(OLTP)系统,LDAP主要是优化数据读取的性能。

LDAP目录的优势

现在该说说LDAP目录到底有些什么优势了。现在LDAP的流行是很多因素共同作用的结果。我在这里说的不过是一些基本的原因,请你注意一下这不过是一小部分原因。

可能LDAP最大的优势是:可以在任何计算机平台上,用很容易获得的而且数目不断增加的LDAP的客户端程序访问LDAP目录。而且也很容易定制应用程序为它加上LDAP的支持。

LDAP 协议是跨平台的和标准的协议,因此应用程序就不用为LDAP目录放在什么样的服务器上操心了。实际上,LDAP得到了业界的广泛认可,因为它是 Internet的标准。产商都很愿意在产品中加入对LDAP的支持,因为他们根本不用考虑另一端(客户端或服务端)是怎么样的。LDAP服务器可以是任 何一个开发源代码或商用的LDAP目录服务器(或者还可能是具有LDAP界面的关系型数据库),因为可以用同样的协议、客户端连接软件包和查询命令与 LDAP服务器进行交互。与LDAP不同的是,如果软件产商想在软件产品中集成对DBMS的支持,那么通常都要对每一个数据库服务器单独定制。

不象很多商用的关系型数据库,你不必为LDAP的每一个客户端连接或许可协议付费。

大多数的LDAP服务器安装起来很简单,也容易维护和优化。

LDAP服务器可以用“推”或“拉”的方法复制部分或全部数据,例如:可以把数据“推”到远程的办公室,以增加数据的安全性。复制技术是内置在LDAP服务器中的而且很容易配置。如果要在DBMS中使用相同的复制功能,数据库产商就会要你支付额外的费用,而且也很难管理。

LDAP 允许你根据需要使用ACI(一般都称为ACL或者访问控制列表)控制对数据读和写的权限。例如,设备管理员可以有权改变员工的工作地点和办公室号码,但是 不允许改变记录中其它的域。ACI可以根据谁访问数据、访问什么数据、数据存在什么地方以及其它对数据进行访问控制。因为这些都是由LDAP目录服务器完 成的,所以不用担心在客户端的应用程序上是否要进行安全检查。

LDAP对于存储下面这样的信息最为有用,也就是数据需要从不同的地点读取,但是不需要经常更新。例如,这些信息存储在LDAP目录中是十分有效的:

  • 公司员工的电话号码簿和组织结构图
  • 客户的联系信息
  • 计算机管理需要的信息,包括NIS映射、email假名,等等
  • 软件包的配置信息
  • 公用证书和安全密匙

什么时候该用LDAP存储数据?

大 多数的LDAP服务器都为读密集型的操作进行专门的优化。因此,当从LDAP服务器中读取数据的时候会比从专门为OLTP优化的关系型数据库中读取数据快 一个数量级。也是因为专门为读的性能进行优化,大多数的LDAP目录服务器并不适合存储需要经常改变的数据。例如,用LDAP服务器来存储电话号码是一个 很好的选择,但是它不能作为电子商务站点的数据库服务器。

如果下面每一个问题的答案都是“是”,那么把数据存在LDAP中就是一个好主意。

  • 需要在任何平台上都能读取数据吗?
  • 每一个单独的记录项是不是每一天都只有很少的改变?
  • 可以把数据保存在平面数据库(flat database)而不是关系型数据库中吗?换句话来说,也就是不管什么范式不范式的,把所有东西都存在一个记录中(差不多只要满足第一范式)。

最 后一个问题可能会唬住一些人,其实用平面数据库去存储一些关系型的数据也是很一般的。例如,一条公司员工的记录就可以包含经理的登录名。用LDAP来存储 这类信息是很方便的。一个简单的判断方法:如果可以把数据保存在一张张的卡片里,就可以很容易地把它存在LDAP目录里。


接上文

LDAP目录树的结构
单条LDAP记录
定制目录的对象类
一个LDAP单个条目的例子
LDAP复制
安全和访问控制

LDAP目录树的结构

LDAP 目录以树状的层次结构来存储数据。如果你对自顶向下的DNS树或UNIX文件的目录树比较熟悉,也就很容易掌握LDAP目录树这个概念了。就象DNS的主 机名那样,LDAP目录记录的分辨名(Distinguished Name,简称DN)是用来读取单条记录,以及回溯到树的顶部。后面会做详细地介绍。

为什么要用层次结构来组织数据呢?原因是多方面的。下面是可能遇到的一些情况:

  • 如果你想把所有的美国客户的联系信息都“推”到位于到西雅图办公室(负责营销)的LDAP服务器上, 但是你不想把公司的资产管理信息“推”到那里。
  • 你可能想根据目录树的结构给予不同的员工组不同的权限。在下面的例子里,资产管理组对“asset-mgmt”部分有完全的访问权限,但是不能访问其它地方。
  • 把LDAP存储和复制功能结合起来,可以定制目录树的结构以降低对WAN带宽的要求。位于西雅图的营销办公室需要每分钟更新的美国销售状况的信息,但是欧洲的销售情况就只要每小时更新一次就行了。

刨根问底:基准DN

LDAP目录树的最顶部就是根,也就是所谓的“基准DN”。基准DN通常使用下面列出的三种格式之一。假定我在名为FooBar的电子商务公司工作,这家公司在Internet上的名字是foobar.com。

o="FooBar, Inc.", c=US

(以X.500格式表示的基准DN)

在 这个例子中,o=FooBar, Inc. 表示组织名,在这里就是公司名的同义词。c=US 表示公司的总部在美国。以前,一般都用这种方式来表示基准DN。但是事物总是在不断变化的,现在所有的公司都已经(或计划)上Internet上。随着 Internet的全球化,在基准DN中使用国家代码很容易让人产生混淆。现在,X.500格式发展成下面列出的两种格式。

o=foobar.com

(用公司的Internet地址表示的基准DN)

这种格式很直观,用公司的域名作为基准DN。这也是现在最常用的格式。

dc=foobar, dc=com

(用DNS域名的不同部分组成的基准DN)

就 象上面那一种格式,这种格式也是以DNS域名为基础的,但是上面那种格式不改变域名(也就更易读),而这种格式把域名:foobar.com分成两部分 dc=foobar, dc=com。在理论上,这种格式可能会更灵活一点,但是对于最终用户来说也更难记忆一点。考虑一下foobar.com这个例子。当 foobar.com和gizmo.com合并之后,可以简单的把“dc=com”当作基准DN。把新的记录放到已经存在的dc=gizmo, dc=com目录下,这样就简化了很多工作(当然,如果foobar.com和wocket.edu合并,这个方法就不能用了)。如果LDAP服务器是新 安装的,我建议你使用这种格式。再请注意一下,如果你打算使用活动目录(Actrive Directory),Microsoft已经限制你必须使用这种格式。

更上一层楼:在目录树中怎么组织数据

在UNIX文件系统中,最顶层是根目录(root)。在根目录的下面有很多的文件和目录。象上面介绍的那样,LDAP目录也是用

posted @ 2006-11-30 20:45 Alex| 编辑 收藏

key words:jotspot,wiki,项目管理

最近听说了jotspot,这个东西确实很好,原来一直打算自己建立一个wiki,作一些公司项目资源的管理,进度管理,以及提供其他团队成员了解项目信息的窗口,原来打算用jspwiki,可是建在哪呢? 在我的笔记本上还是公司的服务器上,似乎都不是很好,还是建在jotspot上比较好。zheng
给我看了一个截图:

jotspot_project.gif


功能还挺强大,我想做我的日常项目管理应该够了


posted @ 2006-11-09 22:03 Alex 阅读(695) | 评论 (2)编辑 收藏


key words:google搜索技巧,in site,inurl,filetype

最常用的是下面这个

IDEA in site:blogjava.net inurl:Alex filetype:ppt


下面转录一篇
玩转GOOGLE的十九招秘技


第一招:

  在输入多个词的时候,Google默认的是并且式的查询,如果想使用或者式的查询,使用OR,例如:java OR c++

第二招
  google是不区分大小写的,搜索Java和搜索JAVA或者java是完全一样的。

  第三招
  逻辑关系优先级使用圆括号,例如查找包含java和(JVM或者虚拟机):Java (JVM或者虚拟机)。

  第四招
  要搜索词组需要使用引号括起来,例如搜索Java虚拟机可以使用:"Java虚拟机"。

  第五招
  要想在搜索结果中不包含某些结果可以使用减号,例如搜索java又不包含培训可以使用:java -培训。

  第六招
  一般情况下是不能使用通配符的,通配符只能使用在词组中,例如:"使用*模式"。

  第七招
  只在网页的标题(即html的title指定的部分)内搜索指定的内容,例如:intitle:java。
第八招
  只在网页的url内搜索指定的内容,例如:inurl:java。

  第九招
  只在网页的正文内搜索指定的内容(忽略链接文字、标题和url),例如:intext:java。

  第十招
  只在链接文字(链接Java研究组织的链接文字就是Java研究组织)内搜索指定的内容,例如:inanchor:java。

  第十一招
  只在指定的网站内搜索指定的内容,可以是某个具体的网站或者是某个域名分类,例如:site:javaresearch.org或者site:org。

  第十二招
  只在指定的文件格式内搜索指定的内容,需要注意google只能支持有限的常用文本格式,包含一些诸如doc,xsl,ppt,pdf之类的富文本格式,例如:filetype:htm。

  第十三招
  google在检索的时候对于输入的内容的顺序是敏感的,如果找不到合适的结果可以试试改变一下搜索的关键字的顺序。


  第十四招
  在搜索多个关键字构成的内容时可以试试加引号和不加引号两者情况,结果可能有很大的差异,例如:java虚拟机和"java虚拟机"。

  第十五招
  搜索的策略可以采用先多后少,假设你想搜索Java的的command模式的例子,可以先试试java command pattern,然后试试java pattern或者java command,每次减少的那个关键字应该是你认为相对不重要的一个。
第十六招
  对于搜索中文而言,搜索“虚拟机”和搜索“虚拟机”是不同的,后者一般比前者的结果多并且相关性差一些,一般情况下你应该在各个词之间加空格,而在词内不加空格,这样得到的结果一般更好。

  第十七招
  一个关键字可以重复两次,对结果的排名和数量也会有影响,重复两次以上好像就没有什么影响了,例如搜索"internet"和搜索"internet internet"的结果是不同的。

  第十八招
  搜索内容最多只能包含十个单词,包括搜索intitle之类的选项,多出的部分被忽略。

  第十九招
  对于intitle,inurl,intext,inanchor和site之类的搜索选项一次不要使用多次,否则要么不是你想要的结果,要么根本就没有结果,混合使用这些选项是合法的,但是规则很复杂。

posted @ 2006-11-09 21:49 Alex 阅读(966) | 评论 (0)编辑 收藏

key words:mappingResources,通配符,mappingDirectoryLocations

平时写mapping的文件需要一个一个的放到配置文件里,比如
   1. <property name="mappingResources">  
   2.             
<list>  
   3.                 
<value>net/foxlog/model/Classes.hbm.xml</value>  
   4.                 
<value>net/foxlog/model/Parent.hbm.xml</value>  
   5.                 
<value>net/foxlog/model/Child.hbm.xml</value>  
   6.                 
<value>net/foxlog/model/User.hbm.xml</value>  
   7.             
</list>  
   8.         
</property>  


可以用更一步到位的方法:

   1. <property name="mappingDirectoryLocations">  
   2.             
<list>  
   3.                 
<value>classpath*:/org/springside/bookstore/commons/model/hbm</value>  
   4.             
</list>  
   5.         
</property>  

posted @ 2006-11-09 21:00 Alex 阅读(5013) | 评论 (0)编辑 收藏

key words:hibernate,复合主键,composite-id



基于业务需求,您会需要使用两个字段来作复合主键,例如在User数据表中,您也许会使用"name""phone"两个字段来定义复合主键。

假设您这么建立User表格:

CREATE TABLE user (

    name 
VARCHAR(100NOT NULL,

    phone 
VARCHAR(50NOT NULL,

    age 
INT,

    
PRIMARY KEY(name, phone)

);

在表格中,"name""age"被定义为复合主键,在映像时,您可以让User类别直接带有"name""age"这两个属性,而Hibernate要求复合主键类别要实作Serializable接口,并定义equals()hashCode()方法:

User.java

package onlyfun.caterpillar;

 

import java.io.Serializable;

import org.apache.commons.lang.builder.EqualsBuilder;

import org.apache.commons.lang.builder.HashCodeBuilder;

 

// 复合主键类的对应类别必须实作Serializable接口

public class User implements Serializable {

    
private String name;

    
private String phone;

    
private Integer age;

   

    
public User() {

    }

 

    
public Integer getAge() {

        
return age;

    }

 

    
public void setAge(Integer age) {

        
this.age = age;

    }

 

    
public String getName() {

        
return name;

    }

 

    
public void setName(String name) {

        
this.name = name;

    }

 

    
public String getPhone() {

        
return phone;

    }

 

    
public void setPhone(String phone) {

        
this.phone = phone;

    }

   

    
// 必须重新定义equals()与hashCode()

    
public boolean equals(Object obj) {

        
if(obj == this) {

            
return true;

        }

       

        
if(!(obj instanceof User)) {

            
return false;

        }

       

        User user 
= (User) obj;

        
return new EqualsBuilder()

                 .append(
this.name, user.getName())

                 .append(
this.phone, user.getPhone())

                 .isEquals();

       

    }

   

    
public int hashCode() {

        
return new HashCodeBuilder()

                 .append(
this.name)

                 .append(
this.phone)

                 .toHashCode();

    }

}

equals()hashCode()方法被用作两笔不同数据的识别依据;接着您可以使用<composite-id>在映射文件中定义复合主键与对象的属性对应:

User.hbm.xml

<?xml version="1.0" encoding="utf-8"?>

<!DOCTYPE hibernate-mapping

    PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"

    "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"
>

 

<hibernate-mapping>

 

    
<class name="onlyfun.caterpillar.User" table="user">

        
<composite-id>

            
<key-property name="name"

                          column
="name"

                          type
="java.lang.String"/>

            
<key-property name="phone"

                          column
="phone"

                          type
="java.lang.String"/>

        
</composite-id>

 

        
<property name="age" column="age" type="java.lang.Integer"/>

   

    
</class>

</hibernate-mapping>

在储存数据方面,复合主键的储存没什么区别,现在的问题在于如何依据复合主键来查询数据,例如使用load()方法,您可以创建一个User实例,并设定复合主键对应的属性,接着再透过load()查询对应的数据,例如:

User user = new User();

user.setName(
"bush");

user.setPhone(
"0970123456");

       

Session session 
= sessionFactory.openSession();

// 以实例设定复合主键并加载对应的数据

user 
= (User) session.load(User.class, user);

       

System.out.println(user.getAge() 
+ "\t" +

                                  user.getName() 
+ "\t" +

                                  user.getPhone());

session.close();

 

 

可以将主键的信息独立为一个类别,例如:

UserPK.java

package onlyfun.caterpillar;

 

import java.io.Serializable;

 

import org.apache.commons.lang.builder.EqualsBuilder;

import org.apache.commons.lang.builder.HashCodeBuilder;

 

public class UserPK implements Serializable {

    
private String name;

    
private String phone;

 

    
public String getName() {

        
return name;

    }

 

    
public void setName(String name) {

        
this.name = name;

    }

 

    
public String getPhone() {

        
return phone;

    }

 

    
public void setPhone(String phone) {

        
this.phone = phone;

    }

   

    
public boolean equals(Object obj) {

        
if(obj == this) {

            
return true;

        }

       

        
if(!(obj instanceof User)) {

            
return false;

        }

       

        UserPK pk 
= (UserPK) obj;

        
return new EqualsBuilder()

                 .append(
this.name, pk.getName())

                 .append(
this.phone, pk.getPhone())

                 .isEquals();

       

    }

   

    
public int hashCode() {

        
return new HashCodeBuilder()

                 .append(
this.name)

                 .append(
this.phone)

                 .toHashCode();

    }

}

现在User类别的主键信息被分离出来了,例如:

User.java

package onlyfun.caterpillar;

 

import java.io.Serializable;

 

public class User implements Serializable {

    
private UserPK userPK; // 主键

    
private Integer age;

   

    
public User() {

    }

 

    
public UserPK getUserPK() {

        
return userPK;

    }

 

    
public void setUserPK(UserPK userPK) {

        
this.userPK = userPK;

    }

 

    
public Integer getAge() {

        
return age;

    }

 

    
public void setAge(Integer age) {

        
this.age = age;

    }

}

在映像文件方面,需要指定主键类的信息,例如:

User.hbm.xml

<?xml version="1.0" encoding="utf-8"?>

<!DOCTYPE hibernate-mapping

    PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"

    "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"
>

 

<hibernate-mapping>

 

    
<class name="onlyfun.caterpillar.User" table="user">

        
<composite-id name="userPK"

                      class
="onlyfun.caterpillar.UserPK"

                      unsaved-value
="any">

            
<key-property name="name"

                          column
="name"

                          type
="java.lang.String"/>

            
<key-property name="phone"

                          column
="phone"

                          type
="java.lang.String"/>

        
</composite-id>

       

        
<property name="age" column="age" type="java.lang.Integer"/>

   

    
</class>

 

</hibernate-mapping>

在查询数据时,必须指定主键信息,例如:

UserPK pk = new UserPK();

pk.setName(
"bush");

pk.setPhone(
"0970123456");

      

Session session 
= sessionFactory.openSession();

// 以主键类实例设定复合主键并加载对应的数据

User user 
= (User) session.load(User.class, pk);

      

System.out.println(user.getAge() 
+ "\t" +

                                  user.getUserPK().getName() 
+ "\t" +

                                  user.getUserPK().getPhone());

session.close();

 


=================================================
再参考robbin的一篇文章
一个简单的复合主键的做关联类的例子  
posted @ 2006-11-09 19:22 Alex 阅读(12889) | 评论 (5)编辑 收藏

key words:jdk错误,Unsupported major.minor version

今天用一个工作流的产品,非要用jdk1.4的版本,没办法,只好切换回来,但是换回来后打开页面jsp出错,提示Unsupported major.minor version 49.0错误,到网上查了一下,49.0错误属于jdk1.5的错误,但是我的jdk1.5已经删除了啊?怎么回事呢?

最后想起来,可能是jboss中的1.5 版本产生临时文件class文件删除,删除后OK


资料:Unsupported major.minor version 49.0
posted @ 2006-11-08 10:13 Alex 阅读(561) | 评论 (2)编辑 收藏

key words:hibernate,load,session.get,session.load()

public class Parent implements Serializable {

    
/** identifier field */
    
private Long id;

    
/** persistent field */
    
private List childs;

    
/** full constructor */
    
public Parent(Long id, List childs) {
        
this.id = id;
        
this.childs = childs;
    }

    
/** default constructor */
    
public Parent() {
    }

    
/** 
     *            @hibernate.id
     *             generator-class="assigned"
     *             type="java.lang.Long"
     *             column="id"
     *
     
*/
    
public Long getId() {
        
return this.id;
    }

    
public void setId(Long id) {
        
this.id = id;
    }

    
/**
     *            @hibernate.set
     *             lazy="true"
     *             inverse="true"
     *             cascade="none"
     *            @hibernate.collection-key
     *             column="parent_id"
     *            @hibernate.collection-one-to-many
     *             class="net.foxlog.model.Child"
     *         
     
*/
    
public List getChilds() {
        
return this.childs;
    }

    
public void setChilds(List childs) {
        
this.childs = childs;
    }

    
public String toString() {
        
return new ToStringBuilder(this)
            .append(
"id", getId())
            .toString();
    }

    
public boolean equals(Object other) {
        
if ( !(other instanceof Parent) ) return false;
        Parent castOther 
= (Parent) other;
        
return new EqualsBuilder()
            .append(
this.getId(), castOther.getId())
            .isEquals();
    }

    
public int hashCode() {
        
return new HashCodeBuilder()
            .append(getId())
            .toHashCode();
    }

}


Parent parent = (Parent)session.load(Parent.class,new Integer(7));

运行提示出错,"can't get Entity.."

知道是什么低级错误么?  其实很弱智的东西,但是不注意还就会出现,呵呵,答案如下:


posted @ 2006-11-07 19:28 Alex 阅读(1627) | 评论 (3)编辑 收藏

key words: mysql,外键,fm150

这次在mysql中建立外键关联时发现问题:
alter table CHILD add constraint FK3D1FCFC74B18345 foreign key (PARENTID) references PARENT;      


提示..fm150错误,最后发现是mysql的版本问题,我用的是4.1的,对外键不知道是不支持还是支持的有问题,下载5.0的解决.
posted @ 2006-11-07 09:44 Alex 阅读(421) | 评论 (0)编辑 收藏

     摘要: 什么玩意  阅读全文
posted @ 2006-11-06 12:00 Alex 阅读(3434) | 评论 (9)编辑 收藏

     摘要:   阅读全文
posted @ 2006-10-31 12:32 Alex 阅读(8473) | 评论 (7)编辑 收藏

访问控制背景

    访问控制技术是由美国国防部(Department of Defense, DoD)资助的研究和开发成果演变而来的。这一研究导致两种基本类型访问控制的产生:自主访问控制(Discretionary Access Control, DAC)和强制访问控制(Mandatory Access Control, MAC)。最初的研究和应用主要是为了防止机密信息被未经授权者访问,近期的应用主要是把这些策略应用到为商业领域。

    自主访问控制,允许把访问控制权的授予和取消留给个体用户来判断。为没有访问控制权的个体用户授予和废除许可。自主访问控制机制允许用户被授权和
取 消访问其控制之下的任何客体(object),换句话说,用户就是他们控制下的客体的拥有者。然而,对于多数组织来说,最终用户对所访问的信息没有拥有 权。对于这些组织,公司或代理机构是事实上的系统客体和处理他们的程序的拥有者。访问优先权受组织控制,而且也常常基于雇员功能而不是数据所有权。

   强制访问控制,在美国国防部 Trusted Computer Security Evaluation Criteria (TCSEC) 中定义如下:“一种限制访问客体的手段,它以包含在这些客体中的信息敏感性和访问这些敏感性信息的主体的正式授权信息(如清除)为基础”。
  
   以上访问控制策略对于处理一些无需保密但又敏感的信息的政府和行业组织的需求并不是特别的适合。在这样的环境下,安全目标支持产生于现有法律、道德规范、 规章、或一般惯例的高端组织策略。这些环境通常需要控制个体行为的能力,而不仅仅是如何根据信息的敏感性为其设置标签从而访问这一信息的个人能力。
    


什么是基于角色访问控制(Role-Based Access Control, RBAC)?NIST 有如下定义。
  
   访问是一种利用计算机资源去做某件事情的的能力,访问控制是一种手段,通过它这种能力在某些情况下被允许或者受限制(通常是通过物理上和基于系统的控 制)。基于计算机的访问控制不仅可规定是“谁”或某个操作有权使用特定系统资源,而且也能规定被允许的访问类型。这些控制方式可在计算机系统或者外部设备 中实现。
  
    就基于角色访问控制而言,访问决策是基于角色的,个体用户是某个组织的一部分。用户具有指派的角色(比如医生、护士、出纳、经理)。定义角色的过程应该基于对组织运转的彻底分析,应该包括来自一个组织中更广范围用户的输入。
   
    访问权按角色名分组,资源的使用受限于授权给假定关联角色的个体。例如,在一个医院系统中,医生角色可能包括进行诊断、开据处方、指示实验室化验等;而研究员的角色则被限制在收集用于研究的匿名临床信息工作上。
   
    控制访问角色的运用可能是一种开发和加强企业特殊安全策略,进行安全管理过程流程化的有效手段。
     


用户(User)和角色(Role)

    用户指访问系统中的资源的主体,一般为人,也可为 Agent 等智能程序。角色指应用领域内一种权力和责任的语义综合体,可以是一个抽象概念,也可以是对应于实际系统中的特定语义体,比如组织内部的职务等。针对角色 属性的不同,某些模型中将角色进一步细分为普通角色和管理员角色(可理解为全局角色)。

 

许可(Permissions)和权限(Permission)

    许可描述了角色对计算机资源的访问和操作所具有的权限,其反映的是授权的结果。比如授予某个角色对计算机资源有读的权限,则代表了一个许可的存在,这个许 可表示:角色获取了对计算机资源的读许可。针对操作来说,其描述的是许可和操作之间的一种关联关系,而这层关系则表示了某一角色对某一操作所具有的权限及 权限状态。


     
角色和指派(Assignment)

    指派包含两个方面,用户指派和许可指派。用户指派表示的是,将用户指派给特定的角色。许可指派表示的是为角色指派计算机资源的访问和操作许可。

 

会话(session)

    会话表示的是用户和角色之间的关系。用户每次必须通过建立会话来激活角色,得到相应的访问权限。

 

角色和角色等级(Role Hierarchies)

    角色本身仅仅只是一个名词,其本身并不能代表权限的大小。比如,我们可以定一个“Director”的角色,也可以定一个“Project Leader”的角色。对于现实中我们来说,看到这样两个角色,就清楚 DIR 的权限要比一个 PL 的权限级别高。但是对计算机来说,这两个角色仅仅是两个“词语”,是等同的。可以采用分等级角色,在角色上实现层次化来解决这些问题。也可以采用复合角色 (其表示的就是一个角色组的概念),对角色实现一定的分组和复合,以便于权限指派。在一些 OA 产品中经常出现分等级角色。
   


限制(Constraints)
   
    模型中的职责分离关系(Separation of Duty),用于控制冲突(Conflict)。静态职责分离(Static SD)指定角色的互斥关系,用于用户指派阶段。避免同一用户拥有互斥的角色。实现简单,角色互斥语义关系清楚,便于管理不够灵活,不能处理某些实际情况。 动态职责分离(Dynamic SD)指定角色的互斥关系,用于角色激活阶段。允许同一用户拥有某些互斥的角色,但是不允许该用户同时激活互斥的角色。更灵活,直接与会话挂钩,适应实际 管理需要,实现复杂,不易管理。

             

 

 

参考文献

《AN INTRODUCTION TO ROLE-BASED ACCESS CONTROL》 NIST

《工作流授权控制模型》        胡长城

《基于角色的权限管理综述》 俞诗鹏

 

 

最后,感谢宏云博士对本文翻译提供的指导。   


请注意!引用、转贴本文应注明原作者:Rosen Jiang 以及出处:http://www.blogjava.net/rosen

posted @ 2006-10-26 21:31 Alex 阅读(461) | 评论 (0)编辑 收藏

     摘要: 前言 : 权限往往是一个极其复杂的问题,但也可简单表述为这样的逻辑表达式:判断“ Who 对 What(Which) ...  阅读全文
posted @ 2006-10-26 21:28 Alex 阅读(360) | 评论 (0)编辑 收藏

转自 javaEye


很多人对二级缓存都不太了解,或者是有错误的认识,我一直想写一篇文章介绍一下hibernate的二级缓存的,今天终于忍不住了。
我的经验主要来自hibernate2.1版本,基本原理和3.0、3.1是一样的,请原谅我的顽固不化。

hibernate的session提供了一级缓存,每个session,对同一个id进行两次load,不会发送两条sql给数据库,但是session关闭的时候,一级缓存就失效了。

二级缓存是SessionFactory级别的全局缓存,它底下可以使用不同的缓存类库,比如ehcache、oscache等,需要设置hibernate.cache.provider_class,我们这里用ehcache,在2.1中就是
hibernate.cache.provider_class=net.sf.hibernate.cache.EhCacheProvider
如果使用查询缓存,加上
hibernate.cache.use_query_cache=true

缓存可以简单的看成一个Map,通过key在缓存里面找value。

Class的缓存
对于一条记录,也就是一个PO来说,是根据ID来找的,缓存的key就是ID,value是POJO。无论list,load还是 iterate,只要读出一个对象,都会填充缓存。但是list不会使用缓存,而iterate会先取数据库select id出来,然后一个id一个id的load,如果在缓存里面有,就从缓存取,没有的话就去数据库load。假设是读写缓存,需要设置:
<cache usage="read-write"/>
如果你使用的二级缓存实现是ehcache的话,需要配置ehcache.xml
<cache name="com.xxx.pojo.Foo" maxElementsInMemory="500" eternal="false" timeToLiveSeconds="7200" timeToIdleSeconds="3600" overflowToDisk="true" />
其中eternal表示缓存是不是永远不超时,timeToLiveSeconds是缓存中每个元素(这里也就是一个POJO)的超时时间,如 果eternal="false",超过指定的时间,这个元素就被移走了。timeToIdleSeconds是发呆时间,是可选的。当往缓存里面put 的元素超过500个时,如果overflowToDisk="true",就会把缓存中的部分数据保存在硬盘上的临时文件里面。
每个需要缓存的class都要这样配置。如果你没有配置,hibernate会在启动的时候警告你,然后使用defaultCache的配置,这样多个class会共享一个配置。
当某个ID通过hibernate修改时,hibernate会知道,于是移除缓存。
这样大家可能会想,同样的查询条件,第一次先list,第二次再iterate,就可以使用到缓存了。实际上这是很难的,因为你无法判断什么时候 是第一次,而且每次查询的条件通常是不一样的,假如数据库里面有100条记录,id从1到100,第一次list的时候出了前50个id,第二次 iterate的时候却查询到30至70号id,那么30-50是从缓存里面取的,51到70是从数据库取的,共发送1+20条sql。所以我一直认为 iterate没有什么用,总是会有1+N的问题。
(题外话:有说法说大型查询用list会把整个结果集装入内存,很慢,而iterate只select id比较好,但是大型查询总是要分页查的,谁也不会真的把整个结果集装进来,假如一页20条的话,iterate共需要执行21条语句,list虽然选择 若干字段,比iterate第一条select id语句慢一些,但只有一条语句,不装入整个结果集hibernate还会根据数据库方言做优化,比如使用mysql的limit,整体看来应该还是 list快。)
如果想要对list或者iterate查询的结果缓存,就要用到查询缓存了

查询缓存
首先需要配置hibernate.cache.use_query_cache=true
如果用ehcache,配置ehcache.xml,注意hibernate3.0以后不是net.sf的包名了
<cache name="net.sf.hibernate.cache.StandardQueryCache"
maxElementsInMemory="50" eternal="false" timeToIdleSeconds="3600"
timeToLiveSeconds="7200" overflowToDisk="true"/>
<cache name="net.sf.hibernate.cache.UpdateTimestampsCache"
maxElementsInMemory="5000" eternal="true" overflowToDisk="true"/>
然后
query.setCacheable(true);//激活查询缓存
query.setCacheRegion("myCacheRegion");//指定要使用的cacheRegion,可选
第二行指定要使用的cacheRegion是myCacheRegion,即你可以给每个查询缓存做一个单独的配置,使用setCacheRegion来做这个指定,需要在ehcache.xml里面配置它:
<cache name="myCacheRegion" maxElementsInMemory="10" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="7200" overflowToDisk="true" />
如果省略第二行,不设置cacheRegion的话,那么会使用上面提到的标准查询缓存的配置,也就是net.sf.hibernate.cache.StandardQueryCache

对于查询缓存来说,缓存的key是根据hql生成的sql,再加上参数,分页等信息(可以通过日志输出看到,不过它的输出不是很可读,最好改一下它的代码)。
比如hql:
from Cat c where c.name like ?
生成大致如下的sql:
select * from cat c where c.name like ?
参数是"tiger%",那么查询缓存的key*大约*是这样的字符串(我是凭记忆写的,并不精确,不过看了也该明白了):
select * from cat c where c.name like ? , parameter:tiger%
这样,保证了同样的查询、同样的参数等条件下具有一样的key。
现在说说缓存的value,如果是list方式的话,value在这里并不是整个结果集,而是查询出来的这一串ID。也就是说,不管是list方 法还是iterate方法,第一次查询的时候,它们的查询方式很它们平时的方式是一样的,list执行一条sql,iterate执行1+N条,多出来的 行为是它们填充了缓存。但是到同样条件第二次查询的时候,就都和iterate的行为一样了,根据缓存的key去缓存里面查到了value,value是 一串id,然后在到class的缓存里面去一个一个的load出来。这样做是为了节约内存。
可以看出来,查询缓存需要打开相关类的class缓存。list和iterate方法第一次执行的时候,都是既填充查询缓存又填充class缓存的。
这里还有一个很容易被忽视的重要问题,即打开查询缓存以后,即使是list方法也可能遇到1+N的问题!相同条件第一次list的 时候,因为查询缓存中找不到,不管class缓存是否存在数据,总是发送一条sql语句到数据库获取全部数据,然后填充查询缓存和class缓存。但是第 二次执行的时候,问题就来了,如果你的class缓存的超时时间比较短,现在class缓存都超时了,但是查询缓存还在,那么list方法在获取id串以 后,将会一个一个去数据库load!因此,class缓存的超时时间一定不能短于查询缓存设置的超时时间!如果还设置了发呆时间的话,保证class缓存 的发呆时间也大于查询的缓存的生存时间。这里还有其他情况,比如class缓存被程序强制evict了,这种情况就请自己注意了。

另外,如果hql查询包含select字句,那么查询缓存里面的value就是整个结果集了。

当hibernate更新数据库的时候,它怎么知道更新哪些查询缓存呢?
hibernate在一个地方维护每个表的最后更新时间,其实也就是放在上面net.sf.hibernate.cache.UpdateTimestampsCache所指定的缓存配置里面。
当通过hibernate更新的时候,hibernate会知道这次更新影响了哪些表。然后它更新这些表的最后更新时间。每个缓存都有一个生成时 间和这个缓存所查询的表,当hibernate查询一个缓存是否存在的时候,如果缓存存在,它还要取出缓存的生成时间和这个缓存所查询的表,然后去查找这 些表的最后更新时间,如果有一个表在生成时间后更新过了,那么这个缓存是无效的。
可以看出,只要更新过一个表,那么凡是涉及到这个表的查询缓存就失效了,因此查询缓存的命中率可能会比较低。

Collection缓存
需要在hbm的collection里面设置
<cache usage="read-write"/>
假如class是Cat,collection叫children,那么ehcache里面配置
<cache name="com.xxx.pojo.Cat.children"
maxElementsInMemory="20" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="7200"
overflowToDisk="true" />
Collection的缓存和前面查询缓存的list一样,也是只保持一串id,但它不会因为这个表更新过就失效,一个collection缓存仅在这个collection里面的元素有增删时才失效。
这样有一个问题,如果你的collection是根据某个字段排序的,当其中一个元素更新了该字段时,导致顺序改变时,collection缓存里面的顺序没有做更新。

缓存策略
只读缓存(read-only):没有什么好说的
读/写缓存(read-write):程序可能要的更新数据
不严格的读/写缓存(nonstrict-read-write):需要更新数据,但是两个事务更新同一条记录的可能性很小,性能比读写缓存好
事务缓存(transactional):缓存支持事务,发生异常的时候,缓存也能够回滚,只支持jta环境,这个我没有怎么研究过

读写缓存和不严格读写缓存在实现上的区别在于,读写缓存更新缓存的时候会把缓存里面的数据换成一个锁,其他事务如果去取相应的缓存数据,发现被锁住了,然后就直接取数据库查询。
在hibernate2.1的ehcache实现中,如果锁住部分缓存的事务发生了异常,那么缓存会一直被锁住,直到60秒后超时。
不严格读写缓存不锁定缓存中的数据。

使用二级缓存的前置条件
你的hibernate程序对数据库有独占的写访问权,其他的进程更新了数据库,hibernate是不可能知道的。你操作数据库必需直接通过 hibernate,如果你调用存储过程,或者自己使用jdbc更新数据库,hibernate也是不知道的。hibernate3.0的大批量更新和删 除是不更新二级缓存的,但是据说3.1已经解决了这个问题。
这个限制相当的棘手,有时候hibernate做批量更新、删除很慢,但是你却不能自己写jdbc来优化,很郁闷吧。
SessionFactory也提供了移除缓存的方法,你一定要自己写一些JDBC的话,可以调用这些方法移除缓存,这些方法是:
void evict(Class persistentClass)
Evict all entries from the second-level cache.
void evict(Class persistentClass, Serializable id)
Evict an entry from the second-level cache.
void evictCollection(String roleName)
Evict all entries from the second-level cache.
void evictCollection(String roleName, Serializable id)
Evict an entry from the second-level cache.
void evictQueries()
Evict any query result sets cached in the default query cache region.
void evictQueries(String cacheRegion)
Evict any query result sets cached in the named query cache region.
不过我不建议这样做,因为这样很难维护。比如你现在用JDBC批量更新了某个表,有3个查询缓存会用到这个表,用evictQueries (String cacheRegion)移除了3个查询缓存,然后用evict(Class persistentClass)移除了class缓存,看上去好像完整了。不过哪天你添加了一个相关查询缓存,可能会忘记更新这里的移除代码。如果你的 jdbc代码到处都是,在你添加一个查询缓存的时候,还知道其他什么地方也要做相应的改动吗?

----------------------------------------------------

总结:
不要想当然的以为缓存一定能提高性能,仅仅在你能够驾驭它并且条件合适的情况下才是这样的。hibernate的二级缓存限制还是比较多的,不方便用jdbc可能会大大的降低更新性能。在不了解原理的情况下乱用,可能会有1+N的问题。不当的使用还可能导致读出脏数据。
如果受不了hibernate的诸多限制,那么还是自己在应用程序的层面上做缓存吧。
在越高的层面上做缓存,效果就会越好。就好像尽管磁盘有缓存,数据库还是要实现自己的缓存,尽管数据库有缓存,咱们的应用程序还是要做缓存。因为 底层的缓存它并不知道高层要用这些数据干什么,只能做的比较通用,而高层可以有针对性的实现缓存,所以在更高的级别上做缓存,效果也要好些吧。

终于写完了,好累……

posted @ 2006-10-23 14:30 Alex 阅读(604) | 评论 (1)编辑 收藏

在Struts或别的框架中集成Spring的时候,Spring向我们提供了获得context的方法 getApplicationContext,那在jsp中如何获得呢?

ApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext
(
this.getServletConfig().getServletContext());

建议在项目中开发的时候提供一个singleton对外公布统一的applicationContext,毕竟不是每个人都一定能获得web环境或servlet.

posted @ 2006-10-23 11:03 Alex 阅读(2521) | 评论 (0)编辑 收藏

碰到一个奇怪的问题,Spring在启动的时候得listener提示启动失败,打开log也没有任何信息,最后把log4j打开 :

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">

<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">

    
<appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
        
<layout class="org.apache.log4j.PatternLayout">
            
<param name="ConversionPattern"
                   value
="%p - %C{1}.%M(%L) | %m%n"/>
        
</layout>
    
</appender>

    
<logger name="org.apache">
        
<level value="WARN"/>
    
</logger>


    
<logger name="net.sf.hibernate">
        
<level value="WARN"/>
    
</logger>

    
<logger name="org.springframework">
        
<level value="DEBUG"/>
    
</logger>
    
<!--
    <logger name="org.appfuse">
        <level value="DEBUG"/>
    </logger>
-->
    
<root>
        
<level value="DEBUG"/>
        
<appender-ref ref="CONSOLE"/>
    
</root>

</log4j:configuration>

提示说applicationContext.xml的编码有问题,最后改为UTF-8解决
posted @ 2006-10-23 10:57 Alex 阅读(1367) | 评论 (0)编辑 收藏

打印可以用控件实现,保存为excel也可以用POI实现,不过如果仅仅是对当前页面的指定区域作打印或者Excel导出可以用js实现,还是挺简单的.

保存为Excel:

function saveAsExcel(HeadName, DivName) {
            
var s = "<center>" + HeadName + "</center>" + "\r\n";
            s 
+= DivName.innerHTML;
            
var xlsWindow = window.open("""_blank""width=1,height=1,scrollbars=no,toolbar=no");
            xlsWindow.document.write(s);
            xlsWindow.document.close();
            xlsWindow.document.execCommand('Saveas', 
true, '%homeDrive%\\Data.xls')
            xlsWindow.close();
        }


打印当前页面:

function PrintDataSoure(HeadName1,HeadName2,HeadName3,DivName,TailName1) {
  
var oldBody=document.body.innerHTML;
  
var Div1=DivName.innerHTML;
  
var css = '<style type="text/css" media=all>+
  'p { line
-height: 120%}' +
  '.fhead {   font
-size: 9pt; text-decoration: none; color: 104A7B}' +
  '.ftitle { line
-height: 120%; font-size: 18px; color: #000000}' +
  'td { font
-size: 10px; color: #000000}' +
  '
</style>' ;

  
var body ='<table width="640" border="0" cellspacing="0" cellpadding="5">+
  ' 
<tr> ' +
  ' 
<td class="fbody">+
  ' 
<b><div align="center">'+'<font size="+1" class=fhead>'+ HeadName1 + '</div>'+'</font></b>'+
  ' 
<b><div align="center">'+'<font size="+1" class=fhead>'+ HeadName2 + '     ' + HeadName3 +'</div></font></b>'+
  ' 
</td>+
  ' 
</tr>+
  ' 
<tr> ' +
  ' 
<td class="fbody">+
  ' 
<div align="center" class=ftitle>+ Div1 + '</div>'+
  ' 
<b><div align="right">'+'<font size="+1" class=fhead>'+ '      </div>'+'</font></b>'+
  ' 
<b><div align="right">'+'<font size="+1" class=fhead>'+ TailName1 + '</div>'+'</font></b>'+
  ' 
</td>+
  ' 
</tr>+
  '
</table>';
document.body.innerHTML 
= '<center>+ css + body +'<OBJECT classid="CLSID:8856F961-340A-11D0-A96B-00C04FD705A2" height=0 id=wb name=wb width=0></OBJECT>'+'</center>';
wb.execwb(
6,6);
document.body.innerHTML
=oldBody;
}




posted @ 2006-10-21 12:19 Alex 阅读(3127) | 评论 (0)编辑 收藏

     摘要: key words:Spring,jdbcTemplate 注:因为Spring是以后的一个趋势,Hibernate的集成已经很好了,对于单独的jdbc的操作用DBUtils感觉已经没有什么必要,不如全部转到Spring的jdbc支持,从成本来考虑似乎更合适。 本文转自 这里 ...  阅读全文
posted @ 2006-10-16 16:28 Alex 阅读(2728) | 评论 (0)编辑 收藏

     摘要:   阅读全文
posted @ 2006-10-10 11:09 Alex 阅读(2031) | 评论 (3)编辑 收藏

Facade用的非常的广了,以前刚接触的时候有个误解,总觉得Facade是简单的,而它后面的支撑服务是复杂的,对于客户来说却是简单的,现在来看,不完全对,或者说只是说对了一半,因为有时候恰恰是Facade是复杂的.

我们举一个例子,比如发送短信,我们一般就定义一个MessageService的服务类,里面只提供一个方法就行了,sendToUser(String phone,String content)
但是到了客户端的时候有了自己的 "方言",比如它不是关心一个抽象的用户,它只知道向教师发送短信,或者向学生发送短信,或者向家长发送短信。

示例如下:

facade.png
由图中可以看到,Facade的内容非常丰富,而支撑它的服务类却很简单,在开发过程中我们一般先实现通用的ServiceA,然后根据进一步的需求做一个面向具体复杂的Facade.



在Spring提供的sample里发现一个小技巧,就是Facade和ServiceA都是接口,然后提供一个实现二者的支撑类:


public class PetStoreAnnotationImpl implements PetStoreFacade, OrderService {

    
private AccountDao accountDao;

    
private CategoryDao categoryDao;

    
private ProductDao productDao;

    
private ItemDao itemDao;

    
private OrderDao orderDao;


    
//-------------------------------------------------------------------------
    
// Setter methods for dependency injection
    
//-------------------------------------------------------------------------

    
public void setAccountDao(AccountDao accountDao) {
        
this.accountDao = accountDao;
    }

    
public void setCategoryDao(CategoryDao categoryDao) {
        
this.categoryDao = categoryDao;
    }

    
public void setProductDao(ProductDao productDao) {
        
this.productDao = productDao;
    }

    
public void setItemDao(ItemDao itemDao) {
        
this.itemDao = itemDao;
    }

    
public void setOrderDao(OrderDao orderDao) {
        
this.orderDao = orderDao;
    }


    
//-------------------------------------------------------------------------
    
// Operation methods, implementing the PetStoreFacade interface
    
//-------------------------------------------------------------------------

    
public Account getAccount(String username) {
        
return this.accountDao.getAccount(username);
    }

    
public Account getAccount(String username, String password) {
        
return this.accountDao.getAccount(username, password);
    }

    
public void insertAccount(Account account) {
        
this.accountDao.insertAccount(account);
    }

    
public void updateAccount(Account account) {
        
this.accountDao.updateAccount(account);
    }

    
public List getUsernameList() {
        
return this.accountDao.getUsernameList();
    }

    
public List getCategoryList() {
        
return this.categoryDao.getCategoryList();
    }

    
public Category getCategory(String categoryId) {
        
return this.categoryDao.getCategory(categoryId);
    }

    
public List getProductListByCategory(String categoryId) {
        
return this.productDao.getProductListByCategory(categoryId);
    }

    
public List searchProductList(String keywords) {
        
return this.productDao.searchProductList(keywords);
    }

    
public Product getProduct(String productId) {
        
return this.productDao.getProduct(productId);
    }

    
public List getItemListByProduct(String productId) {
        
return this.itemDao.getItemListByProduct(productId);
    }

    
public Item getItem(String itemId) {
        
return this.itemDao.getItem(itemId);
    }

    
public boolean isItemInStock(String itemId) {
        
return this.itemDao.isItemInStock(itemId);
    }

    
public void insertOrder(Order order) {
        
this.orderDao.insertOrder(order);
        
this.itemDao.updateQuantity(order);
    }

    
public Order getOrder(int orderId) {
        
return this.orderDao.getOrder(orderId);
    }

    
public List getOrdersByUsername(String username) {
        
return this.orderDao.getOrdersByUsername(username);
    }

}


看起来似乎不错,不过仔细想想个人认为还是不是太好,总的感觉就是层次不清晰,因为很多时候Facade和Service之间是被服务与服务的关系,所以理当分开。 同时,这个类有点倾向于"万能类"了,能分还是分开好.这和我们以前提到的dao又背离过来了(以前我们提倡一个业务一个dao,现在觉得只用一个通用的dao更合适),这个并不矛盾,具体问题具体看待.

欢迎各位拍砖.
posted @ 2006-10-09 22:27 Alex 阅读(1354) | 评论 (2)编辑 收藏

设计模式之Visitor

板桥里人 http://www.jdon.com 2002/05/05(转载请保留)

模式实战书籍《Java实用系统开发指南》

Visitor访问者模式定义
作用于某个对象群中各个对象的操作. 它可以使你在不改变这些对象本身的情况下,定义作用于这些对象的新操作.

在Java中,Visitor模式实际上是分离了collection结构中的元素和对这些元素进行操作的行为.

为何使用Visitor?
Java的Collection(包括Vector和Hashtable)是我们最经常使用的技术,可是Collection好象是个黑色大染缸,本来有 各种鲜明类型特征的对象一旦放入后,再取出时,这些类型就消失了.那么我们势必要用If来判断,如:


Iterator iterator = collection.iterator()
while (iterator.hasNext()) {
   Object o = iterator.next();
   if (o instanceof Collection)
      messyPrintCollection((Collection)o);
   else if (o instanceof String)
      System.out.println("'"+o.toString()+"'");
   else if (o instanceof Float)
      System.out.println(o.toString()+"f");
   else
      System.out.println(o.toString());
}
在上例中,我们使用了 instanceof来判断 o的类型.

很显然,这样做的缺点代码If else if 很繁琐.我们就可以使用Visitor模式解决它.

如何使用Visitor?
针对上例,定义接口叫Visitable,用来定义一个Accept操作,也就是说让Collection每个元素具备可访问性.

被访问者是我们Collection的每个元素Element,我们要为这些Element定义一个可以接受访问的接口(访问和被访问是互动的,只有访问者,被访问者如果表示不欢迎,访问者就不能访问),取名为Visitable,也可取名为Element。

public interface Visitable
{
   public void accept(Visitor visitor);
}

被访问的具体元素继承这个新的接口Visitable:

public class StringElement implements Visitable
{
   private String value;
   public StringElement(String string) {
      value = string;
   }

   public String getValue(){
      return value;
   }


   //定义accept的具体内容 这里是很简单的一句调用
   public void accept(Visitor visitor) {
      visitor.visitString(this);
   }
}


上面是被访问者是字符串类型,下面再建立一个Float类型的:

public class FloatElement implements Visitable
{
   private Float value;
   public FloatElement(Float value) {
      this.value = value;
   }

   public Float getValue(){
      return value;
   }


   //定义accept的具体内容 这里是很简单的一句调用
   public void accept(Visitor visitor) {
      visitor.visitFloat(this);
   }
}


我 们设计一个接口visitor访问者,在这个接口中,有一些访问操作,这些访问操作是专门访问对象集合Collection中有可能的所有类,目前我们假 定有三个行为:访问对象集合中的字符串类型;访问对象集合中的Float类型;访问对象集合中的对象集合类型。注意最后一个类型是集合嵌套,通过这个嵌套 实现可以看出使用访问模式的一个优点。

接口visitor访问者如下:

public interface Visitor
{

   public void visitString(StringElement stringE);
   public void visitFloat(FloatElement floatE);
   public void visitCollection(Collection collection);

}

访问者的实现:

public class ConcreteVisitor implements Visitor
{
   //在本方法中,我们实现了对Collection的元素的成功访问
   public void visitCollection(Collection collection) {
      Iterator iterator = collection.iterator()
      while (iterator.hasNext()) {
         Object o = iterator.next();
         if (o instanceof Visitable)
            ((Visitable)o).accept(this);
      }
   }

   public void visitString(StringElement stringE) {
      System.out.println("'"+stringE.getValue()+"'");
   }
   public void visitFloat(FloatElement floatE){
      System.out.println(floatE.getValue().toString()+"f");
   }

}

在上面的visitCollection我们实现了对Collection每个元素访问,只使用了一个判断语句,只要判断其是否可以访问.

StringElement只是一个实现,可以拓展为更多的实现,整个核心奥妙在accept方法中,在遍历Collection时,通过相应的accept方法调用具体类型的被访问者。这一步确定了被访问者类型,

如果是StringElement,而StringElement则回调访问者的visiteString方法,这一步实现了行为操作方法。

客户端代码:

Visitor visitor = new ConcreteVisitor();

StringElement stringE = new StringElement("I am a String");
visitor.visitString(stringE);

Collection list = new ArrayList();
list.add(new StringElement("I am a String1"));
list.add(new StringElement("I am a String2"));
list.add(new FloatElement(new Float(12)));
list.add(new StringElement("I am a String3"));
visitor.visitCollection(list);

客户端代码中的list对象集合中放置了多种数据类型,对对象集合中的访问不必象一开始那样,使用instance of逐个判断,而是通过访问者模式巧妙实现了。

至此,我们完成了Visitor模式基本结构.

使用Visitor模式的前提
使用访问者模式是对象群结构中(Collection) 中的对象类型很少改变。

在两个接口Visitor和Visitable中,确保Visitable很少变化,也就是说,确保不能老有新的Element元素类型加进来,可以变化的是访问者行为或操作,也就是Visitor的不同子类可以有多种,这样使用访问者模式最方便.

如果对象集合中的对象集合经常有变化, 那么不但Visitor实现要变化,Visistable也要增加相应行为,GOF建议是,不如在这些对象类中直接逐个定义操作,无需使用访问者设计模式。

但是在Java中,Java的Reflect技术解决了这个问题,因此结合reflect反射机制,可以使得访问者模式适用范围更广了。

Reflect技术是在运行期间动态获取对象类型和方法的一种技术,具体实现参考Javaworld的英文原文.
posted @ 2006-09-28 09:30 Alex 阅读(522) | 评论 (0)编辑 收藏

key words:软件设计 建模 UML

这篇文章以前就看到了,后来再想看的时候居然找不到了,感觉写的不错,作为想把软件开发往深里整地朋友有借鉴作用。

转自这里

前段时间把一个界面框架完成了,今天基于这个框架开发一个小模块,在这里把这个模块设计的全过程记录下来,希望大家讨论并指正。

一、起因

公 司交给我一个任务,为测试员写一个手机模拟界面,以方便她们的手机短信测试。过去她们都是用MC4J直接调用公司服务器的MBean服务来模拟进行测试, 以验证我们整个系统平台。这种测试主要是检查收发短信是否正常,而我的要做的工作就是,让她们在测试的时候更方便更直观。

二、需求

我和测试员陈MM(也就是软件的使用者)约定了一个时间,大家一起来讨论这个软件的需求。

1、首先,我大概了解了一下她们的测试工作,知道我要做个什么东东。

2、然后我回去思考了一下,再次找她详细了解其测试的具体步骤,并在一张白纸上以UML用例图的方式,记录下需求的功能。用例是什么?用例就是需求,就是你的软件应该具有的功能,当然用例图只是概括性的对功能进行了描述。

3、最后,我坐在我的电脑前开始用MagicDraw UML来画用例图(我不喜欢用Rose,那玩意太笨重了,界面友好性也不好)。在画用例图的时候,我发现了一些隐含的功能,这些是陈MM在和我做需求时没有考虑到的(注:开发者应该为用户挖掘隐含需求)。我和陈MM一一确定了这些我新发现的需求,最后得到如下的用例图。

(1)手机前台测试操作的用例图(说明:include是指某用例包含(include)子用例)

[用例]手机.jpg
 (2)后台管理
[用例]后台管理.jpg


三、界面设计

接下来是界面设计。既然是手机模拟,我很自然就拿我的motorola手机的操作界面来做参考。不过这里应该注意到,手机操作环境和电脑操作环境不尽相同(比如说电脑有鼠标,还有键盘可以输入文字),所以没有必要唯妙唯肖的完全模枋,还是以使用者操作方便为主。

界 面设计是很重要的一步,不要一上来就写程序,一定要先做到心中有个大概,否则返工的可能性就很大。而且,把界面拿出来给客户看,客户也就能做到心中有数, 还能尽早提出一些新需求和意见来。千万不要等到软件做完了再拿给客户看,到时客户看了如果要修改,那就做太多白费工了。

由于软件界面相对简单,陈MM基本没有提修改意见,但这不是个好兆头。不过极限编程就是要拥抱变化不是^_^。咱不怕她改,只要大致的界面她能定下来就行了。

界面我喜欢用Visio来画,当然也听说有人喜欢用VB来快速构建界面原型的,看个人喜好了。整个界面如下:
[界面设计]手机.jpg



这个是后台管理界面
[界面设计]号码管理.jpg



四、类图

类图反映了软件的数据模型。在设计数据模型,我参考了界面设计图和用例图,找出一个个的类。然后参照用例图的一个个功能,设计出了各类的属性和方法。设计初始的类图当然不可能很详细,但至少应该看到个大概。有错误不要紧,后期可以慢慢修正,但大体关系就算定下来了。

Neil(公司CTO,一个40岁左右的真正的资深程序员)说:看一个软件的设计主要看两个类:类图和时序图。类图确定了软件数据模型的静态关型,时序图则是数据模型的动态关系。

类图如下,看英文大致可以知道类/属性/方法的含义和作用了,就不一一介绍了。


[类图].jpg



五、时序图

时序图是本文最后一个图,时序图表明了用例图中各功能的实现方案,同时也反应了类图中各类的交互关系。以后程序的逻辑和时序图基本一致。不过,有些人会去画得很详细的时序图,详细到都快赶上伪代码级别了,我觉得这没必要。我把时序图看做反映自己思路的大概过程,所以也就画个大概。

我认为时序图要简洁易懂,这样以后你的后继维护者,拿到这个软件的时序图(当然也包括用例图、类图),就能明白你的大概设计思路。另外,画时序图也能整理自己的思路,同时还可以对类图的设计进行验证。在画这个时序图的过程中,我就纠正了在类图中的几处考虑不周的地方。

总结:时序图可以(1)整理思路(2)验证类的设计(3)是很好的软件文档,对维护者理解代码很有帮助。

这里仅给出其中几个时序图(实际上我也没有把用例都画完,有些类似的简单的,就忽略了)

(1)新增一个手机号码
[时序图]add phone number.jpg

(2)关机
[时序图]power off.jpg
(3)开机
[时序图]power on.jpg
(4)发送短信
[时序图]send message.jpg



到这里设计阶段就完成了,用时一天。下一步是编码,将应用TDD先写测试代码的方式来写代码,下次再介绍了。


作者简介

陈刚,广西桂林人,著作有《Eclipse从入门到精通》
您可以通过其博客了解更多信息和文章:http://www.ChenGang.com.cn
版权声明:本博客所有文章仅适用于非商业性转载,并请在转载时注明出处及作者的署名。

posted @ 2006-09-22 13:16 Alex 阅读(917) | 评论 (0)编辑 收藏

key words:软件架构师
转自here

现在软件架构师满天飞,是个写代码的都称自己为软件架构师,就象开个公司管上四五号人就给自己按个CEO头衔一样,着实让人好笑。于是到网上GOOGLE了一下看看软件构架师具体是个啥东东,有想做货真价实的构架师,就朝着那方向努力吧。网摘如下:

软件架构师的职责:将客户的需求转换为规范的开发计划及文本,并制定这个项目的总体架构,指导整个开发团队完成这个计划。

软件架构师的具体工作:
    (
1)在需求阶段,软件架构师主要负责理解和管理非功能性系统需求,比如软件的可维护性、性能、复用性、可靠性、有效性和可测试性等等,此外,架构师还要经常审查和客户及市场人员所提出的需求,确认开发团队所提出的设计;
    (
2)在需求越来越明确后,架构师的关注点开始转移到组织开发团队成员和开发过程定义上;
    (
3)在软件设计阶段,架构师负责对整个软件体系结构、关键构件、接口和开发政策的设计;
    (
4)在编码阶段,架构师则成为详细设计者和代码编写者的顾问,并且经常性地要举行一些技术研讨会、技术培训班等;
    (
5)随着软件开始测试、集成和交付,集成和测试支持将成为软件架构师的工作重点;
    (
6)在软件维护开始时,软件架构师就开始为下一版本的产品是否应该增加新的功能模块进行决策。
 
软件架构师的要求
      (
1)必须对开发技术非常了解,具有丰富的软件设计与开发经验,关键时候能对技术的选择作出及时、有效的决定。
      (
2)有良好的组织管理能力:沟通、领导、团队协作
      (
3)构件通信机制方面的知识:远程调用、JAVARMI、CORBA、COM/DCOM、各种标准的通信协议、网络服务、面对对象数据库、关系数据库等等

成长为软件架构师的几个阶段:
      (
1)构架师胚胎(程序员):语言基础、设计基础、通信基础等,内容包括java、c、c++、uml、RUP、XML、socket通信(通信协议)
      (
2)构架师萌芽(高级程序员):分布式系统组建等内容,包括分布式系统原理、ejb、corba、com/com+、webservice、网络计算机、高性能并发处理等
      (
3)构架师幼苗(设计师):透彻掌握设计模式,包括设计模式(c++版本、java版本)、ejb设计模式、J2EE构架、UDDI、软件设计模式等。此期间,最好能够了解软件工程在实际项目中的应用以及小组开发、团队管理
posted @ 2006-09-22 13:14 Alex 阅读(629) | 评论 (0)编辑 收藏

key words: DAO模式

今天在看一篇文章时提到了DAO,这个东西以前也经常接触,突然想回顾一下,于是打开Appfuse里看看dao模式(记忆中appfuse里就是很多的dao)

截图如下:
appfusedao.png

很清楚,左边的部分是基础模块,原意是想让右边的DAO和实现能够重用左边的,可是我找了半天也没看到需要重用左边的东西,因为在client调用的所有方法中都是明确的getUser或removeUser,就是没有getObject或者removeObject,那么不禁要问,左边的基础dao和它的实现还有什么意义呢?所以我的第一想法就是把左边的去掉得了,还好,果然有支持我想法的做法,打开springside,我们看到如下的结构:
截图2
springside.png
这里的做法更厉害,连interface也不要了,不过效果确实是很简洁,在bookmanager里完全重用了左边的基本方法 :
public Book get(Integer id) {
        
return (Book) super.get(id);
    }

    
public void save(Book book) {
        
super.save(book);
        logger.info(
"保存图书.图书详情:" + book.toString());
    }

    
public void remove(Integer id) {
        
super.remove(id);
        logger.info(
"删除图书.图书ID:" + id);
    }

这是一个更务实的做法,如果你的项目并不是那么那么的复杂完全可以这么做,当然要说其有什么缺点显然和没有了interface的天生属性决定了的,不可强求,若你对测试隔离面向接口以及你能想到的所有关于interface的好处,那就用你自己的方式吧。

现在我在想一个问题,难道appfuse里的继承的基本关于object的做法就没有地方可用了么?


其时正好碰到java视线这一篇文章有点相关,你可以参考一下:
用DAO模式有什么好处?


ps:
以前是一个基本的dao,然后n个业务dao继承于这个基本dao,现在提供一个通用dao,每个要用到的地方直接继承用就是了,更务实了!
不过,有一个小小的瑕疵,就是对于service中类似getUserByName或者getPeopleByEmail方法中需要提供给dao一个sql语句,从mvc的角度看,在service中看到了db层,有点不雅,不过综合来看这个还是可以或略,不要专牛角尖嘛  :)

posted @ 2006-09-21 16:05 Alex 阅读(2425) | 评论 (0)编辑 收藏

key words: VSS IDEA

vss实在是太恶心了,提交各东西还不支此右键,要是要在开发一段时间后提交多个不同的目录下的文件费死劲了,我压根不知道哪个文件还没有提交,Fuck Microsoft!

至少,我们还有IDEA  :)  在开发工具里提交的一个好处就是直观和简便.Let's go!

具体指导请看这里
你需要填写关于VSS的相关信息 ,截图如下:
vss.png

其中的vss project可能有点困难,属于vss自己的术语,你自己实际上是不知道的,方法是打开vss客户端,通过这个工具查找到你所在的项目的vss project到底是什么,右键项目名称,出现如下dialog
vss2.png

OK了

在IDEA里看看效果,截图如下:
vss3.png


Fine.
posted @ 2006-09-21 11:40 Alex 阅读(2993) | 评论 (3)编辑 收藏

key words :Oracle分页 视图

google了一下关于Oracle的分页方法,方法还不少,大多数效果差不多-有点恶心. 恶心也要作,不过后来就是大家都用得这种方式在我这里出现了新问题,奇怪的是怎么没有别人碰到?

String condition = " teacher_id = " + userId + " and school_id="+siteId;
sql 
=
   
" select * " +
   
" from your_table where " + condition +
   
" and rowid not in ( select rowid from your_table where" + condition +
   
" and rownum <= " + (pageIndex - 1* Constants.PAGE_NUMS + "" +
   
" and rownum <= " + Constants.PAGE_NUMS ;

现在的问题是我需要按照table的某个字段排序,于是改成如下:
String condition = " teacher_id = " + userId + " and school_id="+siteId;
sql 
=
    
" select * " +
    
" from your_table where " + condition +
    
" and rowid not in ( select rowid from your_table where" + condition +
    
" and rownum <= " + (pageIndex - 1* Constants.PAGE_NUMS + "order by id desc" +
    
" and rownum <= " + Constants.PAGE_NUMS + " order by id desc";

这个sql有问题么?
答案是可能有问题,也可能没有问题,因为据说在8i的Oracle版本之前都不行,实际上也不尽然,在我的9i和10g我得到的是同样的错误 "missing right parenthesis",还有一位兄弟估计是DBA建议我去metalink打一个patch,埃,动作太大了,不敢动。

问题还是要解决,试了下类似于select a.*,rownum r from (select * from table where ...) a where rownum < 10 等的方法,效果一样,就是不能加嵌套的order by
最后,用视图的方法间接解决问题,因为我要解决的问题实际就是按某个字段排序,那么在视图里先对table进行排序,再在视图的基础上作操作就OK了.

另,还有一种不错的实现方法,即用OracleCachedRowSet,分页也比较简单,有点类似于hibernate,由于时间关系没有时间去看,感兴趣的朋友可以看一下.


BTW: 对于视图可能rowid有问题,可以改成视图的某个主键替换

String condition = " teacher_id = " + userId + " and school_id="+siteId;
sql 
=
    
" select * " +
    
" from your_table where " + condition +
    
" and id not in ( select id from your_table where" + condition +
    
" and rownum <= " + (pageIndex - 1* Constants.PAGE_NUMS + "order by id desc) " +
    
" and rownum <= " + Constants.PAGE_NUMS + " order by id desc";



posted @ 2006-09-20 15:36 Alex 阅读(1677) | 评论 (3)编辑 收藏

转自 robin

Java本身是一种设计的非常简单,非常精巧的语言,所以Java背后的原理也很简单,归结起来就是两点:

1、JVM的内存管理

理解了这一点,所有和对象相关的问题统统都能解决

2、JVM Class Loader

理解了这一点,所有和Java相关的配置问题,包括各种App Server的配置,应用的发布问题统统都能解决

就像张无忌学太极剑,本质就是一圈一圈的画圆,你要是懂得了太极剑的本质,那么太极剑就那么一招而已,本身是很容易学的,只是难度在于你要能够举一 反三,化一式剑意为无穷无尽的剑招,这就需要一点悟性和不断的实践了;反过来说,如果学剑不学本质,光学剑招,你就是学会了1万招,碰到了第1万零1招, 还是不会招架,败下阵来。

技术世界本来就是丰富多彩,企图统一标准,实际上也做不到,但是世界本质其实并不复杂。学习技术,特别是某种具体的软件工具的时候,应该学会迅速把 握事物的本质,不要过多搅缠细节。软件工具应该为我所用,而不是我被工具所驾驭。当你具备了对整个J2EE架构的设计和实施的能力,你还会被具体的工具束 缚吗?哪种工具适合你的架构,你就用什么,哪种不适合你,你就抛弃它,软件皆臣服于你的脚下,而不是你被什么软件牵着鼻子走,到了这种程度,你难道还害怕 学习什么新的软件?

我自己也在一直朝着这个方向努力,在我心中,设计软件,架构是第一位的,采用什么技术要为架构服务。如果我发现什么技术对我的架构来说很重要,那么 我会花时间去学习,去钻研,就像我花时间去钻研ORM一样,如果我觉得什么技术对我的架构来说没有用,即使技术再火爆,我也不去碰它

总之要学会抓住本质,驾驭技术,而不是被技术所驾驭。当你掌握了本质原理,其实学什么都很快,毕竟都是相通的,我先看JDO,后看 Hibernate,其实两者就很类似,所以学得很快,以后如果有工作需要,要我学习别的ORM,那我也不会觉得有什么困难的,一样手到拿来。

更有说服力的是Unix类的操作系统,那就更相似了,只要抓住了Unix最本质的几点,例如shell命令和编程,文件系统结构和配置,系统启动原 理和过程,所有的Unix都是无师自通的。我自己会用Linux,FreeBSD,SCO Unix, Solaris,HP-UX 和 AIX等6种Unix,更体会到一通百通的道理。

拿刚出了光明顶密道的张无忌来说吧,(我很喜欢张无忌这个角色),他也没有练过什么武功,但是他已经把天下武学之本质:九阳神功 + 乾坤大挪移学会了,所以不管什么功夫,他都是看一遍就会,马上为我所用,看了空性用了一遍龙爪手,就会用龙爪手来破对方;和昆仑派打了一架,就会用昆仑剑 法和灭绝师太过招;七伤拳更是无师自通;太极拳也是看一遍就会。

总之,学习方法还是很重要,别被五花八门的技术给搞不清学习方向了。

posted @ 2006-09-20 12:12 Alex 阅读(444) | 评论 (1)编辑 收藏

转自portian

基本上一个应用程序里面的领域相关的模型里面需要3种对象:
1。值对象(Value Object),没有身份,内容表示一切,譬如我和weihello都去银行里面存取100大洋,那这个100RMB是一个值对象

2。实体对象(Entity),需要持久,不是按照内容,而是按照它的身份来区分,也就是说即使内容完全一样,也不是同一个对象。这个身份在内存 里面是它的实例地址,在数据库里面是关键字,最常见的就是OID.这个实体对象并不是纯数据,它处理本身的实体模型,例如Accout,它的 withDraw,它的子Account等等,它也处理自己和其他实体对象之间的关系,例如订单里面的订单行,都是应该在这个Account里面实现的, 而不应该有一个什么控制类。在一个Web应用程序里面,涉及到对象关系的一般只需要一个(或几个)DTOFactory负责所有对象的DTO和 Entity之间的组装和拆份,不需要专门的管理,这一部分也是和数据建模最相近的地方。
 
3。服务对象(Service),这是为我们提供服务的类,譬如银行里面服务员,她帮助我们把钱从一个账户转到另外一个账户,并记录相应的交易。

对象的作用是对它自己的内部状态负责,如果它需要存取很多其它对象的状态进行运算,那叫做特性忌妒,是要重构的。应该把这些代码移到那个持有这些状态的类里面



辨别一些名词:
1。VO:实际上很模糊,通常指ValueObject和ViewObject
2. ViewObject,界面展现需要的对象,如Struts的FormBean
3。Value Object,早期被作为ValueObject和Transfer Object的总称。实际上Value Object的真正意义在于它的内容,而不是身份
4。Transfer Object:数据传输对象,在应用程序不同层次之间传书对象,在一个分布式应用程序中,通常可以提高整体的性能
5。PO:也许就是Persistent Object,基本上就是Entity了
在不同的体系结构和实现方式里面,这些对象有可能重复,也有可能不重叠。如果你要做一个对所有的体系都能够方便移植的框架,那么每一种对象都需要 严格区分。例如JDO的PO不能作为TO,应为它不能脱离PM,譬如你可以选择用ViewObject(如Struts的FOrmBean)直接作为 TO,但在tapestry和Webwork里面就不合适了。但在很多时候,能够方便实用是最重要的,不要过度设计就是了。
posted @ 2006-09-20 09:53 Alex 阅读(402) | 评论 (1)编辑 收藏

key words: 如何做设计 设计步骤
转自robin

一。需求分析(抽象Use case + 分析Use case之间的关系)

分析软件需求,以用户的角度来使用软件,找出发生的scenerio,抽象成为一个一个Use Case,分析出Use Case之间的关系,这一步是非常重要的,这一步做好了,设计就成功了一半。Use Case的抽象有一些可以遵循的原则,这里不详细谈。

然后用语言描述每一个Use Case,描述用户使用一个Use Case发生的主事件流以及异常流。

这样就完成了需求分析阶段。

二。概要设计(找出实体 + 分析实体类之间的关系 + 提取控制类 + 画序列图)

接下来做概要设计,针对每个Use Case,读Use Case的描述,看事件流,找出所有的实体类,这也有一些可以遵循的原则,例如找出所有的名词,画表格排除等等方法。

然后分析实体类之间的关系,是包含,聚合还是依赖,是1:1,还是1:n,还是其他....,根据这些关系,就可以得出实体类和别的实体类想关联的属性,然后再找出每个实体类本身重要的属性。

然后再次分析Use Case的事件流,一方面check实体类的设计是否合理,另一方面你可以找出动词,分析对实体类的控制逻辑,这样就可以可以设计出业务控制类,一般你可 以一个实体类一个控制类,也可以业务逻辑相关的实体类由一个Facade Session Bean(非EJB含义)来统一控制,这里面的控制类的颗粒度就由你自己来掌握了。一般来说先可以设计一些细颗粒度的控制类,然后再按照模块,用粗粒度封 装细颗粒度的控制类,提供给Web层一个Facade。

然后你可以画序列图,就是用序列图来表达事件流,在这个过程中,你需要不断回到类图,给控制类添加方法,而序列图就是控制类的方法调用。

至此,你已经在Rose里面完成了概要设计,当然你不可能一次设计完善,会有很多次迭代,因此你不能一开始把类设计的太详细,只抓住主要的属性和方法,特别需要注意的是,是抽象的设计,不要用具体的编程语言来表达类。

三。实施(结合xdoclet和Schema工具自动生成代码)

然后你就可以抛开Rose了,转到Eclipse+Togehter里面,根据那些类,规划一下package层次,然后在Together里面进行类的详细设计,所有需要的属性一一写上,当然你还是不可能一下把所有的属性方法写全,不过没有关系,把重要的写好就行了。

然后类框架已经生成好了,给所有的实体类加上xdoclet,然后生成hbm,然后用Hibernate的ExportScheme生成DDL,运行一遍自动创建好所有的表。这样所有的实体相关类全部做好了。

你现在就集中精力把控制类那些方法里面的代码填写上就OK了,在这个过程,你会发现有些实体类缺属性,没有关系,加上属性,然后写好xdoclet,运行一遍,自动生成hbm,自动创建好表,然后继续写你的方法,也有可能你发现控制类缺方法,那么就加上。

基本上实体类就是getter/setter,和少量的实体相关方法,所有的控制逻辑都写在控制类里面。

最后你的软件就基本写好了,用Eclipse生成好一堆你的testCase运行测试,反复修改,除bug。

看看使用OOAD的设计思路,是多么的爽的事情阿!你只需要把精力放到Use Case的抽象,实体类的关系总结,控制类的归纳。而当你使用Eclipse+Together之后,你所需要写的代码只不过是控制类的方法实现代码,其 他的都已经生成好了。另外可能需要写少量工具类。


posted @ 2006-09-20 09:02 Alex 阅读(923) | 评论 (1)编辑 收藏

注:关于跨域登陆cookie的问题在网上搜索了一下,没看到有java下的示例,这个asp的也可以参照一下,有空再在java下测一下.

key words:单点登陆 SSO 跨域cookie


摘要:当你有一个Cookie组(或叫Cookie字典)使用Domain属性指定域名之后,当你在对该组的成员进行修改或新增的时候,一定要在操 作之后加上Resonse.Cookies(CookieName).Domain属性。如果没有必要,请不要修改已设置Domain的Cookie组.
关键字:
正文:
    Cookie跨域操作看来是个简单的问题,因为只要指定Domain属性为指定网站的根域名就可以了.但是笔者在实际使用过程中却遇到了一些问题,的确值得注意.    环境介绍   cookie在www主域名下创建,并写入Domain属性,如:(为方便调试以下代码皆为asp代码)   Write.asp <%
Response.Cookies(CookieName)("UserName") = "SunBird"
Response.Cookies(CookieName)("Password") = "xyz1234"
Response.Cookies(CookieName).Domain = "xxxx.com"
%>
  上面文件放在www主域名下,同时在同目录下放置一个读取cookie的Read.asp   Read.asp <%
Response.Write Request.Cookies(CookieName)("UserName")
Response.Write Request.Cookies(CookieName)("Password")
%>
  再放一个Read.asp文件到另外一个子域名站点里,代码同上。最后我们再做一个清除cookie的Clear.asp放在主域名下   Clear.asp <%
Response.Cookies(CookieName)("UserName") = ""
Response.Cookies(CookieName)("Password") = ""
Response.Cookies(CookieName).Domain = "xxxx.com"
%>
  现在可以通过下面的执行顺序来测试,Write.asp-->主域名的Read.asp-->子域名的Read.asp 所有 Read.asp页面都可以读取到Write.asp创建的cookie的值,然后再运行Clear.asp进行清除,一切都Ok,看上去没有什么问题。   但是把这种方法运用到实际的站点时却出现问题了。   问题描述:   第一次登录一切ok,所有子域名都可以访问到主域名存储的cookie,但是,一旦退出之后,子域名的cookie被清除了,但是主域名的 cookie仍然保留着,强行清除主域名的cookie之后,无论怎样登录主域名下都无法保存cookie了,除非关掉浏览器重新打开。   经过多次尝试之后,无意中发现问题所在,以下是测试经过。   创建一个Write2.asp的页面放在主域名下 <%
Response.Cookies(CookieName)("TEST_COOKIE") = "TEST_COOKIE"
%>
  第一步:关闭浏览器后,按以下顺序执行,Write.asp-->主域名的Read.asp-->子域名的Read.asp 到这里所有Read.asp读取正常。   第二步:Clear.asp-->主域名的Read.asp-->子域名的Read.asp 到这里清除操作是成功的。   第三步:Write.asp--> Write2.asp --> 主域名Read.asp --> 子域名Read.asp 到这里两个Read.asp都可以读取到cookie的值。   第四步:重新执行第二步,发现主域名Read.asp仍然输出了值,而子域名下的Read.asp的值已经被清空了。   根据以上测试总结以下几点再跨域使用cookie时需要注意的地方   1、当你有一个Cookie组(或叫Cookie字典)使用Domain属性指定域名之后,当你在对该组的成员进行修改或新增的时候,一定要在操作之后加上Resonse.Cookies(CookieName).Domain属性。   2、如果没有必要,请不要修改已设置Domain的Cookie组,直接使用Response.Cookies("CookieText") = CookieValue 来创建一个新的Cookie。
posted @ 2006-09-18 20:59 Alex 阅读(2464) | 评论 (0)编辑 收藏

     摘要: 终于翻开这本James都称赞的java经典书籍了,发现比一般的英语书籍要难懂一些。但是里面的Item都是非常实用的,是java程序员应该理解的。 Creating and Destroying ObjectItem 1:考虑用静态工厂方法替代构造器例如:public static Boolean valueOf(boolean b)     {          return (b?Boolean...  阅读全文
posted @ 2006-09-11 18:14 Alex 阅读(671) | 评论 (0)编辑 收藏

从工作流状态机实践中总结状态模式使用心得

banq http://www.jdon.com Dec 7, 2003 12:10 AM 回复此消息回复

状态模式好像是很简单的模式,正因为状态好像是个简单的对象,想复杂化实现设计模式就不是容易,误用情况很多。

我个人曾经设计过一个大型游戏系统的游戏状态机,游戏状态可以说是游戏设计的主要架构,但是由于系统过分复杂
和时间仓促,并没有真正实现状态模式。

目前在实现一个电子政务项目中,需要进行流程状态变化,在电子政务设计中,我发现,如果一开始完全按照工作流
规范开发,难度很大,它和具体项目实践结合无法把握,而且工作流规范现在有wfmc,还有bpml,选择也比较难。因
此,我决定走自创的中间道路。

因为,我需要做一个状态机API,或者说状态机框架,供具体系统调用:类如公文流转应用或信息发报送应用等。

好的状态模式必须做到两点:
1. 状态变化必须从外界其它逻辑划分出来。
2. 状态必须可方便拓展,对其它代码影响非常小。

要做到这两点,必须先明确状态变化机制,状态变化实际是由Event事件驱动的,可以认为是Event-condition-State,
在MVC模式一般是Event-condition-Action实现。状态模式需要封装的是Event-condition-State中的condition-State
部分。

清晰理解状态和流程的关系也非常重要,因为状态不是孤立的,可以说和流程是点和线的关系,状态从一个方面说明
了流程,流程是随时间而改变,状态是截取流程某个时间片。因此,必须明白使用状态模式实现状态机实际是为了更
好地表达和说明流程。

状态和流程以及事件的关系如下:


|Event
___currentState__|______newState___




图中表示了是事件改变了流程的状态,在业务逻辑中,经常发生的是事件,如果不使用状态模式,需要在很多业务逻
辑处实现事件到状态判定和转换,这有很多危险性。

最大的危险是系统没有一个一抓就灵的主体结构,以那个游戏系统为例,在没有状态模式对状态提炼的情况下,状态
改变由每个程序员想当然实现,导致每个程序员开发的功能在整合时就无法调试,因为这个程序员可能不知道那个程
序员的代码在什么运行条件下改变了游戏状态,结果导致自己的代码无法运行。

这种现象实际上拒绝了项目管理的协作性,大大地拖延项目进度(程序员之间要反复商量讨论对方代码设计)。从这
一点也说明,一个好的架构设计是一个项目快速成功完成的基础技术保证,没有这个技术基础,再先进的项目管理手
段也是没有效率的,或者是笨拙的。

状态模式对于很多系统来说,确实是架构组成一个重要部分。

下面继续讨论如何实现一个好的状态模式,为了实现好的状态模式,必须在状态模式中封装下面两个部分:
1. 状态转换规则(行为)
2. 状态属性(属性)

状态转换行为有两种划分标准:
1. run和next两个行为,run是当前状态运行行为,next是指在Event参与下,几种可能转向的下一个状态。
2. stateEnter和stateExit, 状态进入和状态退出。

如果用进入一个个房间来表示状态流程的话, 第一种分析是只重视着“在房间里”和“如何转入下一个房间”,这两种行
为一旦确定,可以被反复使用,进而一个流程的状态切换可以全部表达出来。

第二中分析方法有所区别,只重视进入房间和离开房间这个两个行为,同样,这种模型也可以被反复利用在其它房间,
一个流程的状态切换也可以全部表达出来。

具体选择取决于你的需求,比如,如果你在进入一个状态开始,要做很多初始化工作,那么第二种模型就很适合。

状态变化都离不开一个主体对象,主体对象可以说包含状态变化(行为)和状态属性(属性),假设为StateOwner,
StateOwner有两个部分组成:Task/事情和状态。任何一个Task/事情都会对应一个状态。

这样,我们已经抽象出两个主要对象:状态State和StateOwner。

为了封装状态变化细节,我们可以抽象出一个状态机StateMachine来专门实现状态根据事情实现转换。

这样,客户端外界通过状态机可访问状态模式这个匣子。在实践中,外界客户端需要和状态机实现数据交换,我们把
它也分为两种:属性和行为。

其中属性可能需要外界告诉状态状态变化的主体对象Task,解决状态的主人问题,是谁的问题;行为可能是需要持久
化当前这个主体对象的状态到数据库。

这两种数据交换可以分别通过StateOwner和StateMachine与整个状态机实现数据交换,这样,具体状态和状态切换也
和外界实现了解耦隔离。

因此好的状态模式实现必须有下列步骤:
(1)将每个状态变成State的子类,实现每个状态对象化。
(2)在每个状态中,封装着进入下一个状态可能规则,这些规则是状态变化的核心,换句话说,统一了状态转换的规则。
具体可采取run和next这样的转换行为。

下面是一个子状态代码:


publicclass Running extends StateT{

//
publicvoid run(StateOwner stateOwner){
stateOwner.setCurrentState(this);
}

//转换到下一个状态的规则
//当前是Running状态,下一个状态可能是暂停、结束或者强制退出等
//状态,但是绝对不会是Not_Started这样的状态
//转换规则在这里得到了体现。
public State next(Event e) {
if(transitions == null){
addEventState(new EventImp(
"PAUSE"), new Suspended());
addEventState(new EventImp(
"END"), new Completed());
addEventState(new EventImp(
"STOP"), new Aborted());
}
returnsuper.next(e);
}

}



外界直接调用 StateMachine的关键方法transition;实行状态的自动转变。


publicclass StateMachine {

/**
* 状态切换
* 根据Event参数,运行相应的状态。
* 1. 获得当前状态
* 2. 使用当前状态的next()转换
* |Event
* ___currentState__|______newState___
*
* @param inputs
*/

publicfinalvoid transition(String taskid, Event e) throws Exception {
State currentState = readCurrentState(taskid);
//从数据库获得当前状态
StateOwner stateOwner = new StateOwner(taskid, currentState);
//转换状态
currentState = currentState.next(e);
if (currentState != null) {
currentState.run(stateOwner);
saveCurrentState(stateOwner);
//保存当前状态
}
}

}
posted @ 2006-09-09 10:43 Alex 阅读(967) | 评论 (0)编辑 收藏

设计模式之State

板桥里人 http://www.jdon.com 2002/4/6/

模式实战书籍《Java实用系统开发指南》

State模式的定义 : 不同的状态,不同的行为;或者说,每个状态有着相应的行为.

何时使用 ?
State模式在实际使用中比较多,适合"状态的切换".因为我们经常会使用If elseif else 进行状态切换, 如果针对状态的这样判断切换反复出现,我们就要联想到是否可以采取State模式了.

不 只是根据状态,也有根据属性.如果某个对象的属性不同,对象的行为就不一样,这点在数据库系统中出现频率比较高,我们经常会在一个数据表的尾部,加上 property属性含义的字段,用以标识记录中一些特殊性质的记录,这种属性的改变(切换)又是随时可能发生的,就有可能要使用State.

是否使用?
在实际使用,类似开关一样的状态切换是很多的,但有时并不是那么明显,取决于你的经验和对系统的理解深度.

这里要阐述的是"开关切换状态" 和" 一般的状态判断"是有一些区别的, " 一般的状态判断"也是有 if..elseif结构,例如:

    if (which==1) state="hello";
    else if (which==2) state="hi";
    else if (which==3) state="bye";

这是一个 " 一般的状态判断",state值的不同是根据which变量来决定的,which和state没有关系.如果改成:

    if (state.euqals("bye")) state="hello";
    else if (state.euqals("hello")) state="hi";
    else if (state.euqals("hi")) state="bye";

这就是 "开关切换状态",是将state的状态从"hello"切换到"hi",再切换到""bye";在切换到"hello",好象一个旋转开关,这种状态改变就可以使用State模式了.

如 果单纯有上面一种将"hello"-->"hi"-->"bye"-->"hello"这一个方向切换,也不一定需要使用State模 式,因为State模式会建立很多子类,复杂化,但是如果又发生另外一个行为:将上面的切换方向反过来切换,或者需要任意切换,就需要State了.

请看下例:

public class Context{

  private Color state=null;

  public void push(){

    //如果当前red状态 就切换到blue
    if (state==Color.red) state=Color.blue;

    //如果当前blue状态 就切换到green
    else if (state==Color.blue) state=Color.green;

    //如果当前black状态 就切换到red
    else if (state==Color.black) state=Color.red;

    //如果当前green状态 就切换到black
    else if (state==Color.green) state=Color.black;
    
    Sample sample=new Sample(state);
    sample.operate();
  }

  public void pull(){

    //与push状态切换正好相反

    if (state==Color.green) state=Color.blue;
    else if (state==Color.black) state=Color.green;
    else if (state==Color.blue) state=Color.red;
    else if (state==Color.red) state=Color.black;

    Sample2 sample2=new Sample2(state);
    sample2.operate();
  }

}

在上例中,我们有两个动作push推和pull拉,这两个开关动作,改变了Context颜色,至此,我们就需要使用State模式优化它.

另外注意:但就上例,state的变化,只是简单的颜色赋值,这个具体行为是很简单的,State适合巨大的具体行为,因此在,就本例,实际使用中也不一定非要使用State模式,这会增加子类的数目,简单的变复杂.

例如: 银行帐户, 经常会在Open 状态和Close状态间转换.

例如: 经典的TcpConnection, Tcp的状态有创建 侦听 关闭三个,并且反复转换,其创建 侦听 关闭的具体行为不是简单一两句就能完成的,适合使用State

例如:信箱POP帐号, 会有四种状态, start HaveUsername Authorized quit,每个状态对应的行为应该是比较大的.适合使用State

例如:在工具箱挑选不同工具,可以看成在不同工具中切换,适合使用State.如 具体绘图程序,用户可以选择不同工具绘制方框 直线 曲线,这种状态切换可以使用State.

如何使用
State需要两种类型实体参与:

1.state manager 状态管理器 ,就是开关 ,如上面例子的Context实际就是一个state manager, 在state manager中有对状态的切换动作.
2.用抽象类或接口实现的父类,,不同状态就是继承这个父类的不同子类.

以上面的Context为例.我们要修改它,建立两个类型的实体.
第一步: 首先建立一个父类:

public abstract class State{

  public abstract void handlepush(Context c);
  public abstract void handlepull(Context c);
  public abstract void getcolor();

}

父类中的方法要对应state manager中的开关行为,在state manager中 本例就是Context中,有两个开关动作push推和pull拉.那么在状态父类中就要有具体处理这两个动作:handlepush() handlepull(); 同时还需要一个获取push或pull结果的方法getcolor()

下面是具体子类的实现:

public class BlueState extends State{

  public void handlepush(Context c){
     //根据push方法"如果是blue状态的切换到green" ;
     c.setState(new GreenState());

  }
  public void handlepull(Context c){

     //根据pull方法"如果是blue状态的切换到red" ;
    c.setState(new RedState());

  }

  public abstract void getcolor(){ return (Color.blue)}

}

 

同样 其他状态的子类实现如blue一样.

第二步: 要重新改写State manager 也就是本例的Context:

public class Context{

  private Sate state=null; //我们将原来的 Color state 改成了新建的State state;

  //setState是用来改变state的状态 使用setState实现状态的切换
  pulic void setState(State state){

    this.state=state;

  }

  public void push(){

    //状态的切换的细节部分,在本例中是颜色的变化,已经封装在子类的handlepush中实现,这里无需关心
    state.handlepush(this);
    
    //因为sample要使用state中的一个切换结果,使用getColor()
    Sample sample=new Sample(state.getColor());
    sample.operate();

  }

 

  public void pull(){

    state.handlepull(this);
    
    Sample2 sample2=new Sample2(state.getColor());
    sample2.operate();

  }

}

 

至此,我们也就实现了State的refactorying过程.

以上只是相当简单的一个实例,在实际应用中,handlepush或handelpull的处理是复杂的.

状态模式优点:
(1) 封装转换过程,也就是转换规则
(2) 枚举可能的状态,因此,需要事先确定状态种类。

状态模式可以允许客户端改变状态的转换行为,而状态机则是能够自动改变状态,状态机是一个比较独立的而且复杂的机制,具体可参考一个状态机开源项目:http://sourceforge.net/projects/smframework/

状态模式在工作流或游戏等各种系统中有大量使用,甚至是这些系统的核心功能设计,例如政府OA中,一个批文的状态有多种:未办;正在办理;正在批示;正在审核;已经完成等各种状态,使用状态机可以封装这个状态的变化规则,从而达到扩充状态时,不必涉及到状态的使用者。

在网络游戏中,一个游戏活动存在开始;开玩;正在玩;输赢等各种状态,使用状态模式就可以实现游戏状态的总控,而游戏状态决定了游戏的各个方面,使用状态模式可以对整个游戏架构功能实现起到决定的主导作用。

状态模式实质
使用状态模式前,客户端外界需要介入改变状态,而状态改变的实现是琐碎或复杂的。

使用状态模式后,客户端外界可以直接使用事件Event实现,根本不必关心该事件导致如何状态变化,这些是由状态机等内部实现。

这是一种Event-condition-State,状态模式封装了condition-State部分。

每个状态形成一个子类,每个状态只关心它的下一个可能状态,从而无形中形成了状态转换的规则。如果新的状态加入,只涉及它的前一个状态修改和定义。

状态转换有几个方法实现:一个在每个状态实现next(),指定下一个状态;还有一种方法,设定一个StateOwner,在StateOwner设定stateEnter状态进入和stateExit状态退出行为。

状态从一个方面说明了流程,流程是随时间而改变,状态是截取流程某个时间片。


相关文章:

从工作流状态机实践中总结状态模式使用心得

参考资源:
the State and Stategy
How to implement state-dependent behavior
The state patterns

 

posted @ 2006-09-09 10:42 Alex 阅读(632) | 评论 (0)编辑 收藏

转自

.Net中的设计模式——Composite模式

前言:google到一篇关于复合模式的文章,虽然是关于 .NET的,但是对于开发java同样有借鉴意义.

一、模式概述

描述Composite模式的最佳方式莫过于树形图。从抽象类或接口为根节点开始,然后生枝发芽,以形成树枝节点和叶结点。因此, Composite模式通常用来描述部分与整体之间的关系,而通过根节点对该结构的抽象,使得客户端可以将单元素节点与复合元素节点作为相同的对象来看 待。

由于Composite模式模糊了单元素和复合元素的区别,就使得我们为这些元素提供相关的操作时,可以有一个统一的接口。例如,我们要编写一个字 处理软件,该软件能够处理文字,对文章进行排版、预览、打印等功能。那么,这个工具需要处理的对象,就应该包括单个的文字、以及由文字组成的段落,乃至整 篇文档。这些对象从软件处理的角度来看,对外的接口应该是一致的,例如改变文字的字体,改变文字的位置使其居中或者右对齐,也可以显示对象的内容,或者打 印。而从内部实现来看,我们对段落或者文档进行操作,实质上也是对文字进行操作。从结构来看,段落包含了文字,文档又包含了段落,是一个典型的树形结构。 而其根节点正是我们可以抽象出来暴露在外的统一接口,例如接口IElement:

composite1.GIF

既然文字、段落、文档都具有这些操作,因此它们都可以实现IElement接口:

从上图可以看到,对象Word、Paragraph、Document均实现了IElement接口,但Paragraph和Document与 Word对象不同的是,这两者处除了实现了IElement接口,它们还与IElement接口对象之间具有聚合的关系,且是一对多。也就是说 Paragraph与Document对象内可以包含0个到多个IElement对象,这也是与前面对字处理软件分析获得的结果是一致的。

从整个结构来看,完全符合树形结构的各个要素,接口IElement是根节点,而Paragraph和Document类为枝节点,Word对象为 叶节点。既然作为枝节点,它就具有带叶节点的能力,从上图的聚合关系中我们体现出了这一点。也就是说,Paragraph和Document类除了具有排 版、打印方面的职责外,还能够添加、删除叶节点的操作。那么这些操作应该放在哪里呢?

管理对子节点的管理,Composite模式提供了两种方式:一个是透明方式,也就是说在根节点中声明所有用来管理子元素的方法,包括Add()、 Remove()等方法。这样一来,实现根节点接口的子节点同时也具备了管理子元素的能力。这种实现策略,最大的好处就是完全消除了叶节点和枝节点对象在 抽象层次的区别,它们具备完全一致的接口。而缺点则是不够安全。由于叶节点本身不具备管理子元素的能力,因此提供的Add()、Remove()方法在实 现层次是无意义的。但客户端调用时,却看不到这一点,从而导致在运行期间有出错的可能。

另一种策略则是安全方式。与透明方式刚好相反,它只在枝节点对象里声明管理子元素的方法由于叶节点不具备这些方法,当客户端在操作叶节点时,就不会出现前一种方式的安全错误。然而,这种实现方式,却导致了叶节点和枝节点接口的不完全一致,这给客户端操作时带来了不便。

这两种方式各有优缺点,我们在实现时,应根据具体的情况,作出更加合理的抉择。在字处理软件一例中,我选择了安全方式来实现,因为对于客户端而言, 在调用IElement接口时,通常是将其视为可被排版、打印等操作的对象。至于为Paragraph和Document对象添加、删除子对象,往往是一 种初始化的行为,完全可以放到一个单独的模块中。根据单一职责原则(SRP),我们没有必要让IElement接口负累太重。所以,我们需要对上图作稍许 的修改,在Paragraph和Document对象中增加Add()和Remove()方法:

以下是IElement对象结构的实现代码:
public interface IElement
{
      void ChangeFont(Font font);
      void Show();
      //其他方法略;
}
public class Word
{
 public void ChangeFont(Font font)
 {
      this.font = font;
 }
 public void Show()
 {
      Console.WriteLine(this.ToString());
 }
 //其他方法略;
}
public class Paragraph
{
 private ArrayList elements = new ArrayList();
 public void Add(IElement element)
 {
      elements.Add(element);
 }
 public void Remove(IElement element)
 {
      elements.Remove(element);
 }
 public void ChangeFont(Font font)
 {
      foreach (IElement element in elements)
      {
          element.ChangeFont(font);
  }
 }
 public void Show()
 {
      foreach (IElement element in elements)
      {
          element.Show(font);
  }
 }
 //其他方法略;
}
//Document类略;

实际上,我们在为叶节点实现Add(),Remove()方法时,还需要考虑一些异常情况。例如在Paragraph类中,添加的子元素就不能是 Document对象和Paragraph对象。所以在添加IElement对象时,还需要做一些条件判断,以确定添加行为是否正确,如果错误,应抛出异 常。

采用Composite模式,我们将Word、Paragraph、Document抽象为IElement接口。虽然各自内部的实现并不相同,枝 节点和叶节点的实质也不一样,但对于调用者而言,是没有区别的。例如在类WordProcessor中,包含一个GetSelectedElement ()静态方法,它能够获得当前选择的对象:
public class WordProcessor
{
 public static IElement GetSelectedElement(){……}
}

对于字处理软件的UI来说,如果要改变选中对象的字体,则可以在命令按钮cmdChangeFont的Click事件中写下如下代码:
public void cmdChangeFont_Click(object sender, EventArgs e)
{
    WordProcessor.GetSelectedElement().ChangeFont(currentFont);
}

不管当前选中的对象是文字、段落还是整篇文档,对于UI而言,操作都是完全一致的,根本不需要去判断对象的类别。因此,如果在Business Layer的类库设计时,采用Composite模式,将极大地简化UI表示层的开发工作。此外,应用该模式也较好的支持项目的可扩展性。例如,我们为 IElement接口增加了Sentence类,对于前面的例子而言,只需要修改GetSelectedElement()方法,而 cmdChangeFont命令按钮的Click事件以及Business Layer类库原有的设计,都不需要做任何改变。这也符合OO的开放-封闭原则(OCP),即对于扩展是开放的(Open for extension),对于更改则是封闭的(Closed for modification)。

二、.Net Framework中的Composite模式

在.Net中,最能体现Composite模式的莫过于Windows或Web的控件。在这些控件中,有的包含子控件,有的则不包含且不能包含子控 件,这正好符合叶节点和枝节点的含义。所有Web控件的基类为System.Web.UI.Contril类(如果是Windows控件,则基类为 System.Windows.Forms.Control类)。其子类包含有HtmlControl、HtmlContainerControl等。按 照Composite模式的结构,枝节点和叶节点属于根节点的不同分支,同时枝节点与根节点之间应具备一个聚合关系,可以通过Add()、Remove ()方法添加和移除其子节点。设定HtmlControl为叶节点,而HtmlContaiinerControl为枝节点,那么采用透明方式的设计方 法,在.Net中控件类的结构,就应该如下图所示:

虽然根据透明方式的Composite模式,HtmlControl类与其父类Control之间也应具备一个聚合关系,但实质上该类并不具备管理 子控件的职责,因此我在类图中忽略了这个关系。此时,HtmlControl类中的Add()、Remove()方法,应该为空,或者抛出一个客户端能够 捕获的异常。

然而,从具体实现来考虑,由于HtmlControl类和HtmlContainerControl类在实现细节层次,区别仅在于前者不支持子控 件,但从控件本身的功能来看,很多行为是相同或者相近的。例如HtmlControl类的Render()方法,调用了方法RenderBeginTag ()方法:
protected override void Render(HtmlTextWriter writer)
{
      this.RenderBeginTag(writer);
}
protected virtual void RenderBeginTag(HtmlTextWriter writer)
{
      writer.WriteBeginTag(this.TagName);
      this.RenderAttributes(writer);
      writer.Write(’>');
}

而HtmlContainerControl类也具有Render()方法,在这个方法中也调用了RenderBeginTag()方法,且RenderBeginTag方法的实现和前者完全一致:
protected override void Render(HtmlTextWriter writer)
{
      this.RenderBeginTag(writer);
      this.RenderChildren(writer);
      this.RenderEndTag(writer);
}

按照上面的结构,由于HtmlControl和HtmlContainerControl之间并无继承关系,这就要求两个类中,都要重复实现 RenderBeginTag()方法,从而导致产生重复代码。根据OO的特点,解决的办法,就是让HtmlContainerControl继承自 HtmlControl类(因为HtmlContainerControl的接口比HtmlControl宽,所以只能令 HtmlContainerControl作为子类),并让RenderBeginTag()方法成为HtmlControl类的protected方 法,子类HtmlContainerControl可以直接调用这个方法。然而与之矛盾的是,HtmlContainerControl却是一个可以包含 子控件的枝节点,而HtmlControl则是不能包含子控件的叶节点,那么这样的继承关系还成立吗?

HtmlControl类对Add()方法和Remove()方法的重写后,这两个方法内容为空。由于HtmlContainerControl类 继承HtmlControl类,但我们又要求它的Add()和Remove()方法和Control类保持一致,而父类HtmlControl已经重写这 两个方法,此时是无法直接继承来自父类的方法的。以上是采用透明方式的设计。

如果采用安全方式,仍然有问题。虽然在HtmlControl类中不再有Add()和Remove()方法,但由于Control类和 HtmlContainerControl类都允许添加子控件,它们包含的Add()、Remove()方法,只能分别实现。这样的设计必然会导致重复代 码。这也是与我们的期望不符的。

那么在.Net中,Control类究竟是怎样实现的呢?下面,我将根据.Net实现Control控件的源代码,来分析Control控件的真实结构,以及其具体的实现细节。

三、深入分析.Net中的Composite模式

首先,我们来剖析Web控件的基类Control类的内部实现:

public class Control : IComponent, IDisposable, IParserAccessor, IDataBindingsAccessor
{
       // Events;略  
       // Methods
 public Control()
 {
             if (this is INamingContainer)
             {
                    this.flags[0×80] = true;
             }
 }
 public virtual bool HasControls()
 {
              if (this._controls != null)
              {
                    return (this._controls.Count > 0);
              }
              return false;
 }
 public virtual void DataBind()
        {
              this.OnDataBinding(EventArgs.Empty);   
              if (this._controls != null)           
       {                
              string text1 = this._controls.SetCollectionReadOnly(”Parent_collections_readonly”);
                        int num1 = this._controls.Count;
                 for (int num2 = 0; num2 < num1; num2++)                 
   {
                         this._controls[num2].DataBind();
                   }
                   this._controls.SetCollectionReadOnly(text1);
              }
 }
        protected virtual void Render(HtmlTextWriter writer)
 {
              this.RenderChildren(writer);
 }
 protected virtual ControlCollection CreateControlCollection()
 {
              return new ControlCollection(this);
 }
       // Properties
 public virtual ControlCollection Controls
 {
             get
             {
                   if (this._controls == null)
                   {
                         this._controls = this.CreateControlCollection();
                   }
                   return this._controls;
             }
 }
        // Fields
        private ControlCollection _controls;
}

Control基类中的属性和方法很多,为清晰起见,我只保留了几个与模式有关的关键方法与属性。在上述的源代码中,我们需要注意几点:

1、Control类不是抽象类,而是具体类。这是因为在设计时,我们可能会创建Control类型的实例。根据这一点来看,这并不符合OOP的要 求。一般而言,作为抽象出来的基类,必须定义为接口或抽象类。不过在实际的设计中,也不应拘泥于这些条条框框,而应审时度势,根据实际的情况来抉择最佳的 设计方案。
2、公共属性Controls为ControlCollection类型,且该属性为virtual属性。也就是说,这个属性可以被它的子类 override。同时,该属性为只读属性,在其get访问器中,调用了方法CreateControlCollection();这个方法为 protected虚方法,默认的实现是返回一个ControlCollection实例。
3、方法HasControls(),功能为判断Control对象是否有子控件。它判断的依据是根据私有字段_controls(即公共属性 Controls)的Count值。但是需要注意的是,通过HasControls()方法的返回值,并不能决定对象本身属于叶节点,还是枝节点。因为即 使是枝节点其内部仍然可以不包含任何子对象。
4、 方法DataBind()的实现中,首先调用了自身的OnDataBinding()方法,然后又遍历了Controls中的所有控件,并调用其 DataBind()方法。该方法属于控件的共有行为,从这里可以看出不管是作为叶节点的控件,还是作为枝节点的控件,它们都实现统一的接口。对于客户端 调用而言,枝节点和叶节点是没有区别的。
5、 Control类的完整源代码中,并不存在Add()、Remove()等类似的方法,以提供添加和移除子控件的功能。事实上,继承Control类的所有子类均不存在Add()、Remove()等方法。

显然,在Control类的定义和实现中,值得我们重视的是公共属性Controls的类型ControlCollection。顾名思义,该类必 然是一个集合类型。是否有关子控件的操作,都是在ControlCollection类型中实现呢?我们来分析一下ControlCollection的 代码:
public class ControlCollection : ICollection, IEnumerable
{
       // Methods
 public ControlCollection(Control owner)
 {
               this._readOnlyErrorMsg = null;
               if (owner == null)
               {
                       throw new ArgumentNullException("owner");
               }
               this._owner = owner;
        }
 public virtual void Add(Control child)
        {
               if (child == null)
               {
                throw new ArgumentNullException("child");
               }
               if (this._readOnlyErrorMsg != null)
                   {
                throw new HttpException(HttpRuntime.FormatResourceString(this._readOnlyErrorMsg));
               }
               if (this._controls == null)
               {
                   this._controls = new Control[5];
               }
               else if (this._size >= this._controls.Length)
               {
                   Control[] controlArray1 = new Control[this._controls.Length * 4];
                   Array.Copy(this._controls, controlArray1, this._controls.Length);
                   this._controls = controlArray1;
               }
               int num1 = this._size;
               this._controls[num1] = child;
               this._size++;
               this._version++;
               this._owner.AddedControl(child, num1);
        }
        public virtual void Remove(Control value)
 {
               int num1 = this.IndexOf(value);
               if (num1 >= 0)
               {
                this.RemoveAt(num1);
               }
        }
        // Indexer
 public virtual Control this[int index]
 {
               get
               {
                    if ((index < 0) || (index >= this._size))
                    {
                         throw new ArgumentOutOfRangeException(”index”);
                    }
                    return this._controls[index];
               }
 }
 // Properties    
 public int Count
 {
        get
               {
                    return this._size;
               }
        }
 protected Control Owner
 {
               get
               {
                    return this._owner;
               }
 }
        protected Control Owner { get; }   
        // Fields
        private Control[] _controls;
        private const int _defaultCapacity = 5;
        private const int _growthFactor = 4;
 private Control _owner;    
}

一目了然,正是ControlCollection的Add()、Remove()方法完成了对子控件的添加和删除。例如:
Control parent = new Control();
Control child = new Child();
//添加子控件child;
parent.Controls.Add(child);
//移除子控件child;
parent.Controls.Remove(child);

为什么要专门提供ControlCollection类型来管理控件的子控件呢?首先,作为类库使用者,自然希望各种类型的控件具有统一的接口,尤 其是自定义控件的时候,不希望自己重复定义管理子控件的操作;那么采用透明方式自然是最佳方案。然而,在使用控件的时候,安全也是需要重点考虑的,如果不 考虑子控件管理的合法性,一旦使用错误,会导致整个应用程序出现致命错误。从这样的角度考虑,似乎又应采用安全方式。这里就存在一个抉择。故而,.Net 在实现Control类库时,利用了职责分离的原则,将控件对象管理子控件的属性与行为和控件本身分离,并交由单独的ControlCollection 类负责。同时采用聚合而非继承的方式,以一个公共属性Controls,存在于Control类中。这种方式,集保留了透明方式和安全方式的优势,又摒弃 了这两种方式固有的缺陷,因此我名其为“复合方式”。

“复合方式”的设计,其对安全的保障,不仅仅是去除了Control类关于子控件管理的统一接口,同时还通过异常管理的方式,在ControlCollection类的子类中实现:
public class EmptyControlCollection : ControlCollection
{
        // Methods
 public EmptyControlCollection(Control owner) : base(owner)
 {}
 public override void Add(Control child)
 {
            this.ThrowNotSupportedException();
 }                         
        private void ThrowNotSupportedException()
        {
            throw new HttpException(HttpRuntime.FormatResourceString(”Control_does_not_allow_children”, base.Owner.GetType().ToString()));
        }
}

EmptyControlCollection继承了ControlCollection类,并重写了Add()等添加子控件的方法,使其抛出一个 异常。注意,它并没有重写父类的Remove()方法,这是因为ControlCollection类在实现Remove()方法时,对集合内的数据进行 了非空判断。而在EmptyControlCollection类中,是不可能添加子控件的,直接调用父类的Remove()方法,是不会出现错误的。

既然管理子控件的职责由ControlCollection类型负责,且Control类中的公共属性Controls即为 ControlCollection类型。所以,对于控件而言,如果是树形结构中的叶节点,它不能包含子控件,它的Controls属性就应为 EmptyControlCollection类型,假如用户调用了Controls的Add()方法,就会抛出异常。如果控件是树形结构中的枝节点,它 支持子控件,那么Controls属性就是ControlCollection类型。究竟是枝节点还是叶节点,决定权在于公共属性Controls:
public virtual ControlCollection Controls
{
      get
      {
             if (this._controls == null)
             {
                   this._controls = this.CreateControlCollection();
             }
             return this._controls;
      }
}

在属性的get访问器中,调用了protected方法CreateControlCollection(),它创建并返回了一个ControlCollection实例:
protected virtual ControlCollection CreateControlCollection()
{
     return new ControlCollection(this);
}

很明显,在Control基类实现Controls属性时,采用了Template Method模式,它推迟了ControlCollection的创建,将决定权交给了CreateControlCollection()方法。

如果我们需要定义一个控件,要求它不能管理子控件,就重写CreateControlCollection()方法,返回EmptyControlCollection对象:

protected override ControlCollection CreateControlCollection()
{
     return new EmptyControlCollection(this);
}
 
现在再回过头来看HtmlControl和HtmlContainerControl类。根据前面的分析,我们要求 HtmlContainerControl继承HtmlControl类,同时,HtmlContainerControl应为枝节点,能够管理子控件; HtmlControl则为叶节点,不支持子控件。通过引入ControlCollection类和其子类 EmptyControlCollection,以及Template Method模式后,这些类之间的关系与结构如下所示:

HtmlContainerControl继承了HtmlControl类,这两个类都重写了自己父类的protected方法 CreateControlCollection()。HtmlControl类,该方法返回EmptyControlCollection对象,使其成 为了不包含子控件的叶节点;HtmlContainerControl类中,该方法则返回ControlCollection对象,从而被赋予了管理子控 件的能力,成为了枝节点:
public abstract class HtmlControl : Control, IAttributeAccessor
{
        // Methods
 protected override ControlCollection CreateControlCollection()
 {
            return new EmptyControlCollection(this);
 }
}
public abstract class HtmlContainerControl : HtmlControl
{
       // Methods
 protected override ControlCollection CreateControlCollection()
 {
            return new ControlCollection(this);
 }
}

HtmlControl和HtmlContainerControl类均为抽象类。要定义它们的子类,如果不重写其父类的 CreateControlCollection()方法,那么它们的Controls属性,就与父类完全一致。例如HtmlImage控件继承自 HtmlControl类,该控件不能添加子控件;而HtmlForm控件则继承自HtmlContainerControl类,显然,HtmlForm 控件是支持添加子控件的操作的。

.Net的控件设计采用Composite模式的“复合方式”,较好地将控件的透明性与安全性结合起来,它的特点是:

1、在统一接口中消除了Add()、Remove()等子控件的管理方法,而由ControlCollection类实现,同时通过EmptyControlCollection类保障了控件进一步的安全;
2、控件能否管理子控件,不由继承的层次决定;而是通过重写CreateControlCollection()方法,由Controls属性的真正类型来决定。

如此一来,要定义自己的控件就更加容易。我们可以任意地扩展自己的控件类。不管继承自Control,还是HtmlControl或 HtmlContainerControl,都可以轻松地定义出具有枝节点或叶节点属性的新控件。如果有新的需求要求改变管理子控件的方式,我们还可以定 义继承自ControlCollection的类,并在控件类的方法CreateControlCollection()中创建并返回它的实例。

posted @ 2006-09-09 00:51 Alex 阅读(461) | 评论 (0)编辑 收藏

设计模式之Composite(组合)

板桥里人 http://www.jdon.com 2002/04/27(转载请保留)

 

Composite模式定义:
将对象以树形结构组织起来,以达成“部分-整体” 的层次结构,使得客户端对单个对象和组合对象的使用具有一致性.

Composite比较容易理解,想到Composite就应该想到树形结构图。组合体内这些对象都有共同接口,当组合体一个对象的方法被调用执行 时,Composite将遍历(Iterator)整个树形结构,寻找同样包含这个方法的对象并实现调用执行。可以用牵一动百来形容。

所以Composite模式使用到Iterator模式,和Chain of Responsibility模式类似。

Composite好处:
1.使客户端调用简单,客户端可以一致的使用组合结构或其中单个对象,用户就不必关系自己处理的是单个对象还是整个组合结构,这就简化了客户端代码。
2.更容易在组合体内加入对象部件. 客户端不必因为加入了新的对象部件而更改代码。

如何使用Composite?
首先定义一个接口或抽象类,这是设计模式通用方式了,其他设计模式对接口内部定义限制不多,Composite却有个规定,那就是要在接口内部定义一个用于访问和管理Composite组合体的对象们(或称部件Component).

下面的代码是以抽象类定义,一般尽量用接口interface,

public abstract class Equipment
{
  private String name;
  //实价
  public abstract double netPrice();
  //折扣价格
  public abstract double discountPrice();
  //增加部件方法  
  public boolean add(Equipment equipment) { return false; }
  //删除部件方法
  public boolean remove(Equipment equipment) { return false; }
  //注意这里,这里就提供一种用于访问组合体类的部件方法。
  public Iterator iter() { return null; }
  
  public Equipment(final String name) { this.name=name; }
}

抽象类Equipment就是Component定义,代表着组合体类的对象们,Equipment中定义几个共同的方法。

public class Disk extends Equipment
{
  public Disk(String name) { super(name); }
  //定义Disk实价为1
  public double netPrice() { return 1.; }
  //定义了disk折扣价格是0.5 对折。
  public double discountPrice() { return .5; }
}

Disk是组合体内的一个对象,或称一个部件,这个部件是个单独元素( Primitive)。
还有一种可能是,一个部件也是一个组合体,就是说这个部件下面还有'儿子',这是树形结构中通常的情况,应该比较容易理解。现在我们先要定义这个组合体:

abstract class CompositeEquipment extends Equipment
{
  private int i=0;
  //定义一个Vector 用来存放'儿子'
  private Lsit equipment=new ArrayList();

  public CompositeEquipment(String name) { super(name); }

  public boolean add(Equipment equipment) {
     this.equipment.add(equipment);
     return true;
   }


  public double netPrice()
  {
    double netPrice=0.;
    Iterator iter=equipment.iterator();
    for(iter.hasNext())
      netPrice+=((Equipment)iter.next()).netPrice();
    return netPrice;
  }

  public double discountPrice()
  {
    double discountPrice=0.;
    Iterator iter=equipment.iterator();
    for(iter.hasNext())
      discountPrice+=((Equipment)iter.next()).discountPrice();
    return discountPrice;
  }
  

  //注意这里,这里就提供用于访问自己组合体内的部件方法。
  //上面dIsk 之所以没有,是因为Disk是个单独(Primitive)的元素.

  public Iterator iter()
  {
    return equipment.iterator() ;
  {
  //重载Iterator方法
   public boolean hasNext() { return i<equipment.size(); }
  //重载Iterator方法
   public Object next()
   {
    if(hasNext())
       return equipment.elementAt(i++);
    else
        throw new NoSuchElementException();
   }
  

}

上面CompositeEquipment继承了Equipment,同时为自己里面的对象们提供了外部访问的方法,重载了Iterator,Iterator是Java的Collection的一个接口,是Iterator模式的实现.

我们再看看CompositeEquipment的两个具体类:盘盒Chassis和箱子Cabinet,箱子里面可以放很多东西,如底板,电源盒,硬盘盒等;盘盒里面可以放一些小设备,如硬盘 软驱等。无疑这两个都是属于组合体性质的。

public class Chassis extends CompositeEquipment
{
   public Chassis(String name) { super(name); }
   public double netPrice() { return 1.+super.netPrice(); }
   public double discountPrice() { return .5+super.discountPrice(); }
}

public class Cabinet extends CompositeEquipment
{
   public Cabinet(String name) { super(name); }
   public double netPrice() { return 1.+super.netPrice(); }
   public double discountPrice() { return .5+super.discountPrice(); }
}

至此我们完成了整个Composite模式的架构。

我们可以看看客户端调用Composote代码:

Cabinet cabinet=new Cabinet("Tower");

Chassis chassis=new Chassis("PC Chassis");
//将PC Chassis装到Tower中 (将盘盒装到箱子里)
cabinet.add(chassis);
//将一个10GB的硬盘装到 PC Chassis (将硬盘装到盘盒里)
chassis.add(new Disk("10 GB"));

//调用 netPrice()方法;
System.out.println("netPrice="+cabinet.netPrice());
System.out.println("discountPrice="+cabinet.discountPrice());

上面调用的方法netPrice()或discountPrice(),实际上Composite使用Iterator遍历了整个树形结构,寻找同样包含这个方法的对象并实现调用执行.

Composite是个很巧妙体现智慧的模式,在实际应用中,如果碰到树形结构,我们就可以尝试是否可以使用这个模式。

以论坛为例,一个版(forum)中有很多帖子(message),这些帖子有原始贴,有对原始贴的回应贴,是个典型的树形结构,那么当然可以使用Composite模式,那么我们进入Jive中看看,是如何实现的.

Jive解剖
在Jive中 ForumThread是ForumMessages的容器container(组合体).也就是说,ForumThread类似我们上例中的 CompositeEquipment.它和messages的关系如图:
[thread]
   |- [message]
   |- [message]
      |- [message]
      |- [message]
         |- [message]

我们在ForumThread看到如下代码:

public interface ForumThread {
   ....
   public void addMessage(ForumMessage parentMessage, ForumMessage newMessage)
         throws UnauthorizedException;

   public void deleteMessage(ForumMessage message)
         throws UnauthorizedException;

  
   public Iterator messages();
      ....

}

类似CompositeEquipment, 提供用于访问自己组合体内的部件方法: 增加 删除 遍历.

结合我的其他模式中对Jive的分析,我们已经基本大体理解了Jive论坛体系的框架,如果你之前不理解设计模式,而直接去看Jive源代码,你肯定无法看懂。

:)

posted @ 2006-09-09 00:44 Alex 阅读(493) | 评论 (0)编辑 收藏

     摘要: 转自: CCIENET 自从J2EE出现以来,就大大简化了在Java下的企业级开发。但是随着J2EE越来越普遍 地被应用到各个领域中,开发者们渐渐意识到需要一种方法来标准化应用程序的开发过程,他们采用的方法是标准化应用程序的结构层。在结构层通常封装了一些独 立于业务逻辑的复杂技术,以便在业务逻辑和底层的架构之间建立起弱连接。无可否认,J2EE是一个很成功的技术,它为一些基本的任务提供了一...  阅读全文
posted @ 2006-09-08 23:59 Alex 阅读(1966) | 评论 (1)编辑 收藏

     摘要: 引言:最近在看一个开源的聊天室AjaxChat 时看到一个被引用的包:javawebparts,处于好奇去看了一下,突然发现这么好的一个常用web组件不去用实在是太可惜了,下面逐一介绍,详细文档大家可以去官方文档看看,最好看它的Demo,很直观。 javawebparts的口号是:不用重新发明轮子 ! 对这点我是严重支持啊,在我的身边看到N多所谓...  阅读全文
posted @ 2006-09-08 21:18 Alex 阅读(2525) | 评论 (7)编辑 收藏

本文主要谈一下密码学中的加密和数字签名,以及其在java中如何进行使用。对密码学有兴趣的伙伴,推荐看 Bruce Schneier的著作:Applied Crypotography。在jdk1.5的发行版本中安全性方面有了很大的改进,也提供了对RSA算法的直接支持,现在我们从实例入手解决问题(本文 仅是作为简单介绍):

  一、密码学上常用的概念 

  1)消息摘要:

   这是一种与消息认证码结合使用以确保消息完整性的技术。主要使用单向散列函数算法,可用于检验消息的完整性,和通过散列密码直接以文本形式保存等,目前 广泛使用的算法有MD4、MD5、SHA-1,jdk1.5对上面都提供了支持,在java中进行消息摘要很简单, java.security.MessageDigest提供了一个简易的操作方法:

/**
*MessageDigestExample.java
*Copyright 2005-2-16
*/
import java.security.MessageDigest;
/**
*单一的消息摘要算法,不使用密码.可以用来对明文消息(如:密码)隐藏保存
*/
public class MessageDigestExample{
 
public static void main(String[] args) throws Exception{
  
if(args.length!=1){
   System.err.println(
"Usage:java MessageDigestExample text");
   System.exit(
1);
  }

  
byte[] plainText=args[0].getBytes("UTF8");

  
//使用getInstance("算法")来获得消息摘要,这里使用SHA-1的160位算法
  MessageDigest messageDigest=MessageDigest.getInstance("SHA-1");

  System.out.println(
"\n"+messageDigest.getProvider().getInfo());
  
//开始使用算法
  messageDigest.update(plainText);
  System.out.println(
"\nDigest:");
  
//输出算法运算结果
  System.out.println(new String(messageDigest.digest(),"UTF8"));
 }
}

  还可以通过消息认证码来进行加密实现,javax.crypto.Mac提供了一个解决方案,有兴趣者可以参考相关API文档,本文只是简单介绍什么是摘要算法。

这里补充另一个运用消息摘要的方式加密的例子:
public class TestEncrypt {

    
public TestEncrypt() {
    }

    
/**
     * 
@param strSrc  :strSrc is a string will be encrypted,
     * 
@param encName : encName is the algorithm name will be used.
     *                encName dafault to "MD5"
     * 
@return String
     
*/
    
public String Encrypt(String strSrc, String encName) {

        MessageDigest md 
= null;
        String strDes 
= null;

        
byte[] bt = strSrc.getBytes();
        
try {
            
if (encName == null || encName.equals("")) {
                encName 
= "MD5";
            }
            md 
= MessageDigest.getInstance(encName);
            md.update(bt);
            strDes 
= bytes2Hex(md.digest()); //to HexString
        }
        
catch (NoSuchAlgorithmException e) {
            System.out.println(
"Invalid algorithm.");
            
return null;
        }
        
return strDes;
    }

    
public String bytes2Hex(byte[] bts) {
        String des 
= "";
        String tmp 
= null;
        
for (int i = 0; i < bts.length; i++) {
            tmp 
= (Integer.toHexString(bts[i] & 0xFF));
            
if (tmp.length() == 1) {
                des 
+= "0";
            }
            des 
+= tmp;
        }
        
return des;
    }

    
public static void main(String[]args) {
        TestEncrypt te 
= new TestEncrypt();
        String strSrc 
= "可以加密汉字.Oh,and english";
        System.out.println(
"Source String:" + strSrc);
        System.out.println(
"Encrypted String:");
        System.out.println(
"Use Def:" + te.Encrypt(strSrc, null));
        System.out.println(
"Use MD5:" + te.Encrypt(strSrc, "MD5"));
        System.out.println(
"Use SHA:" + te.Encrypt(strSrc, "SHA-1"));
        System.out.println(
"Use SHA-256:" + te.Encrypt(strSrc, "SHA-256"));
    }
}

另外,在javawebparts中的 RequestHelpers里的generateGUID方法也涉及到了MD5的方法,代码如下:
public static String generateGUID(HttpServletRequest request) {

    String out 
= "";
    
try {
      
// Construct a string that is comprised of:
      
// Remote IP Address + Host IP Address + Date (yyyyMMdd) +
      
// Time (hhmmssSSa) + Requested Path + Session ID +
      
// HashCode Of ParameterMap
      StringBuffer sb = new StringBuffer(1024);
      sb.append(request.getRemoteAddr());
      InetAddress ia 
= InetAddress.getLocalHost();
      sb.append(ia.getHostAddress());
      sb.append(
new SimpleDateFormat("yyyyMMddhhmmssSSa").format(new Date()));
      String path 
= request.getServletPath();
      String pathInfo 
= request.getPathInfo();
      
if (pathInfo != null) {
        path 
+= pathInfo;
      }
      sb.append(path);
      sb.append(request.getSession(
false));
      sb.append(request.getParameterMap().hashCode());
      String str 
= sb.toString();
      
// Now encode the string using an MD5 encryption algorithm.
      MessageDigest md = MessageDigest.getInstance("md5");
      md.update(str.getBytes());
      
byte[] digest = md.digest();
      StringBuffer hexStr 
= new StringBuffer(1024);
      
for (int i = 0; i < digest.length; i++) {
        str 
= Integer.toHexString(0xFF & digest[i]);
        
if (str.length() < 2) {
          str 
= "0" + str;
        }
        hexStr.append(str);
      }
      out 
= hexStr.toString();
    } 
catch (NoSuchAlgorithmException nsae) {
      log.error(nsae);
    } 
catch (UnknownHostException uhe) {
      log.error(uhe);
    }
    
// Return the encrypted string.  It should be unique based on the
    
// components that comprise the plain text string, and should always be
    
// 32 characters thanks to the MD5 algorithm.
    return out;

  } 
// End generateGUID().


  2)私钥加密:

  消息摘要只能检查消息的完整性,但是单向的,对明文消息并不能加密,要加密明文的消息的话,就要使用其他的算法,要确保机密性,我们需要使用私钥密码术来交换私有消息。

  这种最好理解,使用对称算法。比如:A用一个密钥对一个文件加密,而B读取这个文件的话,则需要和A一样的密钥,双方共享一个私钥(而在web环境下,私钥在传递时容易被侦听):

   使用私钥加密的话,首先需要一个密钥,可用javax.crypto.KeyGenerator产生一个密钥(java.security.Key), 然后传递给一个加密工具(javax.crypto.Cipher),该工具再使用相应的算法来进行加密,主要对称算法有:DES(实际密钥只用到56 位),AES(支持三种密钥长度:128、192、256位),通常首先128位,其他的还有DESede等,jdk1.5种也提供了对对称算法的支持, 以下例子使用AES算法来加密:

/**
*PrivateExmaple.java
*Copyright 2005-2-16
*/
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import java.security.Key;

/**
*私鈅加密,保证消息机密性
*/
public class PrivateExample{
 
public static void main(String[] args) throws Exception{
  
if(args.length!=1){
   System.err.println(
"Usage:java PrivateExample <text>");
   System.exit(
1);
  }
  
byte[] plainText=args[0].getBytes("UTF8");

  
//通过KeyGenerator形成一个key
  System.out.println("\nStart generate AES key");
  KeyGenerator keyGen
=KeyGenerator.getInstance("AES");
  keyGen.init(
128);
  Key key
=keyGen.generateKey();
  System.out.println(
"Finish generating DES key");

  
//获得一个私鈅加密类Cipher,ECB是加密方式,PKCS5Padding是填充方法
  Cipher cipher=Cipher.getInstance("AES/ECB/PKCS5Padding");
  System.out.println(
"\n"+cipher.getProvider().getInfo());

  
//使用私鈅加密
  System.out.println("\nStart encryption:");
  cipher.init(Cipher.ENCRYPT_MODE,key);
  
byte[] cipherText=cipher.doFinal(plainText);
  System.out.println(
"Finish encryption:");
  System.out.println(
new String(cipherText,"UTF8"));

  System.out.println(
"\nStart decryption:");
  cipher.init(Cipher.DECRYPT_MODE,key);
  
byte[] newPlainText=cipher.doFinal(cipherText);
  System.out.println(
"Finish decryption:");

  System.out.println(
new String(newPlainText,"UTF8"));

 }
}

  3)公钥加密:

   上面提到,私钥加密需要一个共享的密钥,那么如何传递密钥呢?web环境下,直接传递的话很容易被侦听到,幸好有了公钥加密的出现。公钥加密也叫不对称 加密,不对称算法使用一对密钥对,一个公钥,一个私钥,使用公钥加密的数据,只有私钥能解开(可用于加密);同时,使用私钥加密的数据,只有公钥能解开 (签名)。但是速度很慢(比私钥加密慢100到1000倍),公钥的主要算法有RSA,还包括Blowfish,Diffie-Helman等, jdk1.5种提供了对RSA的支持,是一个改进的地方:

/**
*PublicExample.java
*Copyright 2005-2-16
*/
import java.security.Key;
import javax.crypto.Cipher;
import java.security.KeyPairGenerator;
import java.security.KeyPair;
/**
*一个简单的公鈅加密例子,Cipher类使用KeyPairGenerator生成的公鈅和私鈅
*/
public class PublicExample{
 
public static void main(String[] args) throws Exception{
  
if(args.length!=1){
   System.err.println(
"Usage:java PublicExample <text>");
   System.exit(
1);
  }

  
byte[] plainText=args[0].getBytes("UTF8");
  
//构成一个RSA密钥
  System.out.println("\nStart generating RSA key");
  KeyPairGenerator keyGen
=KeyPairGenerator.getInstance("RSA");
  keyGen.initialize(
1024);
  KeyPair key
=keyGen.generateKeyPair();
  System.out.println(
"Finish generating RSA key");

  
//获得一个RSA的Cipher类,使用公鈅加密
  Cipher cipher=Cipher.getInstance("RSA/ECB/PKCS1Padding");
  System.out.println(
"\n"+cipher.getProvider().getInfo());

  System.out.println(
"\nStart encryption");
  cipher.init(Cipher.ENCRYPT_MODE,key.getPublic());
  
byte[] cipherText=cipher.doFinal(plainText);
  System.out.println(
"Finish encryption:");
  System.out.println(
new String(cipherText,"UTF8"));

  
//使用私鈅解密
  System.out.println("\nStart decryption");
  cipher.init(Cipher.DECRYPT_MODE,key.getPrivate());
  
byte[] newPlainText=cipher.doFinal(cipherText);
  System.out.println(
"Finish decryption:");
  System.out.println(
new String(newPlainText,"UTF8"));
 }
}

posted @ 2006-09-07 23:59 Alex 阅读(1149) | 评论 (0)编辑 收藏

key words: Digester  解析xml

假设有下列xml文件:
<?xml version='1.0' encoding='utf-8'?>
<address-book>
    
<contact myType="individual">
        
<name>Zane Pasolini</name>
        
<address>999 W. Prince St.</address>
        
<city>New York</city>
        
<province>NY</province>
        
<postalcode>10013</postalcode>
        
<country>USA</country>
        
<telephone>1-212-345-6789</telephone>
    
</contact>
    
<contact myType="business">
        
<name>SAMOFIX d.o.o.</name>
        
<address>Ilica 47-2</address>
        
<city>Zagreb</city>
        
<province></province>
        
<postalcode>10000</postalcode>
        
<country from="cn">Croatia</country>
        
<telephone>385-1-123-4567</telephone>
    
</contact>
</address-book>

这是一份常用到的文件,现在我们需要将之映射到java bean,用Digester解析显得非常简单
public class AddressBookParser
{
    
/**
     * Prints the contact information to standard output.
     *
     * 
@param contact the <code>Contact</code> to print out
     
*/
    
public void addContact(Contact contact)
    {
        System.out.println(
"TYPE: " + contact.getType());
        System.out.println(
"NAME: " + contact.getName());
        System.out.println(
"    ADDRESS:    " + contact.getAddress());
        System.out.println(
"    CITY:       " + contact.getCity());
        System.out.println(
"    PROVINCE:   " + contact.getProvince());
        System.out.println(
"    POSTALCODE: " + contact.getPostalcode());
        System.out.println(
"    COUNTRY:    " + contact.getCountry());
        System.out.println(
"    COUNTRY-From:    " + contact.getCountryFrom());
        System.out.println(
"    TELEPHONE:  " + contact.getTelephone());
    }

    
/**
     * Configures Digester rules and actions, parses the XML file specified
     * as the first argument.
     *
     * 
@param args command line arguments
     
*/
    
public static void main(String[] args) throws IOException, SAXException
    {
        
// instantiate Digester and disable XML validation
        Digester digester = new Digester();
        digester.setValidating(
false);

        
// instantiate AddressBookParser class
        digester.addObjectCreate("address-book", AddressBookParser.class );
        
// instantiate Contact class
        digester.addObjectCreate("address-book/contact", Contact.class );

        
// set type property of Contact instance when 'type' attribute is found
        
//对有属性的值通过setProperties方法

        digester.addSetProperties(
"address-book/contact",         "myType""type" );

        
// set different properties of Contact instance using specified methods
        
//addCallMethod与addBeanPropertySetter等价
        
// 参数 0代表一个参数,默认就是当前读的数据

        digester.addCallMethod(
"address-book/contact/name",       "setName"0);
        digester.addCallMethod(
"address-book/contact/address",    "setAddress"0);
        digester.addCallMethod(
"address-book/contact/address",    "setAddress",0);
        digester.addCallMethod(
"address-book/contact/city",       "setCity"0);
        digester.addCallMethod(
"address-book/contact/province",   "setProvince"0);
        digester.addCallMethod(
"address-book/contact/postalcode""setPostalcode"0);
        digester.addCallMethod(
"address-book/contact/country",    "setCountry"0);



        
//增加country的属性 : from
        digester.addSetProperties("address-book/contact/country","from","countryFrom");
        digester.addCallMethod(
"address-book/contact/telephone",  "setTelephone"0);

        
// call 'addContact' method when the next 'address-book/contact' pattern is seen
        digester.addSetNext("address-book/contact",               "addContact" );

        
// now that rules and actions are configured, start the parsing process
        AddressBookParser abp = (AddressBookParser) digester.parse(new File("c:\\addressbook.xml"));
    }

    
/**
     * JavaBean class that holds properties of each Contact entry.
     * It is important that this class be public and static, in order for
     * Digester to be able to instantiate it.
     
*/
    
public static class Contact
    {
        
private String type;
        
private String name;
        
private String address;
        
private String city;
        
private String province;
        
private String postalcode;
        
private String country;
        //增加一个country的属性: from
        private String countryFrom;
        private String telephone;

        
public void setType(String newType)
        {
            type 
= newType;
        }
        
public String getType()
        {
            
return type;
        }

        
public void setName(String newName)
        {
            name 
= newName;
        }
        
public String getName()
        {
            
return name;
        }

        
public void setAddress(String newAddress)
        {
            address 
= newAddress;
        }
        
public String getAddress()
        {
            
return address;
        }

        
public void setCity(String newCity)
        {
            city 
= newCity;
        }
        
public String getCity()
        {
            
return city;
        }

        
public void setProvince(String newProvince)
        {
            province 
= newProvince;
        }
        
public String getProvince()
        {
            
return province;
        }

        
public void setPostalcode(String newPostalcode)
        {
            postalcode 
= newPostalcode;
        }
        
public String getPostalcode()
        {
            
return postalcode;
        }

        
public void setCountry(String newCountry)
        {
            country 
= newCountry;
        }
        
public String getCountry()
        {
            
return country;
        }

        
public void setTelephone(String newTelephone)
        {
            telephone 
= newTelephone;
        }
        
public String getTelephone()
        {
            
return telephone;
        }

        
public String getCountryFrom() {
            
return countryFrom;
        }

        
public void setCountryFrom(String countryFrom) {
            
this.countryFrom = countryFrom;
        }
    }
}


AjaxChat 中的读取房间信息的方式显得更简洁
房间的xml配置文件如下:
<rooms>
  
<room id="1" name="General Topics" />
  
<room id="2" name="Programming" />
  
<room id="3" name="Movies" />
  
<room id="4" name="Music" />
  
<room id="5" name="Television" />
</rooms>

解析代码如下 :
public synchronized void init(InputStream isConfigFile) {

        log.debug(
"init()");
        
if (isConfigFile != null) {
            
// Read in rooms config and create beans, hand off to DAO.
            Digester digester = new Digester();
            digester.setValidating(
false);
            digester.push(
this);
            digester.addObjectCreate(
"rooms/room",
                    
"org.apache.struts.apps.ajaxchat.dto.RoomDTO");
            //注意这里,如果xl的属性名称和bean的属性名称完全对应,则直接提供xml的位置即可
            digester.addSetProperties(
"rooms/room");
            digester.addSetNext(
"rooms/room""addRoom");
            
try {
                digester.parse(isConfigFile);
                log.info(
"***** Rooms = " + rooms);
            } 
catch (IOException ioe) {
                ioe.printStackTrace();
            } 
catch (SAXException se) {
                se.printStackTrace();
            }
        }

    } 
// End init().

如果在xml文件中增加非attribute则更改后的配置文件如下:

<rooms>
  
<room id="1" name="General Topics" />
  
<room id="2" name="Programming" />
  
<room id="3" name="Movies" />
  
<room id="4" name="Music" />
  
<room id="5" name="Television" />
  
<room>
    
<id>6</id>
    
<name>shit</name>
  
</room>
  
<room>
    
<id>7</id>
    
<name>haha</name>
  
</room>
</rooms>
对应的解析如下:
public synchronized void init(InputStream isConfigFile) {

        log.debug(
"init()");
        
if (isConfigFile != null) {
            
// Read in rooms config and create beans, hand off to DAO.
            Digester digester = new Digester();
            digester.setValidating(
false);
            digester.push(
this);
            digester.addObjectCreate(
"rooms/room",
                    
"org.apache.struts.apps.ajaxchat.dto.RoomDTO");
            digester.addSetProperties(
"rooms/room");
            //增加addCallMethod方法
            digester.addCallMethod(
"rooms/room/id","setId",0);
            digester.addCallMethod(
"rooms/room/name","setName",0);
            digester.addSetNext(
"rooms/room""addRoom");
            
try {
                digester.parse(isConfigFile);
                log.info(
"***** Rooms = " + rooms);
            } 
catch (IOException ioe) {
                ioe.printStackTrace();
            } 
catch (SAXException se) {
                se.printStackTrace();
            }
        }

    } 
// End init().

posted @ 2006-09-06 23:32 Alex 阅读(20847) | 评论 (19)编辑 收藏

how to set the SSH timeout?
a: set 'TMOUT=3600' in /etc/profile

fine.
posted @ 2006-09-06 16:07 Alex 阅读(833) | 评论 (0)编辑 收藏

key words:struts国际化

一、Struts的国际化
    Struts是一种支持国际化的MVC的Web Framework。可是如何来使用struts国际化是一个问题。下面我们来探讨一下,如何实现Struts的国际化。Web程式的国际化涉及到3个层面的东西。第一、jsp部分的输入/输出;第二、应用处理程序的国际化;第三、DB的国际化问题。这里主要探讨的是jsp部分的输入/输出问题。

二、静态部分的国际化
   Struts的jsp页面静态内容(包括静态文字,静态图片)国际化问题,是通过资源文件来实现的。要实现国际化,需要做如下几项工作:1、定义web.xml的动ActionServlet的参数;2、定义资源文件;3、定义JSP页面的字符集合;4、在JSP页面获取资源文件里面的内容。
1、定义web.xml的动ActionServlet的参数

<servlet>
  
<servlet-name>action</servlet-name>
  
<servlet-class>org.apache.struts.action.ActionServlet</servlet-class>   
  
<init-param>
    
<param-name>config</param-name>
    
<param-value>/WEB-INF/struts-config.xml</param-value>
  
</init-param>
  
<init-param>
    
<param-name>application</param-name>
    
<param-value>ApplicationResources</param-value> <!-- 默认资源文件名 -->
  
</init-param>
  
<load-on-startup>2</load-on-startup>
</servlet>

2、定义资源文件
在/WEB-INF/classes下面添加UTF-8资源束文件。每一个资源文件是“键-值”对的集合。在JSP页面里面可以通过键来找到相应的数据值。本例子的文件名是ApplicationResources,所以相应的资源文件束是(包括e文,简体中文,繁体中文)
ApplicationResources.properties : 默认资源文件。当在其他资源文件里面找不到某个资源的时候,就使用该资源文件里面的定义。
ApplicationResources_zh_CN.properties:简体中文资源文件。
ApplicationResources_zh_TW.properties:繁体中文资源文件。

资源文件的格式为:默认资源文件名_国别_语言.properties。其中每个文件都是通过%JAVA_HONE%/BIN/native2ascii.exe工具转换而来。你也可以使用其他工具来处理得到(http://java.sun.com/products/jilkit/ 有一个工具Internationalization Java Internationalization and Localization Toolkit 可以处理)。下面是一个例子,我们显示如何使用%JAVA_HONE%/BIN/native2ascii.exe命令来定义资源束文件。
2.1 准备文件
//ApplicationResources.properties ;默认资源文件,通常里面的内容是英文的。
label.username=USERNAME :
label.password=PASSWORD :

//ApplicationResources_zh_CN.bak ;简体中文的资源文件。里面的内容是中文的。它需要工具将其中的内容处理成UTF-8
label.username=用户名 :
label.password=密  码 :

//ApplicationResources_zh_TW.bak : 繁体中文的资源文件。里面的内容是中文的。它需要工具将其中的内容处理成UTF-8,下面的内容是繁体码。
label.username=ノめ?W :
label.password=ノめ?W :

2.2 准备完成以后,使用如下的命令创建UTF-8资源文件束
native2ascii -encoding gb2312 ApplicationResources_zh_CN.bak ApplicationResources_zh_CN.properties
native2ascii -encoding big5 Applica tionResources_zh_TW.bak ApplicationResources_zh_TW.properties

3、定义JSP页面的字符集合
定义JSP页面的语言为UTF-8。在每个JSP页面,必须有如下的内容(如果使用的模板技术,则只是需要在模板页面添加,其他使用该模板的页面无需添加)
<%@ page contentType="text/html;charset=UTF-8"%>

4、在JSP页面获取资源文件里面的内容。
在JSP里面需要显示静态内容的地方使用<bean:message />strus的bean tag包里面的message标签。例如下面的页面

<table>
  <tr>
    <td align="right"><bean:message key="label.username" /></td>   
  </tr>
  <tr>
    <td align="right"><bean:message key="label.password" /></td>
  </tr>
</table>

好了,在这个页面显示的时候,如果客户的IE的语言集合是zh_CN的话,就会显示
用户名:
口  令:

如果是客户的IE的语言是zh_TW的话,就会显示
用户名:
用户名:

可以在IE的工具->Internet选项->语言的地方,来选择,定义IE的语言。

三、表单的数据的处理。
对于表单数据的处理,我们是通过添加一个Filter来实现的。所有提交的请求,都需要做字符处理。然后在web.xml里面定义该Filter。这样我们就不需要在程序里面做任何的字符处理。
3.1 定义Filter。下面是一个例子。
package com.webapps.commons;

import java.io.*;
import javax.servlet.*;

public class CharsetEncodingFilter implements Filter{
  private FilterConfig config = null;
  private String defaultEncode = "UTF-8";

  public void init(FilterConfig config) throws ServletException {
    this.config = config;
    if(config.getInitParameter("Charset")!=null){
        defaultEncode=config.getInitParameter("Charset");
    }
  }

  public void destroy() {
    this.config = null;
  }

  public void doFilter(ServletRequest request, ServletResponse response,
                       FilterChain chain) throws IOException, ServletException {
    ServletRequest srequest=request;
    srequest.setCharacterEncoding(defaultEncode);
    chain.doFilter(srequest,response);
  }
}

3.2 在web.xml里面声明使用该Filter
<filter>
  <filter-name>Character Encoding</filter-name>
  <filter-class>com.webapps.commons.CharsetEncodingFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>Character Encoding</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

四、扩展
待续的是应用程序部分的国际化问题,和DB的国际化问题。


posted @ 2006-09-05 22:22 Alex 阅读(709) | 评论 (0)编辑 收藏

使用 AppFuse 的七个理由

学习 Java 开放源码工具 —— 并使用这些工具提高生产效率


级别: 初级

Matt Raible (mraible@virtuas.com), 开放源码实践先驱, Virtuas Open Source Solutions

2006 年 8 月 31 日

开 始学习在 Java™ 平台上使用诸如 Spring、Hibernate 或 MySQL 之类的开放源码工具时可能非常困难。再加上 Ant 或 Maven,以及与 DWR 一起的小 Ajax,还有 Web 框架 —— 即 JSF,我们必须睁大眼睛盯着如何配置应用程序。AppFuse 减少了集成开放源码项目的痛苦。它可以把测试变成一等公民,让我们可以从数据库表生成整个 UI,并使用 XFire 来支持 Web 服务。另外,AppFuse 的社区也非常健全,这是不同 Web 框架用户可以一起融洽相处的地方之一。

AppFuse 是一个开放源码的项目和应用程序,它使用了在 Java 平台上构建的开放源码工具来帮助我们快速而高效地开发 Web 应用程序。我最初开发它是为了减少在为客户构建新 Web 应用程序时所花费的那些不必要的时间。从核心上来说,AppFuse 是一个项目骨架,类似于通过向导创建新 Web 项目时 IDE 所创建的东西。当我们使用 AppFuse 创建一个项目时,它会提示我们将使用开放源码框架,然后才创建项目。它使用 Ant 来驱动测试、代码生成、编译和部署。它提供了目录和包结构,以及开发基于 Java 语言的 Web 应用程序所需要的库。

与大部分 “new project” 向导不同,AppFuse 创建的项目从最开始就包含很多类和文件。这些文件用来实现特性,不过它们同时也会在您开发应用程序时被用作示例。通过使用 AppFuse 启动新项目,我们通常可以减少一到两周的开发时间。我们不用担心如何将开放源码框架配置在一起,因为这都已经完成了。我们的项目都已提前配置来与数据库进 行交互,它会部署到应用服务器上,并对用户进行认证。我们不必实现安全特性,因为这都早已集成了。

当我最初开发 AppFuse 时,它只支持 Struts 和 Hibernate。经过几年的努力,我发现了比 Struts 更好的 Web 框架,因此我还添加了为这些 Web 框架使用的选项。现在,AppFuse 可以支持 Hibernate 或 iBATIS 作为持久性框架。对于 Web 框架来说,我们可以使用 JavaServer Faces(JSF)、Spring MVC、Struts、Tapestry 或 WebWork。

AppFuse 提供了很多应用程序需要的一些特性,包括:

  • 认证和授权
  • 用户管理
  • Remember Me(这会保存您的登录信息,这样就不用每次都再进行登录了)
  • 密码提醒
  • 登记和注册
  • SSL 转换
  • E-mail
  • URL 重写
  • 皮肤
  • 页面修饰
  • 模板化布局
  • 文件上载

这种 “开箱即用” 的功能是 AppFuse 与其他 CRUD 代 框架的区别之一(CRUD 取自创建、检索、更新删除 几个操作的英文首字母),包括 Ruby on Rails、Trails 和 Grails。上面提到的这些框架,以及 AppFuse,都让我们可以从数据库表或现有的模型对象中生成主页/细节页。

图 1 阐述了一个典型 AppFuse 应用程序的概念设计:


图 1. 典型的 AppFuse 应用程序
典型的 AppFuse 应用程序

清单 1 给出了我们在创建 devworks 项目时所使用的命令行交互操作,同时还给出了所生成的结果。这个项目使用了 WebWork 作为自己的 Web 框架(请参考下面 参考资料 一节给出的链接)。


清单 1. 使用 AppFuse 创建新项目
alotta:~/dev/appfuse mraible$ ant new
Buildfile: build.xml

clean:
[echo] Cleaning build and distribution directories

init:

new:
[echo]
[echo] +-------------------------------------------------------------+
[echo] | -- Welcome to the AppFuse New Application Wizard! -- |
[echo] | |
[echo] | To create a new application, please answer the following |
[echo] | questions. |
[echo] +-------------------------------------------------------------+

[input] What would you like to name your application [myapp]?
devworks
[input] What would you like to name your database [mydb]?
devworks
[input] What package name would you like to use [org.appfuse]?
com.ibm
[input] What web framework would you like to use [webwork,tapestry,spring,js
f,struts]?
webwork
[echo] Creating new application named 'devworks'...
[copy] Copying 359 files to /Users/mraible/Work/devworks
[copy] Copying 181 files to /Users/mraible/Work/devworks/extras
[copy] Copying 1 file to /Users/mraible/Work/devworks
[copy] Copying 1 file to /Users/mraible/Work/devworks

install:
[echo] Copying WebWork JARs to ../../lib
[copy] Copying 6 files to /Users/mraible/Work/devworks/lib
[echo] Adding WebWork entries to ../../lib.properties
[echo] Adding WebWork classpath entries
[echo] Removing Struts-specific JARs
[delete] Deleting directory /Users/mraible/Work/devworks/lib/struts-1.2.9
[delete] Deleting directory /Users/mraible/Work/devworks/lib/strutstest-2.1.3
[echo] Deleting struts_form.xdt for XDoclet
[delete] Deleting directory /Users/mraible/Work/devworks/metadata/templates
[echo] Deleting Struts merge-files in metadata/web
[delete] Deleting 7 files from /Users/mraible/Work/devworks/metadata/web
[echo] Deleting unused Tag Libraries and Utilities
[delete] Deleting 2 files from /Users/mraible/Work/devworks/src/web/org/appfu
se/webapp
[echo] Modifying appgen for WebWork
[copy] Copying 12 files to /Users/mraible/Work/devworks/extras/appgen
[echo] Replacing source and test files
[delete] Deleting directory /Users/mraible/Work/devworks/src/web/org/appfuse/
webapp/form
[delete] Deleting directory /Users/mraible/Work/devworks/src/web/org/appfuse/
webapp/action
[copy] Copying 13 files to /Users/mraible/Work/devworks/src
[delete] Deleting directory /Users/mraible/Work/devworks/test/web/org/appfuse
/webapp/form
[delete] Deleting directory /Users/mraible/Work/devworks/test/web/org/appfuse
/webapp/action
[copy] Copying 5 files to /Users/mraible/Work/devworks/test
[echo] Replacing web files (images, scripts, JSPs, etc.)
[delete] Deleting 1 files from /Users/mraible/Work/devworks/web/scripts
[copy] Copying 34 files to /Users/mraible/Work/devworks/web
[delete] Deleting: /Users/mraible/Work/devworks/web/WEB-INF/validator-rules-c
ustom.xml
[echo] Modifying Eclipse .classpath file
[echo] Refactoring build.xml
[echo] ----------------------------------------------
[echo] NOTE: It's recommended you delete extras/webwork as you shouldn't ne
ed it anymore.
[echo] ----------------------------------------------
[echo] Repackaging info written to rename.log
[echo]
[echo] +-------------------------------------------------------------+
[echo] | -- Application created successfully! -- |
[echo] | |
[echo] | Now you should be able to cd to your application and run: |
[echo] | > ant setup test-all |
[echo] +-------------------------------------------------------------+

BUILD SUCCESSFUL
Total time: 15 seconds

为什么使用 WebWork?
Struts 社区最近在热情地拥抱 WebWork,这种结合导致为 Java 平台提供了一个非常优秀的新 Web 框架:Struts 2。当然,Spring MVC 是一个非常优秀的基于请求的框架,但是它不能像 Struts 2 一样支持 JSF。基于内容的框架(例如 JSF 和 Tapestry)也都很好,但是我发现 WebWork 更为直观,更容易使用(更多有关 Structs 2 和 JSF 的内容请参看 参考资料)。

在创建一个新项目之后,我们就得到了一个类似于图 2 所示的目录结构。Eclipse 和 Intellij IDEA 项目文件都是作为这个过程的一部分创建的。


图 2. 项目的目录结构
项目的目录结构

这个目录结构与 Sun 为 Java 2 Platform Enterprise Edition(J2EE)Web 应用程序推荐的目录结构非常类似。在 2.0 版本的 AppFuse 中,这个结构会变化成适合 Apache Maven 项目的标准目录结构(有关这两个目录介绍的内容,请参看 参考资料 中的链接)。AppFuse 还会从 Ant 迁移到 Maven 2 上,从而获得相关下载的能力和对生成 IDE 项目文件的支持。目前基于 Ant 的系统要求提交者维护项目文件,而 Maven 2 可以通过简单地使用项目的 pom.xml 文件生成 IDEA、Eclipse 和 NetBeans 项目文件。(这个文件位于您项目的根目录中,是使用 Maven 构建应用程序所需要的主要组件)。它与利用 Ant 所使用的 build.xml 文件非常类似。)

现在我们对 AppFuse 是什么已经有一点概念了,在本文剩下的部分中,我们将介绍使用 AppFuse 的 7 点理由。即使您选择不使用 AppFuse 来开始自己的项目,也会看到 AppFuse 可以为您提供很多样板代码,这些代码可以在基于 Java 语言的 Web 应用程序中使用。由于它是基于 Apache 许可证的,因此非常欢迎您在自己的应用程序中重用这些代码。

理由 1:测试

测试是在软件开发项目中很少被给予足够信任的一个环节。注意我并不是说在软件开发的一些刊物中没有得到足够的信任!很多文章和案例研究都给出了测试 优先的开发方式和足够的测试覆盖面以提高软件的质量。然而,测试通常都被看作是一件只会延长项目开发时间的事情。实际上,如果我们使用测试优先的方法在编 写代码之前就开始撰写测试用例,我相信我们可以发现这实际上会加速 开发速度。另外,测试优先也可以使维护和重用更加 容易。如果我们不编写代码来测试自己的代码,那么就需要手工对应用程序进行测试 —— 这通常效率都不高。自动化才是关键。

当我们首次开始使用 AppFuse 时,我们可能需要阅读这个项目 Web 站点上提供的快速入门指南和教程(请参看 参考资料 中的链接)。这些教程的编写就是为了您可以首先编写测试用例;它们直到编写接口和/或实现之后才能编译。如果您有些方面与我一样,就会在开始编写代码之前 就已经编写好测试用例了;这是真正可以加速编写代码的惟一方式。如果您首先编写了代码的实现,通过某种方式验证它可以工作,那么您可能会对自己说,“哦, 看起来不错 —— 谁需要测试呢?我还有更多的代码需要编写!”这种情况不幸的一面是您通常都会做一些事情 来测试自己的代码;您简单地跳过了可以自动化进行测试的地方。

AppFuse 的文档展示了如何测试应用程序的所有 层次。它从数据库层开始入手,使用了 DbUnit(请参看 参考资料)在运行测试之前提前使用数据来填充自己的数据库。在数据访问(DAO)层,它使用了 Spring 的 AbstractTransactionalDataSourceSpringContextTests 类(是的,这的确是一个类的名字!)来允许简单地加载 Spring 上下文文件。另外,这个类对每个 testXXX() 方法封装了一个事务,并当测试方法存在时进行回滚。这种特性使得测试 DAO 逻辑变得非常简单,并且不会对数据库中的数据造成影响。

在服务层,jMock (请参看 参考资料)用来编写那些可以消除 DAO 依赖的真正 单元测试。这允许进行验证业务逻辑正确的快速测试;我们不用担心底层的持久性逻辑。

HtmlUnit 支持
HtmlUnit 团队在 1.8 发行版中已经完成了相当多的工作来确保包可以与流行的 Ajax 框架(Prototype 和 Scriptaculous)很好地工作。

在 Web 层,测试会验证操作(Struts/WebWork)、控件(Spring MVC)、页面(Tapestry)和管理 bean(JSF)如我们所期望的一样进行工作。Spring 的 spring-mock.jar 可以非常有用地用来测试所有这些框架,因为它包含了一个 Servlet API 的仿真实现。如果没有这个有用的库,那么测试 AppFuse 的 Web 框架就会变得非常困难。

UI 通常是开发 Web 应用程序过程中最为困难的一部分。它也是顾客最经常抱怨的地方 —— 这既是由于它并不是非常完美,也是由于它的工作方式与我们期望的并不一样。另外,没有什么会比在客户面前作演示的过程中看到看到异常堆栈更糟糕的了!您的 应用程序可能会非常可怕,但是客户可能会要求您做到十分完美。永远不要让这种事情发生。Canoo WebTest 可以对 UI 进行测试。它使用了 HtmlUnit 来遍历测试 UI,验证所有的元素都存在,并可以填充表单的域,甚至可以验证一个假想的启用 Ajax 的 UI 与我们预期的工作方式一样。(有关 WebTest 和 HTMLUnit 的链接请参看 参考资料。)

为了进一步简化 Web 的测试,Cargo(请参看 参考资料)对 Tomcat 的启动和停止(分别在运行 WebTest 测试之前和之后)进行了自动化。





理由 2:集成

正如我在本文简介中提到的一样,很多开放源码库都已经预先集成到 AppFuse 中了。它们可以分为以下几类:

  • 编译、报告和代码生成:Ant、Ant Contrib Tasks、Checkstyle、EMMA、Java2Html、PMD 和 Rename Packages
  • 测试框架:DbUnit、Dumbster、jMock、JUnit 和 Canoo WebTest
  • 数据库驱动程序:MySQL 和 PostgreSQL
  • 持久性框架:Hibernate 和 iBATIS
  • IoC 框架:Spring
  • Web 框架:JSF、Spring MVC、Struts、Tapestry 和 WebWork
  • Web 服务:XFire
  • Web 工具:Clickstream、Display Tag、DWR、JSTL、SiteMesh、Struts Menu 和 URL Rewrite Filter
  • Security:Acegi Security
  • JavaScript 和 CSS:Scriptaculous、Prototype 和 Mike Stenhouse 的 CSS Framework

除了这些库之外,AppFuse 还使用 Log4j 来记录日志,使用 Velocity 来构建 e-mail 和菜单模板。Tomcat 可以支持最新的开发,我们可以使用 1.4 或 5 版本的 Java 平台来编译或构建程序。我们应该可以将 AppFuse 部署到任何 J2EE 1.3 兼容的应用服务器上;这已经经过了测试,我们知道它在所有主要版本的 J2EE 服务器和所有主要的 servlet 容器上都可以很好地工作。

图 3 给出了上面创建的 devworks 项目的 lib 目录。这个目录中的 lib.properties 文件控制了每个依赖性的版本号,这意味着我们可以简单地通过把这些包的新版本放到这个目录中并执行诸如 ant test-all -Dspring.version=2.0 之类的命令来测试这些包的新版本。


图 3. 项目依赖性
AppFuse 项目依赖性

预先集成这些开放源码库可以在项目之初极大地提高生产效率。尽管我们可以找到很多文档介绍如何集成这些库,但是定制工作示例并简单地使用它来开发应用程序要更加简单。

除了可以简化 Web 应用程序的开发之外,AppFuse 让我们还可以将 Web 服务简单地集成到自己的项目中。尽管 XFire 也在 AppFuse 下载中一起提供了,不过如果我们希望,也可以自己集成 Apache Axis(请参看 参考资料 中有关 Axis 集成的教程)。另外,Spring 框架和 XFire 可以一起将服务层作为 Web 服务非常简单地呈现出来,这就为我们提供了开发面向服务架构的能力。

另外,AppFuse 并不会将我们限定到任何特定的 API 上。它只是简单地对可用的最佳开放源码解决方案重新进行打包和预先集成。AppFuse 中的代码可以处理这种集成,并实现了 AppFuse 的基本安全性和可用性特性。只要可能,就会减少代码,以便向 AppFuse 的依赖框架添加一个特性。例如,AppFuse 自带的 Remember Me 和 SSL 切换特性最近就因为类似的特性而从 Acegi Security 中删除了。







理由 3:自动化

Ant 使得简化了从编译到构建再到部署的自动化过程。Ant 是 AppFuse 中的一等公民,这主要是因为我发现在命令行中执行操作比从 IDE 中更加简单。我们可以使用 Ant 实现编译、测试、部署和执行任何代码生成的任务。

尽管这种能力对于有些人来说非常重要,但是它并不适用于所有的人。很多 AppFuse 用户目前都使用 Eclipse 或 Intellij IDEA 来构建和测试自己的项目。在这些 IDE 中运行 Ant 的确可以工作,但是这样做的效率通常都不如使用 IDE 内置的 JUnit 支持来运行测试效率高。

幸运的是,AppFuse 支持在 IDE 中运行测试,不过管理这种特性对于 AppFuse 开发人员来说就变得非常困难了。最大的痛苦在于 XDoclet 用来生成 Hibernate 映射文件和 Web 框架所使用的一些工件(例如 ActionForms 和 Struts 使用的 struts-config.xml)。IDE 并不知道需要生成的代码,除非我们配置使用 Ant 来编译它们,或者安装了一些可以认识 XDoclet 的插件。

这种对知识的缺乏是 AppFuse 2.0 切换到 JDK 5 和 Maven 2 上的主要原因。JDK 5、注释和 Struts 2 将让我们可以摆脱 XDoclet。Maven 2 将使用这些生成的文件和动态类路径来生成 IDE 项目文件,这样对项目的管理就可以进行简化。目前基于 Ant 的编译系统已经为不同的层次生成了一些工件(包括 dao.jar、service.jar 和 webapp.war),因此切换到 Maven 的模型上应该是一个非常自然的调整。

除了 Ant 之外(它对于编译、测试、部署和报告具有丰富的支持),对于 CruiseControl 的支持也构建到了 AppFuse 中。CruiseControl 是一个 Continuous Integration 应用程序,让我们可以在源代码仓库中代码发生变化时自动运行所有的测试。extras/cruisecontrol 目录包含了我们为基于 AppFuse 的项目快速、简单地设置 Continuous Integration 时所需要的文件。

设置 Continuous Integration 是软件开发周期中我们首先要做的事情之一。它不但激发程序员去编写测试用例,而且还通过 “You broke the build!” 游戏促进了团队之间的合作和融合。







理由 4:安全特性和可扩展性

AppFuse 最初是作为我为 Apress 编写的书籍 Pro JSP 中示例应用程序的一部分开发的。这个示例应用程序展示了很多安全特性和用于简化 Struts 开发的特性。这个应用程序中的很多安全特性在 J2EE 的安全框图中都不存在。使用容器管理认证(CMA)的认证方法非常简单,但是 Remember Me、密码提示、SSL 切换、登记和用户管理等功能却都不存在。另外,基于角色的保护方法功能在非 EJB 环境中也是不可能的。

最初,AppFuse 使用自己的代码和用于 CMA 的解决方案完全实现了这些特性。我在 2004 年年初开始学习 Spring 时就听说过有关 Acegi Security 的知识。我对 Acegi 所需要的 XML 的行数(175)与 web.xml 中所需要的 CMA 的行数(20)进行了比较,很快就决定丢弃 Acegi 了,因为它太过复杂了。

一年半之后 —— 在为另外一本书 Spring Live 中编写了一章有关使用 Acegi Security 的内容之后 —— 我就改变了自己的想法。Acegi 的确(目 前仍然)需要很多 XML,但是一旦我们理解了这一点,它实际上是相当简单的。当我们最终作出改变,使用 Acegi Security 的特性来全部取代 AppFuse 的特性之后,我们最终删除了大量的代码。类之上的类都已经没有了,“Acegi handles that now” 中消失的部分现在全部进入了 CVS 的 Attic 中了。

Acegi Security 是 J2EE 安全模型中曾经出现过的最好模型。它让我们可以实现很多有用的特性,这些特性在 Servlet API 的安全模型中都不存在:认证、授权、角色保护方法、Remember Me、密码加密、SSL 切换、用户切换和注销。它让我们还可以将用户证书存储到 XML 文件、数据库、LDAP 或单点登录系统(例如 Yale 的 Central Authentication Service (CAS) 或者 SiteMinder)中。

AppFuse 对很多与安全性相关的特性的实现从一开始都是非常优秀的。现在 AppFuse 使用了 Acegi Security,这些特性 —— 以及更多特性 —— 都非常容易实现。Acegi 有很多地方都可以进行扩充:这是它使用巨大的 XML 配置文件的原因。正如我们已经通过去年的课程对 Acegi 进行集成一样,我们已经发现对很多 bean 的定义进行定制可以更加紧密地与 AppFuse 建立联系。

Spring IoC 容器和 Acegi Security 所提供的简单开发、容易测试的代码和松耦合特性的组合是 AppFuse 是这么好的一种开发平台的主要原因。这些框架都是不可插入的,允许生成干净的可测试代码。AppFuse 集成了很多开放源码项目,依赖注入允许对应用程序层进行简单的集成。







理由 5:使用 AppGen 生成代码

有些人会将代码生成称为代码气味的散播(code smell)。在他们的观点中,如果我们需要生成代码,那么很可能就会做一些错事。我倾向于这种确定自己代码使用的模式和自动化生成代码的能力应该称为代码香味的弥漫(code perfume)。如果我们正在编写类似的 DAO、管理器、操作或控件,并且不想为它们生成代码,那么这就需要根据代码的气味来生成代码。当然,当语言可以为我们提供可以简化任务的特性时,一切都是那么美好;不过代码生成通常都是一个必需 —— 通常其生产率也非常高 —— 的任务。

AppFuse 中提供了一个基于 Ant 和 XDoclet 的代码生成工具,名叫 AppGen。默认情况下,常见的 DAO 和管理器都可以允许我们对任何普通老式 Java 对象(POJO)进行 CRUD 操作,但是在 Web 层上这样做有些困难。AppGen 有几个特性可以用来执行以下任务:

  • (使用 Middlegen 和 Hibernate 工具)从数据库表中生成 POJO
  • 从 POJO 生成 UI
  • 为 DAO、管理器、操作/控制器和 UI 生成测试

在运行 AppGen 时,您会看到提示说 AppGen 要从数据库表或 POJO 中生成代码。如果在命令行中执行 ant install-detailed,AppGen 就会安装 POJO 特定的 DAO、管理器以及它们的测试。运行 ant install 会导致 Web 层的类重用通用的 DAO 和默认存在的管理器。

为了阐述 AppGen 是如何工作的,我们在 devworks MySQL 数据库中创建了如清单 2 所示的表:


清单 2. 创建一个名为 cat 的数据库表
    create table cat (
cat_id int(8) auto_increment,
color varchar(20) not null,
name varchar(20) not null,
created_date datetime not null,
primary key (cat_id)
) type=InnoDB;

在 extras/appgen 目录中,运行 ant install-detailed。这个命令的输出结果对于本文来说实在太长了,不过我们在清单 3 中给出了第一部分的内容:


清单 3. 运行 AppGen 的 install-detailed 目标
$ ant install-detailed
Buildfile: build.xml

init:
[mkdir] Created dir: /Users/mraible/Work/devworks/extras/appgen/build
[echo]
[echo] +-------------------------------------------------------+
[echo] | -- Welcome to the AppGen! -- |
[echo] | |
[echo] | Use the "install" target to use the generic DAO and |
[echo] | Manager, or use "install-detailed" to general a DAO |
[echo] | and Manager specifically for your model object. |
[echo] +-------------------------------------------------------+

[input] Would you like to generate code from a table or POJO? (table,pojo)
table
[input] What is the name of your table (i.e. person)?
cat
[input] What is the name, if any, of the module for your table (i.e. organization)?

[echo] Running Middlegen to generate POJO...

要对 cat 表使用这个新生成的代码,我们需要修改 src/dao/com/ibm/dao/hibernate/applicationContext-hibernate.xml,来为 Hibernate 添加 Cat.hbm.xml 映射文件。清单 3 给出了我们修改后的 sessionFactory bean 的样子:


清单 4. 将 Cat.hbm.xml 添加到 sessionFactory bean 中
    <bean id="sessionFactory" class="...">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
<value>com/ibm/model/Role.hbm.xml</value>
<value>com/ibm/model/User.hbm.xml</value>
<value>com/ibm/model/Cat.hbm.xml</value>
</list>
</property>
...
</bean>

在运行 ant setup deploy 之后,我们就应该可以在部署的应用程序中对 cat 表执行 CRUD 操作了:


图 4. Cat 列表
所生成的主屏幕

图 5. Cat 表单
所生成的详细屏幕

我们在上面的屏幕快照中看到的记录都是作为代码生成的一部分创建的,因此现在就有测试数据了。








理由 6:文档

我们可以找到 AppFuse 各个风味的教程,并且它们都以 6 种不同的语言给出了:中文、德语、英语、韩语、葡萄牙语和西班牙语。使用风味(flavor) 一词,我的意思是不同框架的组合,例如 Spring MVC 加上 iBATIS、Spring MVC 加上 Hibernate 或 JSF 加上 Hibernate。使用这 5 种 Web 框架和两种持久框架,可以有好几种组合。有关它们的翻译,AppFuse 为自己的默认特性提供了 8 种翻译。可用语言包括中文、荷兰语、德语、英语、法语、意大利语、葡萄牙语和西班牙语。

除了核心教程之外,还添加了很多教程(请参看 参考资料) 来介绍与各种数据库、应用服务器和其他开放源码技术(包括 JasperReports、Lucene、Eclipse、Drools、Axis 和 DWR)的集成。





理由 7:社区

Apache 软件基金会对于开放源码有一个有趣的看法。它对围绕开放源码项目开发一个开放源码社区最感兴趣。它的成员相信如果社区非常强大,那么产生高质量的代码就是一个自然的过程。下面的内容引自 Apache 主页:

“我们认为自己不仅仅是一组共享服务器的项目,而且是一个开发人员和用户的社区。”

AppFuse 社区从 2003 年作为 SourceForge 上的一个项目(是 struts.sf.net 的一部分)启动以来,已经获得了极大的增长。通过在 2004 年 3 月转换到 java.net 上之后,它已经成为这里一个非常流行的项目,从 2005 年 1 月到 3 月成为访问量最多的一个项目。目前它仍然是一个非常流行的项目(有关 java.net 项目统计信息的链接,请参看 参考资料),不过在这个站点上它正在让位于 Sun 赞助的很多项目。

在 2004 年年末,Nathan Anderson 成为继我之后第一个提交者。此后有很多人都加入了进来,包括 Ben Gill、David Carter、Mika G?ckel、Sanjiv Jivan 和 Thomas Gaudin。很多现有的提交者都已经通过各种方式作出了自己的贡献,他们都帮助 AppFuse 社区成为一个迅速变化并且非常有趣的地方。

邮件列表非常友好,我们试图维护这样一条承诺 “没有问题是没有人理会的问题”。我们的邮件列表归档文件中惟一一条 “RTFM” 是从用户那里发出的,而不是从开发者那里发出的。我们绝对信奉 Apache 开放源码社区的哲学。引用我最好的朋友 Bruce Snyder 的一句话,“我们为代码而来,为人们而留下”。目前,大部分开发者都是用户,我们通常都喜欢有一段美妙的时间。另外,大部分文档都是由社区编写的;因此, 这个社区的知识是非常渊博的。







结束语

我们应该尝试使用 AppFuse 进行开发,这是因为它允许我们简单地进行测试、集成、自动化,并可以安全地生成 Web 应用程序。其文档非常丰富,社区也非常友好。随着其支撑框架越来越好,AppFuse 也将不断改进。

从 AppFuse 2.0 开始,我们计划迁移到 JDK 5(仍然支持部署到 1.4)和 Maven 2 上去。这些工具可以简化使用 AppFuse 的开发、安装和升级。我们计划充分利用 Maven 2 的功能来处理相关依赖性。我们将碰到诸如 appfuse-hibernate-2.0.jar 和 appfuse-jsf-2.0.jar 之类的工件。这些工件都可以在 pom.xml 文件中进行引用,它们负责提取其他相关依赖性。除了在自己的项目中使用 AppFuse 基类之外,我们还可以像普通的框架一样在 JAR 中对这些类简单地进行扩展,这应该会大大简化它的升级过程,并鼓励更多用户将自己希望的改进提交到这个项目中。

如果没有其他问题,使用 AppFuse 可以让您始终处于 Java Web 开发的技术前沿上 —— 就像我们一样!







参考资料

学习

获得产品和技术
  • AppFuse on java.net:下载不同风味的 AppFuse。

  • WebWork:了解这个易于使用的 Web 框架。

  • DbUnit:查看更多有关 JUnit 扩展的内容。

  • jMock:创建动态仿真对象来简化真正的单元测试。

  • Canoo WebTest:自动化 Web 应用程序的 UI 测试。

  • HtmlUnit:WebTest 的优秀 JavaScript 支持背后的基础。

  • Cargo:自动启动和停止容器。

  • Greenbox:一种代码生成框架。


讨论






关于作者

Matt Raible 居住在美国科罗拉多州的丹佛,他在那里是 Spring 和 Web 框架对 Virtuas Open Source Solutions 的实践先驱。他在开放源码领域具有丰富的经验,是这个领域的专家。他在这个领域中既是用户,又是一名开发人员。Matt 是 SourceBeat PublishingSpring Live 的作者。他还为 Apress 的书籍 Pro JSP Third Edition 作出了很大的贡献。他是很多开放源码会议的积极倡导者,包括 ApacheCon、MySQL User's Conference 和 OSCON,同时他还是 http://raibledesigns.com 上一名非常活跃的博客。Raible 的大部分生活都被计算机所包围了,尽管他是在连电都没有的 Montana 长大的。当不工作的时候时,他总是试图让妻子 Julie 成为世界上最幸福的女人,或者与他们的孩子 Abbie 和 Jack 一起玩耍。

posted @ 2006-09-04 20:34 Alex 阅读(580) | 评论 (0)编辑 收藏

外观模式(Facade pattern)涉及到子系统的一些类。所谓子系统,是为提供一系列相关的特征(功能)而紧密关联的一组类。例如,一个Account类、Address类和CreditCard类相互关联,成为子系统的一部分,提供在线客户的特征。

   在真实的应用系统中,一个子系统可能由很多类组成。子系统的客户为了它们的需要,需要和子系统中的一些类进行交互。客户和子系统的类进行直接的交互会导 致客户端对象和子系统(Figure1)之间高度耦合。任何的类似于对子系统中类的接口的修改,会对依赖于它的所有的客户类造成影响。

  
Figure1: Client Interaction with Subsystem Classes before Applying the Facade Pattern

  外观模式(Facade pattern)很适用于在上述情况。外观模式(Facade pattern)为子系统提供了一个更高层次、更简单的接口,从而降低了子系统的复杂度和依赖。这使得子系统更易于使用和管理。

  外观是一个能为子系统和客户提供简单接口的类。当正确的应用外观,客户不再直接和子系统中的类交互,而是与外观交互。外观承担与子系统中类交互的责任。实际上,外观是子系统与客户的接口,这样外观模式降低了子系统和客户的耦合度(Figure2).

  
Figure2: Client Interaction with Subsystem Classes after Applying the Facade Pattern

  从Figure2中我们可以看到:外观对象隔离了客户和子系统对象,从而降低了耦合度。当子系统中的类进行改变时,客户端不会像以前一样受到影响。

  尽管客户使用由外观提供的简单接口,但是当需要的时候,客户端还是可以视外观不存在,直接访问子系统中的底层次的接口。这种情况下,它们之间的依赖/耦合度和原来一样。

  例子:

  让我们建立一个应用:

  (1) 接受客户的详细资料(账户、地址和信用卡信息)

  (2) 验证输入的信息

  (3) 保存输入的信息到相应的文件中。

  这个应用有三个类:Account、Address和CreditCard。每一个类都有自己的验证和保存数据的方法。

  Listing1: AccountClass

public class Account {
 String firstName;
 String lastName;
 final String ACCOUNT_DATA_FILE = "AccountData.txt";
 public Account(String fname, String lname) {
  firstName = fname;
  lastName = lname;
 }
 public boolean isValid() {
  /*
  Let's go with simpler validation
  here to keep the example simpler.
  */
  …
  …
 }
 public boolean save() {
  FileUtil futil = new FileUtil();
  String dataLine = getLastName() + ”," + getFirstName();
  return futil.writeToFile(ACCOUNT_DATA_FILE, dataLine,true, true);
 }
 public String getFirstName() {
  return firstName;
 }
 public String getLastName() {
  return lastName;
 }
}

  Listing2: Address Class

public class Address {
 String address;
 String city;
 String state;
 final String ADDRESS_DATA_FILE = "Address.txt";
 public Address(String add, String cty, String st) {
  address = add;
  city = cty;
  state = st;
 }
 public boolean isValid() {
  /*
  The address validation algorithm
  could be complex in real-world
  applications.
  Let's go with simpler validation
  here to keep the example simpler.
  */
  if (getState().trim().length() < 2)
   return false;
  return true;
 }
 public boolean save() {
  FileUtil futil = new FileUtil();
  String dataLine = getAddress() + ”," + getCity() + ”," + getState();
  return futil.writeToFile(ADDRESS_DATA_FILE, dataLine,true, true);
 }
 public String getAddress() {
  return address;
 }
 public String getCity() {
  return city;
 }
 public String getState() {
  return state;
 }
}

  Listing3: CreditCard Class

public class CreditCard {
 String cardType;
 String cardNumber;
 String cardExpDate;
 final String CC_DATA_FILE = "CC.txt";
 public CreditCard(String ccType, String ccNumber,
 String ccExpDate) {
  cardType = ccType;
  cardNumber = ccNumber;
  cardExpDate = ccExpDate;
 }
 public boolean isValid() {
  /*
  Let's go with simpler validation
  here to keep the example simpler.
  */
  if (getCardType().equals(AccountManager.VISA)) {
   return (getCardNumber().trim().length() == 16);
  }
  if (getCardType().equals(AccountManager.DISCOVER)) {
   return (getCardNumber().trim().length() == 15);
  }
  if (getCardType().equals(AccountManager.MASTER)) {
   return (getCardNumber().trim().length() == 16);
  }
  return false;
 }
 public boolean save() {
  FileUtil futil = new FileUtil();
  String dataLine = getCardType() + ,”" + getCardNumber() + ”," + getCardExpDate();
  return futil.writeToFile(CC_DATA_FILE, dataLine, true, true);
 }
 public String getCardType() {
  return cardType;
 }
 public String getCardNumber() {
  return cardNumber;
 }
 public String getCardExpDate() {
  return cardExpDate;
 }
}

  
Figure3: Subsystem Classes to Provide the Necessary Functionality to Validate and Save the Customer Data



让我们建立一个客户AccountManager,它提供用户输入数据的用户界面。

  Listing4: Client AccountManager Class

public class AccountManager extends JFrame {
 public static final String newline = "\n";
 public static final String VALIDATE_SAVE = "Validate & Save";
 …
 …
 public AccountManager() {
  super(" Facade Pattern - Example ");
  cmbCardType = new JComboBox();
  cmbCardType.addItem(AccountManager.VISA);
  cmbCardType.addItem(AccountManager.MASTER);
  cmbCardType.addItem(AccountManager.DISCOVER);
  …
  …
  //Create buttons
  JButton validateSaveButton = new JButton(AccountManager.VALIDATE_SAVE);
  …
  …
 }
 public String getFirstName() {
  return txtFirstName.getText();
 }
 …
 …
}//End of class AccountManager

  当客户AccountManage运行的时候,展示的用户接口如下:

  
Figure4: User Interface to Enter the Customer Data

  为了验证和保存输入的数据,客户AccountManager需要:

  (1) 建立Account、Address和CreditCard对象。

  (2) 用这些对象验证输入的数据

  (3) 用这些对象保存输入的数据。

  下面是对象间的交互顺序图:

 
Figure5: How a Client Would Normally Interact (Directly) with Subsystem Classes to Validate and Save the Customer Data

   在这个例子中应用外观模式是一个很好的设计,它可以降低客户和子系统组件(Address、Account和CreditCard)之间的耦合度。应用 外观模式,让我们定义一个外观类CustomerFacade (Figure6 and Listing5)。它为由客户数据处理类(Address、Account和CreditCard)所组成的子系统提供一个高层次的、简单的接口。

CustomerFacade
address:String
city:String
state:String
cardType:String
cardNumber:String
cardExpDate:String
fname:String
lname:String
setAddress(inAddress:String)
setCity(inCity:String)
setState(inState:String)
setCardType(inCardType:String)
setCardNumber(inCardNumber:String)
setCardExpDate(inCardExpDate:String)
setFName(inFName:String)
setLName(inLName:String)
saveCustomerData()

  
Figure6: Facade Class to Be Used by the Client in the Revised Design

  Listing5: CustomerFacade Class

public class CustomerFacade {
 private String address;
 private String city;
 private String state;
 private String cardType;
 private String cardNumber;
 private String cardExpDate;
 private String fname;
 private String lname;
 public void setAddress(String inAddress) {
  address = inAddress;
 }
 public void setCity(String inCity) {
  city = inCity;
 }
 public void setState(String inState) {
  state = inState;
 }
 public void setFName(String inFName) {
  fname = inFName;
 }
 public void setLName(String inLName) {
  lname = inLName;
 }
 public void setCardType(String inCardType) {
  cardType = inCardType;
 }
 public void setCardNumber(String inCardNumber) {
  cardNumber = inCardNumber;
 }
 public void setCardExpDate(String inCardExpDate) {
  cardExpDate = inCardExpDate;
 }
 public boolean saveCustomerData() {
  Address objAddress;
  Account objAccount;
  CreditCard objCreditCard;
  /*
   client is transparent from the following
   set of subsystem related operations.
  */
  boolean validData = true;
  String errorMessage = "";
  objAccount = new Account(fname, lname);
  if (objAccount.isValid() == false) {
   validData = false;
   errorMessage = "Invalid FirstName/LastName";
  }
  objAddress = new Address(address, city, state);
  if (objAddress.isValid() == false) {
   validData = false;
   errorMessage = "Invalid Address/City/State";
  }
  objCreditCard = new CreditCard(cardType, cardNumber, cardExpDate);
  if (objCreditCard.isValid() == false) {
   validData = false;
   errorMessage = "Invalid CreditCard Info";
  }
  if (!validData) {
   System.out.println(errorMessage);
   return false;
  }
  if (objAddress.save() && objAccount.save() && objCreditCard.save()) {
   return true;
  } else {
   return false;
  }
 }
}

   CustomerFacade类以saveCustomData方法的形式提供了业务层次上的服务。客户AccountManager不是直接和子系统 的每一个组件交互,而是使用了由CustomFacade对象提供的验证和保存客户数据的更高层次、更简单的接口(Figure7).

 
Figure7: Class Association with the Fa?ade Class in Place 。

  在新的设计中,为了验证和保存客户数据,客户需要:

  (1) 建立或获得外观对象CustomFacade的一个实例。

  (2) 传递数据给CustomFacade实例进行验证和保存。

  (3) 调用CustomFacade实例上的saveCustomData方法。

  CustomFacade处理创建子系统中必要的对象并且调用这些对象上相应的验证、保存客户数据的方法这些细节问题。客户不再需要直接访问任何的子系统中的对象。

  Figure8展示了新的设计的消息流图:

 
Figure 22.8: In the Revised Design, Clients Interact with the Fa?ade Instance to Interface with the Subsystem

  重要提示

  下面是应用外观模式的注意事项:

  (1) 在设计外观时,不需要增加额外的功能。

  (2) 不要从外观方法中返回子系统中的组件给客户。例如:有一个下面的方法:

  CreditCard getCreditCard()

  会报漏子系统的细节给客户。应用就不能从应用外观模式中取得最大的好处。

  (3)应用外观的目的是提供一个高层次的接口。因此,外观方法最适合提供特定的高层次的业务服务,而不是进行底层次的单独的业务执行。
posted @ 2006-09-03 00:07 Alex 阅读(477) | 评论 (0)编辑 收藏

小结:适配器模式用插座的适配器最为形象,插头是2口的,插座是3口的,中间的适配器就是同时支持2口和三口的。从对象的角度就是一般继承一个实现一个,总之,前方百计把两者都关联起来 。


通常,客户类(clients of class)通过类的接口访问它提供的服务。有时,现有的类(existing class)可以提供客户类的功能 需要,但是它所提供的接口不一定是客户类所期望的。这是由于现有的接口太详细或者缺乏详细或接口的名称与客户类所查找的不同等诸多不同原因导致的。

  在这种情况下,现有的接口需要转化(convert) 为客户类期望的接口,这样保证了对现有类的重用。如果不进行这样的转化,客户类就不能利用现有类所提供的功能。适配器模式 (Adapter Pattern)可以完成这样的转化。适配器模式建议定义一个包装类,包装有不兼容接口的对象。这个包装类指的就是适配器 (Adapter),它包装的对象就是适配者(Adaptee)。适配器提供客户类需要的接口,适配器接口的实现是把客户类的请求转化为对适配者的相应接 口的调用。换句话说:当客户类调用适配器的方法时,在适配器类的内部调用适配者类的方法,这个过程对客户类是透明的,客户类并不直接访问适配者类。因此, 适配器可以使由于借口不兼容而不能交互的类可以一起工作(work together)。

  在上面讨论的接口:

  (1)    不是指在JAVA编程语言中接口的概念,虽然类的接口可以通过JAVA借扩来定义。

  (2)    不是指由窗体和GUI控件所组成的GUI应用程序的用户接口。

  (3)    而是指类所报漏的,被其他类调用的编程接口,

  类适配器(Class Adapter)VS对象适配器(Object Adapter)

  适配器总体上可以分为两类??类适配器(Class Adapter)VS对象适配器(Object Adapter)
    

 类适配器:


  类适配器是通过继承类适配者类(Adaptee Class)实现的,另外类适配器实现客户类所需要的接口。当客户对象调用适配器类方法的时候,适配器内部调用它所继承的适配者的方法。
    

 对象适配器:

  对象适配器包含一个适配器者的引用(reference),与类适配器相同,对象适配器也实现了客户类需要的接口。当客户对象调用对象适配器的方法的时候,对象适配器调它所包含的适配器者实例的适当方法。

  下表是类适配器(Class Adapter)和对象适配器(Object Adapter)的详细不同

  
    
  例子:

  让我们建立一个验证给定客户地址的应用。这个应用是作为大的客户数据管理应用的一部分。

  让我们定义一个Customer类:

Customer 



Figure 20.1: Customer Class 
Listing 20.1: Customer Class 
  1. class Customer { 
  2.   public static final String US = "US"
  3.   public static final String CANADA = "Canada"
  4.   private String address; 
  5.   private String name; 
  6.   private String zip, state, type; 
  7.   public boolean isValidAddress() { 
  8.           … 
  9.           … 
  10.   } 
  11.   public Customer(String inp_name, String inp_address, 
  12.                   String inp_zip, String inp_state, 
  13.                   String inp_type) { 
  14.     name = inp_name; 
  15.     address = inp_address; 
  16.     zip = inp_zip; 
  17.     state = inp_state; 
  18.     type = inp_type; 
  19.   } 
  20. }//end of class 
   不同的客户对象创建Customer对象并调用(invoke)isValidAddress方法验证客户地址的有效性。为了验证客户地址的有效性, Customer类期望利用一个地址验证类(address validator class),这个验证类提供了在接口 AddressValidator中声明的接口。

  Listing 20.2: AddressValidator as an Interface 
  1. public interface AddressValidator { 
  2.   public boolean isValidAddress(String inp_address, 
  3.      String inp_zip, String inp_state); 
  4. }//end of class 

  让我们定义一个USAddress的验证类,来验证给定的U.S地址。

  Listing 20.3: USAddress Class 
  1. class USAddress implements AddressValidator { 
  2.   public boolean isValidAddress(String inp_address, 
  3.      String inp_zip, String inp_state) { 
  4.    if (inp_address.trim().length() < 10) 
  5.      return false
  6.    if (inp_zip.trim().length() < 5) 
  7.      return false
  8.    if (inp_zip.trim().length() > 10) 
  9.      return false
  10.    if (inp_state.trim().length() != 2) 
  11.      return false
  12.    return true
  13.   } 
  14. }//end of class 

  USAddress类实现AddressValidator接口,因此Customer对象使用USAddress实例作为验证客户地址过程的一部分是没有任何问题的。

  Listing 20.4: Customer Class Using the USAddress Class 
  1. class Customer { 
  2.           … 
  3.           … 
  4.  public boolean isValidAddress() { 
  5.    //get an appropriate address validator 
  6.    AddressValidator validator = getValidator(type); 
  7.    //Polymorphic call to validate the address 
  8.    return validator.isValidAddress(address, zip, state); 
  9.  } 
  10.  private AddressValidator getValidator(String custType) { 
  11.    AddressValidator validator = null
  12.    if (custType.equals(Customer.US)) { 
  13.      validator = new USAddress(); 
  14.    } 
  15.    return validator; 
  16.  } 
  17. }//end of class 
 


Figure 20.2: Customer/USAddress Validator?Class Association 

  但是当验证来自加拿大的客户时,就要对应用进行改进。这需要一个验证加拿大客户地址的验证类。让我们假设已经存在一个用来验证加拿大客户地址的使用工具类CAAddress。

从下面的CAAdress类的实现,可以发现CAAdress提供了客户类Customer类所需要的验证服务。但是它所提供的接口不用于客户类Customer所期望的。

  Listing 20.5: CAAdress Class with Incompatible Interface 
  1. class CAAddress { 
  2.   public boolean isValidCanadianAddr(String inp_address, 
  3.      String inp_pcode, String inp_prvnc) { 
  4.    if (inp_address.trim().length() < 15) 
  5.      return false
  6.    if (inp_pcode.trim().length() != 6) 
  7.      return false
  8.    if (inp_prvnc.trim().length() < 6) 
  9.      return false
  10.    return true
  11.   } 
  12. }//end of class 

  CAAdress类提供了一个isValidCanadianAddr方法,但是Customer期望一个声明在AddressValidator接口中的isValidAddress方法。

  接口的不兼容使得Customer对象利用现有的CAAdress类是困难的。一种意见是改变CAAdress类的接口,但是可能会有其他的应用正在使用CAAdress类的这种形式。改变CAAdress类接口会影响现在使用CAAdress类的客户。

  应用适配器模式,类适配器CAAdressAdapter可以继承CAAdress类实现AddressValidator接口。

 


  Figure 20.3: Class Adapter for the CAAddress Class 
Listing 20.6: CAAddressAdapter as a Class Adapter 
  1. public class CAAddressAdapter extends CAAddress 
  2.   implements AddressValidator { 
  3.   public boolean isValidAddress(String inp_address, 
  4.      String inp_zip, String inp_state) { 
  5.     return isValidCanadianAddr(inp_address, inp_zip, 
  6.            inp_state); 
  7.   } 
  8. }//end of class 

   因为适配器CAAdressAdapter实现了AddressValidator接口,客户端对象访问适配器CAAdressAdapter对象是没 有任何问题的。当客户对象调用适配器实例的isValidAddress方法的时候,适配器在内部把调用传递给它继承的 isValidCanadianAddr方法。

  在Customer类内部,getValidator私有方法需要扩展,以至于它可以 在验证加拿大客户的时候返回一个CAAdressAdapter实例。返回的对象是多态的,USAddress和CAAddressAdapter都实现 了AddressValidator接口,所以不用改变。

Listing 20.7: Customer Class Using the CAAddressAdapter Class 
  1. class Customer { 
  2.           … 
  3.           … 
  4.   public boolean isValidAddress() { 
  5.     //get an appropriate address validator 
  6.     AddressValidator validator = getValidator(type); 
  7.     //Polymorphic call to validate the address 
  8.     return validator.isValidAddress(address, zip, state); 
  9.   } 
  10.   private AddressValidator getValidator(String custType) { 
  11.     AddressValidator validator = null
  12.     if (custType.equals(Customer.US)) { 
  13.       validator = new USAddress(); 
  14.     } 
  15.     if (type.equals(Customer.CANADA)) { 
  16.       validator = new CAAddressAdapter(); 
  17.     } 
  18.     return validator; 
  19.   } 
  20. }//end of class 
  CAAddressAdapter设计和对AddressValidator(声明期望的接口)对象的多态调用使Customer可以利用接口不兼容CAAddress类提供的服务。

 


  Figure 20.4: Address Validation Application?Using Class Adapter 

 


  Figure 20.5: Address Validation Message Flow?Using Class Adapter 

  作为对象适配器的地址适配器

   当讨论以类适配器来实现地址适配器时,我们说客户类期望的AddressValidator接口是Java接口形式。现在,让我们假设客户类期望 AddressValidator接口是抽象类而不是java接口。因为适配器CAAdapter必须提供抽象类AddressValidatro中声明 的接口,适配器必须是AddressValidator抽象类的子类、实现抽象方法。
  1. Listing 20.8: AddressValidator as an Abstract Class 
  2. public abstract class AddressValidator { 
  3.   public abstract boolean isValidAddress(String inp_address, 
  4.      String inp_zip, String inp_state); 
  5. }//end of class 
  6. Listing 20.9: CAAddressAdapter Class 
  7. class CAAddressAdapter extends AddressValidator { 
  8.           … 
  9.           … 
  10.   public CAAddressAdapter(CAAddress address) { 
  11.     objCAAddress = address; 
  12.   } 
  13.   public boolean isValidAddress(String inp_address, 
  14.      String inp_zip, String inp_state) { 
  15.           … 
  16.           … 
  17.   } 
  18. }//end of class 

  因为多继承在JAVA中不支持,现在适配器CAAddressAdapter不能继承现有的CAAddress类,它已经使用了唯一一次继承其他类的机会。

  应用对象适配器模式,CAAddressAdapter可以包含一个适配者CAAddress的一个实例。当适配器第一次创建的时候,这个适配者的实例通过客户端传递给适配器。通常,适配者实例可以通过下面两种方式提供给包装它的适配器。

  (1)    对象适配器的客户端可以传递一个适配者的实例给适配器。这种方式在选择类的形式上有很大的灵活性,但是客户端感知了适配者或者适配过程。这种方法在适配器不但需要适配者对象行为而且需要特定状态时很适合。

  (2)    适配器可以自己创建适配者实例。这种方法相对来说缺乏灵活性。适用于适配器只需要适配者对象的行为而不需要适配者对象的特定状态的情况。

 


  Figure 20.6: Object Adapter for the CAAddress Class 

  Listing 20.10: CAAddressAdapter as an Object Adapter 
  1. class CAAddressAdapter extends AddressValidator { 
  2.   private CAAddress objCAAddress; 
  3.   public CAAddressAdapter(CAAddress address) { 
  4.     objCAAddress = address; 
  5.   } 
  6.   public boolean isValidAddress(String inp_address, 
  7.      String inp_zip, String inp_state) { 
  8.     return objCAAddress.isValidCanadianAddr(inp_address, 
  9.            inp_zip, inp_state); 
  10.   } 
  11. }//end of class 

  当客户对象调用CAAddressAdapter(adapter)上的isValidAddress方法时, 适配器在内部调用CAAddress(adaptee)上的isValidCanadianAddr方法。


 


  Figure 20.7: Address Validation Application?Using Object Adapter 

  从这个例子可以看出,适配器可以使Customer(client)类访问借口不兼容的CAAddress(adaptee)所提供的服务!

 



  Figure 20.8: Address Validation Message Flow?Using Object Adapter
posted @ 2006-09-03 00:02 Alex 阅读(1031) | 评论 (0)编辑 收藏

Template模板模式定义:
定义一个操作中算法的骨架,将一些步骤的执行延迟到其子类中.

使用Java的抽象类时,就经常会使用到Template模式,因此Template模式使用很普遍.而且很容易理解和使用。

 

public abstract class Benchmark
{
  /**
  * 下面操作是我们希望在子类中完成
  */
  public abstract void benchmark();

  /**
  * 重复执行benchmark次数
  */
  public final long repeat (int count) {
    if (count <= 0)
      return 0;
    else {
      long startTime = System.currentTimeMillis();

    for (int i = 0; i < count; i++)
      benchmark();

    long stopTime = System.currentTimeMillis();
    return stopTime - startTime;
  }
}
}

在上例中,我们希望重复执行benchmark()操作,但是对benchmark()的具体内容没有说明,而是延迟到其子类中描述:

public class MethodBenchmark extends Benchmark
{
  /**
  * 真正定义benchmark内容
  */
  public void benchmark() {

    for (int i = 0; i < Integer.MAX_VALUE; i++){
      System.out.printtln("i="+i);    
    }
  }
}

至此,Template模式已经完成,是不是很简单?

我们称repeat方法为模板方法, 它其中的benchmark()实现被延迟到子类MethodBenchmark中实现了,

看看如何使用:

Benchmark operation = new MethodBenchmark();
long duration = operation.repeat(Integer.parseInt(args[0].trim()));
System.out.println("The operation took " + duration + " milliseconds");

 

也许你以前还疑惑抽象类有什么用,现在你应该彻底明白了吧? 至于这样做的好处,很显然啊,扩展性强,以后Benchmark内容变化,我只要再做一个继承子类就可以,不必修改其他应用代码.

posted @ 2006-09-02 23:16 Alex 阅读(300) | 评论 (0)编辑 收藏

Command模式是最让我疑惑的一个模式,我在阅读了很多代码后,才感觉隐约掌握其大概原理,我认为理解设计模式最主要是掌握起原理构造,这样才 对自己实际编程有指导作用.Command模式实际上不是个很具体,规定很多的模式,正是这个灵活性,让人有些confuse.

Command定义
n 将来自客户端的请求传入一个对象,无需了解这个请求激活的 动作或有关接受这个请求的处理细节。

这是一种两台机器之间通讯联系性质的模式,类似传统过程语 言的 CallBack功能。

优点:
解耦了发送者和接受者之间联系。 发送者调用一个操作,接受者接受请求执行相应的动作,因为使用Command模式解耦,发送者无需知道接受者任何接口。

不少Command模式的代码都是针对图形界面的,它实际就是菜单命令,我们在一个下拉菜单选择一个命令时,然后会执行一些动作.

将 这些命令封装成在一个类中,然后用户(调用者)再对这个类进行操作,这就是Command模式,换句话说,本来用户(调用者)是直接调用这些命令的,如菜 单上打开文档(调用者),就直接指向打开文档的代码,使用Command模式,就是在这两者之间增加一个中间者,将这种直接关系拗断,同时两者之间都隔 离,基本没有关系了.

显然这样做的好处是符合封装的特性,降低耦合度,Command是将对行为进行封装的典型模式,Factory是将创建进行封装的模式,
从Command模式,我也发现设计模式一个"通病":好象喜欢将简单的问题复杂化, 喜欢在不同类中增加第三者,当然这样做有利于代码的健壮性 可维护性 还有复用性.

如何使用?
具体的Command模式代码各式各样,因为如何封装命令,不同系统,有不同的做法.下面事例是将命令封装在一个Collection的List中,任何 对象一旦加入List中,实际上装入了一个封闭的黑盒中,对象的特性消失了,只有取出时,才有可能模糊的分辨出:

典型的Command模式需要有一个接口.接口中有一个统一的方法,这就是"将命令/请求封装为对象":
public interface Command {
  public abstract void execute ( );
}

具体不同命令/请求代码是实现接口Command,下面有三个具体命令
public class Engineer implements Command {

  public void execute( ) {
    //do Engineer's command
  }
}

public class Programmer implements Command {

  public void execute( ) {
    //do programmer's command
  }
}

public class Politician implements Command {

  public void execute( ) {
    //do Politician's command
  }
}


按照通常做法,我们就可以直接调用这三个Command,但是使用Command模式,我们要将他们封装起来,扔到黑盒子List里去:

public class producer{
  public static List produceRequests() {
    List queue = new ArrayList();
    queue.add( new DomesticEngineer() );
    queue.add( new Politician() );
    queue.add( new Programmer() );
    return queue;
  }

}

这三个命令进入List中后,已经失去了其外表特征,以后再取出,也可能无法分辨出谁是Engineer 谁是Programmer了,看下面客户端如何调用Command模式:

public class TestCommand {
  public static void main(String[] args) {
    
    List queue = Producer.produceRequests();
    for (Iterator it = queue.iterator(); it.hasNext(); )
      
 //客户端直接调用execute方法,无需知道被调用者的其它更多类的方法名。
    ((Command)it.next()).execute();
  

  }
}

由此可见,调用者基本只和接口打交道,不合具体实现交互,这也体现了一个原则,面向接口编程,这样,以后增加第四个具体命令时,就不必修改调用者TestCommand中的代码了.

理解了上面的代码的核心原理,在使用中,就应该各人有自己方法了,特别是在如何分离调用者和具体命令上,有很多实现方法,上面的代码是使用"从List过一遍"的做法.这种做法只是为了演示.

使用Command模式的一个好理由还因为它能实现Undo功能.每个具体命令都可以记住它刚刚执行的动作,并且在需要时恢复.

Command模式在界面设计中应用广泛.Java的Swing中菜单命令都是使用Command模式,由于Java在界面设计的性能上还有欠缺,因此界面设计具体代码我们就不讨论,网络上有很多这样的示例.

参考:
http://www.patterndepot.com/put/8/command.pdf

http://www.javaworld.com/javaworld/javatips/jw-javatip68.html

posted @ 2006-08-31 21:46 Alex 阅读(264) | 评论 (0)编辑 收藏


Your Ad Here

小结

本章主要介绍了一个基于 J2EE Web 技术进行设计开发的论坛系统,通过这个系统的剖析,能够了解和掌握 GOF 设计模式,学会 Java 实战中一些处理技巧和技术。

使用 GOF 设计模式的主要优点:使得复杂系统的架构变得更加清晰而且有条理,而这一点正是许多程序员在开发实用系统中所缺乏的,可能导致的结果是大大降低 Java 系统可维护性以及可拓展性,重新回到了传统编程语言的陷阱中。

因此, GOF 设计模式对于 Java 设计编程的重要性是无论怎么强调也不过分,它能够帮助程序员更加深入地理解 Java 完全面向对象特性,从而以真正的面向对象设计概念进行实用系统的设计和开发。

Jive 系统是一个完全的 Web 系统,整个系统的最大特点是自我定制实现,它为了提高数据库的访问性能,使用了自己开发的数据库连接池;为了提高系统的数据处理系统,它使用了缓存机制;为了实现用户安全管理机制,它使用 Proxy 模式实现了角色权限的定位和检查等。这些模块功能在很多系统中都是需要的,但是如果想从 Jive 系统提炼出这些模块功能以达到重用,又是非常困难的。

因此,开发者需要一种具有一定高度的框架技术。在这个框架技术中,所有这些通用技术都能够自动实现,无需再自行设计和开发,能够将更多精力投入到与业务有关的特定功能开发中。 J2EE EJB 技术实际就是这种框架技术。

学习和研究 Jive 论坛系统也非常有助于程序员学习和理解 EJB J2EE 完整的框架技术,因为它们的目的都是一样,只不过实现的途径不一样而已。

posted @ 2006-08-31 12:29 Alex 阅读(418) | 评论 (0)编辑 收藏

7  Jive 安装调试运行

Jive 默认编码方式是 ISO8859_1 。下面以安装在英文 Linux 操作系统上为例:

1 )安装数据库。在 database 目录中选择对应数据库,如果数据库是 MySQL ,则使用 jive_mysql.sql ,然后通过数据库的管理工具将 jive_mysql.sql 导入相应数据库中。 Jive 所需要的数据库都已经准备完成。

2 )由于 Jive Servlet/JSP 系统,必须在 Web 容器支持下才能运行。因此,必须安装 Web 服务器软件,例如 Tomcat 等。

最后需要将 Jive 部署到 Tomcat 中。有两种办法,其中直接复制的办法比较简单,修改 server.xml 办法可以见其他章节介绍。

Jive application 目录下所有文件复制到 Tomcat 应用目录 webapps 的指定目录下,如 Tomcat/webapps/jive 下。

3 )在运行设置 Jive 之前,还需要设定 Jive 的初始值,在上述应用目录的 WEB-INF/classes/ 下,创建或编辑 jive_init.properties ,加入行:

jiveHome=C:\\javasource\\jive\\WEB-INF\\jiveHome

这是指定 jivHome 的目录, jiveHome 主要放置 Jive 的配置文件和搜索等内部工作文件,一般是建立在 WEB-INF 目录下,这样, jiveHome 的内容就不会被外界通过浏览器直接访问到,要注意是绝对路径。

4 )可以安装设置 Jive 系统。通过浏览输入网址: http://localhost:8080/jive/admin/setup/ Jive 的安装导航程序会自动进行安装检查。由于 Jive 编码是 ISO8859_1 ,如果涉及需要设置 Java 编码方式的提问,都设置为英文。

管理员密码最好更改一下,默认用户名和密码是 admin admin

全部设置完成后,进入 http://localhost:8080/jive/ 可以浏览。

进入 http://localhost:8080/jive/admin 可以实现论坛管理,可以设置全局过滤器,一般设置如下过滤器:

·          TextStyle 文本格式。

·          Newline 新一行。

·          Profanity 过渡亵渎或不恰当词语。

·          URLConverter URLConverter

·          ImageFilter ImageFilter 支持上传图片。        

·          CodeHighlighter CodeHighlighter

另外,缓存设置也很重要,可根据访问量、缓存击中率以及实际内存大小调整,可以提高论坛的性能。

图片上传路径的设置需要通过手工修改 jiveHome 下的 jive_config.xml 。打开 jive_config.xml 会发现,所有在管理配置中配置的信息都保存在这里,找到下列配置:

<upload>

        <dir>/home/bqlr/jive/upload/</dir>

        <relurl>upload/</relurl>

</upload>

其中, dir 是上传图片存放的绝对路径; relurl 是网址的相对路径,比如 upload/ ,那就表示输入下列网址:“ http://localhost:8080/jive/upload/xxxx.jpg ”,就可以看到上传的图片。因为上传图片有自动缩小功能,因此需要 Linux 安装时是带图形 X86 的完全安装。用户如为中文名,将影响图片文件名。

以上是 Jive 论坛的安装

posted @ 2006-08-31 12:29 Alex 阅读(757) | 评论 (0)编辑 收藏

6  Jive 图形处理

Jive 提供了强大的论坛功能,但是有些功能离实际需求还是有一定的距离,例如论坛是用于信息交流讨论的场所。而信息不只是文字,有时还包括图片或一些 PDF 等类型的文件,那么如何在 Jive 中实现这样的功能呢?

6.1  图片上传处理

HTML 中,使用表单 Form 主要是用来向服务器提交数据,格式如下:

FORM ACTION="URL"

METHOD="GET|POST"

ENCTYPE="…" TARGET="..."

. . .

/FORM

enctype 指定了表单提交给服务器时的内容形式( Content-Type ),默认值是 "application/x-www-form-urlencoded" ,这时,表单信息一般采用 URL 编码制。

但是,当向服务器传送图片或文件这样包含非 ASCII 字符或二进制数的数据时,根据 RFC1867 规定,就必须使用“ multipart/form-data ”内容类型。

其实无论是默认表单信息,还是图片文件,这些内容都是装载在 HTTP 协议的正文内容部分,都可以看成 HTTP 协议携带的对象,只是两种正文内容形式不一样。前者是 String 字符串类型,而后者则是一个通用的数据对象类型( Object )。在以后章节中将专门讨论 HTTP 协议装载数据对象的底层细节。

使用“ multipart/form-data ”上传文件的格式写法如下:

FORM ACTION="URL" METHOD="GET|POST" ENCTYPE=" multipart/form-data "

  <INPUT TYPE FILE NAME file1>

/FORM

文件通过 HTTP 协议传送到服务器端后,需要在服务器端对该文件进行专门的接受。 HttpServletRequest 没有提供直接获取文件数据的方法,因此需要开发专门的服务器文件处理组件。

目前有两种上传文件处理组件:一种是基于完全 JSP 结构的,使用 JSP 来处理上传的文件,以 SmartUpload http://www.jspsmart.com )最常用;还有一种是完全的 JavaBeans 组件: Cos 文件上传组件包( http://www.servlets.com/cos/index.html ), Cos 可以使用在 JSP 中,也可以使用在 Servlet Servlet Filter 中。

由于在实际应用中,文件上传功能往往和其他正常表单参数一起混合使用,而不是独立使用的。因此,可以设定一个 Servlet 专门用来处理这类混合表单的请求,在将文件接受处理后,自动导向到处理表单正常参数的 JSP/Servlet 去处理。

表单调用示例如下:

<form action="<%=request.getContextPath()%>/multipartformserv"

     method="post" enctype="multipart/form-data">

   <input type="hidden" name="FORWARDNAME" value="login.jsp" > 

   <input type="file" name="file1" >

   <input type="hidden" name="maxwidth" value="120" >

   <input type="hidden" name="maxheight" value="60" >

   <input type="text" name="username" >

   <input type="text" name="password" >

</form>

在这个表单中,既有文件提交,也有 username 这样正常的参数需要提交,提交的 Servlet 名为 multipartformserv 。由 multipartformserv 来处理上传的文件,然后再自动转交到 FORWARDNAME 的值 login.jsp 进行 username 等正常参数的处理。

在表单中,如果设定 maxwidth maxiheight ,那么表示如果上传的图片超过这个尺寸,服务器将缩小图片到这个尺寸。

编制一个专门用来统一处理 Jive 系统中所有文件处理的 Servlet ,这样有利于简化系统。编制 MultipartFormServ 如下:

public class MultipartFormServ extends HttpServlet {

  static final private String CONTENT_TYPE = "text/html";

  // 文件上传处理

  private static MultipartFormHandle mf = MultipartFormHandle.getInstance();

  // 文件上传后存放的临时目录

  private String dirName;

  private ServletContext context;

  public void init() throws ServletException {

    // web.xml 中读取上传目录参数

    dirName = mf.getUploaddir();

    if (dirName == null) {

      throw new ServletException("Please supply uploadDir parameter");

    }

  }

 

  // 处理带有文件内容的请求

  public void doPost(HttpServletRequest request, HttpServletResponse response)

         throws ServletException, IOException {

    try {

       mf.init(dirName,request); // 调用文件上传处理器处理

       // 得到 FORWARDNAME 参数值

       String forward=mf.getForwardProgram();

      if (forward.equals(""))

       {

         errorMessage("no forward program", response);

         return;

      }

      String param=mf.getForwardProgramParam();

      mf.clear();

       // 引导分流到 forward 参数值进行其他文本参数处理

      getServletConfig().getServletContext().

           getRequestDispatcher("/"+forward+"?"+param).forward(request, response);

 

    }catch (Exception Ex) {

          throw new ServletException(Ex.getMessage());

    }

  }

3-8  上传文件处理框架

}

MultipartFormHandler 主要调用 Cos 组件处理上传文件,对上传文件图形大小进行处理,然后将图形搬迁到指定的目录;同时也将请求信号中的正常参数提取出来,以作为转发使用。

这样一个图形或文件上传系统已经形成了一个框架结构,可以重复使用在任何需要图片或文件上传处理的系统中,如图 3-8 所示。

3-8 中上传文件处理额 Servlet 相当于请求信号 Request 的一个过滤器,既然正常的 Request 处理机制不能从 Request 提取携带的文件,那么,使用一个过滤器先将文件提取出来,剩余的再通过正常 Request 处理机制去处理。

6.2  服务器端图形处理

Java 最初是以 Applet 等客户端图形处理为技术起点的,而本节讨论的是如何在 Servlet/JSP 中实现图形处理。

Jive 中,图片可以用来显示用户的头像,用户在上传自己头像图片时,该图片的大小可能不一,但是由于版面原因,显示的头像图片又有大小限制,那么就需要在用户上传图片时对图片大小做一个检查。如果超过规定大小,就进行一定的缩放处理。

缩放处理有两种方式:是在 HTML 显示时,使用 image 语法的 width height 来 限制大小,但是这样做只是解决了表面问题,无法解决大字节图片传送到客户端带来的性能影响,这个图片因为是用户发言的头像,将会在每个帖子里面显示。如果 头像都是巨大图片,对帖子显示速度的影响是很大,因此必须在服务器端进行缩小后,再传送到客户端,这样提高了论坛系统性能。

服务器端的图形处理需要使用到 Java 的图形处理技术,而且图形处理是在服务器端的 Web 容器中进行的。和以往 Java 在客户端进行图形处理稍微有所不同,相同的是都要使用计算机的底层图形支持资源。

J2SE 1.4 提供新的增强的图形处理功能, JDK1.4 中最新的 javax.imageio.ImageIO 对图片进行读写操作,而使用 java.awt.geom.AffineTransform 对图片进行尺寸缩放处理。

import java.io.File;

import java.awt.image.BufferedImage;

import java.awt.Image;

import java.awt.image.AffineTransformOp;

import javax.imageio.ImageIO;

import java.awt.geom.AffineTransform;

 

public class UploadImg{

     /**

     * 参数设置

     * @param fromdir 图片的原始目录

     * @param todir 处理后的图片存放目录

     * @param imgfile 原始图片

     * @param sysimgfile 处理后的图片文件名前缀

     */

           public void init(String fromdir,String todir,String imgfile,String sysimgfile)

           {

                  this.fromdir=fromdir;

                  this.todir=todir;

                  this.imgfile=imgfile;

                  this.sysimgfile=sysimgfile;

             }

    …

    public boolean CreateThumbnail() throws Exception

    {

        //ext 是图片的格式 gif JPG png

        String ext=""

        double Ratio=0.0;

        File oldFile = new File(fromdir,imgfile);

        if (!F.isFile())  // 检查是否存在此图片文件

             throw new Exception(F+" is not image file error in CreateThumbnail!");

 

         // 首先判断上传的图片是 gif 还是 JPG ImageIO ,只能将 gif 转换为 png

         if (isJpg(imgfile)){

            ext="jpg";

        }else{

           ext="png";

        }

        File newFile = new File(todir,sysimgfile+"."+ext);

 

        BufferedImage Bi = ImageIO.read(oldFile);  // 读取原始图片

        if ((Bi.getHeight()>120) || (Bi.getWidth()>120)){

            if (Bi.getHeight()>Bi.getWidth())

              Ratio = 120.0/Bi.getHeight();

            else

              Ratio = 120.0/Bi.getWidth();

       }

       // 进行图片转换

       AffineTransformOp op =

          new AffineTransformOp(AffineTransform.getScaleInstance(Ratio, Ratio), null);

       Image itemp = op.filter(Bi, null);

 

       try { // 写入转换后的图片

           ImageIO.write((BufferedImage) itemp, ext, newFile);

       }catch (Exception ex) {

            throw new Exception(ex.getMessage());

       }

       return (true);

   }

}

该类中由于使用到了 Java AWT ,虽然没有实际显示,但 Linux 系统下需要 X11 Windows 的支持(安装 Linux 时需安装 XFree86 Linux 完全安装方式包括安装 XFree86 )。

该缩放功能是在图片上传到服务器后再进行的处理,可以对 JPG 进行缩小放大;对上传是 GIF 的图片,缩放后变成 PNG 图片格式文件。

posted @ 2006-08-31 12:28 Alex 阅读(420) | 评论 (0)编辑 收藏

     摘要: 5  Jive 的其他组件技术 Jive 是一个比较丰富的知识宝藏,从中可以学习到很多新的实战技巧和具体功能实现方式。前面基本介绍了 Jive 中的一些主要架构技术,通过这些技术可以基本上掌握 Jive 论坛系统。 Jive 中还有很多非常实用的组件技术和工具库,分...  阅读全文
posted @ 2006-08-31 12:27 Alex 阅读(640) | 评论 (0)编辑 收藏

4  Jive 的缓存机制

Jive 论坛的一个主要特点就是其性能速度快,因此很多巨大访问量的网站都采用了 Jive 论坛。这些都是由于 Jive 采取了高速缓存机制。

缓存( Cache )机制是提高系统运行性能必不可少的技术。缓存机制从原理上讲比较简单,就是在原始数据第一次读取后保存在内存中,下次读取时,就直接从内存中读取。原始数据有可能保存在持久化介质或网络上。缓存机制也是代理模式的一种实现。

4.1  缓存原理和实现

Jive Cache 总体来说实现得不是非常精简和有效。它是针对每个具体数据对象逐个实现缓冲,这种“穷尽”的办法不是实践所推荐的用法。通过使用动态代理模式,可以根据具体方法的不同来实现缓存是值得推荐的做法。 Jive 的缓存实现得比较简单,可以用来学习和研究缓存机制。

Jive 中的 Cache 实现了缓存机制的大部分行为,它是将对象用惟一的关键字 Key 作标识保存在 HashMap Hashtable 中。当然,必须知道这些对象的大小,这个前提条件的设定可以保证缓存增长时不会超过规定的最大值。

如果缓存增长得太大,一些不经常被访问的对象将首先从缓存中删除。如果设置了对象的最大生命周期时间,即使这个对象被反复频繁访问,也将从缓存中删除。这个特性可以适用于一些周期性需要刷新的数据,如来自数据库的数据。

Cach 中除了 getObject() 方法的性能依据缓存大小,其他方法的性能都是比较快的。一个 HashMap 用来实现快速寻找,两个 LinkedList 中一个以一定的访问顺序来保存对象,叫 accessed LinkedList ;另外一个以它们加入缓存的顺序保存这些对象,这种保存对象只是保存对象的引用,叫 age LinkedList 。注意,这里的 LinkedList 不是 JDK 中的 LinkedList ,而是 Jive 自己定义的 LinkedList

当对象被加入缓存时,首先被 CacheObject 封装。封装有以下信息:对象大小(以字节计算),一个指向 accessed LinkedList 的引用,一个指向 age LinkedList 的引用。

当从缓存中获取一个对象如 ObjectA 时,首先, HashMap 寻找到指向封装 ObjectA 等信息的 CacheObject 对象。然后,这个对象将被移动到 accessed LinkedList 的前面,还有其他一些动作如缓存清理、删除、过期失效等都是在这个动作中一起触发实现的。

public class Cache implements Cacheable {

    /**

     * 因为 System.currentTimeMillis() 执行非常耗费性能,因此如果 get 操作都执行

* 这条语句将会形成性能瓶颈, 通过一个全局时间戳来实现每秒更新

* 当然,这意味着在缓存过期时间计算上有一到几秒的误差

     */

    protected static long currentTime = CacheTimer.currentTime;

    //CacheObject 对象

    protected HashMap cachedObjectsHash;

    //accessed LinkedList 最经常访问的排列在最前面

    protected LinkedList lastAccessedList;

    // 以缓存加入顺序排列,最后加入排在最前面;越早加入的排在最后面

    protected LinkedList ageList;

    // 缓存最大限制 默认是 128k 可根据内存设定,越大性能越高

    protected int maxSize =  128 * 1024;

    // 当前缓存的大小

    protected int size = 0;

    // 最大生命周期时间,默认是没有

    protected long maxLifetime = -1;

    // 缓存的击中率,用于评测缓存效率

    protected long cacheHits, cacheMisses = 0L;

 

    public Cache() {

        // 构造 HashMap. 默认 capacity 11

        // 如果实际大小超过 11 HashMap 将自动扩充,但是每次扩充都

// 是性能开销,因此期初要设置大一点

        cachedObjectsHash = new HashMap(103);

        lastAccessedList = new LinkedList();

        ageList = new LinkedList();

    }

    public Cache(int maxSize) {

        this();

        this.maxSize = maxSize;

    }

    public Cache(long maxLifetime) {

        this();

        this.maxLifetime = maxLifetime;

    }

    public Cache(int maxSize, long maxLifetime) {

        this();

        this.maxSize = maxSize;

        this.maxLifetime = maxLifetime;

    }

    public int getSize() {        return size;    }

    public int getMaxSize() {        return maxSize;    }

 

    public void setMaxSize(int maxSize) {

        this.maxSize = maxSize;

        // 有可能缓存大小超过最大值,需要激活删除清理动作

        cullCache();

    }

    public synchronized int getNumElements() {

        return cachedObjectsHash.size();

    }

 

    /**

     * 增加一个 Cacheable 对象

* 因为 HashMap 不是线程安全的,所以操作方法要使用同步

* 如果使用 Hashtable 就不必同步

     */

    public synchronized void add(Object key, Cacheable object) {

        // 删除已经存在的 key

        remove(key);

        int objectSize = object.getSize();

        // 如果被缓存对象的大小超过最大值,就放弃

        if (objectSize > maxSize * .90) {            return;        }

        size += objectSize;

        // 创建一个 CacheObject 对象

        CacheObject cacheObject = new CacheObject(object, objectSize);

        cachedObjectsHash.put(key, cacheObject);  // 保存这个 CacheObject

        // 加入 accessed LinkedList Jive 自己的 LinkedList 在加入时可以返回值

        LinkedListNode lastAccessedNode = lastAccessedList.addFirst(key);

        // 保存引用

        cacheObject.lastAccessedListNode = lastAccessedNode;

        // 加入到 age LinkedList

        LinkedListNode ageNode = ageList.addFirst(key);

        // 这里直接调用 System.currentTimeMillis(); 用法值得讨论

        ageNode.timestamp = System.currentTimeMillis();

        // 保存引用

        cacheObject.ageListNode = ageNode;

        // 做一些清理工作

        cullCache();

    }

    /**

     * 从缓存中获得一个被缓存的对象,这个方法在下面两种情况返回空

     *    <li> 该对象引用从来没有被加入缓存中

     *    <li> 对象引用因为过期被清除 </ul>

     */

    public synchronized Cacheable get(Object key) {

        // 清除过期缓存

        deleteExpiredEntries();

        // Key 从缓存中获取一个对象引用

        CacheObject cacheObject = (CacheObject)cachedObjectsHash.get(key);

        if (cacheObject == null) {

            // 不存在,增加未命中率

            cacheMisses++;

            return null;

        }

        // 存在,增加命中率

        cacheHits++;

        // accessed LinkedList 中将对象从当前位置删除

        // 重新插入在第一个

        cacheObject.lastAccessedListNode.remove();

        lastAccessedList.addFirst(cacheObject.lastAccessedListNode);

        return cacheObject.object;

    }

    …

}

Cache 中,关键字 Key 是一个对象,为了再次提高性能,可以进一步将 Key 确定为一个 long 类型的整数。

4.2  缓存使用

建立 LongCache 只是为了提高原来的 Cache 性能,本身无多大意义,可以将 LongCache 看成与 Cache 一样的类。

LongCache 的关键字 Key Forum ForumThread 以及 ForumMessage long 类型的 ID ,值 Value Forum ForumThread 以及 ForumMessage 等的对象。这些基本是通过 DatabaseCacheManager 实现完成,在主要类 DbForumFactory 的初始化构造时,同时构造了 DatabaseCacheManager 的实例 cacheManager

前面过滤器功能分析中, Message 对象获得方法的第一句如下:

protected ForumMessage getMessage(long messageID, long threadID, long forumID) throws

      ForumMessageNotFoundException {

    DbForumMessage message = cacheManager.messageCache.get(messageID);

    …

}

其中, cacheManager DatabaseCacheManager 的实例, DatabaseCacheManager 是一个缓存 Facade 类。在其中包含了 5 种类型的缓存,都是针对 Jive 5 个主要对象, DatabaseCacheManager 主要代码如下:

public class DatabaseCacheManager {

    public UserCache userCache;                          // 用户资料缓存

    public GroupCache groupCache;                       // 组资料缓存

    public ForumCache forumCache;                       //Forum 论坛缓存

    public ForumThreadCache threadCache;                //Thread 主题缓存

    public ForumMessageCache messageCache;          //Message 缓存

    public UserPermissionsCache userPermsCache;     // 用户权限缓存

 

    public DatabaseCacheManager(DbForumFactory factory) {

        …

        forumCache =

            new ForumCache(new LongCache(forumCacheSize, 6*HOUR), factory);

        threadCache =

            new ForumThreadCache(

                  new LongCache(threadCacheSize, 6*HOUR), factory);

        messageCache = new ForumMessageCache(

                  new LongCache(messageCacheSize, 6*HOUR), factory);

        userCache = new UserCache(

                  new LongCache(userCacheSize, 6*HOUR), factory);

        groupCache = new GroupCache(

                  new LongCache(groupCacheSize, 6*HOUR), factory);

        userPermsCache = new UserPermissionsCache(

                new UserPermsCache(userPermCacheSize, 24*HOUR), factory

        );

    }

    …

}

从以上代码看出, ForumCache 等对象生成都是以 LongCache 为基础构建的,以 ForumCache 为例,代码如下:

public class ForumCache extends DatabaseCache {

    // Cache 构建 ID 缓存

    protected Cache forumIDCache = new Cache(128*1024, 6*JiveGlobals.HOUR);

    // LongCache 构建整个对象缓存

    public ForumCache(LongCache cache, DbForumFactory forumFactory) {

        super(cache, forumFactory);

    }

 

    public DbForum get(long forumID) throws ForumNotFoundException {

        …

        DbForum forum = (DbForum)cache.get(forumID);

        if (forum == null) {    // 如果缓存没有从数据库中获取

            forum = new DbForum(forumID, factory);

            cache.add(forumID, forum);

        }

        return forum;

    }

 

public Forum get(String name) throws ForumNotFoundException {

         // name key ,从 forumIDCache 中获取 ID

 CacheableLong forumIDLong = (CacheableLong)forumIDCache.get(name);

        if (forumIDLong == null) { // 如果缓存没有 从数据库获得

            long forumID = factory.getForumID(name);

            forumIDLong = new CacheableLong(forumID); // 生成一个缓存对象

            forumIDCache.add(name, forumIDLong);

        }

        return get(forumIDLong.getLong());

    }

    …

}

由此可以看到, LongCache 封装了 Cache 的核心功能,而 ForumCache 等类则是在 LongCache 核心外又包装了与应用系统相关的操作,这有点类似装饰( Decorator )模式。

从中也可以看到 Cache LongCache 两种缓存的用法。

使用 Cache 时的关键字 Key 是任何字段。如上面代码中的 String name ,如果用户大量帖子主题查询中, Key query + blockID ,见 DbForum 中的 getThreadBlock 方法;而值 Value 则是 Long 类型的 ID ,如 ForumID ThreadID 等。

LongCache 的关键字 Key Long 类型的 ID ,如 ForumID ThreadID 等;而值 Value 则是 Forum ForumThread ForumMessage 等主要具体对象。

在实际使用中,大多数是根据 ID 获得对象。但有时并不是这样,因此根据应用区分了两种 Cache ,这其实类似数据库的数据表,除了主关键字外还有其他关键字。

4.3  小结

缓存中对象是原对象的映射,如何确保缓存中对象和原对象的一致性?即当原对象发生变化时,缓存中的对象也必须立即更新。这是缓存机制需要解决的另外一个基本技术问题。

Jive 中是在原对象发生变化时,立即进行清除缓存中对象,如 ForumMessage 对象的创建。在 DbForumThread AddMessage 方法中有下列语句:

factory.cacheManager.threadCache.remove(this.id);

factory.cacheManager.forumCache.remove(this.forumID);

即当有新的帖子加入时,将 ForumThreadCache ForumCache 相关缓冲全部清除。这样,当有相关对象读取时,将直接从数据库中读取,这是一种非常简单的缓存更新方式。

在复杂的系统,例如有一台以上的服务器运行着 Jive 系统。如果一个用户登陆一台服务器后,通过这台服务器增加新帖。那么按照上述原理,只能更新本服务器 JVM 中的缓存数据,而其他服务器则无从得知这种改变,这就需要一种分布式的缓存机制。

3-7  Jive 主要对象的访问

到目前可以发现 整个 Jive 系统其实是围绕 Forum ForumThread ForumMessage 等这些主要对象展开的读取、修改或创建等操作。由于这些对象原先持久化保存在数据库中,为了提高性能和加强安全性, Jive 在这些对象外面分别实现两层包装,如图 3-7 所示。

客户端如果需要访问这些对象,首先要经过它们的代理对象。进行访问权限的检查,然后再从缓存中获取该对象。只有缓存不存在时,才会从数据库中获取。

这套机制是大多数应用系统都面临的必须解决的基本功能,因此完全可以做成一个通用的可重复使用的框架。这样在具体应用时,不必每个应用系统都架设开发这样的机制。其实 EJB 就是这样一套框架,实体 Bean 都由缓存机制支持,而通过设定 ejb-jar.xml 可以实现访问权限控制,这些工作都直接由 EJB 容器实现了,不必在代码中自己来实现。剩余的工作是调整 EJB 容器的参数,使之适合应用系统的具体要求,这些将在以后章节中讨论。

Jive 中,图 3-7 的机制是通过不同方式实现的。基本上是一配二模式:一个对象有一个缓冲对象和一个代理对象,这样做的一个缺点是导致对象太多,系统变得复杂。这点在阅读 Jive 源码时可能已经发现。

如果建立一个对象工厂,工厂内部封装了图 3-7 机制实现过程,客户端可以根据不同的工厂输入参数获得具体不同的对象。这样也许代码结构要更加抽象和紧凑, Java 的动态代理 API 也许是实现这个工厂的主要技术基础。有兴趣者可以进一步研究提炼。

posted @ 2006-08-31 12:27 Alex 阅读(428) | 评论 (0)编辑 收藏

3  Jive 安全管理机制

Jive 中除了前面介绍的有关设计模式实现组件外,还有其他有一定特点的组件功能,分析研究这些组件功能可以更加完整透彻地理解 Jive 论坛系统。

Jive 安全管理机制基本是由下列部分组成:

·          安全验证机制。主要是验证用户名和密码组合是否与数据库中注册时的数据一致,以确认该用户身份为注册用户。这是对所有的 JSP 访问都进行拦截访问。

·          访问权限控制( ACL )。对不同的数据不同用户拥有不同的访问权限,例如,一个帖子普通用户可以浏览,但是不能更该;但是管理员却可以编辑删除。这部分功能是通过代理模式实现,为每个关键数据都建立一个代理类用来实现访问权限检查,这在前面讨论过。

·          用户资料管理系统。主要是管理用户的资料数据,进行用户组和用户关系的建立等。

安全验证机制

Jive 的安全验证机制是按照比较通用的思路设计的。类似前面“简单的用户注册管理系统”中的介绍, Jive 也是在所有的 JSP 页面中 include 一个安全检验功能的 global.jsp 。由于 global.jsp 是在每个 JSP 一开始必须执行的功能,因此通过拦截 global.jsp 拦截发往各个 JSP 页面的请求( request )。如果这个请求是合法的,将被允许通过;如果不是,将注明请求者身份是 Anonymous (匿名者)。

global.jsp 代码如下:

boolean isGuest = false;

Authorization authToken = SkinUtils.getUserAuthorization(request, response);

if (authToken == null) {// 未被验证通过

    authToken = AuthorizationFactory.getAnonymousAuthorization();

    isGuest=true;

}

Jive 中,以 Authorization 对象作为验证通过的标志,它的接口代码如下:

public interface Authorization {

    public long getUserID();   

    public boolean isAnonymous();

}

具体实现是 DbAuthorization ,代码如下:

public final class DbAuthorization implements Authorization, Serializable {

    private long userID;

    protected DbAuthorization(long userID) {

        this.userID = userID;

    }

    public long getUserID() {

        return userID;

    }

    public boolean isAnonymous() {

        return userID == -1;

    }

}

此类只是一个 userID ,因此只是一个象征性的标志。

SkinUtils 是一个为 JSP 服务的类,它的 getUserAuthorization 代码如下:

public static Authorization getUserAuthorization

        (HttpServletRequest request, HttpServletResponse response)

  {

    HttpSession session = request.getSession();

    // HttpSession 中获取 Authorization 实例

    Authorization authToken =

(Authorization)session.getAttribute(JIVE_AUTH_TOKEN);

    if (authToken != null) {     return authToken;  }

 

    // 如果 HttpSession 中没有,检查用户浏览器 cookie

    Cookie cookie = getCookie(request, JIVE_AUTOLOGIN_COOKIE);

    if (cookie != null) {

        try {

           String[] values = decodePasswordCookie(cookie.getValue());

           String username = values[0];

           String password = values[1];

           // cookie 中获得用户名和密码后,进行安全验证

           authToken = AuthorizationFactory.getAuthorization(username,password);

        }catch (Exception e) {}

        // put that token in the user's session:

        if (authToken != null) {// 如果通过验证,保存 authToken http Session

           session.setAttribute(JIVE_AUTH_TOKEN, authToken);

        }

       // return the authorization token

        return authToken;

    }

    return null;

}

用户验证预先通过两个步骤。首先检查 HttpSession 中是否保存了该用户的验证信息,如果用户第一次验证通过,反复访问,这道关口检查就可以通过。

如果 HttpSession 中没有验证信息,那么从该用户的浏览器 cookie 中寻找用户名和密码。如果该用户激活了 cookie 保存这些登录信息,那么应该可以找到用户名和密码,这样就省却了用户再次从键盘输入用户名和密码,将用户名和密码通过下列语句进行数据库验证:

authToken = AuthorizationFactory.getAuthorization(username,password);

这一举是验证关键。 AuthorizationFactory 是一个抽象类,定义了 Jive 安全验证机制所需的所有方法, AuthorizationFactory 的实现类似前面讨论的 ForumFactory 实现,是使用工厂模式加动态类反射机制完成的,代码如下:

public abstract class AuthorizationFactory {

   // 定义一个数据库具体实现

    private static String className =

        " com.Yasna.forum.database.DbAuthorizationFactory";

 

    private static AuthorizationFactory factory = null;

    // 验证方法 如果没有 UnauthorizedException 抛出,表示验证通过

    public static Authorization getAuthorization(String username,

            String password) throws UnauthorizedException

    {

        loadAuthorizationFactory();

        return factory.createAuthorization(username, password);

    }

    // 匿名者处理方法

    public static Authorization getAnonymousAuthorization() {

        loadAuthorizationFactory();

        return factory.createAnonymousAuthorization();

    }

    // 需要具体实现的抽象方法

    protected abstract Authorization createAuthorization(String username,

            String password) throws UnauthorizedException;

    protected abstract Authorization createAnonymousAuthorization();

    // 动态配置 AuthorizationFactory 的具体实现,可以在配置文件中定义一个

    // 基于 LDAP 的实现。类似 ForumFactory getInstance 方法

    private static void loadAuthorizationFactory() {

        …

    }

}

AuthorizationFactory 看上去很复杂,实际只有一个核心方法 getAuthorization 。实现用户名和密码的验证。如果无法通过验证,有两个信息实现显示:一个是抛出 UnauthorizedException ,另外一个是返回空的 Authorization 对象。

那么,子类 DbAuthorizationFactory 毫无疑问就是查询数据库,将输入的用户名和密码与数据库保存的用户名和密码进行校验。

Jive 的安全验证机制比较简单易懂,值得在实践中学习借鉴。但是注意到这套安全验证机制只是 Web 层的“手工”验证,资源访问权限( ACL )也是自己“手工”来实现的。如果使用 EJB 技术,因为 EJB 容器本身有一定的资源访问控制体系,因此在 Web 层验证通过后,需要将这些登录信息传递到 EJB 层。当然如果直接使用 Web 容器的安全验证机制,那么 Web 层与 EJB 层之间的登录信息传递将由容器实现,这样就更加简单方便。

Jive 这种的安全验证并不是使用 Web 容器的安全验证机制,如何使用 Web 容器的安全验证机制将在以后章节介绍。尽管如此, Jive 这套安全验证机制对付小型系统的应用也是足够的。


用户资料管理

Jive 中,用户 User 对象的操作访问类似于论坛 Forum 对象的访问,与 User 对象有关的操作都封装在一个类中操作,这是外观( Facade )模式的应用。

Jive 中,用户资料管理属于大系统中的一个子系统,在这个子系统中,用户子系统和其他系统又有一定的关系,涉及的类不少,通过建立一个 UserManager 类来统一对外接口,使得整个子系统条目结构清晰。

UserManager 中无外乎用户数据的管理,如用户的创建、修改、查询和删除。 DbUserManager UserManager 的一个数据库实现,可是看看 DbUserManager 中除了删除功能是直接通过 SQL 语句进行数据库删除操作外,其他都委托给 User 的具体实现 DbUser 实现的。这种实现非常类似于 EJB Session Bean 和实体 Bean 之间的关系。以创建用户资料为例,代码如下:

public User createUser(String username, String password, String email)

            throws UserAlreadyExistsException

 {

        User newUser = null;

        try {

            // username 查询改用户是否存在

            User existingUser = getUser(username);

            // 如果没有抛出 UserNotFoundException 异常,表示该用户存在

            //The user already exists since now exception, so:

            throw new UserAlreadyExistsException();

        } catch (UserNotFoundException unfe) {

            // 该用户不存在,创建一个新用户

            newUser = new DbUser(username, password, email, factory);

        }

        return newUser;

}

DbUser 的构造方法实际是用户资料的新增创建:

protected DbUser(String username, String password, String email,

            DbForumFactory factory)

{

        this.id = SequenceManager.nextID(JiveGlobals.USER);  // 获得自增 ID

        this.username = username;

        // Compute hash of password.

        this.passwordHash = StringUtils.hash(password);  // 获得加密的密码

        this.email = email;

        this.factory = factory;

        long now = System.currentTimeMillis();

        creationDate = new java.util.Date(now);

        modifiedDate = new java.util.Date(now);

        properties = new Hashtable();

        insertIntoDb();              // 数据库插入数据

}

Jive 中,数据修改的保存是由 DbUser saveToDb 方法实现的,而 saveToDb 方法调用是在每个 setXXXX 方法中。即每当外界调用 DbUser setXXXX ,则表示需要改变某些字段属性值,在这个方法中直接进行数据库存储,这也类似 EJB CMP 实体 Bean 的数据字段修改保存。

Jive 中组 Group 与用户 User 处理几乎差不多,只是在 Group 中整合了权限方面的信息,这种做法是有一定的局限性,不是很值得借鉴,要想设计一个动态扩展灵活的权限系统,必须在用户或组与权限之间引入角色概念,也就是比较先进的基于角色的权限系统( RBAC Roled-Based Access Control ,相关网址: http://csrc.nist.gov/rbac/ )。

RBAC 中,用户组只是用户的一个集合,应该是通过角色和权限发生联系。所以 RBAC 认为,如果给用户组赋予权限,那么用户组也接近角色的概念。

posted @ 2006-08-31 12:26 Alex 阅读(677) | 评论 (0)编辑 收藏

     摘要: 2  Jive 与设计模式 Jive 论坛系统使用大量设计模式巧妙地实现了一系列功能。因为设计模式的通用性和可理解性,将帮助更多人很快地理解 Jive 论坛源码,从而可以依据一种“协定”来动态地扩展它。那么使用设计模式还有哪些好处? ...  阅读全文
posted @ 2006-08-31 12:26 Alex 阅读(742) | 评论 (0)编辑 收藏

1  Jive 功能需求

Jive 功能需求分析类似于一个新系统的需求分析。只有了解 Jive 系统实现了哪些论坛功能,才能进一步研究和学习它是怎样巧妙、优雅地实现这些功能的。

论坛系统是网络交流的一种主要互动功能系统,如图 3-1 所示。通过论坛系统,用户可以共同就某个话题不断进行讨论,通过发贴功能发布新的话题,通过回贴功能回复别人的话题。 Jive 论坛系统可以允许管理员动态地创建新的论坛、编辑论坛的内容、设置论坛过滤信息以及管理注册用户等。

3-1  Jive 用例图

Jive 论坛系统中,用户角色和权限是紧密联系在一起的。主要分两大角色:普通用户和管理员,具体的表现形式是通过权限组合来体现的。管理方面的权限有:

·          SYSTEM_ADMIN ,系统管理员,可以管理整个系统。

·          FORUM_ADMIN ,论坛管理员,可以管理某个特定的论坛。

·          USER_ADMIN GROUP_ADMIN ,用户和组管理员,可以管理一些特定用户和用户 组。

论坛的读写权限包括:读权限,创建一个新主题,创建一个新的帖子等。

Jive 中没有明确定义普通用户和管理员角色,而是直接通过以上权限组合和具体用户直接建立联系,并将这种直接联系保存到数据库中。

在权限不是很复杂的情况下,这种没有引入角 色的做法比较简单直接。但由于用户和权限直接挂钩,而用户和权限都可能在不断地动态变化,那么它们之间由于联系太直接和紧密,对各自变化形成了限制。所 以,对于复杂的权限系统,引入了基于角色的权限系统,这将在以后章节中进一步讨论。

Jive 论坛业务对象主要分为 Forum ForumThread ForumMessage ,它们之间的关系如图 3-2 所示。

每个论坛 Forum 包含一系列 ForumThread (主题),而每个主题都是由很多内容帖子 ForumMessage 组成的,这是一个聚集关系。这 3 种对象中每一个对象都涉及到对象数据的创建、编辑、查询和删除,这些对象数据分别保存在数据库中。这 3 个对象对于不同的角色可操作访问权限是不一样的,只有系统管理员和论坛管理员可以对 Forum 相关数据实行操作,普通用户可以创建或编辑 ForumThread ForumMessage

Jive 论坛为了实现不同用户对不同基本对象的不同操作权限,通过设定一个统一的入口,在这个入口将检查客户端每次对数据的操作权限,如图 3-3 所示。

  

3-2  基本对象关系图                             3-3  入口示意图

客户端每次对数据库的操作,都要经过 ForumFactory 入口进入。在 ForumFactory 中会动态生成一个访问控制代理 ForumFactoryProxy ,通过 ForumFactoryProxy 检查客户端访问方法是否符合整体权限访问控制要求。

下面将从 ForumFactory 作为 Jive 论坛系统分析入手,结合设计模式逐步分解论坛功能的具体实现。

posted @ 2006-08-31 12:25 Alex 阅读(2084) | 评论 (1)编辑 收藏

JDBMonitor是一个开源项目。使用它开发者可以很轻松为系统增加数据库执行日志功能。它使用十分方便,您所需要做的唯一事情就是在您系统的JDBC连接字符串前增加类似于 "listenerconfig=/config.xml:url=" 的字符即可,不用写任何代码。

使用 JDBMonitor,您可以把数据库执行情况记录通过各种方式记录下来,比如打印到控制台、输出到文件或者通过socket传送给远程客户端。JDBMonitor是可扩展的,您可以扩展它来将执行情况通过其他方式记录下来,您所需要做的就是写一个实现IDBListener接口的类即可。

JDBMonitor遵守 GNU Lesser General Public Licence (LGPL)协议。此协议包含在发行包中。

入门

几乎所有大型数据库应用都包含有自己的SQL执行日志功能,此功能不仅能帮助开发人员调试,而且可以为DBA(数据库管理员)提供系统的运行信息。

(1)很难将业务逻辑同日志代码分离

(2)降低了代码的可读性。

(3)降低了系统的运行速度。在记录日志的时候,程序会暂停运行等待直到记录完成,而I/O操作是相当耗时的。

(4)很难记录运行耗时、语句参数等其他信息

(5)很难为我们无法修改代码的系统(例如没有源代码的系统)或者很难增加记录日志功能代码的系统(比如系统使用了ORMapping)增加日志功能。

JDBMonitor 则不同:

(1)您最多只需要修改一行代码。您需要修改的代码就是这一行:Class.forName("com.cownew.JDBMonitor.jdbc.DBDriver") ,然后再修改一下 JDBC连接字符串,只要从 “jdbc:db2://10.74.198.247:50000/app”修改成” listenerconfig=config.xml:url= jdbc:db2://10.74.198.247:50000/app”就可以了。在您使用WebLogic ,Tomcat或其他服务器的数据源功能的时候,连修改代码这一步都是无需的。

(2)JDBMonitor另起一个线程来记录SQL,所以它不会对程序运行速度有任何影响。

(3)它是高度可扩展的,所以您可以扩展它来把执行情况通过其他方式记录。比如,您可以写一个扩展类,来通过电子邮件将日志发送出去。

取得 JDBMonitor

JDBMonitor的最新稳定版本可以在JDBMonitor的网站上取得:

http://www.cownew.com/JDBMonitor

使用 JDBMonitor

1 将 jdbmonitor.jar放到您系统的类路径下。

2 让系统加载 JDBMonitor的JDBC驱动。

这一步将会依您系统加载JDBC驱动的方式的不同而不同。

(1)如果您通过代码的形式加载JDBC驱动,例如:

   Class.forName(“com.microsoft.jdbc.sqlserver.SQLServerDriver”);
   Connection cn = DriverManager.getConnection(……);

在这种情况下 ,您必须修改 “Class.forName”这一句来加载JDBMonitor的JDBC驱动(“com.cownew.JDBMonitor.jdbc.DBDriver”),而非以前的数据库JDBC驱动。

例如:

Class.forName(“com.cownew.JDBMonitor.jdbc.DBDriver”);
   Connection cn = DriverManager.getConnection(……);

(2)如果您在配置文件中指定JDBC驱动,比如,数据源配置文件或者其他类似的文件。

请修改原来的  JDBC驱动类为 “com.cownew.JDBMonitor.jdbc.DBDriver” 。

3 让 JDBMonitor加载能够加载原来的JDBC驱动

JDBMonitor的工作原理就是截获JDBC驱动的SQL语句调用、记录SQL语句,然后将SQL语句重新转发给原来的JDBC驱动,所以JDBMonitor必须首先向DriverManager注册JDBC驱动。

原来的JDBC驱动定义在配置文件的“JdbcDrivers” 段中。
<JdbcDrivers>
    <JdbcDriver class=" com.mysql.jdbc.Driver"/>
  </JdbcDrivers>

4 在原来的JDBC连接字符串前增加 JDBMonitor所需的信息。

您所需要做的就是将” listenerconfig=<configfilepath>:url=” 增加到原来的JDBC连接字符串前。“<configfilepath>”代表配置文件的路径,下面集中路径都是合法的:

/com/jdbmonitor/config.xml
com/jdbmonitor/config.xml
c:/ jdbmonitor /config.xml

JDBMoinitor使用getClass().getResourceAsStream加载类似于“/com/jdbmonitor/config.xml” and “com/jdbmonitor/config.xml” 的类路径文件,使用 FileInputStream加载类似于 “c:/ jdbmonitor /config.xml”的配置文件。

5 指定您要使用监听器:

您可以把数据库执行情况记录通过各种方式记录下来,比如打印到控制台、输出到文件或者通过socket传送给远程客户端。

我们已经开发了如下常用的监听器:FileDBListener、ConsoleDBListener、 SocketDBListene、DataBaseDBListener。当然您也可以开发满足您要求的监听器。
监听器定义在配置文件的 “Listeners”段中:

<Listeners>
    <!--ConsoleDBListener no arguments-->
    <Listener class="com.cownew.JDBMonitor.listenerImpl.ConsoleDBListener" arg=""/>
   
    <!--the arguments of FileDBListener is the file to log the SQL statement -->
    <Listener class="com.cownew.JDBMonitor.listenerImpl.FileDBListener" arg="c:/aaa.txt"/>
   
    <!--the arguments of SocketDBListener is the bound socket port of the listener server -->
    <Listener class="com.cownew.JDBMonitor.listenerImpl.SocketDBListener" arg="9527"/>
  </Listeners>

搞定!启动您的系统。耶!SQL语句被记录下来了,我们可以在控制台、文件甚至远程监视器中看到日志了。

举例

mvnforum的例子:

您可以从http://www.mvnForum.com得到mvnforum。我演示用的版本是1.0。

(1)打开webapp\WEB-INF\classes\ mvncore.xml,重新配置:

修改之前:

<driver_class_name>com.mysql.jdbc.Driver</driver_class_name>
<database_url>listenerconfig=c:/log/jdbmonitor/config.xml:url= jdbc:mysql://localhost/mvnforum?useUnicode=true&amp;characterEncoding=utf-8</database_url>

修改之后:
<driver_class_name> com.cownew.JDBMonitor.jdbc.DBDriver </driver_class_name>
        <database_url>jdbc:mysql://localhost/mvnforum?useUnicode=true&amp;characterEncoding=utf-8</database_url>

(2)创建文件 c:/log/jdbmonitor/config.xml。我只想将SQL语句记录到文本文件中,所以我做如下配置:
<config>
  <Listeners>
    <!--the arguments of FileDBListener is the file to log the SQL statement -->
    <Listener class="com.cownew.JDBMonitor.listenerImpl.FileDBListener" arg="c:/log.txt"/>
  </Listeners>
  <JdbcDrivers>
    <JdbcDriver class="com.mysql.jdbc.Driver"/>
  </JdbcDrivers>
</config>
(3) 将 jdbmonitor.jar放到webapp\WEB-INF\lib下。
(4) 搞定!

Jive的例子:

您可以从http://www.jivesoftware.com得到Jive。我演示用的版本是 Jive 2.0 beta版。

(1)打开http://localhost:8080/jive/admin/

“jdbc” 填为:com.cownew.JDBMonitor.jdbc.DBDriver

“server” 填为:c:/log/jdbmonitor/config.xml:url=jdbc:mysql://locahost/jive
(2)将 jdbmonitor.jar放到WEB-INF\lib下
(3) 象mvnforum中一样创建同样的 c:/log/jdbmonitor/config.xml 文件.
(4) 搞定!

代码方式的例子:

尽管直接在代码中指定系统所用的JDBC驱动类名和JDBC连接字符串是不推荐的,但是仍然有系统是这么做的。

比如:

              Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
              Connection conn = null;
              PreparedStatement ps = null;
              try
              {
                     conn = DriverManager
                                   .getConnection("jdbc:odbc:MQIS");
                     for (int i = 0; i < 1000; i++)
                     {                   
                            ps = conn.prepareStatement("update T_Material set fid=fid");
                            ps.execute();
                            ps.close();
                     }
              } finally
              {
                     ....         
              }

(1)修改一下代码为:
               Class.forName("com.cownew.JDBMonitor.jdbc.DBDriver");
              Connection conn = null;
              PreparedStatement ps = null;
              try
              {
                     conn = DriverManager.getConnection("listenerconfig= c:/log/jdbmonitor/config.xml:url=jdbc:odbc:MQIS");
                     for (int i = 0; i < 1000; i++)
                     {
                            ps = conn.prepareStatement("update T_Material set fid=fid");
                            ps.execute();
                            ps.close();
                     }
              } finally
              {
                     ....         
              }

(2)创建c:/log/jdbmonitor/config.xml文件。我想记录SQL语句到文本文件中同时输出到控制台,这样可以辅助我进行调试,所以我配置如下:
<config>
  <Listeners>
    <!--the arguments of FileDBListener is the file to log the SQL statement -->
<Listener class="com.cownew.JDBMonitor.listenerImpl.FileDBListener" arg="c:/log.txt"/>

<!--ConsoleDBListener no arguments-->
<Listener class="com.cownew.JDBMonitor.listenerImpl.ConsoleDBListener" arg=""/>
  </Listeners>
  <JdbcDrivers>
    <JdbcDriver class="com.mysql.jdbc.Driver"/>
  </JdbcDrivers>
</config>
(3) 将 jdbmonitor.jar放到类路径下。
(4) 搞定!

监听器

我们已经开发了如下常用的监听器:FileDBListener、ConsoleDBListener、 SocketDBListener、DataBaseDBListener。

1、ConsoleDBListener 控制台监听器

ConsoleDBListener会将SQL语句打印到控制台中。

这个监听器很容易配置:

<Listener class="com.cownew.JDBMonitor.listenerImpl.ConsoleDBListener" arg=""/>

2、FileDBListener 文件监听器

FileDBListener 会将SQL语句保存到文本文件中。

如下配置:

<Listener class="com.cownew.JDBMonitor.listenerImpl.FileDBListener" arg="c:/aaa.txt"/>

arg="c:/aaa.txt"表示日志将保存到文件c:/aaa.txt中。

3、SocketDBListener Socket监听器

SocketDBListener是一个socket服务器,客户端连接到它上边以后就可以接收到它发出的SQL语句。

如下配置:

<Listener class="com.cownew.JDBMonitor.listenerImpl.SocketDBListener" arg="9527"/>

arg="9527"表示服务器将在9527端口监听。

我们已经开发了如下两种客户端:SocketConsoleClient(Socket控制台客户端) 和 SocketSwingClient(Socket Swing客户端)。

SocketConsoleClient工作在控制台中:

SocketSwingClient是一个Swing GUI客户端:

您可以运行"java -classpath jdbmonitor.jar com.cownew.JDBMonitor.listenerImpl.sckListenerClient.SocketConsoleClient" 来启动SocketConsoleClient,运行"java -classpath jdbmonitor.jar com.cownew.JDBMonitor.listenerImpl.sckListenerClient.SocketSwingClient"启动SocketSwingClient

您可以编写符合您自己要求的客户端,具体细节请参考com.cownew.JDBMonitor.listenerImpl.sckListenerClient.ListenerClientcom.cownew.JDBMonitor.listenerImpl.sckListenerClient.IDBSocketClientListener.

4、DataBaseDBListener

DataBaseDBListener将会把SQL语句记录到数据库中:

如下配置:

<Listener class="com.cownew.JDBMonitor.listenerImpl.DataBaseDBListener"
arg="dburl=jdbc:odbc:MQIS;user=;password=;logtable=T_Log_SQLLog"/>

"dburl=jdbc:odbc:MQIS;user=;password=;"表示目标数据库的JDBC连接字符串。"logtable=T_Log_SQLLog" 表示SQL记录将被保存到哪个表中,默认的是T_Log_SQLLog

如果目标数据库用的JDBC驱动与被监控的数据库不同,请将它加入配置文件的 "JdbcDrivers" 部分,例如:

<config>
<Active>true</Active>
<Listeners>

<Listener class="com.cownew.JDBMonitor.listenerImpl.ConsoleDBListener" arg=""/>

<Listener class="com.cownew.JDBMonitor.listenerImpl.DataBaseDBListener"
arg="dburl=jdbc:odbc:MQIS;user=;password=;logtable=T_Log_SQLLog"/>
</Listeners>
<JdbcDrivers>
<JdbcDriver class="com.microsoft.jdbc.sqlserver.SQLServerDriver"/>
<JdbcDriver class="sun.jdbc.odbc.JdbcOdbcDriver"/>
</JdbcDrivers>
</config>

"T_Log_SQLLog"的结构是:

"T_Log_SQLLog"的建库脚本在com/cownew/JDBMonitor/listenerImpl/dataBaseListener,(db2.sql,mssqlserver.sql,oracle.sql)。

DataBaseDBListener是跨数据库的,你可以把记录SQL到任何关系数据库中。

FAQ:

1 如果我暂时不想记录SQL语句执行怎么办?难道我要重新修改成原来的样子?

答:无须如此。您只要修改config.xml,增加<Active>false</Active>到文件中即可。

如下:

<config>
  <Active> false </Active>
  <Listeners>
......
</config>

如何扩展JDBMonitor?

我们已经开发了如下常用的监听器:FileDBListener、ConsoleDBListener、 SocketDBListener、DataBaseDBListener。当然您也可以开发满足您要求的监听器。所有的监听器必须实现接口:com.cownew.JDBMonitor.commo. IDBListener。IDBListener有两个方法需要实现:

public void init(String arg);
public void logSql(SQLInfo info);

JDBMonitor会将配置文件中监听器定义中“arg”的值传递给 “init”方法、将代表SQL语句执行信息的SQLInfo传递给“logSql”方法。

更多信息请参考API文档。

posted @ 2006-08-31 10:59 Alex 阅读(1572) | 评论 (0)编辑 收藏

     摘要: UML 类图介绍 一、 UML 简介 UML ( Unified Modeling ...  阅读全文
posted @ 2006-08-29 22:30 Alex 阅读(1115) | 评论 (0)编辑 收藏

这一段时间,参加了部门组织的 RUP 教学项目,由一位“外援”架构师为我们指导教练。最近一直在忙于业务建模,今天刚刚将自己负责部分的系统用例识别了一遍。其间一直有一个问题,缠绕着包括我在内的很多同事,那就是用例之间的关系——包含、扩展、泛化——到底该如何使用。

    翻阅了同事去年参加 RUP 培训时带来的材料,终于能基本分清三者之间的关系。

 

用例是从系统外部可见的行为,是系统为某一个或几个参与者( Actor )提供的一段完整的服务。从原则上来讲,用例之间都是独立、并列的,它们之间并不存在着包含从属关系。但是为了体现一些用例之间的业务关系,提高可维护性和一致性,用例之间可以抽象出包含 (include) 、扩展 (extend) 和泛化 (generalization) 这几种关系。

在分开介绍它们之前,先说下它们的共性:都是从现有的用例中抽取出公共的那部分信息,作为一个单独的用例,然后通后过不同的方法来重用这个公共的用例,以减少模型维护的工作量。

 

1 、包含 (include)

  包含关系:使用包含 Inclusion 用例来封装一组跨越多个用例的相似动作(行为片断),以便多个基( Base )用例复用。基用例控制与包含用例的关系,以及被包含用例的事件流是否会插入到基用例的事件流中。基用例可以依赖包含用例执行的结果,但是双方都不能访问对方的属性。


 

包 含关系对典型的应用就是复用,也就是定义中说的情景。但是有时当某用例的事件流过于复杂时,为了简化用例的描述,我们也可以把某一段事件流抽象成为一个被 包含的用例;相反,用例划分太细时,也可以抽象出一个基用例,来包含这些细颗粒的用例。这种情况类似于在过程设计语言中,将程序的某一段算法封装成一个子 过程,然后再从主程序中调用这一子过程。 

    例如:业务中,总是存在着维护某某信息的功能,如果将它作为一个用例,那新建、编辑以及修改都要在用例详述中描述,过于复杂;如果分成新建用例、编辑用例和删除用例,则划分太细。这时包含关系可以用来理清关系。

 

 

 

2 扩展 (extend)

扩展关系:将基用例中一段相对独立并且可选的动作,用扩展( Extension )用例加以封装,再让它从基用例中声明的扩展点( Extension Point )上进行扩展,从而使基用例行为更简练和目标更集中。

扩展用例为基用例添加新的行为。扩展用例可以访问基用例的属性,因此它能根据基用例中扩展点的当前状态来判断是否执行自己。但是扩展用例对基用例不可见。

对于一个扩展用例,可以在基用例上有几个扩展点。

 

例如,系统中允许用户对查询的结果进行导出、打印。对于查询而言,能不能导出、打印查询都是一样的,导出、打印是不可见的。导入、打印和查询相对独立,而且为查询添加了新行为。因此可以采用扩展关系来描述:

 

 

用例详述里面大致可以这样来写:

执行查询

  基本流:

1. 员工选择查询功能

        员工期望查询业务数据时,选择查询链接,从而启动本用例的执行。

2. 系统转入查询页面,并显示备选的查询选项

       

3. 员工填写查询条件并提交

               

4. 系统验证查询条件的合法性

                验证条件的格式以及简单逻辑,如大小、前后、范围

5. 系统将符合条件的信息返回

        系统将查询结果以分页列表的形式显示在页面上

6. 员工退出查询功能

        员工点击退出链接,返回到上一级页面

    扩展点:导出、打印扩展点定义在步骤 5

 

导出

    该用例是在“导出、打印”扩展点上扩展了执行查询用例

    基本流:

        1 .如果员工要求导出,选择导出按钮

        。。。。。。

 

由上例可以看出 : 扩展用例的事件流往往可以也可抽象为基用例的备选流。但是在基用例本身已经是一个很复杂的情况下,选用扩展关系将备选流抽象成为单独的用例可以使基用例行为更简练和目标更集中(当然上面的例子中基用例可能简单了些)。

 

 

4 泛化 (generalization)

泛化关系:子用例和父用例相似,但表现出更特别的行为;子用例将继承父用例的所有结构、行为和关系。子用例可以使用父用例的一段行为,也可以重载它。父用例通常是抽象的。在实际应用中很少使用泛化关系,子用例中的特殊行为都可以作为父用例中的备选流存在。

 

例如,业务中可能存在许多需要部门领导审批的事情,但是领导审批的流程是很相似的,这时可以做成泛化关系表示:

 

用例详述里面大致可以这样来写:

审批

  基本流:

7. 领导选择要审批的记录

        领导期望审批记录时,选择待审批记录链接,从而启动本用例的执行。

8. 系统转入审批页面,并显示记录的详细信息

       

9. 领导填写审批意见

                领导根据记录的合理性来填写个人审批意见

10.              提交审批结果

       

 

工资调整审批

    该用例是审批用例的子用例

    基本流:

        1 .领导选择要审批的记录

 

2. 系统转入审批页面,并显示记录的详细信息

 

3. 领导填写审批意见

 

4. 提交审批结果

                如果调整幅度较大,则要提交上级审批

 

 

上面分析了用例之间的三种关系。其中最容易让人迷惑的就是包含关系和扩展关系得区别。如果你现在对两者还有迷惑,请再仔细的对比一下上面两者的描述:)。


posted @ 2006-08-29 22:23 Alex 阅读(511) | 评论 (1)编辑 收藏

Java 数据库连接(JDBC)由一组用 Java 编程语言编写的类和接口组成。JDBC 为工具/数据库开发人员提供了一个标准的 API,使他们能够用纯Java API 来编写数据库应用程序。然而各个开发商的接口并不完全相同,所以开发环境的变化会带来一定的配置变化。本文主要集合了不同数据库的连接方式。
一、连接各种数据库方式速查表
    下面罗列了各种数据库使用JDBC连接的方式,可以作为一个手册使用。

    1
Oracle8/8i/9i数据库(thin模式) 
Class.forName("oracle.jdbc.driver.OracleDriver").newInstance(); 
String url="jdbc:oracle:thin:@localhost:1521:orcl"; //orcl
为数据库的SID 
String user="test"; 
String password="test"; 
Connection conn= DriverManager.getConnection(url,user,password); 

    2
DB2数据库 
Class.forName("com.ibm.db2.jdbc.app.DB2Driver ").newInstance(); 
String url="jdbc:db2://localhost:5000/sample"; //sample
为你的数据库名 
String user="admin"; 
String password=""; 
Connection conn= DriverManager.getConnection(url,user,password); 

    3
Sql Server7.0/2000数据库 
Class.forName("com.microsoft.jdbc.sqlserver.SQLServerDriver").newInstance(); 
String url="jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=mydb"; 
//mydb
为数据库 
String user="sa"; 
String password=""; 
Connection conn= DriverManager.getConnection(url,user,password); 

    4
Sybase数据库 
Class.forName("com.sybase.jdbc.SybDriver").newInstance(); 
String url =" jdbc:sybase:Tds:localhost:5007/myDB";//myDB
为你的数据库名 
Properties sysProps = System.getProperties(); 
SysProps.put("user","userid"); 
SysProps.put("password","user_password"); 
Connection conn= DriverManager.getConnection(url, SysProps); 

    5
Informix数据库 
Class.forName("com.informix.jdbc.IfxDriver").newInstance(); 
String url = 
"jdbc:informix-sqli://123.45.67.89:1533/myDB:INFORMIXSERVER=myserver; 
user=testuser;password=testpassword"; //myDB
为数据库名 
Connection conn= DriverManager.getConnection(url); 

    6
MySQL数据库 
Class.forName("org.gjt.mm.mysql.Driver").newInstance(); 
String url ="jdbc:mysql://localhost/myDB?user=soft&password=soft1234&useUnicode=true&characterEncoding=8859_1" 
//myDB
为数据库名 
Connection conn= DriverManager.getConnection(url); 

    7
PostgreSQL数据库 
Class.forName("org.postgresql.Driver").newInstance(); 
String url ="jdbc:postgresql://localhost/myDB" //myDB
为数据库名 
String user="myuser"; 
String password="mypassword"; 
Connection conn= DriverManager.getConnection(url,user,password); 


    8、access
数据库直连用ODBC的
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver") ;
String url="jdbc:odbc:Driver={MicroSoft Access Driver (*.mdb)};DBQ="+application.getRealPath("/Data/ReportDemo.mdb");
Connection conn = DriverManager.getConnection(url,"","");
 Statement stmtNew=conn.createStatement() ;

二、JDBC连接MySql方式
    下面是使用JDBC连接MySql的一个小的教程     

    1、查找驱动程序
    MySQL目前提供的java驱动程序为Connection/J,可以从MySQL官方网站下载,并找到mysql-connector-java-3.0.15-ga-bin.jar文件,此驱动程序为纯java驱动程序,不需做其他配置。

    2、动态指定classpath
    如果需要执行时动态指定classpath,就在执行时采用-cp方式。否则将上面的.jar文件加入到classpath环境变量中。

    3、加载驱动程序
try{
    Class.forName(com.mysql.jdbc.Driver);
    System.out.println(Success loading Mysql Driver!);
}catch(Exception e)
{
    System.out.println(Error loading Mysql Driver!);
    e.printStackTrace();
}

    4、设置连接的url
    jdbc:mysql://localhost/databasename[?pa=va][&pa=va]

三、以下列出了在使用JDBC来连接Oracle数据库时可以使用的一些技巧,这些技巧能够使我们更好地发挥系统的性能和实现更多的功能(系转载)。

  1、在客户端软件开发中使用Thin驱动程序

  在开发Java软件方面,Oracle的数据库提供了四种类型的驱动程序,二种用于应用软件、applets、 servlets等客户端软件,另外二种用于数据库中的Java存储过程等服务器端软件。在客户机端软件的开发中,我们可以选择OCI驱动程序或Thin 驱动程序。OCI驱动程序利用Java本地化接口(JNI),通过Oracle客户端软件与数据库进行通讯。Thin驱动程序是纯Java驱动程序,它直 接与数据库进行通讯。为了获得最高的性能,Oracle建议在客户端软件的开发中使用OCI驱动程序,这似乎是正确的。但我建议使用Thin驱动程序,因 为通过多次测试发现,在通常情况下,Thin驱动程序的性能都超过了OCI驱动程序。

  2、关闭自动提交功能,提高系统性能

  在第一次建立与数据库的连接时,在缺省情况下,连接是在自动提交模式下的。为了获得更好的性能,可以通过调用带布尔值false参数的Connection类的setAutoCommit()方法关闭自动提交功能,如下所示:

  conn.setAutoCommit(false);

  值得注意的是,一旦关闭了自动提交功能,我们就需要通过调用Connection类的commit()和rollback()方法来人工的方式对事务进行管理。

  3、在动态SQL或有时间限制的命令中使用Statement对象

  在执行SQL命令时,我们有二种选择:可以使用PreparedStatement对象,也可以使用 Statement对象。无论多少次地使用同一个SQL命令,PreparedStatement都只对它解析和编译一次。当使用Statement对象 时,每次执行一个SQL命令时,都会对它进行解析和编译。这可能会使你认为,使用PreparedStatement对象比使用Statement对象的 速度更快。然而,我进行的测试表明,在客户端软件中,情况并非如此。因此,在有时间限制的SQL操作中,除非成批地处理SQL命令,我们应当考虑使用 Statement对象。

  此外,使用Statement对象也使得编写动态SQL命令更加简单,因为我们可以将字符串连接在一起,建立一个有效的SQL命令。因此,我认为,Statement对象可以使动态SQL命令的创建和执行变得更加简单。

  4、利用helper函数对动态SQL命令进行格式化

  在创建使用Statement对象执行的动态SQL命令时,我们需要处理一些格式化方面的问题。例如,如果我们 想创建一个将名字O'Reilly插入表中的SQL命令,则必须使用二个相连的“''”号替换O'Reilly中的“'”号。完成这些工作的最好的方法是 创建一个完成替换操作的helper方法,然后在连接字符串心服用公式表达一个SQL命令时,使用创建的helper方法。与此类似的是,我们可以让 helper方法接受一个Date型的值,然后让它输出基于Oracle的to_date()函数的字符串表达式。

  5、利用PreparedStatement对象提高数据库的总体效率

  在使用PreparedStatement对象执行SQL命令时,命令被数据库进行解析和编译,然后被放到命令 缓冲区。然后,每当执行同一个PreparedStatement对象时,它就会被再解析一次,但不会被再次编译。在缓冲区中可以发现预编译的命令,并且 可以重新使用。在有大量用户的企业级应用软件中,经常会重复执行相同的SQL命令,使用PreparedStatement对象带来的编译次数的减少能够 提高数据库的总体性能。如果不是在客户端创建、预备、执行PreparedStatement任务需要的时间长于Statement任务,我会建议在除动 态SQL命令之外的所有情况下使用PreparedStatement对象。

  6、在成批处理重复的插入或更新操作中使用PreparedStatement对象

  如果成批地处理插入和更新操作,就能够显著地减少它们所需要的时间。Oracle提供的Statement和 CallableStatement并不真正地支持批处理,只有PreparedStatement对象才真正地支持批处理。我们可以使用 addBatch()和executeBatch()方法选择标准的JDBC批处理,或者通过利用PreparedStatement对象的 setExecuteBatch()方法和标准的executeUpdate()方法选择速度更快的Oracle专有的方法。要使用Oracle专有的批 处理机制,可以以如下所示的方式调用setExecuteBatch():
PreparedStatement pstmt3D null;
try {
((OraclePreparedStatement)
pstmt).setExecuteBatch(30);
...
pstmt.executeUpdate();
}

  调用setExecuteBatch()时指定的值是一个上限,当达到该值时,就会自动地引发SQL命令执行, 标准的executeUpdate()方法就会被作为批处理送到数据库中。我们可以通过调用PreparedStatement类的sendBatch ()方法随时传输批处理任务。

  7、使用Oracle locator方法插入、更新大对象(LOB)

  Oracle的PreparedStatement类不完全支持BLOB和CLOB等大对象的处理,尤其是 Thin驱动程序不支持利用PreparedStatement对象的setObject()和setBinaryStream()方法设置BLOB的 值,也不支持利用setCharacterStream()方法设置CLOB的值。只有locator本身中的方法才能够从数据库中获取LOB类型的值。 可以使用PreparedStatement对象插入或更新LOB,但需要使用locator才能获取LOB的值。由于存在这二个问题,因此,我建议使用 locator的方法来插入、更新或获取LOB的值。

  8、使用SQL92语法调用存储过程

  在调用存储过程时,我们可以使用SQL92或Oracle PL/SQL,由于使用Oracle PL/SQL并没有什么实际的好处,而且会给以后维护你的应用程序的开发人员带来麻烦,因此,我建议在调用存储过程时使用SQL92。

  9、使用Object SQL将对象模式转移到数据库中

  既然可以将Oracle的数据库作为一种面向对象的数据库来使用,就可以考虑将应用程序中的面向对象模式转到数 据库中。目前的方法是创建Java bean作为伪装的数据库对象,将它们的属性映射到关系表中,然后在这些bean中添加方法。尽管这样作在Java中没有什么问题,但由于操作都是在数据 库之外进行的,因此其他访问数据库的应用软件无法利用对象模式。如果利用Oracle的面向对象的技术,可以通过创建一个新的数据库对象类型在数据库中模 仿其数据和操作,然后使用JPublisher等工具生成自己的Java bean类。如果使用这种方式,不但Java应用程序可以使用应用软件的对象模式,其他需要共享你的应用中的数据和操作的应用软件也可以使用应用软件中的 对象模式。

  10、利用SQL完成数据库内的操作

  我要向大家介绍的最重要的经验是充分利用SQL的面向集合的方法来解决数据库处理需求,而不是使用Java等过程化的编程语言。

  如果编程人员要在一个表中查找许多行,结果中的每个行都会查找其他表中的数据,最后,编程人员创建了独立的 UPDATE命令来成批地更新第一个表中的数据。与此类似的任务可以通过在set子句中使用多列子查询而在一个UPDATE命令中完成。当能够在单一的 SQL命令中完成任务,何必要让数据在网上流来流去的?我建议用户认真学习如何最大限度地发挥SQL的功能。

posted @ 2006-08-29 22:01 Alex 阅读(370) | 评论 (0)编辑 收藏

一、 引子

在大学的数据结构这门课上,树是最重要的章节之一。还记得树是怎么定义的吗?树 (Tree) n(n≥0) 个结点的有限集 T T 为空时称为空树,否则它满足如下两个条件:

(1)    有且仅有一个特定的称为根 (Root) 的结点;

(2)   其余的结点可分为 m(m≥0) 个互不相交的子集 Tl T2 Tm ,其中每个子集本身又是一棵树,并称其为根的子树 (SubTree)

上面给出的递归定义刻画了树的固有特性:一棵非空树是由若干棵子树构成的,而子树又可由若干棵更小的子树构成。而这里的子树可以是叶子也可以是分支。

今天要学习的组合模式就是和树型结构以及递归有关系。

 

二、 定义与结构

组合 (Composite) 模式的其它翻译名称也很多,比如合成模式、树模式等等。在《设计模式》一书中给出的定义是:将对象以树形结构组织起来,以达成 部分-整体 的层次结构,使得客户端对单个对象和组合对象的使用具有一致性。

从定义中可以得到使用组合模式的环境为: 在设计中想表示对象的 部分 整体 层次结构;希望用户忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象。

看下组合模式的组成。

1)         抽象构件角色 Component :它为组合中的对象声明接口,也可以为共有接口实现缺省行为。

2)       树叶构件角色 Leaf :在组合中表示叶节点对象 —— 没有子节点,实现抽象构件角色声明的接口。

3)       树枝构件角色 Composite :在组合中表示分支节点对象 —— 有子节点,实现抽象构件角色声明的接口;存储子部件。

下图为组合模式的类图表示。

 

如图所示:一个 Composite 实例可以像一个简单的 Leaf 实例一样,可以把它传递给任何使用 Component 的方法或者对象,并且它表现的就像是一个 Leaf 一样。

可以看出来,使用组合模式使得这个设计结构非常灵活,在下面的例子中会得到进一步的印证。

      

三、 安全性与透明性

组合模式中必须提供对子对象的管理方法,不然无法完成对子对象的添加删除等等操作,也就失去了灵活性和扩展性。但是管理方法是在 Component 中就声明还是在 Composite 中声明呢?

一种方式是在 Component 里面声明所有的用来管理子类对象的方法,以达到 Component 接口的最大化(如下图所示)。目的就是为了使客户看来在接口层次上树叶和分支没有区别 —— 透明性。但树叶是不存在子类的,因此 Component 声明的一些方法对于树叶来说是不适用的。这样也就带来了一些安全性问题。

 

另一种方式就是只在 Composite 里面声明所有的用来管理子类对象的方法(如下图所示)。这样就避免了上一种方式的安全性问题,但是由于叶子和分支有不同的接口,所以又失去了透明性。

    


    《设计模式》一书认为:在这一模式中,相对于安全性,我们比较强调透明性。对于第一种方式中叶子节点内不需要的方法可以使用空处理或者异常报告的方式来解决。

 

四、 举例

这里以 JUnit 中的组合模式的应用为例(JUnit)。

JUnit 是一个单元测试框架,按照此框架下的规范来编写测试代码,就可以使单元测试自动化。为了达到“自动化”的目的, JUnit 中定义了两个概念: TestCase TestSuite TestCase 是对一个类或者 jsp 等等编写的测试类;而 TestSuite 是一个不同 TestCase 的集合,当然这个集合里面也可以包含 TestSuite 元素,这样运行一个 TestSuite 会将其包含的 TestCase 全部运行。

然而在真实运行测试程序的时候,是不需要关心这个类是 TestCase 还是 TestSuite ,我们只关心测试运行结果如何。这就是为什么 JUnit 使用组合模式的原因。

JUnit 为了采用组合模式将 TestCase TestSuite 统一起来,创建了一个 Test 接口来扮演抽象构件角色,这样原来的 TestCase 扮演组合模式中树叶构件角色,而 TestSuite 扮演组合模式中的树枝构件角色。下面将这三个类的有关代码分析如下:

 

//Test 接口 —— 抽象构件角色

public interface Test {

       /**

        * Counts the number of test cases that will be run by this test.

        */

       public abstract int countTestCases();

       /**

        * Runs a test and collects its result in a TestResult instance.

        */

       public abstract void run(TestResult result);

}

 

//TestSuite 类的部分有关源码 ——Composite 角色,它实现了接口 Test

public class TestSuite implements Test {

// 用了较老的 Vector 来保存添加的 test

       private Vector fTests= new Vector(10);

       private String fName;

       …… 

/**

        * Adds a test to the suite.

        */

       public void addTest(Test test) {          

// 注意这里的参数是 Test 类型的。这就意味着 TestCase TestSuite 以及以后实现 Test 接口的任何类都可以被添加进来

              fTests.addElement(test);

       }

       ……

       /**

        * Counts the number of test cases that will be run by this test.

        */

       public int countTestCases() {

              int count= 0;

              for (Enumeration e= tests(); e.hasMoreElements(); ) {

                     Test test= (Test)e.nextElement();

                     count= count + test.countTestCases();

              }

              return count;

       }

       /**

        * Runs the tests and collects their result in a TestResult.

        */

       public void run(TestResult result) {

              for (Enumeration e= tests(); e.hasMoreElements(); ) {

                    if (result.shouldStop() )

                           break;

                     Test test= (Test)e.nextElement();
                           //关键在这个方法上面

                     runTest(test, result);

              }

       }
            //这个方法里面就是递归的调用了,至于你的Test到底是什么类型的只有在运行的时候得知
            public void runTest(Test test, TestResult result) {
                   test.run(result);
            }

……

}

 

//TestCase 的部分有关源码 ——Leaf 角色,你编写的测试类就是继承自它

public abstract class TestCase extends Assert implements Test {

       ……

       /**

        * Counts the number of test cases executed by run(TestResult result).

        */

       public int countTestCases() {

              return 1;

       }

/**

        * Runs the test case and collects the results in TestResult.

        */

       public void run(TestResult result) {

              result.run(this);

       }

……

}

       可以看出这是一个偏重安全性的组合模式。因此在使用TestCase和TestSuite时,不能使用Test来代替。

 

五、 优缺点

从上面的举例中可以看到,组合模式有以下优点:

1)         使客户端调用简单,客户端可以一致的使用组合结构或其中单个对象,用户就不必关心自己处理的是单个对象还是整个组合结构,这就简化了客户端代码。

2)       更容易在组合体内加入对象部件 . 客户端不必因为加入了新的对象部件而更改代码。这一点符合开闭原则的要求,对系统的二次开发和功能扩展很有利!

当然组合模式也少不了缺点:组合模式不容易限制组合中的构件。

 

六、 总结

组合模式是一个应用非常广泛的设计模式,在前面已经介绍过的解释器模式、享元模式中都是用到了组合模式。它本身比较简单但是很有内涵,掌握了它对你的开发设计有很大的帮助。

这里写下了我学习组合模式的总结,希望能给你带来帮助,也希望您能给与指正。
posted @ 2006-08-29 21:44 Alex 阅读(437) | 评论 (0)编辑 收藏

一、 引子

这是一个很简单的模式,却被非常广泛的使用。之所以简单是因为在这个模式中仅仅使用到了继承关系。

继承关系由于自身的缺陷,被专家们扣上了 罪恶 的帽子。 使用委派关系代替继承关系 尽量使用接口实现而不是抽象类继承 等等专家警告,让我们这些菜鸟对继承 另眼相看

其实,继承还是有很多自身的优点所在。只是被大家滥用的似乎缺点更加明显了。合理的利用继承关系,还是能对你的系统设计起到很好的作用的。而模板方法模式就是其中的一个使用范例。

 

二、 定义与结构

GOF 给模板方法( Template Method )模式定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。这里的算法的结构,可以理解为你根据需求设计出来的业务流程。特定的步骤就是指那些可能在内容上存在变数的环节。

可以看出来,模板方法模式也是为了巧妙解决变化对系统带来的影响而设计的。使用模板方法使系统扩展性增强,最小化了变化对系统的影响。这一点,在下面的举例中可以很明显的看出来。

来看下这个简单模式的结构吧:

1)         AbstractClass (抽象类):定义了一到多个的抽象方法,以供具体的子类来实现它们;而且还要实现一个模板方法,来定义一个算法的骨架。该模板方法不仅调用前面的抽象方法,也可以调用其他的操作,只要能完成自身的使命。

2)         ConcreteClass (具体类):实现父类中的抽象方法以完成算法中与特定子类相关的步骤。

 

下面是模板方法模式的结构图。直接把《设计模式》上的图拿过来用下:

 

三、 举例

还是在我刚刚分析完源码的 JUnit 中找个例子吧。 JUnit 中的 TestCase 以及它的子类就是一个模板方法模式的例子。在 TestCase 这个抽象类中将整个测试的流程设置好了,比如先执行 Setup 方法初始化测试前提,在运行测试方法,然后再 TearDown 来取消测试设置。但是你将在 Setup TearDown 里面作些什么呢?鬼才知道呢!!因此,而这些步骤的具体实现都延迟到子类中去,也就是你实现的测试类中。

来看下相关的源代码吧。

这是 TestCase 中,执行测试的模板方法。你可以看到,里面正像前面定义中所说的那样,它制定了“算法”的框架——先执行 setUp 方法来做下初始化,然后执行测试方法,最后执行 tearDown 释放你得到的资源。

public void runBare() throws Throwable {

       setUp();

       try {

              runTest();

       }

       finally {

              tearDown();

       }

}

这 就是上面使用的两个方法。与定义中不同的是,这两个方法并没有被实现为抽象方法,而是两个空的无为方法(被称为钩子方法)。这是因为在测试中,我们并不是 必须要让测试程序使用这两个方法来初始化和释放资源的。如果是抽象方法,则子类们必须给它一个实现,不管用到用不到。这显然是不合理的。使用钩子方法,则 你在需要的时候,可以在子类中重写这些方法。

protected void setUp() throws Exception {

}

protected void tearDown() throws Exception {

}

 

四、 适用情况

根据上面对定义的分析,以及例子的说明,可以看出模板方法适用于以下情况:

1)         一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。

2)         各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。其实这可以说是一种好的编码习惯了。

3)         控制子类扩展。模板方法只在特定点调用操作,这样就只允许在这些点进行扩展。比如上面 runBare ()方法就只在 runTest 前面适用 setUp 方法。如果你不愿子类来修改你的模板方法定义的框架,你可以采用两种方式来做:一是在 API 中不体现出你的模板方法;二、将你的模板方法置为 final 就可以了。

       可以看出,使用模板方法模式可以将代码的公共行为提取出来,达到复用的目的。而且,在模板方法模式中,是由父类的模板方法来控制子类中的具体实现。这样你在实现子类的时候,根本不需要对业务流程有太多的了解。

 

五、 总结

匆匆忙忙写完了。希望大家指正!
posted @ 2006-08-29 21:39 Alex 阅读(233) | 评论 (0)编辑 收藏

key words: 门面模式 facade模式

一、 引子

门面模式是非常简单的设计模式。

 

二、 定义与结构

门面模式( facade )又称外观模式。 GOF 在《设计模式》一书中给出如下定义:为子系统中的一组接口提供一个一致的界面, Facade 模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

定义中提到的子系统是指在设计中为了降低复杂性根据一定的规则(比如业务、功能),对系统进行的划分。子系统中封装有一些类。客户程序在使用子系统的时候,可能会像下图一样零乱。

在上面的实现方法中,客户类紧紧地依赖在子系统的实现上。子系统发生的变化,很可能要影响到客户类的调用。而且子系统在不断优化、可重用化的重构路上,会产生更多更小的类。这对使用子系统的客户类来说要完成一个工作流程,似乎要记住的接口太多了。

门面模式就是为了解决这种问题而产生的。看看使用了门面模式后的图:

这样就减少了客户程序和子系统之间的耦合,增加了可维护性。

很明显,门面模式有三个角色组成:

1)         门面角色( facade ):这是门面模式的核心。它被客户角色调用,因此它熟悉子系统的功能。它内部根据客户角色已有的需求预定了几种功能组合。

2)         子系统角色:实现了子系统的功能。对它而言, façade 角色就和客户角色一样是未知的,它没有任何 façade 角色的信息和链接。

3)         客户角色:调用 façade 角色来完成要得到的功能。

   

   

三、 举例

Facade 一个典型应用就是进行数据库连接。一般我们在每一次对数据库进行访问,都要进行以下操作:先得到 connect 实例,然后打开 connect 获得连接,得到一个 statement ,执行 sql 语句进行查询,得到查询结果集。

       我们可以将这些步骤提取出来,封装在一个类里面。这样,每次执行数据库访问只需要将必要的参数传递到这个类中就可以了。

       有兴趣可以在你正在进行的系统中实现一下。

   

   

四、 使用环境和优点

《设计模式》给出了门面模式的使用环境:

1)         当你要为一个复杂子系统提供一个简单接口时。在上面已经描述了原因。

2)         客户程序与抽象类的实现部分之间存在着很大的依赖性。引入 facade 将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性和可移植性(上面也提到了)。

3)         当你需要构建一个层次结构的子系统时,使用 facade 模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,你可以让它们仅通过 facade 进行通讯,从而简化了它们之间的依赖关系。

以下是它的优点:

1)         它对客户屏蔽子系统组件,因而减少了客户处理的对象的数目并使得子系统使用起来更加方便。

2)         它实现了子系统与客户之间的松耦合关系,而子系统内部的功能组件往往是紧耦合的。松耦合关系使得子系统的组件变化不会影响到它的客户。 Facade 模式有助于建立层次结构系统,也有助于对对象之间的依赖关系分层。 Facade 模式可以消除复杂的循环依赖关系。这一点在客户程序与子系统是分别实现的时候尤为重要。在大型软件系统中降低编译依赖性至关重要。在子系统类改变时,希望尽量减少重编译工作以节省时间。用 Facade 可以降低编译依赖性,限制重要系统中较小的变化所需的重编译工作。 Facade 模式同样也有利于简化系统在不同平台之间的移植过程,因为编译一个子系统一般不需要编译所有其他的子系统。

3)         如果应用需要,它并不限制它们使用子系统类。因此你可以让客户程序在系统易用性和通用性之间加以选择。

 

 

  五、 java 中的门面模式

先来想想门面模式和我们已经讲过的哪个模式相像?答案就是 抽象工厂 模式 两者虽然在分类上有所区别,但是都是为了方便客户程序的使用而建立。两者的不同应该在于门面模式不仅方便了客户的使用,而且隐藏了不该让客户知道的类(这些类仅仅为子系统的其他类服务)。

但是在 java 语言中提供的包的概念已经能够很好的解决上面门面模式提到的问题。你可以把一个子系统放在一个包里面,里面要提供给外面访问的类定义为 public ,而不该公布的类就可以设计为非 public

因此,在一定程度上,门面模式在 java 中基本上可以不使用了。

标准的门面模式虽然可以不再使用,但是这种提供一个中间类或者中间方法来方便客户程序使用的思想应该值得我们来实践的

   

   

六、 总结  

门面模式从整体来看,给我的感觉是,它对于使两层之间的调用粗颗粒化很有帮助,避免了大量细颗粒度的访问。这和 SOA 中的一些观点是相同的。

   门面模式大体介绍完了。请指正:)

门面模式从整体来看,给我的感觉是,它对于使两层之间的调用粗颗粒化很有帮助,避免了大量细颗粒度的访问。这和 SOA 中的一些观点是相同的。

   门面模式大体介绍完了。请指正:)

posted @ 2006-08-29 20:46 Alex 阅读(4275) | 评论 (0)编辑 收藏

一、 引子

单例模式是设计模式中使用很频繁的一种模式,在各种开源框架、应用系统中多有应用,在我前面的几篇文章中也结合其它模式使用到了单例模式。这里我们就单例模式进行系统的学习。并对有人提出的 单例模式是邪恶的 这个观点进行了一定的分析。

 

二、 定义与结构

单例模式又叫做单态模式或者单件模式。在 GOF 书中给出的定义为:保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例模式中的 单例 通常用来代表那些本质上具有唯一性的系统组件(或者叫做资源)。比如文件系统、资源管理器等等。

单 例模式的目的就是要控制特定的类只产生一个对象,当然也允许在一定情况下灵活的改变对象的个数。那么怎么来实现单例模式呢?一个类的对象的产生是由类构造 函数来完成的,如果想限制对象的产生,就要将构造函数变为私有的(至少是受保护的),使得外面的类不能通过引用来产生对象;同时为了保证类的可用性,就必 须提供一个自己的对象以及访问这个对象的静态方法。

现在对单例模式有了大概的了解了吧,其实单例模式在实现上是非常简单的 —— 只有一个角色,而客户则通过调用类方法来得到类的对象。

放上一个类图吧,这样更直观一些:

 

单例模式可分为有状态的和无状态的。有状态的单例对象一般也是可变的单例对象, 多个单态对象在一起就可以作为一个状态仓库一样向外提供服务。没有状态的单例对象也就是不变单例对象,仅用做提供工具函数。

 

三、 实现

在单例模式的实现上有几种不同的方式,我在这里将一一讲解。先来看一种方式,它在《 java 与模式》中被称为饿汉式。

 

public class Singleton {

// 在自己内部定义自己一个实例

// 注意这是 private 只供内部调用

private static Singleton instance = new Singleton();

// 如上面所述,将构造函数设置为私有

private Singleton(){

}  

// 静态工厂方法, 提供了一个供外部访问得到对象 的静态方法  
   public static Singleton getInstance() {
     return instance;   
   }
}

 

       下面这种方式被称为懒汉式: P

 

public class Singleton {

       // 和上面有什么不同?

private static Singleton instance = null;

// 设置为私有的构造函数

private Singleton(){

}  

// 静态工厂方法

public static synchronized Singleton getInstance() {

// 这个方法比上面有所改进
          if (instance==null)

              instance new Singleton();
          return instance;   

}

}

 

先让我们来比较一下这两种实现方式。

首先他们的构造函数都是私有的,彻底断开了使用构造函数来得到类的实例的通道,但是这样也使得类失去了多态性(大概这就是为什么有人将这种模式称作单态模式)。  

在第二种方式中,对静态工厂方法进行了同步处理,原因很明显——为了防止多线程环境中产生多个实例;而在第一种方式中则不存在这种情况。

       在第二种方式中将类对自己的实例化延迟到第一次被引用的时候。而在第一种方式中则是在类被加载的时候实例化,这样多次加载会照成多次实例化。但是第二种方式由于使用了同步处理,在反应速度上要比第一种慢一些。

       java 与模式》书中提到,就 java 语言来说,第一种方式更符合 java 语言本身的特点。

       以 上两种实现方式均失去了多态性,不允许被继承。还有另外一种灵活点的实现,将构造函数设置为受保护的,这样允许被继承产生子类。这种方式在具体实现上又有 所不同,可以将父类中获得对象的静态方法放到子类中再实现;也可以在父类的静态方法中进行条件判断来决定获得哪一个对象;在 GOF 中认为最好的一种方式是维护一张存有对象和对应名称的注册表(可以使用 HashMap 来实现)。下面的实现参考《 java 与模式》采用带有注册表的方式。

 

import java.util.HashMap;

 

public class Singleton

{

// 用来存放对应关系

       private static HashMap sinRegistry = new HashMap();

       static private Singleton s = new Singleton();

       // 受保护的构造函数

       protected Singleton()

       {}

       public static Singleton getInstance(String name)

       {

              if(name == null)

                     name = "Singleton";

              if(sinRegistry.get(name)==null)

              {

                     try{

                            sinRegistry.put(name , Class.forName(name).newInstance());

                     }catch(Exception e)

                     {

                            e.printStackTrace();

                     }     

              }

              return (Singleton)(sinRegistry.get(name)); 

       }

       public void test()

       {

              System.out.println("getclasssuccess!");      

       }

}

 

public class SingletonChild1 extends Singleton

{

       public SingletonChild1(){}

       static    public SingletonChild1 getInstance()

       {

              return (SingletonChild1)Singleton.getInstance("SingletonChild1");     

       }

       public void test()

       {

              System.out.println("getclasssuccess111!"); 

       }

}

 

java 中子类的构造函数的范围不能比父类的小,所以可能存在不守规则的客户程序使用其构造函数来产生实例。

 

四、单例模式邪恶论

看这题目也许有点夸张,不过这对初学者是一个很好的警告。单例模式在 java 中的使用存在很多陷阱和假象,这使得没有意识到单例模式使用局限性的你在系统中布下了隐患……

其实这个问题早在 2001 年的时候就有人在网上系统的提出来过,我在这里只是老生常谈了。但是对于大多的初学者来说,可能这样的观点在还很陌生。下面我就一一列举出单例模式在 java 中存在的陷阱。

 

多个虚拟机

当系统中的单例类被拷贝运行在多个虚拟机下的时候,在每一个虚拟机下都可以创建一个实例对象。在使用了 EJB JINI RMI 技术的分布式系统中,由于中间件屏蔽掉了分布式系统在物理上的差异,所以对你来说,想知道具体哪个虚拟机下运行着哪个单例对象是很困难的。

因此,在使用以上分布技术的系统中,应该避免使用存在状态的单例模式,因为一个有状态的单例类,在不同虚拟机上,各个单例对象保存的状态很可能是不一样的,问题也就随之产生。而且在 EJB 中不要使用单例模式来控制访问资源,因为这是由 EJB 容器来负责的。在其它的分布式系统中,当每一个虚拟机中的资源是不同的时候,可以考虑使用单例模式来进行管理。

 

多个类加载器

当存在多个类加载器加载类的时候,即使它们加载的是相同包名,相同类名甚至每个字节都完全相同的类,也会被区别对待的。因为不同的类加载器会使用不同的命名空间( namespace )来区分同一个类。因此,单例类在多加载器的环境下会产生多个单例对象。

也许你认为出现多个类加载器的情况并不是很多。其实多个类加载器存在的情况并不少见。在很多 J2EE 服务器上允许存在多个 servlet 引擎,而每个引擎是采用不同的类加载器的;浏览器中 applet 小程序通过网络加载类的时候,由于安全因素,采用的是特殊的类加载器,等等。

       这种情况下,由状态的单例模式也会给系统带来隐患。因此除非系统由协调机制,在一般情况下不要使用存在状态的单例模式。

 

       错误的同步处理

       在使用上面介绍的懒汉式单例模式时,同步处理的恰当与否也是至关重要的。不然可能会达不到得到单个对象的效果,还可能引发死锁等错误。因此在使用懒汉式单例模式时一定要对同步有所了解。不过使用饿汉式单例模式就可以避免这个问题。

 

       子类破坏了对象控制

       在上一节介绍最后一种扩展性较好的单例模式实现方式的时候,就提到,由于类构造函数变得不再私有,就有可能失去对对象的控制。这种情况只能通过良好的文档来规范。

 

       串行化(可序列化)

为了使一个单例类变成可串行化的,仅仅在声明中添加“ implements Serializable ”是不够的。因为一个串行化的对象在每次返串行化的时候,都会创建一个新的对象,而不仅仅是一个对原有对象的引用。为了防止这种情况,可以在单例类中加入 readResolve 方法。 关于这个方法的具体情况请参考《 Effective Java 》一书第 57 条建议。

其实对象的串行化并不仅局限于上述方式,还存在基于 XML 格式的对象串行化方式。这种方式也存在上述的问题,所以在使用的时候要格外小心。

 

       上面罗列了一些使用单例模式时可能会遇到的问题。而且这些问题都和 java 中的类、线程、虚拟机等基础而又复杂的概念交织在一起,你如果稍不留神……。但是这并不代表着单例模式就一无是处,更不能一棒子将其打死。它还是不可缺少的一种基础设计模式,它对一些问题提供了非常有效的解决方案,在 java 中你完全可以把它看成编码规范来学习,只是使用的时候要考虑周全些就可以了。

 

五、 题外话

抛开单例模式,使用下面一种简单的方式也能得到单例,而且如果你确信此类永远是单例的,使用下面这种方式也许更好一些。

public static final Singleton INSTANCE = new Singleton();

而使用单例模式提供的方式,这可以在不改变 API 的情况下,改变我们对单例类的具体要求。

 

六、 总结

竭尽所能写下了关于单例模式比较详细的介绍,请大家指正。
posted @ 2006-08-29 19:05 Alex 阅读(360) | 评论 (0)编辑 收藏

一、引子

装饰模式?肯定让你想起又黑又火的家庭装修来。其实两者在道理上还是有很多相像的地方。家庭装修无非就是要掩盖住原来实而不华的墙面,抹上一层华而不实的涂料,让生活多一点色彩。而墙还是那堵墙,他的本质一点都没有变,只是多了一层外衣而已。

那设计模式中的装饰模式,是什么样子呢?

 

二、定义与结构

装饰模式( Decorator )也叫包装器模式( Wrapper )。 GOF 在《设计模式》一书中给出的定义为:动态地给一个对象添加一些额外的职责。就增加功能来说, Decorator 模式相比生成子类更为灵活。

       让我们来理解一下这句话。我们来设计 这个类。假设你根据需求为 类作了如下定义:

现在,在系统的一个地方需要一个能够报警的 Door ,你来怎么做呢?你或许写一个 Door 的子类 AlarmDoor ,在里面添加一个子类独有的方法 alarm() 。嗯,那在使用警报门的地方你必须让客户知道使用的是警报门,不然无法使用这个独有的方法。而且,这个还违反了 Liskov 替换原则。

也许你要说,那就把这个方法添加到 Door 里面,这样不就统一了?但是这样所有的门都必须有警报,至少是个 哑巴 警报。而当你的系统仅仅在一两个地方使用了警报门,这明显是不合理的 —— 虽然可以使用缺省适配器来弥补一下。

       这时候,你可以考虑采用装饰模式来给门动态的添加些额外的功能。

       下面我们来看看装饰模式的组成,不要急着去解决上面的问题,到了下面自然就明白了!

1)         抽象构件角色( Component ):定义一个抽象接口,以规范准备接收附加责任的对象。

2)         具体构件角色 (Concrete Component) :这是被装饰者,定义一个将要被装饰增加功能的类。

3)         装饰角色 (Decorator) :持有一个构件对象的实例,并定义了抽象构件定义的接口。

4)         具体装饰角色 (Concrete Decorator) :负责给构件添加增加的功能。

看下装饰模式的类图:

图中 ConcreteComponent 可能继承自其它的体系,而为了实现装饰模式,他还要实现 Component 接口。整个装饰模式的结构是按照组合模式来实现的,但是注意两者的目的是截然不同的(关于两者的不同请关注我以后的文章)。

 

三、举例

这个例子还是来自我最近在研究的 JUnit ,如果你对 JUnit 还不太了解,可以参考 JUnit入门》 JUnit源码分析(一)》 JUnit源码分析(二)》 JUnit源码分析(三)》 。不愧是由 GoF 之一的 Erich Gamma 亲自开发的,小小的东西使用了 N 种的模式在里面。下面就来看看 JUnit 中的装饰模式。

       JUnit 中, TestCase 是一个很重要的类,允许对其进行功能扩展。

       junit.extensions 包中, TestDecorator RepeatedTest 便是对 TestCase 的装饰模式扩展。下面我们将它们和上面的角色对号入座。

       呵呵,看看源代码吧,这个来的最直接!

       // 这个就是抽象构件角色

       public interface Test {

       /**

        * Counts the number of test cases that will be run by this test.

        */

       public abstract int countTestCases();

       /**

        * Runs a test and collects its result in a TestResult instance.

        */

       public abstract void run(TestResult result);

}

 

// 具体构件对象,但是这里是个抽象类

public abstract class TestCase extends Assert implements Test {

       ……

       public int countTestCases() {

              return 1;

       }

       ……

       public TestResult run() {

              TestResult result= createResult();

              run(result);

              return result;

       }

       public void run(TestResult result) {

              result.run(this);

       }

       ……

}

 

// 装饰角色

public class TestDecorator extends Assert implements Test {

       // 这里按照上面的要求,保留了一个对构件对象的实例

       protected Test fTest;

 

       public TestDecorator(Test test) {

              fTest= test;

       }

       /**

        * The basic run behaviour.

        */

       public void basicRun(TestResult result) {

              fTest.run(result);

       }

       public int countTestCases() {

              return fTest.countTestCases();

       }

       public void run(TestResult result) {

              basicRun(result);

       }

       public String toString() {

              return fTest.toString();

       }

       public Test getTest() {

              return fTest;

       }

}

 

       // 具体装饰角色,这个类的增强作用就是可以设置测试类的执行次数

public class RepeatedTest extends  TestDecorator {

    private int fTimesRepeat;

 

    public RepeatedTest(Test test, int repeat) {

           super(test);

           if (repeat < 0)

                  throw new IllegalArgumentException("Repetition count must be > 0");

           fTimesRepeat= repeat;

    }

    // 看看怎么装饰的吧

    public int countTestCases() {

           return super.countTestCases()*fTimesRepeat;

    }

    public void run(TestResult result) {

           for (int i= 0; i < fTimesRepeat; i++) {

                  if (result.shouldStop())

                         break;

                  super.run(result);

           }

    }

    public String toString() {

           return super.toString()+"(repeated)";

    }

}

       使用的时候,就可以采用下面的方式:

TestDecorator test = new RepeatedTest(new TestXXX() , 3);

让我们在回想下上面提到的 的问题,这个警报门采用了装饰模式后,可以采用下面的方式来产生。

DoorDecorator alarmDoor = new AlarmDoor(new Door());

 

 

四、应用环境

       GOF 书中给出了以下使用情况:

1)         在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。

2)         处理那些可以撤消的职责。

3)         当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。

来分析下 JUnit 的使用是属于哪种情况。首先实现了比静态继承更加灵活的方式,动态的增加功能。试想为 Test 的所有实现类通过继承来增加一个功能,意味着要添加不少的功能类似的子类,这明显是不太合适的。

而且,这就避免了高层的类具有太多的特征,比如上面提到的带有警报的抽象门类。

 

五、透明和半透明

       对于面向接口编程,应该尽量使客户程序不知道具体的类型,而应该对一个接口操作。这样就要求装饰角色和具体装饰角色要满足 Liskov 替换原则。像下面这样:

Component c = new ConcreteComponent();

Component c1 = new ConcreteDecorator(c);

JUnit 中就属于这种应用,这种方式被称为透明式。而在实际应用中,比如 java.io 中往往因为要对原有接口做太多的扩展而需要公开新的方法(这也是为了重用)。所以往往不能对客户程序隐瞒具体的类型。这种方式称为 半透明式

java.io 中,并不是纯装饰模式的范例,它是装饰模式、适配器模式的混合使用。

 

六、其它

采用 Decorator 模式进行系统设计往往会产生许多看上去类似的小对象,这些对象仅仅在他们相互连接的方式上有所不同,而不是它们的类或是它们的属性值有所不同。尽管对于那些了解这些系统的人来说,很容易对它们进行定制,但是很难学习这些系统,排错也很困难。这是 GOF 提到的装饰模式的缺点,你能体会吗?他们所说的小对象我认为是指的具体装饰角色。这是为一个对象动态添加功能所带来的副作用。

 

七、总结

       终于写完了,不知道说出了本意没有。请指正!
posted @ 2006-08-29 18:57 Alex 阅读(468) | 评论 (0)编辑 收藏

一、 引子

还记得警匪片上,匪徒们是怎么配合实施犯罪的吗?一个团伙在进行盗窃的时候,总     有一两个人在门口把风——如果有什么风吹草动,则会立即通知里面的同伙紧急撤退。也许放风的人并不一定认识里面的每一个同伙;而在里面也许有新来的小弟不认识这个放风的。但是这没什么,这个影响不了他们之间的通讯,因为他们之间有早已商定好的暗号。

呵呵,上面提到的放风者、偷窃者之间的关系就是观察者模式在现实中的活生生的例子。

 

二、 定义与结构

观察者( Observer )模式又名发布 - 订阅( Publish/Subscribe )模式。 GOF 给观察者模式如下定义:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

在这里先讲一下面向对象设计的一个重要原则——单一职责原则。因此系统的每个对象应该将重点放在问题域中的离散抽象上。因此理想的情况下,一个对象只做一件事情。这样在开发中也就带来了诸多的好处:提供了重用性和维护性,也是进行重构的良好的基础。

因此几乎所有的设计模式都是基于这个基本的设计原则来的。观察者模式的起源我觉得应该是在 GUI 和业务数据的处理上,因为现在绝大多数讲解观察者模式的例子都是这一题材。但是观察者模式的应用决不仅限于此一方面。

下面我们就来看看观察者模式的组成部分。

1)         抽象目标角色( Subject ):目标角色知道它的观察者,可以有任意多个观察者观察同一个目标。并且提供注册和删除观察者对象的接口。目标角色往往由抽象类或者接口来实现。

2)         抽象观察者角色( Observer ):为那些在目标发生改变时需要获得通知的对象定义一个更新接口。抽象观察者角色主要由抽象类或者接口来实现。

3)         具体目标角色( Concrete Subject ):将有关状态存入各个 Concrete Observer 对象。当它的状态发生改变时 , 向它的各个观察者发出通知。

4)         具体观察者角色( Concrete Observer ):存储有关状态,这些状态应与目标的状态保持一致。实现 Observer 的更新接口以使自身状态与目标的状态保持一致。在本角色内也可以维护一个指向 Concrete Subject 对象的引用。

放上观察者模式的类图,这样能将关系清晰的表达出来。

       可以看得出来,在 Subject 这个抽象类中,提供了上面提到的功能,而且存在一个通知方法: notify 。还可以看出来 Subject ConcreteSubject 之间可以说是使用了模板模式(这个模式真是简单普遍到一不小心就用到了)。

       这样当具体目标角色的状态发生改变,按照约定则会去调用通知方法,在这个方法中则会根据目标角色中注册的观察者名单来逐个调用相应的 update 方法来调整观察者的状态。这样观察者模式就走完了一个流程。

       在下面的例子中会更深刻的体验到这个流程的。

 

三、 举例

观察者模式是我在《 JUnit 源代码分析》中遗留的一个模式,因此这里将采用 JUnit 来作为例子。

JUnit 为用户提供了三种不同的测试结果显示界面,以后还可能会有其它方式的现实界面……。怎么才能将测试的业务逻辑和显示结果的界面很好的分离开?不用问,就是观察者模式!

下面我们来看看 JUnit 中观察者模式的使用代码:

// 下面是我们的抽象观察者角色, JUnit 是采用接口来实现的,这也是一般采用的方式。

// 可以看到这里面定义了四个不同的 update 方法,对应四种不同的状态变化

public interface TestListener {

       /**

        * An error occurred.

        */

       public void addError(Test test, Throwable t);

       /**

        * A failure occurred.

        */

       public void addFailure(Test test, AssertionFailedError t); 

       /**

        * A test ended.

        */

       public void endTest(Test test);

       /**

        * A test started.

        */

       public void startTest(Test test);

}

 

// 具体观察者角色,我们采用最简单的 TextUI 下的情况来说明( AWT Swing 对于整天做 Web 应用的人来说,已经很陌生了)

public class ResultPrinter implements TestListener {

       // 省略好多啊,主要是显示代码

……

       // 下面就是实现接口 TestListener 的四个方法

       // 填充方法的行为很简单的说

       /**

        * @see junit.framework.TestListener#addError(Test, Throwable)

        */

       public void addError(Test test, Throwable t) {

              getWriter().print("E");

       }

       /**

        * @see junit.framework.TestListener#addFailure(Test, AssertionFailedError)

        */

       public void addFailure(Test test, AssertionFailedError t) {

              getWriter().print("F");

       }

       /**

        * @see junit.framework.TestListener#endTest(Test)

        */

       public void endTest(Test test) {

       }

       /**

        * @see junit.framework.TestListener#startTest(Test)

        */

       public void startTest(Test test) {

              getWriter().print(".");

              if (fColumn++ >= 40) {

                     getWriter().println();

                     fColumn= 0;

              }

       }

}

 

来看下我们的目标角色,随便说下,由于 JUnit 功能的简单,只有一个目标—— TestResult ,因此 JUnit 只有一个具体目标角色。

// 好长的代码,好像没有重点。去掉了大部分与主题无关的信息

// 下面只列出了当 Failures 发生时是怎么来通知观察者的

public class TestResult extends Object {

       // 这个是用来存放测试 Failures 的集合

protected Vector fFailures;

// 这个就是用来存放注册进来的观察者的集合

       protected Vector fListeners;

 

       public TestResult() {

              fFailures= new Vector();

              fListeners= new Vector();

       }

       /**

        * Adds a failure to the list of failures. The passed in exception

        * caused the failure.

        */

       public synchronized void addFailure(Test test, AssertionFailedError t) {

              fFailures.addElement(new TestFailure(test, t));

              // 下面就是通知各个观察者的 addFailure 方法

              for (Enumeration e= cloneListeners().elements(); e.hasMoreElements(); ) {

                     ((TestListener)e.nextElement()).addFailure(test, t);

              }

       }

       /**

        * 注册一个观察者

        */

       public synchronized void addListener(TestListener listener) {

              fListeners.addElement(listener);

       }

       /**

        * 删除一个观察者

        */

       public synchronized void removeListener(TestListener listener) {

              fListeners.removeElement(listener);

       }

       /**

        * 返回一个观察者集合的拷贝,当然是为了防止对观察者集合的非法方式操作了

     * 可以看到所有使用观察者集合的地方都通过它

        */

       private synchronized Vector cloneListeners() {

              return (Vector)fListeners.clone();

       }

       ……

}

 

嗯,观察者模式组成所需要的角色在这里已经全了。不过好像还是缺点什么……。呵呵,对!就是它们之间还没有真正的建立联系。在 JUnit 中是通过 TestRunner 来作的,而你在具体的系统中可以灵活掌握。

看一下 TestRunner 中的代码:

public class TestRunner extends BaseTestRunner {

       private ResultPrinter fPrinter;

public TestResult doRun(Test suite, boolean wait) {

// 就是在这里注册的

              result.addListener(fPrinter);

……

 

四、 使用情况

GOF 给出了以下使用观察者模式的情况:

1)         当一个抽象模型有两个方面 , 其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。

2)         当对一个对象的改变需要同时改变其它对象 , 而不知道具体有多少对象有待改变。

3)         当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之 , 你不希望这些对象是紧密耦合的。

其实观察者模式同前面讲过的桥梁、策略有着共同的使用环境:将变化独立封装起来,以达到最大的重用和解耦。观察者与后两者不同的地方在于,观察者模式中的目标和观察者的变化不是独立的,而是有着某些联系。

 

五、 我推你拉

观 察者模式在关于目标角色、观察者角色通信的具体实现中,有两个版本。一种情况便是目标角色在发生变化后,仅仅告诉观察者角色“我变化了”;观察者角色如果 想要知道具体的变化细节,则就要自己从目标角色的接口中得到。这种模式被很形象的称为:拉模式——就是说变化的信息是观察者角色主动从目标角色中“拉”出 来的。

还有一种方法,那就是我目标角色“服务一条龙”,通知你发生变化的同时,通过一个参数将变化的细节传递到观察者角色中去。这就是“推模式”——管你要不要,先给你啦。

这两种模式的使用,取决于系统设计时的需要。如果目标角色比较复杂,并且观察者角色进行更新时必须得到一些具体变化的信息,则“推模式”比较合适。如果目标角色比较简单,则“拉模式”就很合适啦。

 

六、 总结

大概的介绍了下观察者模式。希望能对你有所帮助。

posted @ 2006-08-29 18:08 Alex 阅读(287) | 评论 (0)编辑 收藏

一、 引子

记得一年前,我开始陆陆续续在自己的 blog 上连载《深入浅出设计模式》。其内容无出经典巨著《设计模式》之右,仅仅偶有己见,但是它记录了我学习、思考和讲述设计模式的过程。一晃,距离写成最后一片设计模式的文章已有 3 月余,我却迟迟没有对设计模式做一个总结。心想,总不能虎头蛇尾吧,于是便有了这篇文章。

 

二、 回顾 23 种设计模式

先来回顾下这 23 种经典的设计模式吧,下图给出了 GOF 对它们的分类:

 

   

    图中从两个纬度将 23 种设计模式划分为六大类:创建型类模式、创建型对象模式、结构型类模式、结构型对象模式、行为型类模式、行为型对象模式。 GOF 对这 23 种模式的划分是有一定道理的,虽然人为的类型划分,说到底还是有些牵强,但是如 GOF 所说,它至少可以帮助记忆学习。

    被分为六大块的 23 种设计模式并不是割裂开来的,很多模式的使用往往是相生相伴的,像工厂与单例,装饰与组合等等。 GOF 给出了模式间的关系详细描述如下图:

 

   

    记得曾经不止一次有人问我:这模式和那模式感觉上一样啊,有什么区别啊。同样,在很多论坛上也充满了这样的疑问。其实这是很正常的,面向对象设计、编程所能使用的方式不外乎这几种:继承、组合、封装行为、利用多态等等,所以 23 种模式中翻来覆去的使用这几种方式,看起来当然是似曾相逢。有人曾留言给我,让我着重表述这些模式之间的区别与类似。我当时也许诺会在最后写一篇总结性的文章专门讨论这个话题,但是现在我不打算这样干了。

 

三、俯瞰全局、 追踪溯源

    什么是设计模式? GOF 在书中如是说: 设计模式是对被用来在特定场景下解决一般设计问题的类和相互通信的对象的描述; John Vlissides 曾说过:在设计模式中,仅有的、最重要的就是不断的反省;而我将它比作软件开发中经验积累出来的“公式”。

    通看这 23 种模式,就应了 Dennis DeBruler 曾说过的一句话:计算机科学是一门相信所有问题都可以通过多一个间接层 indirection )来解决的科学。在前面关于具体模式的文章中,我曾经不只一次的提到“中间层”。但是直到读到这句话,才使我跳出具体模式的束缚俯瞰全局。

    23 种模式似乎不再神奇,它们在解决问题上的思路是如此的相似——添加间接层。何止模式如此,正如 Dennis DeBruler 所言,我们所接触的很多技术都是采用这种手段,如: J2EE 的分层技术、重构等等。但是在具体操作上, 23 种模式中间接层的应用可大可小,无比灵活。观察者模式在动作触发端与动作执行端之间加入了目标角色层,解除了两端之间的耦合,巧妙地解决了一对多的关系;单例模式将构造方法私有化,并在使用者与构造方法之间添加一个获得唯一实例的静态方法,达到控制实例个数的目的。

    间接层应用如此广泛,得益于它能带来如下好处:共享逻辑(重用)、分离意图和实现(提高灵活性)、 隔 离变化(封装)、解耦等等。既然我们知道了间接层这么一回事,似乎我们可以不用知道设计模式也能做出像设计模式那样好的设计来。但是要记住,间接层应用过 于泛滥,则会过犹不及,它会导致简单问题复杂化、跳跃阅读难以理解等问题。那如何在设计中把握使用间接层的度呢?设计模式也许是很好的范例——你毕竟是站 在了巨人的肩上。

   

再深入一层细看,在设计模式中,广泛遵循了两条设计原则:面向接口编程,而不是实现;优先使用组合,而不是继承。这两条原则带来的好处,自然不用再说了。说到设计原则, 现在为人熟知的设计原则有很多,如: 单一职责原则( SRP )、开闭原则( OCP )、 Liskov 替换原则( LSP )、依赖倒置原则( DIP )和接口隔离原则( ISP )等等。这些原则的出现大多都早于设计模式,但同设计模式一样,是 OOD 过程中经验积累的结晶。它们在 23 种设计模式中都有体现,因此了解设计原则可以帮助你更好的分析和理解设计模式。

当然,这并不是说设计模式就是建立在设计原则的基础之上的。两者之间的关系是互相促进的。设计原则的诞生,也许会促成新的设计模式;设计模式的出现,也许会提炼出新的设计原则。

 

现在不禁要问,为什么使用设计模式。也许你的回答会是:提高设计的重用度、灵活性、可维护性等等。但是我认为更准确的回答应该是:解决系统设计中现有的问题。这便又回到了 GOF 给设计模式下的定义上了,绕了一个圈子,原来答案就这么简单。

这也就是我不想详细讲解各种模式区别的原因了,要解决的问题不一样就是它们之间最大的不同。如果还要详细分析,那它们都已写在 GOF 巨著中了——就是它们的定义、使用范围、优缺点等等。

 

四、 活学活用

是否我们必须按照这本巨著上描述的形式来使用设计模式呢?肯定不是这样的。数学物理公式在不同的条件下还会有不同的衍生式,何况这些在实践中总结的经验呢。

Martin 建 议根据问题的复杂性逐步的引入设计模式。这是个很好的建议,它避免你套用模式而带来了过度的设计,而这些过度的设计也许直到最后都派不上用场。比如,你的 系统中现在就仅有一个适配器角色,或者各个适配器角色没有什么共性,那么目标角色和适配器角色就可以合为一个,这样使得设计模式更加符合你系统的特性。

不 仅如此,做到设计模式的活学活用,我认为还要做到以解决问题为中心,将设计模式融合使用,避免为了设计而模式。当然这是建立在对各种设计模式了如指掌的情 况下。比如,有一段解析字符串的对象,而在使用它之前,还要做一些参数的判断等其他非解析操作,这时,你很快就会想起使用代理模式;但是这个代理提供的接 口可能有两三个(满足不同情形),而解析对象仅有一个接口,命名还不相同,这时你又想到了适配器模式。这时也许将两者融合成你独有的解决方案比笨拙的套用 两个模式好的多得多。

 

五、现在看模式

GOF 说过,这 23 种模式仅仅是对一般设计问题的总结。现在许多专有领域也开始出现了大量的设计模式,至少在我最了解的企业应用这个方向是这样的。其中有一部分模式仅仅是对 GOF 设计模式的再次包装。我们不妨叫 GOF 23 种设计模式为原始设计模式。

但是遗憾的是,越来越方便的框架支持,领域模型简化造成代码过程化、脚本化,使得在企业应用中很难看到原始设计模式的影子(当然还是可以看到遍地的命令模式)。比如: IoC 容器将单例、工厂、原型模式包装了起来,你现在需要做的仅仅是填写配置文件;框架集成了观察者、模版等等模式,你仅仅按照框架说明实现具体对象就可以了;过程化、脚本化的代码里面更不要提什么设计模式了!更甚者在 EJB 中单例模式差点变成了反模式。

原 始的设计模式没有用,过时了吗?如果你不甘心仅仅做一名代码组装工;想对你们部门的高手设计的框架品头论足的话,答案就是否定的。原始设计模式有很多的确 是难得一见了,但是了解它们绝对不是在浪费你的时间,它可以让你在解决问题的时候思路更开阔一些——它的思想比它的架势更重要。          

 

 

写在最后(其实应该在最前面)

    细想自己在学习设计模式时,常常埋怨《 Java 与模式》肤浅无物,为了模式而模式;又感叹 GOF 写 得高度概括,苦于理解。于是便有了将自己对设计模式的认识写下来的想法。正巧参与了部门组织的一次设计模式讲座,触发了第一篇文章的诞生。从现在来看,文 章倒是全写成了,可内容上却不能让自己满意,却又懒得动手修改(谈何容易)。“深入”二字说来容易,做到何其难,自以为这些文章的分量远远够不上;倒是 “浅出”,自以为还可以沾上点边。你可以把本系列文章看作是《 Java 与模式》的替代品,帮你叩开设计模式之门。如果你要深入研究设计模式,我劝你还是去研读《设计模式》一书吧。
posted @ 2006-08-29 17:56 Alex 阅读(218) | 评论 (0)编辑 收藏

一、引子
      
话说十年前,有一个暴发户,他家有三辆汽车 ——Benz 奔驰、 Bmw 宝马、 Audi 奥迪,还雇了司机为他开车。不过,暴发户坐车时总是怪怪的:上 Benz 车后跟司机说 开奔驰车! ,坐上 Bmw 后他说 开宝马车! ,坐上 Audi 开奥迪车! 。你一定说:这人有病!直接说开车不就行了?!
       
而当把这个暴发户的行为放到我们程序设计中来时,会发现这是一个普遍存在的现象。幸运的是,这种有病的现象在 OO (面向对象)语言中可以避免了。下面就以 Java 语言为基础来引入我们本文的主题:工厂模式。

二、分类
      
工厂模式主要是为创建对象提供过渡接口,以便将创建对象的具体过程屏蔽隔离起来,达到提高灵活性的目的。

工厂模式在《 Java 与模式》中分为三类:
      1
)简单工厂模式( Simple Factory

2 )工厂方法模式( Factory Method

3 )抽象工厂模式( Abstract Factory
      
这三种模式从上到下逐步抽象,并且更具一般性。
       GOF
在《设计模式》一书中将工厂模式分为两类:工厂方法模式( Factory Method )与抽象工厂模式( Abstract Factory )。将简单工厂模式( Simple Factory )看为工厂方法模式的一种特例,两者归为一类。

两者皆可,在本文使用《 Java 与模式》的分类方法。下面来看看这些工厂模式是怎么来 治病 的。

 

三、简单工厂模式

简单工厂模式又称静态工厂方法模式。重命名上就可以看出这个模式一定很简单。它存在的目的很简单:定义一个用于创建对象的接口。
      
先来看看它的组成:

1)         工厂类角色:这是本模式的核心,含有一定的商业逻辑和判断逻辑。在 java 中它往往由一个具体类实现。

2)         抽象产品角色:它一般是具体产品继承的父类或者实现的接口。在 java 中由接口或者抽象类来实现。

3)         具体产品角色:工厂类所创建的对象就是此角色的实例。在 java 中由一个具体类实现。

来用类图来清晰的表示下的它们之间的关系(如果对类图不太了解,请参考我关于类图的文章):  

那么简单工厂模式怎么来使用呢?我们就以简单工厂模式来改造暴发户坐车的方式 —— 现在暴发户只需要坐在车里对司机说句: 开车 就可以了。

// 抽象产品角色

public interface Car{

public void drive();
}

 

// 具体产品角色
public class Benz implements Car{

public void drive()  {

System.out.println("Driving Benz ");

}

}

public class Bmw implements Car{

public void drive()  {

System.out.println("Driving Bmw ");

}

}
。。。(奥迪我就不写了 :P


//
工厂类角色

public class Driver{

// 工厂方法 . 注意 返回类型为抽象产品角色
       public staticCar driverCar(String s)throws Exception    {

              // 判断逻辑,返回具体的产品角色给 Client
              if(s.equalsIgnoreCase("Benz"))  

                     return new Benz();

              else if(s.equalsIgnoreCase("Bmw"))

                     return new Bmw();

                    ......   
             else throw new Exception();

       。。。


//
欢迎暴发户出场 ......

public class Magnate{

       public static void main(String[] args){

              try{
                     //
告诉司机我今天坐奔驰
              
                     Car car = Driver.driverCar("benz");
                     //
下命令:开车
                   
                     car.drive();

              。。。

    将本程序空缺的其他信息填充完整后即可运行。如果你将所有的类放在一个文件中,请不要忘记只能有一个类被声明为 public 。本程序在 jdk1.4 下运行通过。

      程序中各个类的关系表达如下:

 

这便是简单工厂模式了。怎么样,使用起来很简单吧?那么它带来了什么好处呢?
      
首先,使用了简单工厂模式后,我们的程序不在 有病 ,更加符合现实中的情况;而且客户端免除了直接创建产品对象的责任,而仅仅负责 消费 产品(正如暴发户所为)。

       下 面我们从开闭原则(对扩展开放;对修改封闭)上来分析下简单工厂模式。当暴发户增加了一辆车的时候,只要符合抽象产品制定的合同,那么只要通知工厂类知道 就可以被客户使用了。所以对产品部分来说,它是符合开闭原则的;但是工厂部分好像不太理想,因为每增加一辆车,都要在工厂类中增加相应的业务逻辑或者判断 逻辑,这显然是违背开闭原则的。可想而知对于新产品的加入,工厂类是很被动的。对于这样的工厂类(在我们的例子中是为司机师傅),我们称它为全能类或者上 帝类。
      
我们举的例子是最简单的情况,而在实际应用中,很可能产品是一个多层次的树状结构。由于简单工厂模式中只有一个工厂类来对应这些产品,所以这可能会把我们的上帝累坏了,也累坏了我们这些程序员 :(
      
于是工厂方法模式作为救世主出现了。


四、工厂方法模式

       工厂方法模式去掉了简单工厂模式中工厂方法的静态属性,使得它可以被子类继承。这样在简单工厂模式里集中在工厂方法上的压力可以由工厂方法模式里不同的工厂子类来分担。

你应该大致猜出了工厂方法模式的结构,来看下它的组成:

1)         抽象工厂角色:   这是工厂方法模式的核心,它与应用程序无关。是具体工厂角色必须实现的接口或者必须继承的父类。在 java 中它由抽象类或者接口来实现。

2)         具体工厂角色:它含有和具体业务逻辑有关的代码。由应用程序调用以创建对应的具体产品的对象。

3)         抽象产品角色:它是具体产品继承的父类或者是实现的接口。在 java 中一般有抽象类或者接口来实现。

4)         具体产品角色:具体工厂角色所创建的对象就是此角色的实例。在 java 中由具体的类来实现。

用类图来清晰的表示下的它们之间的关系:

工厂方法模式使用继承自抽象工厂角色的多个子类来代替简单工厂模式中的 上帝类 。正如上面所说,这样便分担了对象承受的压力;而且这样使得结构变得灵活起来 —— 当有新的产品(即暴发户的汽车)产生时,只要按照抽象产品角色、抽象工厂角色提供的合同来生成,那么就可以被客户使用,而不必去修改任何已有的代码。可以看出工厂角色的结构也是符合开闭原则的!

       我 们还是老规矩,使用一个完整的例子来看看工厂模式各个角色之间是如何来协调的。话说暴发户生意越做越大,自己的爱车也越来越多。这可苦了那位司机师傅了, 什么车它都要记得,维护,都要经过他来使用!于是暴发户同情他说:看你跟我这么多年的份上,以后你不用这么辛苦了,我给你分配几个人手,你只管管好他们就 行了!于是,工厂方法模式的管理出现了。代码如下:

// 抽象产品角色,具体产品角色与简单工厂模式类似,只是变得复杂了些,这里略。
//
抽象工厂角色
public interface Driver{
       public Car driverCar();
}
public class BenzDriver implements Driver{
       public Car driverCar(){
              return new Benz();
       }
}
public class BmwDriver implements Driver{
       public Car driverCar()   {

return new Bmw();
       }
}

// 应该和具体产品形成对应关系 ...
//
有请暴发户先生

 public class Magnate

{

              public static void main(String[] args)

              {

                     try{ 
                            Driver driver = new BenzDriver();

                            Car car = driver.driverCar();

                            car.drive();

                     }

       ……

}

可 以看出工厂方法的加入,使得对象的数量成倍增长。当产品种类非常多时,会出现大量的与之对应的工厂对象,这不是我们所希望的。因为如果不能避免这种情况, 可以考虑使用简单工厂模式与工厂方法模式相结合的方式来减少工厂类:即对于产品树上类似的种类(一般是树的叶子中互为兄弟的)使用简单工厂模式来实现。

 

五、小结

工厂方法模式仿佛已经很完美的对对象的创建进行了包装,使得客户程序中仅仅处理抽象产品角色提供的接口。那我们是否一定要在代码中遍布工厂呢?大可不必。也许在下面情况下你可以考虑使用工厂方法模式:

1)         当客户程序不需要知道要使用对象的创建过程。

2)         客户程序使用的对象存在变动的可能,或者根本就不知道使用哪一个具体的对象。

 

简 单工厂模式与工厂方法模式真正的避免了代码的改动了?没有。在简单工厂模式中,新产品的加入要修改工厂角色中的判断语句;而在工厂方法模式中,要么将判断 逻辑留在抽象工厂角色中,要么在客户程序中将具体工厂角色写死(就象上面的例子一样)。而且产品对象创建条件的改变必然会引起工厂角色的修改。

       面对这种情况, Java 的反射机制与配置文件的巧妙结合突破了限制 —— 这在 Spring 中完美的体现了出来。

 

六、抽象工厂模式

       先来认识下什么是产品族: 位于不同产品等级结构中,功能相关联的产品组成的家族。还是让我们用一个例子来形象地说明一下吧。

 

图中的 BmwCar BenzCar 就是两个产品树(产品层次结构);而如图所示的 BenzSportsCar BmwSportsCar 就是一个产品族。他们都可以放到跑车家族中,因此功能有所关联。同理 BmwBussinessCar BenzSportsCar 也是一个产品族。
     
回到抽象工厂模式的话题上。

可以说,抽象工厂模式和工厂方法模式的区别就在于需要创建对象的复杂程度上。而且抽象工厂模式是三个里面最为抽象、最具一般性的。

抽象工厂模式的用意为:给客户端提供一个接口,可以创建多个产品族中的产品对象

而且使用抽象工厂模式还要满足一下条件:

1)         系统中有多个产品族,而系统一次只可能消费其中一族产品。

2)         同属于同一个产品族的产品以其使用。

来看看抽象工厂模式的各个角色(和工厂方法的如出一辙):

1)         抽象工厂角色:   这是工厂方法模式的核心,它与应用程序无关。是具体工厂角色必须实现的接口或者必须继承的父类。在 java 中它由抽象类或者接口来实现。

2)         具体工厂角色:它含有和具体业务逻辑有关的代码。由应用程序调用以创建对应的具体产品的对象。在 java 中它由具体的类来实现。

3)         抽象产品角色:它是具体产品继承的父类或者是实现的接口。在 java 中一般有抽象类或者接口来实现。

4)         具体产品角色:具体工厂角色所创建的对象就是此角色的实例。在 java 中由具体的类来实现。

类图如下:

看过了前两个模式,对这个模式各个角色之间的协调情况应该心里有个数了,我就不举具体的例子了。只是一定要注意满足使用抽象工厂模式的条件哦。
posted @ 2006-08-29 17:26 Alex 阅读(466) | 评论 (0)编辑 收藏

工厂模式定义:提供创建对象的接口.

为何使用?
工厂模式是我们最常用的模式了,著名的Jive论坛 ,就大量使用了工厂模式,工厂模式在Java程序系统可以说是随处可见。

为什么工厂模式是如此常用?因为工厂模式就相当于创建实例对象的new,我们经常要根据类Class生成实例对象,如A a=new A() 工厂模式也是用来创建实例对象的,所以以后new时就要多个心眼,是否可以考虑实用工厂模式,虽然这样做,可能多做一些工作,但会给你系统带来更大的可扩展性和尽量少的修改量。

我们以类Sample为例, 如果我们要创建Sample的实例对象:

Sample sample=new Sample();

可是,实际情况是,通常我们都要在创建sample实例时做点初始化的工作,比如赋值 查询数据库等。

首先,我们想到的是,可以使用Sample的构造函数,这样生成实例就写成:

Sample sample=new Sample(参数);

但是,如果创建sample实例时所做的初始化工作不是象赋值这样简单的事,可能是很长一段代码,如果也写入构造函数中,那你的代码很难看了(就需要Refactor重整)。

为什么说代码很难看,初学者可能没有这种感觉,我们分析如下,初始化工作如果是很长一段代码,说明要做的工作很多,将很多工作装入一个方法中,相当于将很多鸡蛋放在一个篮子里,是很危险的,这也是有背于Java 面向对象的原则,面向对象的封装(Encapsulation)和分派(Delegation)告诉我们,尽量将长的代码分派“切割”成每段,将每段再 “封装”起来(减少段和段之间偶合联系性),这样,就会将风险分散,以后如果需要修改,只要更改每段,不会再发生牵一动百的事情。

在本例中,首先,我们需要将创建实例的工作与使用实例的工作分开, 也就是说,让创建实例所需要的大量初始化工作从Sample的构造函数中分离出去。

这时我们就需要Factory工厂模式来生成对象了,不能再用上面简单new Sample(参数)。还有,如果Sample有个继承如MySample, 按照面向接口编程,我们需要将Sample抽象成一个接口.现在Sample是接口,有两个子类MySample 和HisSample .我们要实例化他们时,如下:

Sample mysample=new MySample();
Sample hissample=new HisSample();

随着项目的深入,Sample可能还会"生出很多儿子出来", 那么我们要对这些儿子一个个实例化,更糟糕的是,可能还要对以前的代码进行修改:加入后来生出儿子的实例.这在传统程序中是无法避免的.

但如果你一开始就有意识使用了工厂模式,这些麻烦就没有了.

工厂方法
你会建立一个专门生产Sample实例的工厂:

public class Factory{

  public static Sample creator(int which){

  //getClass 产生Sample 一般可使用动态类装载装入类。
  if (which==1)
    return new SampleA();
  else if (which==2)
    return new SampleB();

  }

}

那么在你的程序中,如果要实例化Sample时.就使用

Sample sampleA=Factory.creator(1);

这样, 在整个就不涉及到Sample的具体子类,达到封装效果,也就减少错误修改的机会,这个原理可以用很通俗的话来比喻:就是具体事情做得越多,越容易范错 误.这每个做过具体工作的人都深有体会,相反,官做得越高,说出的话越抽象越笼统,范错误可能性就越少.好象我们从编程序中也能悟出人生道理?呵呵.

使用工厂方法 要注意几个角色,首先你要定义产品接口,如上面的Sample,产品接口下有Sample接口的实现类,如SampleA,其次要有一个factory类,用来生成产品Sample,如下图,最右边是生产的对象Sample:

进一步稍微复杂一点,就是在工厂类上进行拓展,工厂类也有继承它的实现类concreteFactory了

抽象工厂
工厂模式中有: 工厂方法(Factory Method) 抽象工厂(Abstract Factory).

这两个模式区别在于需要创建对象的复杂程度上。如果我们创建对象的方法变得复杂了,如上面工厂方法中是创建一个对象Sample,如果我们还有新的产品接口Sample2.

这里假设:Sample有两个concrete类SampleA和SamleB,而Sample2也有两个concrete类Sample2A和SampleB2

那么,我们就将上例中Factory变成抽象类,将共同部分封装在抽象类中,不同部分使用子类实现,下面就是将上例中的Factory拓展成抽象工厂:

public abstract class Factory{

  public abstract Sample creator();

  public abstract Sample2 creator(String name);

}

public class SimpleFactory extends Factory{

  public Sample creator(){
    .........
    return new SampleA

  }

  public Sample2 creator(String name){
    .........
    return new Sample2A

  }

}

public class BombFactory extends Factory{

  public Sample creator(){
    ......
    return new SampleB

  }

  public Sample2 creator(String name){
    ......
    return new Sample2B
  }

}

 

从上面看到两个工厂各自生产出一套Sample和Sample2,也许你会疑问,为什么我不可以使用两个工厂方法来分别生产Sample和Sample2?

抽 象工厂还有另外一个关键要点,是因为 SimpleFactory内,生产Sample和生产Sample2的方法之间有一定联系,所以才要将这两个方法捆绑在一个类中,这个工厂类有其本身特 征,也许制造过程是统一的,比如:制造工艺比较简单,所以名称叫SimpleFactory。

在实际应用中,工厂方法用得比较多一些,而且是和动态类装入器组合在一起应用,

举例

我们以Jive的ForumFactory为例,这个例子在前面的Singleton模式中我们讨论过,现在再讨论其工厂模式:

public abstract class ForumFactory {

  private static Object initLock = new Object();
  private static String className = "com.jivesoftware.forum.database.DbForumFactory";
  private static ForumFactory factory = null;

  public static ForumFactory getInstance(Authorization authorization) {
    //If no valid authorization passed in, return null.
    if (authorization == null) {
      return null;
    }
    //以下使用了Singleton 单态模式
    if (factory == null) {
      synchronized(initLock) {
        if (factory == null) {
            ......

          try {
              //动态转载类
              Class c = Class.forName(className);
              factory = (ForumFactory)c.newInstance();
          }
          catch (Exception e) {
              return null;
          }
        }
      }
    }

    //Now, 返回 proxy.用来限制授权对forum的访问
    return new ForumFactoryProxy(authorization, factory,
                    factory.getPermissions(authorization));
  }

  //真正创建forum的方法由继承forumfactory的子类去完成.
  public abstract Forum createForum(String name, String description)
  throws UnauthorizedException, ForumAlreadyExistsException;

  ....

}

 

 

因为现在的Jive是通过数据库系统存放论坛帖子等内容数据,如果希望更改为通过文件系统实现,这个工厂方法ForumFactory就提供了提供动态接口:

private static String className = "com.jivesoftware.forum.database.DbForumFactory";

你可以使用自己开发的创建forum的方法代替com.jivesoftware.forum.database.DbForumFactory就可以.

在上面的一段代码中一共用了三种模式,除了工厂模式外,还有Singleton单态模式,以及proxy模式,proxy模式主要用来授权用户对forum的访问,因为访问forum有两种人:一个是注册用户 一个是游客guest,那么那么相应的权限就不一样,而且这个权限是贯穿整个系统的,因此建立一个proxy,类似网关的概念,可以很好的达到这个效果.  

看看Java宠物店中的CatalogDAOFactory:

public class CatalogDAOFactory {

  /**

  * 本方法制定一个特别的子类来实现DAO模式。
  * 具体子类定义是在J2EE的部署描述器中。
  */

  public static CatalogDAO getDAO() throws CatalogDAOSysException {

    CatalogDAO catDao = null;

    try {

      InitialContext ic = new InitialContext();
      //动态装入CATALOG_DAO_CLASS
      //可以定义自己的CATALOG_DAO_CLASS,从而在无需变更太多代码
      //的前提下,完成系统的巨大变更。

      String className =(String) ic.lookup(JNDINames.CATALOG_DAO_CLASS);

      catDao = (CatalogDAO) Class.forName(className).newInstance();

    } catch (NamingException ne) {

      throw new CatalogDAOSysException("
        CatalogDAOFactory.getDAO: NamingException while
          getting DAO type : \n" + ne.getMessage());

    } catch (Exception se) {

      throw new CatalogDAOSysException("
        CatalogDAOFactory.getDAO: Exception while getting
          DAO type : \n" + se.getMessage());

    }

    return catDao;

  }

}


CatalogDAOFactory 是典型的工厂方法,catDao是通过动态类装入器className获得CatalogDAOFactory具体实现子类,这个实现子类在Java宠物 店是用来操作catalog数据库,用户可以根据数据库的类型不同,定制自己的具体实现子类,将自己的子类名给与CATALOG_DAO_CLASS变量 就可以。

由此可见,工厂方法确实为系统结构提供了非常灵活强大的动态扩展机制,只要我们更换一下具体的工厂方法,系统其他地方无需一点变换,就有可能将系统功能进行改头换面的变化。

posted @ 2006-08-29 17:13 Alex 阅读(214) | 评论 (0)编辑 收藏

key words:正则表达式  模式匹配 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*$)

应用:javascript中没有像vbscript那样的trim函数,我们就可以利用这个表达式来实现,如下:

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- ./?%&=]*)?

得用正则表达式从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,''))"

"^\\d+$"  //非负整数(正整数 + 0)
"^[0-9]*[1-9][0-9]*$"  //正整数
"^((-\\d+)|(0+))$"  //非正整数(负整数 + 0)
"^-[0-9]*[1-9][0-9]*$"  //负整数
"^-?\\d+$"    //整数
"^\\d+(\\.\\d+)?$"  //非负浮点数(正浮点数 + 0)
"^(([0-9]+\\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\\.[0-9]+)|([0-9]*[1-9][0-9]*))$"  //正浮点数
"^((-\\d+(\\.\\d+)?)|(0+(\\.0+)?))$"  //非正浮点数(负浮点数 + 0)
"^(-(([0-9]+\\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\\.[0-9]+)|([0-9]*[1-9][0-9]*)))$"  //负浮点数
"^(-?\\d+)(\\.\\d+)?$"  //浮点数
"^[A-Za-z]+$"  //由26个英文字母组成的字符串
"^[A-Z]+$"  //由26个英文字母的大写组成的字符串
"^[a-z]+$"  //由26个英文字母的小写组成的字符串
"^[A-Za-z0-9]+$"  //由数字和26个英文字母组成的字符串
"^\\w+$"  //由数字、26个英文字母或者下划线组成的字符串
"^[\\w-]+(\\.[\\w-]+)*@[\\w-]+(\\.[\\w-]+)+$"    //email地址
"^[a-zA-z]+://(\\w+(-\\w+)*)(\\.(\\w+(-\\w+)*))*(\\?\\S*)?$"  //url

"^[A-Za-z0-9_]*$"

posted @ 2006-08-29 15:02 Alex 阅读(576) | 评论 (1)编辑 收藏

     摘要: key words:正则表达式 深入浅出之正则表达式(一) 前言:        半年前我对正则表达式产生了兴趣,在网上查找过...  阅读全文
posted @ 2006-08-28 23:21 Alex 阅读(707) | 评论 (0)编辑 收藏

     摘要: 本程序最初是由wanghr100(灰豆宝宝.net)的checkForm基础上进行修改的,增加了很多功能,如下: 对非ie的支持 增加了内置表达式和内置提示 增加了显示方式(弹出式和页面显示式) 增加了显示一条和显示全部 进行了封装(CLASS_CHECK) 支持外接函数或表达式(应用在密码一致) ...  阅读全文
posted @ 2006-08-28 22:20 Alex 阅读(497) | 评论 (0)编辑 收藏

反向工程生成的PDM,表中NAME和CODE都一样,现在要修改NAME.
在修改一个表的NAME属性时,CODE属性也会跟着一起改变,怎么样才能让CODE不跟着NAME变呢?



Tools->General Options->Dialog->Name to Code Mirroring (去掉)
posted @ 2006-08-26 23:04 Alex 阅读(725) | 评论 (0)编辑 收藏

在应用中有这样一个情况,
在A窗口中打开B窗口,在B窗口中操作完以后关闭B窗口,同时自动刷新A窗口


function closeWin(){
        hasClosed 
= true;
        window.opener.location
="javascript:reloadPage();";
        window.close();
    }
    
function window.onbeforeunload(){
        
if(!hasClosed){
            window.opener.location
="javascript:reloadPage();";
        }
    }

</script>

上面的代码在关闭B窗口的时候会提示错误,说缺少Object,正确的代码如下:
function closeWin(){
        hasClosed 
= true;
        window.opener.location
="javascript:reloadPage();";
        window.opener
=null;
        window.close();
    }
    
function window.onbeforeunload(){
        
if(!hasClosed){//如果已经执行了closeWin方法,则不执行本方法
            window.opener.location
="javascript:reloadPage();";
        }
    }

</script>

reloadPage方法如下:
function reloadPage() {
        history.go(
0);
        document.execCommand(
"refresh")
        document.location 
= document.location;
        document.location.reload();
    }

PS:由于需要支持正常关闭和强制关闭窗口时能捕捉到事件,用了全局变量hasClosed

==============================================

补充,在父窗口是frame的时候在刷新父窗口的时候会出现问题:

The page cannot be refreshed without resending the information.

后修改如下:
window.opener.parent.document.frames.item('mainFrame').location.href = window.opener.location.href;

不需要执行自带的reload()方法,注意,不要再画蛇添足加上这一句:

window.opener.parent.document.frames.item('mainFrame').location.reload();

========================================================================================
最后,为了同时支持刷新普通父窗口和frame父窗口,代码如下:
function closeWin() {
        hasClosed 
= true;
    
<%if(null != frame){%>
        window.opener.parent.document.frames.item('mainFrame').location.href 
= window.opener.location.href;
    
<%}else{%>
        window.opener.location 
= "javascript:reloadPage();";
    
<%}%>
        
//window.opener.top.mainFrame.location="javascript:reloadPage();";
        //self.opener.frames.mainFrame.location.reload(true);
        window.opener = null;
        window.close();
    }
    
function window.onbeforeunload(){
        
if (!hasClosed) {
        
<%if(null != frame){%>
            window.opener.parent.document.frames.item('mainFrame').location.href 
= window.opener.location.href;
        
<%}else{%>
            window.opener.location 
= "javascript:reloadPage();";
        
<%}%>
            window.opener 
= null;
        }
    }




posted @ 2006-08-23 10:54 Alex 阅读(3736) | 评论 (0)编辑 收藏

key words:select text值
获得select的value很容易,但是如何获得其text的内容呢,下面的代码借助了prototype.js库:
<script language="javascript">
        
function getSelect(selectName){
        
var options = $(selectName).getElementsByTagName('option');
        options 
= $A(options);
        
var opt = options.find( function(employee){
            
return (employee.value == $F(selectName));
        });
        alert(opt.innerHTML);
        }
    
</script>

<select name="hi" onchange="getSelect('hi');">
                
<option value="01"></option>
                
<option value="02"></option>
                
<option value="03"></option>
                
<option value="04"></option>
</select>

PS: $A : 转换成数组格式
    opt.innerHTML:nodeText
posted @ 2006-08-21 22:58 Alex 阅读(6816) | 评论 (1)编辑 收藏

key words: 锁表 解锁
查询sql:
SELECT A.OWNER,
A.
OBJECT_NAME,
B.XIDUSN,
B.XIDSLOT,
B.XIDSQN,
B.SESSION_ID,
B.ORACLE_USERNAME, 
B.OS_USER_NAME,
B.PROCESS, 
B.LOCKED_MODE, 
C.MACHINE,
C.STATUS,
C.SERVER,
C.SID,
C.SERIAL#,
C.PROGRAM
FROM ALL_OBJECTS A,
V$LOCKED_OBJECT B,
SYS.GV_$SESSION C 
WHERE ( A.OBJECT_ID = B.OBJECT_ID )
AND (B.PROCESS = C.PROCESS )
ORDER BY 1,2 

释放session Sql:
alter system kill session 'sid, serial#'

alter system kill session '379, 21132'
alter system kill session '374, 6938'

posted @ 2006-08-21 21:58 Alex 阅读(15498) | 评论 (2)编辑 收藏

key words: 在子窗口中如何调用父窗口的方法,属于"回调",直接拿到父窗口的句柄,然后直接调用方法不行,比如:
parent.window.show(b)
window.opener.show(b)

都不行,正确的做法是:
opener.location="javascript:show('hello')";

PS: 直接调用父窗口的对象与此不同,参考这篇: 用javaScript操作两个页面

posted @ 2006-08-21 21:55 Alex 阅读(18420) | 评论 (3)编辑 收藏

key words: radio的值 Form

在jsp中获取radio的值很容易,直接 request.getParameter("myRadio");
但是在js中不行,我试验了一下,如果选择第一个没问题,但是你选第二个第三个的话就出现undefined.办法如下:

function Foo()
{
    
var selectedIndex = -1;
    
var form1 = document.getElementById("form1");
    
var i = 0;
    
    
for (i=0; i<form1.r.length; i++)
    {
        
if (form1.r[i].checked)
        {
            selectedIndex 
= i;
            alert(
"您选择项的 value 是:" + form1.r[i].value);
            
break;
        }
    }
    
    
if (selectedIndex < 0)
    {
        alert(
"您没有选择任何项");
    }
}


posted @ 2006-08-21 19:12 Alex 阅读(13682) | 评论 (7)编辑 收藏

key words: 脚本编码 中文乱码 js
在jsp中,通过js访问java代码比较容易(不过,也有缺陷,比如java代码的声明必须先于js),反过来则不方便,网上看到有通过其他组件的方式来做,感觉也不是很好,其实平时用的更多的是在js中有中文编码,而需要到下一个jsp页面中得到这个对应的值。

js中有函数escape和unescape,可惜的是java.net.*中的编码和解码与js的不一致,所以他们不能协同工作,有一个方法就是在java中重新实现一遍js里对应的方法,代码如下:
/**
 * java版本的escape和 unescape[对应javaScript里的函数]
 
*/
public class EscapeUnescape {
    
/**
     * escape ==> escape
     * 
@param src
     * 
@return String
     
*/
    
public static String escape(String src) {
        
int i;
        
char j;
        StringBuffer tmp 
= new StringBuffer();
        tmp.ensureCapacity(src.length() 
* 6);
        
for (i = 0; i < src.length(); i++) {
            j 
= src.charAt(i);
            
if (Character.isDigit(j) || Character.isLowerCase(j)
                    
|| Character.isUpperCase(j))
                tmp.append(j);
            
else if (j < 256) {
                tmp.append(
"%");
                
if (j < 16)
                    tmp.append(
"0");
                tmp.append(Integer.toString(j, 
16));
            } 
else {
                tmp.append(
"%u");
                tmp.append(Integer.toString(j, 
16));
            }
        }
        
return tmp.toString();
    }

    
/**
     * unescape ===>js
     * 
@param src
     * 
@return String
     
*/
    
public static String unescape(String src) {
        StringBuffer tmp 
= new StringBuffer();
        tmp.ensureCapacity(src.length());
        
int lastPos = 0, pos = 0;
        
char ch;
        
while (lastPos < src.length()) {
            pos 
= src.indexOf("%", lastPos);
            
if (pos == lastPos) {
                
if (src.charAt(pos + 1== 'u') {
                    ch 
= (char) Integer.parseInt(src.substring(pos + 2, pos + 6), 16);
                    tmp.append(ch);
                    lastPos 
= pos + 6;
                } 
else {
                    ch 
= (char) Integer.parseInt(src.substring(pos + 1, pos + 3), 16);
                    tmp.append(ch);
                    lastPos 
= pos + 3;
                }
            } 
else {
                
if (pos == -1) {
                    tmp.append(src.substring(lastPos));
                    lastPos 
= src.length();
                } 
else {
                    tmp.append(src.substring(lastPos, pos));
                    lastPos 
= pos;
                }
            }
        }
        
return tmp.toString();
    }
}


posted @ 2006-08-21 18:02 Alex 阅读(635) | 评论 (0)编辑 收藏

     摘要: OSWorkflow的第一支程式 (史帝芬, 2005/6/4, hi.steven@gmail.com) 自從C的聖經版以Hello World程式開始引導初學者學習程式語言以來,...  阅读全文
posted @ 2006-08-15 16:25 Alex 阅读(4315) | 评论 (0)编辑 收藏

key words : textarea空格问题
<textarea name="parentRemark" rows="10" cols="80" class="editor">
<%=rem[7]==null?"":rem[7]%>
</textarea>

猜猜这个textarea会有什么问题?

效果如下:

jiazhang.png

是不是很奇怪,哪来的空格啊,还以为从数据库中取出的数据有空格,检查也不是,后来才知道,原来是textarea换行惹得祸,不换行就OK了

<textarea name="parentRemark" rows="10" cols="80" class="editor"><%=rem[7]==null?"":rem[7]%></textarea>

posted @ 2006-08-15 11:08 Alex 阅读(342) | 评论 (0)编辑 收藏

十大事件

1990-1994:Java缘起
文/孟岩

Larry Wall说,优秀程序员应有的三个特点:懒惰、急躁和傲慢。Java就是诞生在一群懒惰、急躁而傲慢的程序天才之中。
1990年12月,Sun的工程师Patrick Naughton被当时糟糕的Sun C++工具折磨的快疯了。他大声抱怨,并威胁要离开Sun转投当时在Steve Jobs领导之下的NeXT公司。领导层为了留住他,给他一个机会,启动了一个叫做Stealth(秘密行动)的项目。随着James Gosling等人的加入,这个项目更名为Green。其目标是使用C++为嵌入式设备开发一种新的基础平台技术,James Gosling本人负责开发一个SGML编辑器。正如人们事后分析的那样,这位天才的程序员太懒惰,所以没有把C++学好,开发中碰了一头包;太急躁—— 所以不愿意停下来读读Scott Meyers的新书《Effective C++》;太傲慢——所以轻易地决定开发一中新的编程语言。他把这种语言命名为C++++--,意思是C++“加上一些好东西,减去一些坏东西”。显然这 个糟糕的名字不可能长命百岁,很快这种颇受同伴喜爱的小语言被命名为Oak。
到了1992年9月,Oak语言连同Green OS和一些应用程序一起发布在称做Start 7的小设备上,从而使之有了第一次精彩的亮相。随后,Sun开了一家名为FirstPerson的公司,整个团队被转移到这家公司里研发机顶盒,以投标时 代华纳公司的一个项目。这帮天才被技术狂热所鼓舞,开发出了一个高交互性的设备,结果没想到时代华纳公司和有线电视服务商并不愿意用户拥有那么大的控制 权,从而在竞标之战中败给了SGI。Oak的锋芒之锐,竟然把客户都给吓懵了。Sun沮丧地关闭了FirstPerson,召回了整个团队。事实证明,传 统行业中那些脑满肥肠的保守主义者是腐朽没落的。回去!回到激情澎湃的IT产业,抓住互联网的大潮,这才是出路!1994年,Oak被命名为Java,针 对互联网的新一轮开发如火如荼,一切已经就绪,熔岩在地下奔流,火山即将喷发。


1995: Java香浓世界
文/马伟

1995年,Sun正式对外公布了Java,并且发布了JDK 1.0。这种外形酷似C++,却包含一颗Smalltalk般纯洁的面向对象之心的全新程序设计语言及其平台,几乎在一夜之间就成为软件产业的新宠儿。 Java当时仅仅被用来为网站制作一些动态应用,诸如动画图片之类,但这仍然引起了很多Web开发者们的注意,他们非常渴望有一种安全的语言,可以在静态 的HTML网页上制作动画图片。Sun最终把Java集成到NetScape浏览器。同时因为它具有“只写一次,随处运行”的特性,而引起了很多开发者的 注意,他们可以再也不用为了使程序能够在不同型号的硬件上运行而耗费大量的时间来编译代码了。
当时的Web浏览器的出现也为Java的出现起到了很好的推动作用,通过Java和Web浏览器的结合,人们似乎看到了什么,有人甚至预言PC 将在一两年内退出历史的舞台,取而代之的是基于Java的浏览器应用程序,通过网络计算设备来进行应用。Java的出现为当时的软件产业带来了无限的遐 想。


1996:Java大跃进,盟主地位就此定
文/马伟

SUN在1996年一开始首先成立了JavaSoft组织,并在1月23日正式发布自己的Java 1.0,作为20世纪业界出现的最重要的技术之一,Java引起了编程世界的革命。直到现在,Java仍然是互联网上最流行的语言。
在Sun正式发布Java 1.0之后,Java这门新生的语言就拥有了自己的会议——JavaOne,这次会议初试啼音就吸引了600多名参与者。除了拥有这么多的积极参与者来进 行Java的开发之外,各大知名公司也纷纷向Sun申请Java的许可。一时间,NetScape、惠普、IBM、Oralce、Sybase甚至当时刚 推出Windows 95的微软都是Java的追随者。
Java的应用就像是世界上的顶级玩家们组成的一个公开联盟,告诉全世界我们大家就是都在用着Java。也正是因为如此,Java也找到了自己的归宿。现在的J2EE已经成为中大型企业级应用的标准,成为承接数据库和Web之间的一个重要桥梁。
当年Java的机会实在太多了,以至于很难知道到底该做什么。最终Java在应用服务器市场获得了难以取代的地位,也确定了J2EE的发展方向,并且仍将延续下去。


1997-2001: 微软与Sun的Java官司
文/孟岩

Java诞生的1995年,正是微软在软件产业地位达到巅峰的时代,Windows 95发布时的风光场面给人们留下的深刻印象至今难忘。尽管如此,作为最卓越的技术领袖,比尔?盖茨仍然敏锐地注意到Java。当他了解了Java的一些细 节之后,给予了这样的评价:“Java是很长时间以来最优秀的程序设计语言。”基于此,微软于1996年3月申请并获得了Java许可证。微软对于 Java的这一热情态度在当时大大提高了人们对Java的兴趣和信心,但也有不少人担心微软会依靠自己强大的影响力在标准之外另立标准,从而破坏Java 的纯洁性。
果然,从1997年发布Visual J++的第一个版本开始,微软就开始在Java中掺入自己的私有扩展。这毫无疑问引起Sun的高度重视。1997年10月,Sun向美国加州地方法院起诉 微软公司违反两公司就微软使用Java技术所签定的合同,指控微软公司在自己的Java产品中做了“不恰当的修改”,违反了合同中承诺向用户提供Java 兼容产品的条款。这一官司旷日持久,直到2001年1月双方达成和解,微软将继续提供采用Sun开发的Java技术的现有产品(包括测试版)。不过, Sun有限制地仅对包括Java 1.1.4的微软产品提供许可。到了2001年7月,微软公布新版的Windows XP将不再支持Sun的JVM,并且推出了.NET平台与Java分庭抗礼。
现在回过头去看,当时的这一场官司对Java世界产生了深远的影响。如果没有这一场官司,也许很多Java程序员都在使用Visual J++,基于WFC开发Windows客户端程序,同时不得不面对被两个不同的事实标准所分裂的Java世界。


1998:Java 2平台发布
文/陶文

1998年,Java 2平台正式发布。经过了三年时间的发展、热热闹闹的攻关宣传、红红火火的众厂商的热情参与,Sun终于知道Java适合干什么了。对比Java刚发明时的 技术定位,与Java的戏剧性触“网”的那段历史,Java 2平台的发布可真算得上是有的放矢了。根据官方的文档,Java 2是Sun意识到“one size doesn’t fit all”之后,把最初的Java技术打包成三个版本的产物,也就是著名的J2ME、J2SE、J2EE。
之所以说Java自从Java 2平台发布之后,进入了现代。那是因为之前的历史怎么看来都和现在程序员日常开发使用的技术无什么关系,比如Applet,已经很少有人使用了。Java 2之后的历史就不一样了,至少人们在推崇轻量级开发,猛批EJB时还不时会引用J2EE这个词是如何诞生的。而Java 2的三大版本中,除了J2EE得到了长足发展和广泛使用之外,J2ME也在手机市场上取得了遍地开花的结果。相较之下,J2SE难免落寞,只剩SWT这个 血统不纯的家伙在Rich Client回归的时代吸引着人们的眼球了。无论今天看来当时的Java 2有多么的不成熟,至少经过市场和时间的检验,Java 2规划出来的三大方向把Java技术指向了光明的方向是勿庸置疑的。


1998:JCP成立并正式运作,
Java开源社群开始蓬勃发展
文/黄海波

1998年,JCP组织成立,并且开始把握Java的发展方向。JCP组织的开放性,不但使得所有对Java感兴趣的商业公司可以参与Java的 发展,更重要的是JCP允许个人、非盈利组织、学校等加入,这就给Java带来了巨大的活力。随之兴起的Java开源运动的最大贡献是实现和鼓励了知识共 享,在众多热情的开源程序员们的努力和分享下,很多原先只被商业公司掌握的技术、思想和产品可以被所有需要的开发人员免费或者以较低的价格获得使用权, 并通过开放源代码更容易的获得反馈和改进意见从而进一步演化发展。我们知道,所谓知识不是孤立发展认知,而是人们的经验,认识是思考交流和积累的产物。而 开源运动所带来的开放、反馈、交流的风气正是符合人类社会知识形成和发展的规律。
开源运动起源于西方的发达国家,有其现实背景和文化根源。1990年代可以说是IT产业的一个黄金时代。信息时代的兴起对IT人员,特别是软件 人员有着巨大的需求。而软件开发又是一种类似艺术创作的脑力活动,和所有的艺术家、作家们一样,在作品打上自己的印记并流传在世界上是每一个创作人员的梦 想。互联网时代下的高收入的舒适生活,早九晚五的编写公司的代码并不能满足很多有激情的软件开发人员的梦想,再加上西方传统的基督教文化中十分推崇的分享 和交流,开源的出现和兴起也就水到渠成了。今天,开源运动已经不仅仅是一些个人天才程序员们的游乐园地,而是发展成为一项开源软件产业。


1998:WebLogic打开J2EE的魔匣
文/霍泰稳

Java语言的出现使得互联网络有了良好的交互性能,但这些很“酷”的技术仅被人们认为是一些小花招,它还无法消除企业级用户对它的怀疑。 1998年,BEA公司宣布收购WebLogic公司,并接着推出由Sun公司第一个授权使用J2EE许可证的WebLogic Server应用服务器,这个Java版的AppServer一推出就引起业界极大的兴趣。WebLoigc Server以其对标准的支持、强悍的运算能力和安全的架构设计等特性也很快征服了那些怀疑J2EE应用的人们。推出市场后不到一年,WebLogic Server就成为业内第一Java应用服务器。
这里我们援引一些当时著名咨询公司的调查数据来说明问题,“在IDC的报告中,BEA在应用服务器和交易服务器领域市场份额第一;在 Gartner的报告中,BEA WebLogic Server拥有业内最广泛的EJB应用安装基础;在Giga Group的报告中,BEA WebLogic Server市场份额占32%”。
因为应用服务器市场极大的发展潜力,在WebLogic Server之后,其它的很多公司也推出了自己的AppServer,如IBM的WebSphere、Sun公司的iPlanet等,逐渐地应用服务器取 代了传统意义上的各类中间件,成为企业应用的基础平台。应用服务器的出现使得Java有了真正意义上的发展。


2002-2004: Sun与微软的法律碰撞最终以喜剧收场
文/恶魔

2003年4月2 日,Sun与微软达成16亿美元的法律和解。如果不是晚了一天,许多人会以为这是一个在4月1日愚人节开的玩笑。尽管当时所有人都像是看到“太阳从西边出来了”那样张大了嘴巴,但这的确是事实。
根据两家公司达成的版权协议,双方会为采用对方的技术而支付专利费用,微软向Sun提前支付3.5亿美元使用费,Sun则承诺,如果Sun集成微软的某些技术,也会向微软付款。
毫无疑问,“私下了结”的方式对双方而言都是最好的结果。就在协议签署的当天,在美国旧金山由Sun和微软为“抛弃十年恩怨、携手合作“举行的新 闻发布会上,尽管比尔?盖茨没有到场,但这并没有防碍现场看起来异常轻松的气氛。麦克尼利和鲍尔默各自穿了一件密歇根州底特律“Red Wings”曲棍球队的运动服,并谈及了一起在哈佛大学读书的经历,麦克尼利还说:“当时我们两人是非常要好的朋友,当然我们也有吵架的时候。”人与人当 然可能成为终生的知己,但是公司与公司之间有的只能是利益上的分分合合。


2000-2004: JBoss和Eclipse
——Java开源软件的王者
文/莫映

Java和开源几乎就是天生的一对,这可以从无比兴盛繁荣的Java开源软件社区得到佐证。目前最有影响力的Java开源软件项目,要数 JBoss和Eclipse。可以说,几乎所有的Java开发人员都获多或少的听到过或接触和使用过它们。前者是目前最优秀、应用最为广泛的企业级开源 J2EE应用服务器,后者是功能完全可以替代商业产品的Java IDE。二者的覆盖功能之全、支持工具之广、子项目之多,几乎可以仅凭借它俩来完成企业应用的开发构建到部署实施的全过程,而软件开发者和客户也都可以最 大程度上享受高质量,高可靠Java开源软件所带来的低成本优势。
JBoss和Eclipse的巨大成功,几乎令各自领域的商用竞争者抓狂,其中BEA的WebLogic和IBM的WebSphere在商业利 润上受到JBoss的巨大侵蚀,而Borland的JBuilder、JetBrains的IDEA等诸多优秀的商用开发工具也不得不面对Eclipse 独大的现实。JBoss的CEO兼创始人 Marc Fleury曾直言不讳地表示,希望占据市场主导地位。“我们希望打败IBM,成为中间件领域里最大的厂商。”JBoss在4.0以前还只是以一个 Group存在,盈利手段主要靠服务和销售文档。但在最近,JBoss已经发展成为一个有限公司,并吸纳多家风险投资,专注于获取利润为目标之一的第二代 开源软件模式(JBoss自己称为“Professional Open Source”)的创新和运营。这区别于以理论研究为爱好的学院型开源或大公司为基础的非盈利组织开源,如Linux和Apache。当然JBoss的这 种运营方式势必会导致更多的代码控制和专有修改权,但按JBoss的说法是这样更能获得企业客户的信赖。JBoss的这种模式是否能获得成功还要我们拭目 以待。
不管JBoss和Eclipse的未来发展如何,JBoss和Eclipse的成功已经让我们看到了Java开源软件的威力,祝愿它们一路走好。


2004:Java 5.0
文/莫映

2004年9月30日,代号为“Tiger”,研发历时近三年的J2SE 5.0发布正式版本,这是Java平台历来发布版本中改动面波及最大的一次。
纵观Tiger,“Ease of development”是其核心主题,这一点着重体现于语言特性上的改进,这在很大程度上,简化了开发人员日常的编程任务,以往一些琐碎的手工劳动都代 之以轻松自然,而又安全可靠的自动化实现。其中的注解功能,以及随之而来的声明式编程,还对构筑于J2SE 5.0之上的J2EE 5.0产生了巨大影响。尽管Tiger在语言特性上做了很大的动作,但作为Java技术的基础支撑,这些改动都是深思熟虑的结果。
Tiger发布至今也有大半年了,那么Sun又是如何规划J2SE的未来蓝图的呢?据悉,J2SE的下两个版本分别是代号为“Mustang” 的J2SE 6.0和代号为“Dolphin”的J2SE 7.0,预计Mustang将于明年发布。在吸取了Tiger研发周期过长的教训之后,Sun副总裁Graham Hamilton表示,Mustang的发布周期将不会那么长。并且,Sun还将“Becoming more open” 作为Mustang的主题之一。未来JCP对Java技术的影响将会愈加深入,而整个研发过程也将会愈加透明。Mustang在正式发布前的内部版本也会 陆续见诸于众,如此,广大Java开发者便可以更加及时的了解到Java发展的最新情况。在语言层面上的扩展依然会比较谨慎,比如像AOP这样的当下热门 技术,依然不太可能会见诸其中。据Hamilton所言,一个有可能被引入的语法特性被称作“friends”import机制,它将使由多个包组成的大 型项目变得易于管理。

----------------------------------------------------------------------------------------


十大人物

James Gosling : Java之父
文/陶文

作为Java之父,James Gosling的名字可谓是耳熟能详。当人们评论一种编程语言时,总喜欢捎带着把下蛋的母鸡一起带上。Java做为中国的编程语言学习者餐桌上有限的那么 几样餐点中的流行款式,自然是让James Gosling风光不已。虽然James Gosling现在已经不是领导Java发展潮流的领军人物了,做为Sun的开发者产品组的CTO,怎么算来也是身居高位了,俗事缠身吧,但是这并不妨碍 其对于Java一如既往的爱护,表达着各式各样鲜明的观点,引发一场又一场的争论。
James Gosling是很爱Java的——是啊,哪有当父母的不爱自己的孩子的呢。James Gosling也是很爱Sun的——是啊,哪有当领导的不爱自己的公司的呢。于是我们在批评.NET的安全性的队伍前头,在褒扬Java性能的队伍前头, 在抨击SWT开倒车的队伍前头,在给NetBeans大唱赞歌的队伍前头,我们都看到了James Gosling的身影。无论对错、偏见或者固执,至少说明了Gosling的鲜明个性丝毫没有受到年龄的影响。也许也只有这种天才而偏执的人物才能创造出 Java这般伟大的语言来吧。


Bill Joy : 软件业的爱迪生
文/徐昊

Joy生于1954年,1982年与Vinod Khosla, Scott McNealy和Andy Bechtolsheim一起创建了Sun Microsystems,并从那时起担任首席科学家,直到2003年离开。他是一位令人崇敬的软件天才,他在软件和硬件的历史上留下了无数令人仰止的传 奇。
在上个世纪80年代早期,DARPA与BBN达成协议,准备将Vinton Cerf和Bob Kahn设计的TCP/IP协议添加到Berkeley UNIX中。Bill Joy被委派来完成这项任务,然而他却拒绝将BBN的TCP/IP协议栈添加到BSD中,因为在他的眼中BBN的TCP/IP实现还远不够好,于是他就写 了一个高性能的TCP/IP协议栈。John Gage回忆道,“BBN和DARPA签署了巨额合同来实现TCP/IP协议,然而他们的员工所编写的代码远没有一个研究生所做的好。于是他们邀请 Bill Joy参加他们的一个会议,这位研究生穿着一件T-Shirt就出现了,他们询问他,‘你是如何做到的呢?’Bill回答说,‘这是非常简单的一件事,你 读一下协议然后就可以编码了’”。除了TCP/IP协议,基于分页的虚拟内存系统最早也是由Bill Joy添加到Berkeley UNIX内核当中的。同时他还是vi、csh、早期Pascal编译器的作者。
关于Bill Joy惊人的软件才能流传最广的一个传奇是,据说他在上研究生的时候,想看看自己能不能写一个操作系统出来,于是就在三天里写了一个非常简陋,但是可以使 用的Unix系统, 传说就是BSD的前身。虽然如此夸张的才情令人难以置信,但是考虑到主角是Bill Joy,还是有一定的可信度的。Bill Joy硕士毕业之后,决定到工业界发展,于是就到了当时只有一间办公室的Sun, 他作为主要设计者参与了SPARC微处理器的设计,负责设计最为关键的一部分电路。这样兼精软硬件的天才实在是让人不得不佩服啊。1995年,Sun发布 了轰动世界的Java语言。当然,Bill Joy对Java也作出了不少的贡献,首先是JINI——一种针对分布式服务的基础连接技术。任何可以内嵌JVM的电子设备都可以通过JINI相互连接; JXTA是基于Java的P2P协议,允许互联网上的软件进行点对点交流和协作。
这个其貌不扬的瘦高个,有着凌乱的亚麻色头发,被《财富》杂志誉为“网络时代的爱迪生”的技术狂人,在短短的二十年间,创造了无数令人心动的软 件。在MIT的BBS上曾有一个帖子,说微软电话面试有一道题,问“Who do you think is the best coder, and why?”虽然回复的帖子中大家都声明列举的best coder排名不分先后,然而大多数人仍把Bill Joy列在第一位,或许可以从一个侧面验证Bill Joy在广大Programmer心目中的地位吧。


Joshua Bloch : Java 2 元勋
文/莫映

早在1996年,适逢Java刚刚崭露头角,年内好事连连。先是1月份发布JDK 1.0,然后是5月底在旧金山召开首届JavaOne大会,年末又是JDK 1.1紧跟其后。正是在Java技术如火如荼、大展拳脚的背景之下,Joshua Bloch来到了Sun,开始了他带领Java社区步入“迦南美地”的漫长历程。
很快,他被从安全组调入核心平台组,从事底层API设计。至此以后,每逢JDK的重大版本发布,总能在其中见到Joshua的“妙笔”。JDK 1.1中的java.math、1.4中的assertions,还有大家所熟识的Collections Framework皆是Joshua一手打造。其中的Collections Framework还获得了当年的Jolt大奖。到了J2SE 5.0研发阶段,身为平台组构架师的Joshua接掌了Tiger大旗,其核心地位已然无人可以替代。作为Tiger的代言人和领路人,没有谁比 Joshua更清楚Tiger。相信大家一定还记得Joshua当年仿效英国诗人William Blake所做的咏Tiger诗八首,优雅的笔调,透出大师深厚底蕴的同时,也道出了Tiger的几大重要特性,这些特性是自JDK 1.1引入Inner Class以来,Java最大的语法改进。
Java风雨十年,从JDK 1.1到J2SE 5.0,Joshua实在功不可没。难怪有人戏言,假如将James Gosling比作Java之父,那么Joshua就是一手将Java “哺育”成人的Java之母。Joshua对Java的贡献还不止于JDK,提起他的大作《Effective Java》(Addison Wesley, 2001),相信Java粉丝们一定耳熟能详。该书荣膺2002年度Jolt大奖,且备受James Gosling推崇。书中57条颇具实用价值的经验规则,来自Joshua多年来在JDK开发工作中,尤其是Collections Framework设计中的实践心得,各个有理有据,剖析深入,也足见其深厚功力。该书对Java社群的影响,犹如C++社群中的《Effective C++》。Joshua对JCP的贡献也不小。他是JSR201和JSR175的领导者,前者包含了Tiger四大语言特性,后者则为Java提供了元数 据支持。此外,他还是JSR166的发起人之一(该JSR由Doug Lea领导),并且是许多其他JSR的参与者。Joshua目前是JCP为数不多的几个执行委员会成员之一。
Joshua Bloch给人的印象是谦逊平和,行事低调而不喜抛头露面,一个典型的技术人员和实干家。不过即便如此,也丝毫不会减弱他对Java技术的卓越贡献和对 Java社区的绝对影响力。有人说,如果他能更彰显一些,就很有可能成为Java开发者中的领军人物,就有如Don Box之于微软社群。
2004年7月初,就在Tiger发布在即之时,就在Jusha Bloch刚刚荣获Sun“杰出工程师(Distinguished Engineer)”的称号之时,他突然离开Sun而去了正值发展态势迅猛的Google。当他离开Sun的消息在TSS发布之后,众多拥趸表达了怀念与 不舍之情。一年过去了,我们还没有获知Joshua的任何近闻,似乎又是他行事低调的一贯作风所致,不知他在Google状况如何。希望Joshua依然 能继续“摩西未尽的事业”,以他的影响力推动Java社群继续前行。据称,《Effective Java》的下一版会加入Java 5.0的部分,让我们翘首以待吧。


Bruce Eckel : 功勋卓著的机会主义分子
文/孟岩

Bruce Eckel原本是一位普通的汇编程序员。不知道是什么因缘际会,他转行去写计算机技术图书,却在此大红大紫。他成功的秘诀不外乎两点:超人的表达能力和捕 捉机会的能力。他最早的一本书是1990年代初期的《C++ Inside & Out》,随后,在1995年他写出了改变自己命运的《Thinking in C++》。如果说这本书充分表现了他作为优秀技术作家的一面,那么随后他写作《Thinking in Java》并因此步入顶级技术作家行列,则体现了他作为优秀的机会主义分子善于捕捉机会的另一面。写作中擅长举浅显直接的小例子来说明问题,语言生动,娓 娓道来,特别适合于缺乏实践经验的初学者。因此《Thinking in Java》俨然成为天字第一号的Java教科书,对Java的普及与发展发挥着不可忽略的作用。不过公允地说,Bruce Eckel的书欠深刻。比如在“Thinking in…”系列中对设计模式的解说就有失大师水准。这一方面是因为书的定位非常清晰,另一方面也是因为Bruce太过分心赶潮流,未能深入之故。TIJ之 后,他预言Python将火,就匆匆跑去写了半本《Thinking in Python》。后来Python并未如期而旺,于是他也就把书稿撂在那里不过问了,机会主义的一面暴露无遗。我们也可以善意的猜测一下,他的下一个投机 对象会是什么呢?Ruby?.NET?MDA?总之,是什么我都不奇怪。


Rickard Oberg :J2EE奇才
文/熊节

Oberg的作品很多,流行的代码生成工具XDoclet和MVC框架WebWork都出自他的手笔。这两个框架有一个共同的特点,即它们的功能 虽然简单,但设计都非常优雅灵活,能够很方便地扩展新功能甚至移植到新环境下使用。优雅的设计源自Oberg的过人才华,简单的功能则折射出他玩世不恭的 人生态度。正是这两种特质的融合,才造就了这个不世出的奇才。
1999年,JDK 1.3发布,其中带来了一个重要的新特性:动态代理(Dynamic Proxy)。当所有人都还在对这项新技术的用途感到迷惑时,Oberg发现用它便可以轻松攻克EJB容器实现中的一些难关。这一发现的产物就是一本 《Mastering RMI》,以及大名鼎鼎的JBoss应用服务器。但Oberg很快又让世人见识了他的玩世不恭。由于和总经理Marc Fleury在经营理念上不合,Oberg抱怨“法国的天空总让我感到压抑”,甩手离开了自己一手打造的JBoss。此后的几年里,他和老友Hani Suleiman不断地对JBoss的“专业开源”模式和Marc Fleury的商人味道冷嘲热讽,让众人为他的孩子气扼腕叹息。
2002年10月,微软推出Petstore示例应用的.NET版本,并宣称其性能比Java Petstore高出数倍。正是Oberg深入分析这个示例应用的源代码,在第一时间指出它大量运用了SQL Server专有的特性,性能对比根本不具参考价值。后来Oberg又先后关注了AOP和IoC容器,两者都成为了J2EE架构的新宠。


Doug Lea : 世界上对Java影响力最大的个人
文/KIT

如果IT的历史,是以人为主体串接起来的话,那么肯定少不了Doug Lea。这个鼻梁挂着眼镜,留着德王威廉二世的胡子,脸上永远挂着谦逊腼腆笑容,服务于纽约州立大学Oswego分校计算器科学系的老大爷。
说他是这个世界上对Java影响力最大的个人,一点也不为过。因为两次Java历史上的大变革,他都间接或直接的扮演了举足轻重的脚色。一次是由 JDK 1.1到JDK 1.2,JDK1.2很重要的一项新创举就是Collections,其Collection的概念可以说承袭自Doug Lea于1995年发布的第一个被广泛应用的collections;一次是2004年所推出的Tiger。Tiger广纳了15项JSRs(Java Specification Requests)的语法及标准,其中一项便是JSR-166。JSR-166是来自于Doug编写的util.concurrent包。
值得一提的是: Doug Lea也是JCP (Java小区项目)中的一员。
Doug是一个无私的人,他深知分享知识和分享苹果是不一样的,苹果会越分越少,而自己的知识并不会因为给了别人就减少了,知识的分享更能激荡出 不一样的火花。《Effective JAVA》这本Java经典之作的作者Joshua Blosh便在书中特别感谢Doug是此书中许多构想的共鸣板,感谢Doug大方分享丰富而又宝贵的知识。这位并发编程的大师级人物的下一步,将会带给 Java怎样的冲击,不禁令人屏息以待。


Scott McNealy :SUN十年来的掌舵者
文/KIT

McNealy,Sun的CEO、总裁兼董事长。他曾经狂傲的说:“摧毁微软是我们每个人的任务。”这位英勇的硅谷英雄,似乎带头起义,试图组织 一个反微软阵线联盟,以对抗微软这股庞大的托拉斯恶势力。他时常口出惊人之语,在公开场合大肆的批评微软,并曾经说微软的.NET是.NOT。
Scott McNealy先后毕业于哈佛大学及史丹佛大学,分别持有经济学学士学位及企管硕士。1982年MBA毕业的他和三个同学共同合伙创建了Sun,并于 1984年成为Sun的执行官。“要么吞了别人,不然就被别人吞了”是Scott McNealy的名言录之一。他擅长以信念带动员工,鼓舞士气。极富自信的他,对于认定的事,总是坚持自己的想法,因此有人形容他是一个刚愎自用的决策 者。
身为Sun这艘船的掌舵者,Scott McNealy能够看多远,Sun就能走多远。Scott McNealy认为将来软件界是一个只有服务,没有产品的世代。他希望打造出Sun不是一个纯靠硬件赚钱的公司。从Open Source到Open Solaris,Sun希望可以成为提供整合性解决方案的服务厂商。Solaris 10 + UltraSPARC是否可以像Scott McNealy希望的是下一匹世纪黑马呢?Sun是否能以股价来证明华尔街分析师及普罗大众的诽短流长?Scott McNealy是否能带领着Sun成为继微软之后的下一个巨人,一场场IT界的争霸战值得我们拭目以待。


Rod Johnson : 用一本书改变了Java世界的人
文/ 刘铁锋

Rod在悉尼大学不仅获得了计算机学位,同时还获得了音乐学位。更令人吃惊的是在回到软件开发领域之前,他还获得了音乐学的博士学位。有着相当丰 富的C/C++技术背景的Rod早在1996年就开始了对Java服务器端技术的研究。他是一个在保险、电子商务和金融行业有着丰富经验的技术顾问,同时 也是JSR-154(Servlet 2.4)和JDO 2.0的规范专家、JCP的积极成员。
真正引起了人们的注意的,是在2002年Rod Johnson根据多年经验撰写的《Expert One-on-One J2EE Design and Development》。其中对正统J2EE架构的臃肿、低效的质疑,引发了人们对正统J2EE的反思。这本书也体现了Rod Johnson对技术的态度,技术的选择应该基于实证或是自身的经验,而不是任何形式的偶像崇拜或者门户之见。正是这本书真正地改变了Java世界。基于 这本书的代码,Rod Johnson创建了轻量级的容器Spring。Spring的出现,使得正统J2EE架构一统天下的局面被打破。基于Struts+Hibernate +Spring的J2EE架构也逐渐得到人们的认可,甚至在大型的项目架构中也逐渐开始应用。
Rod Johnson的新作《Expert One-on-one J2EE Development without JEB》则更让人吃惊,单单“Without EJB”一词就会让大多数J2EE架构师大跌眼镜了。不过Rod Johnson可能仅仅是想通过“Without EJB”一词表明应该放开门户之见。这也是Rod Johnson一贯的作风,。也许正是这种思想,促使得Rod Johnson创建了Spring,真正改变了Java世界。



Alan Kay :Java的精神先锋
文/徐昊

Sun的官方Java教材中有一句话,说Java是“C++的语法与Smalltalk语义的结合”。而Smalltalk的创造者就是Alan Kay。
Alan Kay于1970年加入Xerox公司的Palo Alto研究中心。早在70年代初期,Alan Kay等人开发了世界上第二个面向对象语言Smalltalk,因此,Alan Kay被誉为Smalltalk之父。2003年,Alan Key因为在面向对象程序设计上的杰出贡献,获得了有计算机界的诺贝尔奖之称的ACM Turing Award。
Alan Kay成名于Smapltalk和OOP,而Java虽然在语言上类似于C,但是在语义上非常接近Smalltalk,很多Java中的设计思想在 Alan Kay的文献中找到根源,也有些人将Alan Kay尊为Java思想的先驱。不过遗憾的是似乎Alan Kay老先生对Java并不买账,反倒攻击说Java是存在致命缺陷的编程语言,Java的成功不是由于Java本身的内在价值,而是其商业化的成功。 Alan Kay欣赏的是Lisp,他认为Lisp是软件的麦克斯韦方程,其中的许多想法是软件工程和计算机科学的一部分。看来拥有Alan Kay这样一位重量级的Java先驱仍是我们Java一厢情愿的单恋吧。



Kent Beck : 领导的敏捷潮
文:刘铁锋

Beck全家似乎都弥漫着技术的味道。生长在硅谷, 有着一个对无线电痴迷的祖父,以及一个电器工程师父亲。从小就引导Kent Beck成为了业余无线电爱好者。
在俄勒冈州大学读本科期间,Kent Beck就开始研究起模式。然而在他最终拿到计算机学位之前,他却是在计算机和音乐中交替学习。似乎Java大师都能够有这样的能耐,另一Java大牛Rod Johnson同样也拥有音乐学的博士学位。
Kent Beck一直倡导软件开发的模式定义。早在1993年,他就和Grady Booch(UML之父)发起了一个团队进行这个方面的研究。虽然著有了《Smalltalk Best Practice Patterns》一书,但这可能并不是Kent Beck最大的贡献。他于1996年在DaimlerChrysler启动的关于软件开发的项目,才真正地影响后来的软件开发。这次的杰作就是XP(极限 编程)的方法学。
和软件开发大师Martin Fowler合著的《Planning Extreme Programming》可谓是关于XP的奠基之作。从此,一系列的作品如《Test Driven Development: By Example》,《Extreme Programming Explained: Embrace Change》让更多的人领略到了极限编程的精髓,也逐步导致了极限编程的流行。
Kent Beck的贡献远不仅如此。对于众多的Java程序员来说,他和Erich Gamma共同打造的JUnit,意义更加重大。也许正式这个简单而又强大的工具,让众多的程序员更加认可和信赖极限编程,从而引起了Java敏捷开发的狂潮吧。

------------------------------------------------------------------------------------------

十大产品

Sun JDK :Java的基石
文/莫映

众所周知,流传于市的JDK不单Sun一家,比如IBM的JDK、BEA的JRocket、GNU的GCJ,以及如Kaffe这样的开源实现,不一而足。但是,根正苗红的Sun官方JDK一直以来都是备受瞩目的主流,它对Java社区的影响也是举足轻重。
1996年1月,Sun在成立了JavaSoft部门之后,推出了JDK 1.0,这是Sun JDK(Java Development Kit)的首个正式版本;当年12月,JDK1.1出炉。该版除了对前序版本部分特性做了改进以外,重写了AWT,采用了新的事件模型。1998年12 月,JDK 1.2正式发布。此时的类库日臻完善,API已从当初的200个类发展到了1600个类。在1.2版本中引入了用100%纯Java代码写就的 Swing,同时,Sun将Java更名为Java 2。
1999年,Java 技术形成了J2SE、J2EE和J2ME三大格局。Sun向世人公布了Java HotSpot性能引擎技术的研究成果。HotSpot旨在进一步改善JVM性能,提高Java ByteCode的产生品质,加快Java应用程序的执行速度。J2SE 1.3发布于2000年;2002年2月间,J2SE 1.4问世,这是有JCP参与以来首个J2SE的发行版本。2004年9月30日,代号为“Tiger”的J2SE 5.0终于出笼了,这次发布被誉为Java平台历来发布中特性变动最大的一次。包括泛型在内的若干重大语法改进、元数据支持,包括多线程、JDBC在内的 多项类库改进,都令广大Java程序员激动不已。自此,Sun的官方JDK(J2SE Development Kit)已经步入了一个新的高度。



Eclipse :以架构赢天下
文/恶魔

IBM是在2001年以4000万美元种子基金成立Eclipse联盟,并且捐赠了不少程序代码。如今,该组织有91个会员,包含许多全球最大的软件商。根据Evans Data公司的资料,Eclipse是目前最受欢迎的Java开发工具。
Java厂商若要共同对抗微软,彼此之间就要有共同的开发工具才行。
在Eclipse平台上,程序员可使用好几种不同的语言。在前端方面,用户可整合多种工具来撰写Plug-in程序或Unit Test。Eclipse最大的特色就在于其完全开放的体系结构,这代表任何人都可下载并修改程序代码,给Eclipse写插件,让它做任何你能想到的事 情,即所谓“Design for everything but nothing in particular”。
Eclipse基金会的架构比较特别,反映出企业现今对于开放原始码计划也越来越积极主动。Eclipse不像一般开放源码软件容许个人的捐献程序,该基金会是由厂商主导。不论是董事会成员或者是程序赞助者几乎都来自于独立软件开发商(ISVs)的员工。
Eclipse首席执行官Mike Milinkovich说,这种厂商会员制是特意设计的;他说Eclispe软件开发快速就是因为会员制的关系,同时又加上开放源码开发模式的临门一脚。 这与一般透过标准组织的做法全然不同。 这其实正好验证了一句老话:“开放即标准”。


JUnit/Ant : 让Java自动化的绝代双骄
文/刘铁锋

在Java程序员必备的工具中,共 同拥有且交口称赞的恐怕就非JUnit、Ant莫属了。一个是单元测试的神兵利器,一个是编译部署的不二之选,它们让Java的开发更简单。
JUnit由XP和TDD的创始人、软件大师Kent Back以及Eclipse架构师之一、设计模式之父Erich Gamma共同打造。名家的手笔和理念使得JUnit简单而强大,它将Java程序员代入了测试驱动开发的时代。JUnit连任了2001、2002年 “Java World编辑选择奖”以及2003年“Java World最佳测试工具”和2003年“Java Pro最佳Java测试工具”等众多奖项,深受Java程序员好评。
Ant是开源项目的典范,它让IDE的功能更加强大,从Sun的NetBeans到JBuilder,主流的IDE中处处都有它的身影。 “Another Neat Tool”原是它的本名,但这已经渐渐不为人知。它彻底地让部署自动化,而程序员需要做的仅仅是几条简单的配置命令。和JUnit一样,Ant也荣获了众 多的殊荣:2003年JavaWorld“最有用的Java社区开发的技术编辑选择奖”, 2003年Java Pro“最有价值的Java部署技术读者选择奖”,2003年“JDJ编辑选择奖”,也让Ant受到的多方的认可。
Ant对JUnit的全面集成,则使得一切都变得更加完美。只需简单地配置,从自动测试到报告生成,从编译到打包部署均可自动完成。强大的功能,简单的配置,让Java程序员高枕无忧。实可谓让Java自动化的绝代双骄。

Websphere : 活吞市场的大鲸
文/jini

1999年, IBM与Novell签订合作协议,成功地提供电子商务的解决方案给予原先使用NetWare的用户。同年更是推出了WebSphere Application Server 3.0,并且推出WebSphere Studio与VisualAge for Java让工程师可以快速开发相关的程序。2001年,IBM更是宣布将应用服务器、开发工具整合在一起,与DB2、 Tivoli及Lotus结合成为一套共通解决方案,如今、IBM更是并入了Rational Rose ( UML tools )让开发流程更是完整化。
Sun在Web Services的策略方面远远落后于微软与IBM, 当他们手拉手在研订Web Services规范, 加上IBM买硬件送软件或是买WebSphere送DB2的策略让企业大佬们纷纷转向IBM的阵营, Sun才惊觉大势已去。WebSphere复杂的安装,深奥的设定,难以理解的出错讯息不断地挑战开发者的耐心与毅力。
IBM如今已经不是将WebSphere定义为单一产品,它已经是一个平台的代名词。它里面的产品目前包含了应用服务器、商业整合、电子商务、 数据讯息管理、网络串流、软件开发流程、系统管理、无线语音等等。非常多样化,也让企业界愿意相信WebSphere可以带给他们一套完整的解决方案。同 时, IBM也在推广SOA的概念, 简单来说, 利用Web Service的耦合性与工作流程的整合, 为企业内部打造以服务为导向的架构。
IBM捐献出Eclipse带给Java开发人员对IDE的重新掌握。未来是否会捐献出WebSphere的哪一个部分成为OpenSources, 或许, 又是改写Java世界的时刻了。



WebLogic : 技术人的最爱
文/jini

1995年, BEA成立了, 初期以Tuxedo数据转换的产品为基础, 成长之迅速是历年来最强的企业。 1998年, BEA推出以Java为基础的网络解决方案, 提供了完整的中间层架构, 更同时支持EJB 1.0 及微软的COM组件, 方便的管理接口掳掠了工程师的心。 在IBM和Oracle尚未准备好迎击的时候, BEA已经席卷企业应用平台的市场。 WebLogic无论在市场领先度与技术领导性与策略远观性都优于当年的所有应用服务器厂商。
如今WebLogic不仅仅是应用平台服务器的名称, 而是BEA对于整个企业解决方案的总称, 无论是WebLogic Portal或是WebLogic Integration配合着Workshop开发环境, 来自微软的UI开发团队让Workshop几乎达到所见即所得。 接着, 在下一个版本之中, BEA的BeeHive开放源代码计划将释出中间层控件的开发模块, 并且与Eclipse合作共同打造新一代的开发环境。 如此强而有力的技术支持, 更是让顾客愿意使用WebLogic平台的最大原因。
代号为“Diablo”的 WebLogic Server 9.0小恶魔已经出现了, 目前虽然仅仅是BETA版, 以Portlet 方式打造的管理接口与完整且美妙的WebServices支持, 实在很难找到可以挑剔的地方, 虽然去年被IBM的技术性推销超越了市场占有率, 不过接下来SOA的平台竞争现在才开始, BEA的LOGO也加入“Think liquid”并且推出新的AquaLogic平台做为数据服务平台, 可见, Java的应用服务器的战争, 还会继续进行着。



JBuilder : Java开发工具的王者
文/刘铁锋

Java的开发工具中,最出名的莫过于Borland公司的JBuilder了。对于一些没有弄清楚开发工具与JDK的区别的Java入门者来 说,JBuilder就如同Visual C++之于C++,以为JBuilder就是Java的全部。比起捆绑在服务器上销售的JDeveloper,JBuilder应该是唯一的仅靠自身的实 力而占领了大部分市场的Java商用开发工具了。而JBuilder作为Java 开发工具的王者,其夺冠之路并非一帆风顺。直到Java的天才Blake Stone成为JBuilder的Architect之后,JBuilder 2.0以及3.0才逐渐推出。2000年3月14日,JBuilder 3.5的推出别具意义,它成为了业界第一个用纯Java打造的开发工具,也风靡了整个Java开发工具市场。在同年11月份推出的JBuilder 4.0乘胜追击,冲破了50%的市场占有率,成为了真正Java开发工具的王者。
Borland以每半年左右推出一个新版本的速度,让众多的对手倒在了沙场。而Microsoft因为与Sun的官司,也使得一个强大的对手退 出了战争。2001年,加入了对企业协作支持的JBuilder 5以及强化了团队开发工具的JBuilder 6打败了最后一个对手Visual Age For Java。2002年JBuilder 7推出之后,再也没有其他厂商与JBuilder竞争。
孤独的王者并没有停下脚步,在2003年到2005年间,JBuilder也仍然延续了其半年一个版本的速度,推出了8、9、10、2005四 个版本。强大的功能以及持续的改进,也让Java程序员多了一分对能够在开发工具市场上与Microsoft血拼十数年的Borland的敬仰。



Oracle : Java人永远的情结
文/熊节

在林林总总的数据库之中,有一种尤其令人又爱又恨、印象深刻,那就是关系型数据库市场的“大佬”——Oracle。
从公司的角度,Oracle和Sun有着诸多相似之处,例如:两家公司都拥有一位个性鲜明的CEO。早在Java诞生之初的1995年, Oracle就紧随NetScape从而第二个获得了Java许可证。从那以后,Oracle对Java的鼎力支持是Java能够在企业应用领域大获成功 的重要原因之一。
所有J2EE程序员都知道,Oracle的JDBC驱动虽然与Oracle数据库配合良好,但在不少地方使用了专有特性。其中最为著名的就是 “CLOB/BLOB问题”,诸如此类的问题给开发者带来了很多麻烦。为了同时兼顾不同的数据库,他们不得不经常把自己的一个DAO(数据访问对象)写成 两份版本:针对Oracle的版本和针对其他数据库的版本。有不少人为了开发便利,舍弃了数据库之间的可移植性,将自己的产品绑定在Oracle的专有特 性上。
Oracle提供的Java开发工具也与此大同小异。不管是数据库内置的Java支持还是JDeveloper IDE, Oracle的Java工具都和Oracle数据库有着千丝万缕的联系。看起来,只要Oracle还是数据库市场上的“头牌”,了解、学习Oracle的 专有特性,周旋于Oracle特有的问题和解决方案之中,就将仍旧是J2EE程序员在数据库基础和SQL之外的必修功课。对Oracle的爱与恨,也将仍 旧是Java人心头一个难解的情结。



Struts、Hibernate : 让官方框架相形失色的产品
文/刘铁锋

好的框架能够让项目的开发和维护更加便捷和顺利。相比Sun官方标准的迟钝以及固执,开源框架也更得到Java程序员的共鸣。Struts以及Hibernate就是这样一类产品,它们简单、优雅,更让官方的产品相形失色。
谈起Struts,不可避免地就要提及MVC(Model-View-Controller)的理念。而准确地讲,MVC的提出却最早源于JSP 的标准。在1998年10月7号,Sun发布的JSP的0.92的规范中提出的Model 2就是MVC的原型。在1999年12月Java World的大会中,Gavind Seshadri的文章最早阐述了Model 2就是一种MVC的架构,同时也提及了MVC架构是一种最好的开发方法。2000年3月,由Craig McClanahan发布的Struts成为了最早支持MVC的框架。Struts在设计上虽然存在一些诟病,但是不可否认的是,它使得Java Web应用的开发更加简洁和清晰,也让更多的程序员爱上了Java,并开始遗忘官方的JSP。时至今日,比起如WebWork、Tapestry以及 Sun官方的JSF,Struts或多或少存在些不足,但是众多成功项目的实施,仍然使其牢牢占据的Java Web应用框架的首位。
Hibernate则在某种程度上改变了人们对构建J2EE的思路。相比其EJB的Entity Bean的映射技术,Hibernate则显得更加简洁和强大。五分钟就能把Hibernate跑起来,让更多的Java程序员享受到了开发的乐趣。第 15届Jolt大奖中,最优秀数据库、框架以及组件的奖项中,Hibernate当仁不让获得头筹;不仅如此, Hibernate甚至还影响了官方的标准。在众多Java程序员翘首以待的EJB 3.0的规范中,Hibernate得到了支持。
Java开源的繁荣不仅让众多Java的开发者享受到了更多的便利,甚至影响了官方的标准。恐怕这也是作为Java人独有的乐趣之一吧。



PetStore : J2EE人的必修课
文/陶文

很少有一个例子项目如PetStore这 般广为人知,而这很大程度上要归功于Sun很“英明”地把PetStore做成一个只展示架构而在性能调优上留下了大大余地的例子。围绕着性能话题,产生 了颇为有趣的厂商之间以及平台之间的Pet Wars。除去这些关于性能的流言蜚语乃至中伤,PetStore在展示J2EE1.3平台的架构、演示什么叫分层方面还是有着很大的功劳的。而且 PetStore在架构方面的丰富性使得其成为J2EE的那些轻量级小兄弟们展示自身的一个必选科目。
不谈那些围绕PetStore的口水,那些数不尽的盗版,PetStore给开发新手带来的最重大的影响,我想应该是架构的观念而不是性能,也 不是业务。做为一种技术的Demo,这无可非议。但是如果你是一个新手,跟着PetStore亦步亦趋地学习J2EE开发,难免会陷入过度设计、华而不实 之类的困境。围绕着.NET的PetStore的克隆PetShop展开的架构与性能的大讨论,是不是也在促使我们学习新技术时应该以解决问题为导向呢? 特别是当你想把一个如PetStore这般的Sample Project的技术照搬到你的现实世界的Real Project来时。
------------------------------------------------------------------------------------------

十大组织

Sun : 因为Java而永被荣光
文/孟岩

Sun是1980年代初期由斯坦福大学三位年轻学生创立的公司。与一般人的印象不同,“SUN”的本意并不是企图剽窃天上那颗温暖的恒星的威名,而是“斯 坦福大学网络”的意思。Sun在“前Java”时代就因为SPARC芯片、Solaris操作系统和“网络就是计算机”的口号而为人所知。1990年12 月,Sun启动了一个看上去没什么意思的嵌入式软件项目。然而,基于C++的开发很快遇到了麻烦。一个创新型技术公司的特色立刻显示出来,一群天才不是去 深入C++,而是另辟蹊径,发明了Java。这个传奇故事已经尽人皆知,但是其中所包含的精神却始终令人望空凝思。
Java的发明,使得Sun真正有机会在软件的历史天空中放射出太阳的光芒。Sun发明了Java,并且在长达十年的时间里始终走在Java大 潮的最前端。Sun是Java的老家,是Java慈爱的母亲,这一切任何人都改变不了。虽然Sun似乎没能够从Java中获得应有的金钱回报,但这丝毫没 有挫伤Sun对于Java的母爱,还有对于Java大潮的舍我其谁的领导气概。
所有人都迷恋富有的感觉,但是也迟早会意识到钱不是世上最宝贵的东西。这个世界并不缺少会赚钱的公司,但是能够靠着创新型技术推动整个世界进步的公司却是凤毛麟角。Sun应该感到骄傲,他们将因为Java而在历史的天空里发射出太阳的光芒。



IBM : Java经济的最大受益人
文/恶魔

Sun公司是Java的发明人,但IBM却是Java最大的受益者。是IBM抢占了利润丰厚的应用服务器市场的头把交椅,是IBM在Java技术 上投入最多的金钱,拥有最大的影响力和最好的开发者社区。可以毫不夸张地说,Java使IBM的软件体系得到复兴,在某种意义上,甚至可以说,是Java 创造了这种复兴。Java之后又来了Linux,这种建造在不属于自己的平台上以获得成功的理念更是变得非常有影响力。正是这种理念铸就了今天IBM “按需计算,服务为王”的王者风范。
2004年三月,IBM以Java的解放者的姿态借机向Sun发难。IBM公司负责新兴技术的副总裁史密斯在一封公开信中表示,IBM愿意与Sun合作成立一个项目,意在通过开放源代码开发模式管理Java的开发工作。
墙内开花,墙外香。面对IBM的成功,到底是谁妒嫉呢?或许去程序的社区中逛逛聊聊,明眼人是不难发现事实真相的。也许Sun应该好好向IBM学 习经营之道。尽管利润额不如硬件及服务部门,但IBM软件部门的利润率是最高的——高达85%的利润率足以令人惊叹。在最近的一个季度里,IBM软件部的 利润率上升了8%,其中WebSphere产品组的利润率上升了14%。
正是IBM在开源和Java上的全身心地投入又秉承开放性的原则,今日的Java才能以日进千里的速度将许多竞争对手远远抛在后面。Java 10年,IBM功不可没。



BEA : 用AppServer影响Java阵营
文/霍泰稳

十年前诞生的Java并不是一开始 就那么引人注目的,虽然用Applet也曾为互联网络带来一抹亮色,但毕竟只是Toy。在企业级应用市场上,Java一直没有什么起色,虽然Java的支 持者一直在鼓吹它有着大型企业级应用的强悍功能。过高的期望与低能的产品,一时间颇让人怀疑Java的路是否已经走到了尽头?可以说是WebLogic Server的出现逐渐打消了人们的顾虑,BEA公司慧眼独具在2001年收购的这个产品将人们的目光吸引到电信、金融、政府等Java企业级应用方面, WebLogic Server以其优良的性能让人们看到Java应用广阔的未来。虽然随后在Java应用服务器方面出现了像IBM公司的WebSpere、开源软件 JBoss等Java应用服务器,但WebLogic Server几乎占领世界前500强所有企业的应用服务器市场地位依然无法撼动。
Java现在已经不单纯是一个语言,从另一方面它也代表着开放与创新。很多以Java产品为基础的公司或者从事Java开发的程序员骨子里都有 着开放与创新的烙印,BEA公司的发展深深地印证了这一点。与合作伙伴的密切合作向Java社区贡献产品基础源代码、加入权威开源组织参与Java标准的 制定等证实着BEA的开放,而其产品从WebLogic Server一种拓展到WebLogic Platform、WebLogic Portal、WebLogic Workshop等其它领域又证实着它的创新能力。



Oracle : 早起的鸟儿有虫吃
文/孟岩
Oracle的老板拉里?艾利森是有名的混世魔王和花花公子,所以尽管他也是软件产业成功人士的代表,却绝不是程序员们心目中的英雄,程序员们毕 竟不是央视《对话》节目里群众演员,没必要为了节目需要而对权贵财阀们做出一副贱骨头状。但是,任何人都不能不钦佩Oracle在技术上的前瞻性和坚决 性。Oracle是1996年获得Java许可证的,紧接着就大胆地将Java作为战略性的发展方向而予以全面支持。要知道当时Java的前景并不是十分 确定的,而Oracle的坚决投入,使得它在后来的Java世界中抢得一席之地。1998年9月发布的Oracle 8i为数据库用户提供了全方位的Java支持。Oracle 8i成为第一个完全整合了本地Java运行时环境的数据库,开发者用Java就可以编写Oracle的存储过程,这意味着可以仅在Oracle数据库中就 完成几乎全部的应用开发。J2EE兴起后,Oracle更是有心进入开发工具市场,因而购买了JBuilder的源码,并在此基础上开发出 JDeveloper。如今Oracle除了数据库稳居第一之外,在Java开发工具世界里也自成一派。这一切不能不归功于当初的眼光远大。


Apache : 开源软件的品牌保证
文/陶文

Java程序员的日常工具箱中,我们可以发现Ant、Tomcat、Log4、Lucene这些鼎鼎大名的开源产品。而它们的共同点在于,都是由 Apache Software Foundation社群中杰出的开发者开发的开源项目。Apache这个名字在Java的世界中实在太出名了,以至于“Apache”这六个字母成为开 源项目品质保证的代名词。Apache是自由开源的一面旗帜,其Apache License更是成为商业友好的License的首选,只SourceForge上就有1000多个以Apache License授权的项目,其流行程度可见一斑。
但是,如我们所知,Apache最早闻名IT界是靠高性能的Web服务器,其历史甚至和Java一样长。Apache对于Java的偏爱,以及 其发展的速度也映射出了Java繁荣的一角。现在去它的主页上看看,满目望去全部都是Java的开源项目,早就不光是其C服务器的老本行了。Apache 对Java最大的贡献就是提供了这么一个精品的开放舞台,让杰出的开发者和成熟的开源项目走到一起,共同给Java语言提供一个丰富的工具仓库。对于一种 语言、一个平台来说,其库的丰富程度对于开发者来说的重要性再怎么强调也不为过。勿庸置疑,Aapache上会出现越来越多的Java开源项目,而我们开 发者也将更多地得益于这令人目不暇接的繁荣。



TheServerSide : 论坛的专业精神
文/刘天北

成立于2000年5月,TSS最初以一本书而广为人知。它的创始人Ed Roman同时也是J2EE名著《Mastering EJB》的作者;Roman运营着一个J2EE咨询/培训公司TheMiddlewareCompany(简称TMC),TSS当时是TMC的下属部门; 为了扩大企业的影响,Roman在TSS网站上免费发布了那本书的电子版。J2EE程序员要吃下这个香饵,就得在论坛中注册;注册的同时,多半也会看一眼 论坛的内容;一看之下,大部分人都被吸引住,成了社区的忠实成员。
TSS究竟有什么吸引人的秘诀?首先,它有一支能力过人的运营团队,除了Roman本人之外,其中还有好几人都是J2EE领域的顶尖专家;第 二,TSS和TMC定期会推出专家研讨会/视频访谈、技术白皮书、评测报告,通读TSS提供的这些内容,基本上就可以把握技术的当前趋势。但这还不是全 部。最可贵的还是TSS的社区风格:他们深谙技术,但不盛气凌人;思想敏锐,但不因此缺乏审慎和大局感。其中大多数人都已在自己的开发领域颇有建树,在 TSS上的活动既给他们提供了与同行进行深度交流的机会。一个新成员进入社区,就像参加了一个起点很高的专业俱乐部,这不是一个求解“怎样设置 JAVA_HOME环境变量”之类问题的地方。事实上,在J2EE技术发展的若干转折点上,TSS都起到了关键的推动作用。
几经易主之后,J2EE咨询培训公司TMC在2004年关闭;TSS则被IT媒体集团TechTarget收购。我们期待着它更加繁荣的未来。

JBoss : 职业开源软件组织
文/刘天北

J2EE的婴儿期,“应用服务器”原本是“昂贵”的代名词。但从1999年起,Marc Fleury和Rickard Oberg等人就已经着手改变这种状况。他们开发的开源EJB容器当时叫做“EJBoss”,在Sun公司的干预下(注意,“EJB”是注册商标), JBoss获得了今天的名字。虽然从问世起就一直受到关注,但JBoss第一个达到产品化标准的版本可能是它的2.2版。它的易用让人一见难忘:除了标准 部署描述符,无需编写专用的xml配置文件。Oberg自豪地说,“我们的架构并不是按照EJB规范指定的路线设计的,因此也没有走大多数应用服务器走过 的弯路。”
Jboss 3.x版本保持了一贯的创新精神,在用户中间获得了更广泛的认可。但是,文档要收费下载、在邮件列表上提问常常会遭到Fleury等人的斥责。无疑, JBoss的创始者也意识到了自己的幼稚:开源软件只能靠服务盈利,卖文档赚钱有限、骂用户当然更损害企业形象。
虽然以Oberg为首的许多程序员退出了开发队伍(其中很多人成了JBoss的死敌),在开源软件领域也面临JOnAS Geronimo等新老对手的竞争,但JBoss还是以不断推出的新版本站稳了脚跟。在技术上,它是策动J2EE演进的重要力量:拟议中的EJB 3也要追随Jboss 4倡导的开发范式,以至于二者的代码样本之间的差别几乎难以分辨;在商业上,JBoss与Sun公司言和修好,甚至还获得了数量可观的风险投资。 JBoss已经像拥护者预期的那样,成为了应用服务器领域的Linux。



Borland : 深度介入Java
文/左轻候


除了Sun以外,也许没有一家公司 像Borland这样深层地介入Java。Borland开发了最早的Java编译器之一,Borland的工程师参与了早期JDK的设计, Borland的JBCL(JavaBeans Component Library) 技术也成为后来Java Bean规范的基础。但是Borland对Java世界最大的影响还是JBuilder。
1997年11月,Borland JBuilder 1.0发布。虽然第一个版本相对于竞争对手并没有表现出明显的优势,但是Borland凭借深厚的技术实力和正确的市场策略,不断地超越了对手。 JBuilder 3.5成为业界第一个100%基于Java架构的开发工具,并且市场份额很快超过了50%。在随后的版本中,JBuilder持续改进对团队开发、 J2EE架构、Mobile技术等方面的支持,最终成为了Java开发工具市场,特别是大型企业级Java开发市场中的霸主。JBuilder的成功,很 大一个原因来自于Borland坚持的平台中立性,即对不同厂商的解决方案提供一视同仁的支持。
2005年初,随着Eclipse社区的迅速崛起,Borland进入了Eclipse的董事会,成为战略开发者(Strategy Developer) ,并宣布将推动Borland的其它产品与Eclipse的集成。在随后发布的一份文件中,Borland宣称JBuilder的未来版本将放弃原有的 PrimeTime架构,而基于Eclipse架构。这个代号为“Peloton”的版本预计于2006年下半年发布。
Borland对Java的另外两个主要贡献来自Together和BES(Borland Enterprise Server)。Together是著名的建模工具,能够与包括JBuilder在内的许多开发工具进行集成,全球市场份额占有率排名第二。BES AppServer是一种J2EE服务器,在全球市场份额占有率上次于WebLogic和WebSphere,排名第三。



JCP : Java世界的联合国
文/黄海波


当联合国正在为安理会改革问题 吵得如火如荼时,Java世界的“联合国安理会”已经成功地运作了七个年头。JCP(Java Community Process)在1998年由Sun发起成立,目标是通过一个开放、合作和鼓励参与的非盈利组织来发展和推进Java和相关的技术。正是由于JCP计划 的推出可以让所有对Java感兴趣的软硬件厂商,个人和组织都能参与到技术规范的制定和发展过程中,协调各方的兴趣和利益、集思广益,才可以让Java在 短短的几年内异军突起,成为可以和微软开发平台抗衡的一个主流开发语言。JCP计划既然是一个组织,自然也有一定的架构。JCP组织架构主要包括PMO (Program Management Office)、JCP成员、EC、EG。事实上,JCP的架构就好像一个Java世界的联合国。虽然也有不少人批评JCP成为各派利益的角力场,因而效 率低下;但是,它毕竟为Java的顺利发展很好地掌握了方向。



微软与Java : 不得不说的故事
文/孟岩


微软跟Java不对付,地球人都知 道。跟Sun和解了又怎么样? .NET跟Java就是竞争对手,没什么说的。但是有点IT掌故的人都知道,微软并非一开始就跟Java过不去。当年比尔?盖茨盛赞Java是“长期以来 最好的程序设计语言”,而且很早就购买了Java许可证。但是微软作为村里的老大,看着人家的儿子茁壮呈长,不由得生了私心杂念,搞起了小动作,在 Visual J++中加入了一些破坏纯洁性的东西。单独来看,Visual J++是COM时代微软最棒的开发工具,用WFC写Windows应用程序和COM组件实在是一种享受。但是放在Java大家庭里,这个家伙就显得多少有 点不怀好意。一场官司下来,微软被逐出Java大家庭,Visual J++无疾而终。以后的事情尽人皆知,.NET出笼,利齿直指Java,几年撕咬下来,没占着便宜也没吃大亏,如今也算是南北朝对峙,二分天下有其一。设 想如果当时微软能够摒弃帝国主义心态,正确对待Java,与其他人一起共建美好的Java“共产主义社会”,那么今天我们的软件开发世界应该会美好得多。 可惜黄粱一梦,终究是蚂蚁的喜事。2004年,微软与Sun实现了和解,但愿到Java 20周年的时候,我们能更正面地描述微软对Java发挥的作用。

posted @ 2006-08-13 21:35 Alex 阅读(293) | 评论 (0)编辑 收藏

一、软件开发技术

1)服务器端

在最近5年内,Java还是主流,不光是因为当前的普及程度和遗留系统问题,而且除Microsoft几乎所有大公司都投资到Java上面的原因,此外开 源也是一股无法忽略的力量:除了Java方面的开源框架在推动Java,也有Linux在带动java企业应用在普及(别忘记dotnet只能在 Windows Server上面运行)

dotnet有自己的优势,但是在五年内无法和Java取得均势,不光是因为Java普及带来的优势,也不光因为开源界对java的推动,也不光 因为其他大公司在java上面的投资,而是很多公司的行业性质决定了dotnet的出局,例如电信行业,金融行业,电子政务行业等等,是根本没有可能采用 dotnet的。

Python和Ruby算不上后起,但是很有竞争实力,不过基于上面的原因,仍然不能成为主流。

在Java服务器端技术中,清晰的分为两条路线:高端的商业路线,这条路线是EJB3,J2EE5.0;低端的开源路线,这条路线是 Hibernate, Spring。这两条路线也有重叠的地方,例如开源的Struts几乎成为J2EE Web层的标准,开源的Hibernate奠定了EJB3的基础。但是划分路线不是基于技术上的区别,而是基于商业运作上的区别。注重技术支持和商业服务 的公司会选择前者,注重成本控制和选择自由的公司会选择后者。

商业路线的技术方案是:EJB3+Struts;
开源路线的技术方案是:Spring+Hibernate+Struts/Webwork

Struts是一个很成功的开源框架,它的地位短期内还无法动摇,JavaEye有一项使命,就是动摇Struts在Java Web领域的地位,把它赶下王座,把Webwork扶上位!

商业的Web层技术,JSTL算是一个不错的东西,但是和灵活的模板语言如FreeMarker相比,却有很大的差距。JSF基本上是一个没有前途的东西。商业Web层技术因为一直没有出现好的应用,这样也导致了Struts的上位。

服务器端业务层和持久层框架,我非常看好EJB3,原因也不用多谈了,从商业上来说,需要这样一个东西,跨国公司们也需要这样一个产品来卖,来取 代糟糕的 EJB2。开源的方案里面,Spring+Hibenrate是一个很好的商业方案的开源替代,他们不存在很直接的竞争,而是一个互补的关系。这里比较尴 尬的反而是JDO:JDO是商业产品(目前没有好的开源实现),造成开源应用不会对它感兴趣,JDO没有一个像EJB容器那样的脱管环境,造成商业方案对 它不感兴趣。不过有了JDO,我觉得是对EJB3,对Hibernate形成一个良好的竞争环境,这一点是非常有利的。

2)客户端技术

准确的说是RIA应用。虽然我前面对XAML进行了正面的评价,但是我认为我前面有些结论给错了。经过这段时间,我觉得,XAML即时在多年之后,也未必能够成为一个非常成功的解决方案。道理很二:

1、XAML会带来比ActiveX更严重的安全性问题。
XAML本质上就是一个本地应用程序,虽然号称可以在IE浏览器里面运行,但IE就是一个皮而已,XAML应用具备对本地资源完全的访问能力(就 算IE限制也没有用,IE限制就丧失功能,那样的话,功能并不会比Javascript来得更多;不限制的话,就为所欲为了),因此只要IE具备了运行 XAML的能力,黑客将可以非常轻易的通过IE进行入侵,这仅仅需要引导用户在不知不觉中访问一个恶意的网页就搞定了!用户必须面临选择:要么禁止IE对 XAML的运行能力,要么接受随时被攻击的危险。

2、XAML应用本质上也是RIA应用,因此必须进行大量的RPC调用
当前XAML采用XML Web Services进行通讯,这是一种低效的RPC。当前的XAML案例中并没有注意到RPC领域,实际上根据我现在做RIA的体验来说,RPC绝对不是一个简单的事情,要考虑的问题非常多。

从当前的阶段来说,最实际可用的方案有两个:

1、AJAX
实际上就是基于XMLHTTP的JS异步交互,这个东西已经出现很多年了,最近随着Google应用和Sun Blueprint的推出开始火热。我原来对这个东西持否定态度,但是后来转变了。我原来否定态度的一个前提就是:XMLHTTP缺乏成熟的组件库!但是 没有想到的是,现在XMLHTTP从去年下半年开始,如雨后春笋般冒出来。AJAX应用最大的好处就是充分利用现有资源,我认为应成为RIA应用的首选。

2、Flash
Flash的优势也很明显,强大的AS支持,强大的组件可视化设计,强大的交互能力和很炫的用户体验,并且Flash Remoting也已经非常成熟了。Flash的缺点就是Flash虽然嵌入网页,但是和网页没有数据交互能力,Flash另一个缺点就是不适合处理大量 文本内容(HTML最适合)。现在有些人开始滥用Flash了。

因此比较好的方式可能是两种混用,一般不过度复杂的交互交给AJAX,非常复杂,甚至需要托拽操作的,交给Flash。

总结一下:

软件开发领域服务器端技术Java是主流,两个技术路线,一个是EJB3,一个是Spring+Hibernate,此外iBATIS也有一席之地;客户端技术就是AJAX和Flash。

二、数据库技术

基本上格局不会发生多大变化,Oracle还是高高在上,SQL Server进一步蚕食NT平台其他数据库的领地。开源方面,MySQL将一枝独秀,但是开源数据库在很多方面还是和商业数据库有无法拉近的巨大差距。这 也使得商业数据库的地位不可替代。我会比较关注Oracle,MySQL这两个数据库。面向对象数据库仍然不会有什么起色。

三、桌面编程技术

我还是相信一点,对于桌面应用来说,本地代码的位置永远无法被取代,所以我总觉得XAML那样的东西效率实在很成问题。Longhorn要像成 熟,也不是第一个版本就可以达到的。当前桌面应用开发技术,还是首推Delphi,不过我觉得Python是后起之秀,非常有可能在未来取代Delphi

初探在下一代 Windows 中编写和部署应用程序
http://www.microsoft.com/china/MSDN/library/windev/longhorn/DevelopAppLonghorn.mspx

首先,以Microsoft公司的实力和Windows操作系统的占有率来说,Longhorn迟早会被普及,而XAML的开发方式也有可能普及的。记得 当初WindowsXP刚出来的时候,因为资源占用率和新的激活制度招致一片骂声,但是慢慢的,现在也都接受了下来。由此可以推断,Longhorn以其 更加丰富的桌面功能和诱人的外观,会在将来成为主流。

但是Longhorn什么时候才会全面普及,这是很值得琢磨的问题。WindowsXP是2001年推出的,在随后的几年,Microsoft采 用了一些商业手段来迫使用户升级,例如企图取消Windows98的技术支持,不再提供WindowsNT技术支持,不再销售 WindowsNT/Windows98,将Windows2000保持在一个比较高的售价的同时,对WindowsXP推出优惠价格,让 WindowsXP的售价低于Windows2000等等手段。但是直到现在,Windows2000仍然占据了非常高的份额,据我个人的观察是比 WindowsXP略高。按照这种情况来推断,Longhorn要普及,恐怕难度更大,非常多的用户现在仍然是Windows2000的死忠派, WindowsXP推广了四年还未能超过Windows2000,那么Longhorn究竟要几年才能超过WindowsXP呢?我估计四年以上是起码 的。

XAML应用程序不同以往,它只能跑在Longhorn上面,甚至比Java和dotnet要求更严格,后者仅仅下载安装一个运行环境就可以了, 但是前者要求你必须更新操作系统。XAML在IE浏览器中运行虽然肯定是下一代RIA的主流,但是不可忽视的问题是,只要Longhorn没有彻底淘汰 Windows2000/XP,软件开发商和网站开发商就不敢大面积采用XAML。而根据我的观察,现在企业中,Windows98仍有少部分市场份额。 因此Longhorn必须要等待到彻底的,毫不残留的淘汰Windows98,Windows2000,WindowsXP之后,才会全面普及,而在此之 前,不得不经历一个漫长的过渡期。

就好像现在,假设你开发桌面应用程序,你敢只针对WindowsXP开发吗?而彻底不支持98和2000吗?我想,没有哪个软件开发商敢这样做。 除非 Windows2000几乎被彻底淘汰了,你才敢这样做,但是WindowsXP已经推出四年了,还没有Windows2000占用率高,哪全面淘汰究竟 要几年呢?再看看现在dotnet winforms应用,推出也已经五年时间了,但是到现在仍然没有普及开来,根本的原因就是Windows2000/WindowsXP没有预装 dotnet framework。仅仅是需要打包安装一个运行环境就使得winforms五年都推广不了,更何况要求你升级操作系统呢?

我个人的估计是,假设2006年Longhorn如期上市,那么将需要7-9年时间来彻底淘汰Windows2000/WindowsXP。 Longhorm上面XAML应用的初步普及也至少需要4-5年时间以后才会有软件开发商大量去做(想向dotnet是2000年开始宣传和推广的,到 2004年开始普及,今年和明年才会全面普及)。因此,基于XAML应用的普及可能是在2010年以后!上面的估计中还没有包括MacOS 和Linux在桌面会否有什么表现。

先说说服务器端吧:

从可预见的未来来看,服务器和客户端TCP通讯的主流方式一定是HTTP协议(即时通讯软件走UDP端口,不在讨论范围)。在基于HTTP协议之 上,又分为两类:一类是SOAP协议,异构系统支持良好,但是性能很差,目前Microsoft很喜欢用这种方式;一类是轻量级二进制协议,例如 Flash的 AMF协议,Resin的Hessian协议。值得一提的是,不管哪种方式,他们都支持异构的系统,所以完全可用在客户端采用dotnet,在服务器端采 用Java或者Python。因此,XAML的流行不会对服务器端技术产生致命的影响(肯定会提高dotnet的服务器的市场份额)。所以我们可用抛开客 户端影响,单独来看服务器端技术:

1、Java
Java是当前服务器端技术当之无愧的王者,在未来五年内,也不会有任何动摇(受到dotnet和python的影响,市场份额会下降一些)。 Java特别有利的一点是,现在有太多的现存系统基于Java,这些系统都不会轻易迁移到其他平台上。另外还有一个决定因素是除了Microsoft之外 的几乎全部 IT大公司都在Java方面的投资巨大,放弃Java对他们来说也意味着沉重的打击,甚至毁灭性的打击。这些公司可以列很长很长,IBM,HP, Oracle,SAP,Sun,BEA,Macromedia等等。

2、dotnet
由于Microsoft的影响力,dotnet会成为为仅次于Java的第二大服务器端技术,但是Microsoft有一个隐忧,就是Linux 操作系统在服务器端的高速成长。虽然现在Linux在整个服务器端市场的出货量只有13%左右,但是成长率惊人,根据我看到的资料显示,到2008年,将 占据 25%以上的市场份额。考虑到很多公司是自己安装Linux,因此不会被硬件服务器厂商统计进来,因此Linux的服务器端的市场份额应该比25%高一 些。并且现在主要的服务器厂商都对Linux有非常巨大的投入和支持,这些公司包括IBM,HP,Dell(只有Sun不支持),因此Linux在未来会 对Windows在服务器端的市场构成最严重的威胁。

不要忘记dotnet只能在Windows平台上面跑,虽然有mono,但是你不可能移植MTS,COM+,SQL Server etc。所以只要Linux在服务器市场对Windows构成持续的威胁,dotnet就不可能超过Java,Java的地位还是稳稳的老大。从某种程度 上来说,Java的命运是和Linux联系在一起的,只要Linux在服务器端不输于Windows,Java就稳稳压制dotnet。

BTW:从未来来看,Linux和Windows会在低端和中端服务器市场成为主要竞争对手,由于各自都有其不可替代性,所以双方都不可能彻底消灭对方,最大的可能性是Linux和Windows平分市场,或者Windows市场份额略高一点。

3、Python
我个人认为Python会成长为第三大服务器端技术,Python成长于开源,但是又有商业公司来商业运作,并且背后还有大公司的支持,在欧洲普 及的非常好。当然最重要的原因是我觉得Python在技术上非常先进,并且技术发展方向上比较统一,不会出现Java那种吵架的事情。

4、PHP
PHP这东西是不错,Yahoo也在用,IBM现在也对他感兴趣,但是我还是要说PHP没有太广阔的前途,原因很简单,PHP没有服务端中间件, 例如 Java有App Server,dotnet有IIS/MTS,Python有Zope,但是PHP他就是一个脚本,没有自己的中间件就是致命问题。Yahoo用PHP有 其特定的原因,主要是从原先自己的技术迁移到PHP很方便,而IBM支持PHP,显然醉翁之意不在酒,IBM意不在推广PHP,而在于争取到那些使用 PHP的商业大客户们,向他们卖服务。

BTW:感觉欧洲用Python/PHP的很多,似乎开源在欧洲非常深入人心。

从服务器端技术来说,Java还是我们最需要下功夫去学习和掌握的,此外,我会比较倾向于钻研和应用Python,而不是dotnet。原因也很 简单,跟随Micorsoft的技术会很辛苦,Microsoft产生的新概念多,他总是会猛的推出n多种技术,然后让他们在市场上自己生存,最后根据市 场反馈,无情的抛弃某些东西,大力推进有市场前景的东西,这样的例子太多了,举不胜举了。我的感觉就是这种方式会让Microsft经过市场尝试在技术竞 争中筛选最优秀的技术,但是对于Microsoft技术的跟随者来说,未免有点太不公平,整天吭哧吭哧被Microsoft拿来当免费的试验品来用。我特 别不理解的是MSDN宇宙版,Microsoft总是把无穷无尽的文档灌给你,让你永远学不完,但实际上我真的不需要那么多概念,我只需要能够很好的完成 我工作的技术,并且这个技术可以持续的完善就好了。而不是今天给我这样一个东西,明天灌给我无穷的文档,后天当我用顺手以后,又告诉我这东西作废了,你给 我重新学习新东西,然后又是无穷的文档,总之很恼火。

所以就是:重点学习Java,有时间去学习Python,保持对dotnet的关注即可。


客户端:

前面说了那么多XAML的东西,都是和这有关,七年以后肯定是XAML的天下,但是五到七年之内还不是:

1、Java
Java在客户端真的是扶不起的阿斗,这都怪Sun。Sun造就了Java的成功,又一手毁了Java在客户端的市场。那些个Swing和SWT 的死忠团也不要和我争什么,我也懒得和你们争,你们觉得好就好吧,道不同不相与谋,你觉得好你就用你的,我觉得不好我就用别的。用不着缠着我非逼我说 Java做客户端好,没必要,况且就算你逼我承认又怎样?我就是玉皇大帝金口玉言了?得到我的承认,Java就有前途了?我好像还没有那么大本领吧?就是 IBM, Sun也没有那么大本领,所以好不好也不是我说了算,用不着逼我。

2、dotnet winforms
由于Windows2000/WindowsXP不带dotnet CLR,所以winforms一直没有能够普及得很好,等Longhorn一出来,又变成了XAML了,winforms又被淘汰了,所以 winforms的地位特别尴尬,但是在这5-7年中,你想开发既能够在Windows2000/WindowsXP,又能够在Longhorn上面跑的 桌面程序,winforms好像又是Microsoft技术中最好的选择。所以只好一直尴尬下去。

3、VC,VB
dotnet出来以后就开始尴尬了,说用吧,好像很落伍了,都dotnet时代了,说不用吧,又没有好的替代品,现阶段开发桌面程序,还真得不得不用,而且还挺好用的。所以VC6SP5,VB6的死忠团也比较多。

4、Delphi
dotnet出来以后Borland就开始跟风了,这一跟风,连老本都跟没有了。未来的XAML时代,我也不知道Borland怎样找自己的定 位,但不管怎么说,从历史来看,本地代码的应用程序永远有它一席之地!就算XAML又如何如何做得漂亮了,关键的地方,和特定资源处理相关的部分,还是本 地代码的程序管用。你看VB出来多少年了,用VB开发的都是一些上层的项目级别的应用软件,一旦涉及产品领域,还是VC和Delphi管用。所以现在大家 还是不得不用Delphi7阿。

BTW:XAML应用致力于快速开发项目级别的应用,特别是可以跑在IE浏览器里面的,因此是RIA的首选。但是毕竟也有很多不适合用RIA的场 所,特别是例如我要备份某些文件,你用XAML?那性能就不用提了。所以Delphi如果好好发展VCL,封装Windows32 API,我觉得也是一条路,未必比现在跟随dotnet差。

5、Flash RIA
其实我觉得Flash不适合做RIA的,但是Flash普及率太高,XAML又离普及太遥远,而Flash现在就可以用了,所以是当前RIA的首 选。不过我对Macromedia公司比较失望,如果Macromedia能够公布Flash实现细节,作为一个公开的标准向ISO提交,同时免费开源 Flex,我敢说,Flash RIA会迅速普及的。等5-7年XAML的时代,由于Flash的市场占有率,XAML就未必能拼得过Flash。可惜的是Macromedia公司目光 过于短浅,只知道赚眼前的小钱。

6、Python
这5-7年内,RIA应用和RCP应用不会统一,XAML才具备将RIA和RCP统一的实力。从这5-7年来看,Flash是RIA的首选,而RCP的首选,我要推荐Python。原因前面已经提过,简单总结一下:
1)wxWidgets是一个比MFC优雅的库,TortoiseCVS用wxWidges而不用MFC,就是因为wxWidgets好用,而不是为了可以移植。
2)Python的面向对象脚本语言编程适合快速界面开发
3)Python在服务器端和客户端都非常有前途,可以形成一个统一的解决方案,这一点明显比Java有优势
4)Python桌面应用程序可以完全编译为本地代码,脱离Python运行环境,这一点比dotnet winforms都有优势
5)Python可以不受限制的任意调用Windows32 API,所以凡是VC6可以做的事情,Python就可以做

试想一下,现在我们开发桌面应用程序有什么要求?
一、不要附带一个JRE或者CLR的累赘
二、可以快速开发
三、性能要有保证
四、方便的远程方法调用支持
此外如果能够跨平台就最好了

Java前三点都不符合;dotnet winforms不符合一;VC6不符合二和四,VB6不符合三和四;Delphi7符合前四点;Flash RIA不符合三;Python全部都符合!并且请记住Python是一个完全开源免费的方案!

客户端技术在这5-7年中,在RIA领域我会学习一下Flash,在RCP领域我会重点学习Python,此外会观望一下XAML。

posted @ 2006-08-13 19:24 Alex 阅读(365) | 评论 (0)编辑 收藏

一.  概述

企 业在进行业务处理时,政府在进行公文审批时,都是以流程形式而进行的,在信息化的过程中,企业、政府也将这些业务处理、公文审批的过程信息化了,早期通常 是通过程序硬编码的方式来处理这些业务、公文的流转,随着业务、公文的复杂的处理情况不断出现以及需求的不断变更,这种硬编码的方式显然已无法应对,这个 时候工作流管理系统应运而生,掀起了一股工作流管理系统的热潮。

那么到底工作流管理系统能够带来什么好处?工作流管理系统通过对业务、公文流转进行分析以及抽象,将不变和变化的部分进行划分,用户可轻松的通过可视化的工具对事项的流程、流程环节涉及的人员 ( 角色 ) 、流程环节的表单、流程环节的操作进行修改,从而到达了应对不断变化的需求的目的,而工作流管理系统通常提供的流程监控、查询统计模块更是极大程度的为用户优化流程提供支持,以提高企业、政府的工作效率。

本文主要描述工作流管理系统通常的结构、参考模型以及通常使用的调度算法。

二.  构成

工作流管理系统,简称 WFMS , 经过对业务、公文流转过程的分析以及抽象,工作流管理系统围绕业务交互逻辑、业务处理逻辑以及参与者三个问题进行解决,业务交互逻辑对应的为业务的流转过 程,在工作流管理系统中对应的提出了工作流引擎、工作流设计器、流程操作来解决业务交互逻辑的问题,业务处理逻辑对应业务流转过程中的表单、文档等的处 理,在工作流管理系统中对应的提出了表单设计器、与表单的集成来解决业务处理逻辑的问题,参与者对应到的为流转过程中环节对应的人或程序,在工作流管理系 统中通过与应用程序的集成来解决参与者的问题。

工作流管理系统为方便业务交互逻辑、业务处理逻辑以及参与者的修改,多数通过提供可视化的流程设计器以及表单设计器来实现,为实现工作流管理系统的扩展性,多数提供了一系列的 API

一个完整的工作流管理系统通常由工作流引擎、工作流设计器、流程操作、工作流客户端程序、流程监控、表单设计器、与表单的集成以及与应用程序的集成八个部分组成。

2.1.       工作流引擎

工 作流引擎作为工作流管理系统的核心部分,主要提供了对于工作流定义的解析以及流程流转的支持。工作流定义文件描述了业务的交互逻辑,工作流引擎通过解析此 工作流定义文件按照业务的交互逻辑进行业务的流转,工作流引擎通常通过参考某种模型来进行设计,通过调度算法来进行流程的流转 ( 流程的启动、终止、挂起、恢复等 ) ,通过各种环节调度算法 (SPLIT AND OR ) 来实现对于环节的流转 ( 环节的合并、分叉、选择、条件性的选择等 )

2.2.       工作流设计器

工作流设计器为可视化的流程设计工具,用户通过拖放等方式来绘制流程,并通过对于环节的配置来实现环节操作、环节表单、环节参与者的配置。

工作流设计器为用户以及开发商提供了快速绘制、修改流程的方式,工作流设计器的好坏决定到工作流管理系统的易用性。

2.3.       流程操作

流程操作指所支持的对于流程环节的操作,如启动流程、终止流程、挂起流程、直流、分流 ( 单人办理 ) 、并流 ( 多人同时办理 ) 、联审等,象这些流程操作都是可直接基于引擎所提供的环节调度算法来直接支持的,而在实际的需求中,通常需要自由的对于流程进行干涉,如取回、回退、跳转、追加、传阅、传阅办理等,而这些流程操作对于工作流引擎来说是不合理的,因此必须单独的去实现。

流程操作支持的好坏直接决定到一个工作流管理系统的实用性。

2.4.       工作流客户端程序

工作流客户端程序为工作流系统的表现形式,通常使用 Web 方式进行展现,通过提供待办列表、已办列表、执行流程操作、查看流程历史信息等来展现工作流系统的功能。

2.5.       流程监控

流程监控通过提供图形化的方式来对流程执行过程进行监控,包括流程运转状况,每个环节所耗费的时间等等,而通过这些可相应的进行流程的优化,以提高工作效率。

2.6.       表单设计器

表单设计器为可视化的表单设计工具,用户通过拖放的方式来绘制业务所需的表单,并可相应的进行表单数据的绑定。

表单设计器为客户以及开发商提供了快速修改表单的方法,表单设计器的易用与否以及功能的完善与否影响到工作流管理系统的易用性。

2.7.       与表单的集成

通常业务流转需要表单来表达实际的业务,因此需要与表单进行集成来实现业务意义,与表单的集成通常包括表单数据的自动获取、存储、修改,表单域的权限控制、流程相关数据的维护以及流程环节表单的绑定。

与表单的集成的好坏影响到工作流管理系统是否能提高开发效率。

2.8.       与应用程序的集成

通过与应用程序的集成来完善工作流管理系统的业务意义,主要涉及到的是与权限系统以及组织机构的集成。流程环节需要相应的绑定不同的执行角色,而流程操作通常需要与权限系统、组织机构进行关联。

三.  参考模型

工作流系统通常通过参考一些标准的模型来进行设计,主要的有 WFMC OMG ,在这里主要介绍一下 WFMC

3.1.       WFMC

WFMC 是国际工作流管理联盟,它于 1993 年成立,发布了一系列的工作流定义、软件接口的草案文本,是目前世界上公认的最具权威性的工作流标准制定机构,得到了广泛的支持和应用。

2002 10 25 WFMC 发布了基于 XML 的流程定义语言 1.0 版的最终文本( Workflow Process Definition Interface----XML Process Definition Language  文档编号: WFMC-TC-1025 ),以及此前发布的工作流应用软件接口规范 WFMC-TC-1009, WFMC-TC-1013 等系列文件,构成了工作流定义及系统的设计标准。

为了实现不同工作流产品之间的互操作, WfMC 在 工作流管理系统的相关术语、体系结构及应用编程接口等方面制定了一系列标准。工作流管理联盟给出的工作流定义是:工作流是指整个或部分经营过程在计算机支 持下的全自动或半自动化。在实际情况中可以更广泛地把凡是由计算机软件系统(工作流管理系统)控制其执行的过程都称为工作流。

一 个工作流包括一组活动及它们的相互顺序关系,还包括过程及活动的启动和终止条件,以及对每个活动的描述。工作流管理系统指运行在一个或多个工作流引擎上用 于定义、实现和管理工作流运行的一套软件系统,它与工作流执行者(人、应用)交互,推进工作流实例的执行,并监控工作流的运行状态。

WFMC 主要提出了五个接口与工作流执行服务一起共同组成了工作流系统:

l         接口一 ( 工作流定义交换 ) ,用于在建模和定义工具与执行服务之间交换工作流定义。主要是数据交换格式和 API 。数据交换通过 XPDL API 通过 WAPI

l         接口二 ( 工作流客户端应用接口 ) ,用于工作流客户端应用访问工作流引擎和工作列表,通过 WAPI 完成。

l         接口三 ( 被调用的应用接口 ) ,用于调用不同的应用系统。

l         接口四 ( 工作流系统互操作接口 ) ,用于不同工作流系统之间的互操作。

l         接口五 ( 系统管理和监控 ) ,用于系统管理应用访问工作流执行服务。

四.  核心调度算法

通常流程引擎采用的核心调度算法主要有 FSM 以及 PetriNet 两种,基于调度算法来完成流程的流转。

4.1.       FSM( 有限状态机 )

FSM 的定义为包含一组状态集( states )、一个起始状态( start state )、一组输入符号集( alphabet )、一个映射输入符号和当前状态到下一状态的转换函数( transition function )的计算模型。当输入符号串,模型随即进入起始状态。它要改变到新的状态,依赖于转换函数。在有限状态机中,会有有许多变量,例如,状态机有很多与动作( actions )转换 (Mealy ) 或状态(摩尔机)关联的动作,多重起始状态,基于没有输入符号的转换,或者指定符号和状态(非定有限状态机)的多个转换,指派给接收状态(识别者)的一个或多个状态,等等。

遵循 FSM 流程引擎通过状态的切换来完成流程的流转。

4.2.       PetriNet

信息流的一个抽象的、形式的模型。指出一系统的静态和动态性质。 petrinet 通常表示成图。图中有两类用弧彼此相连的结点(称为地点和变换)和指示其动态性能的标记(称为记号)。

遵循 PetriNet 流程引擎通过令牌来决定流程的流转。

posted @ 2006-08-11 18:26 Alex 阅读(494) | 评论 (0)编辑 收藏

1. 从奴隶社会到封建后期

工作流(WorkFlow)的概念是在现代信息系统的建设中逐步形成的,它有一个从局部到整体、从初级到高级、从简单到复杂、从奴隶社会到封建盛世的发展过程,按其发展历程,我们一般把它分为三个阶段:

﹡ EDF(电子数据流)阶段

EDF(电子数据流)阶段是工作流的奴隶社会阶段。此时的工作流在信息技术中的应用,仅着眼于利用信息技术减轻人们在流程中的计算强度,如设计一个流程用来协调多个会计统计帐目。所以,EDF最主要的特点是仅对企业单项业务进行处理,基本不涉及管理的内容。

﹡ TPF(事务处理流)阶段

TPF(事务处理流)阶段是工作流的封建初期。TPF并没有形成对企业的全局业务的管理,而着眼于对企业局部业务的管理,比如,设计一套工作流程,来管理物资的采购和供应。

﹡ IMF(信息管理流)阶段

当 今的工作流已经发展到封建后期-------IMF(信息管理流)阶段, IMF强调对企业业务的全局的整体性的管理。在这个阶段,工作流就是为了完成同一目标而相互衔接、自动进行的一系列业务活动或任务。目前,工作流技术与信 息技术以及企业管理紧密结合,已经悄悄渗入MIS系统、ERP系统和CRM系统等企业级关键系统中,并迅速成为这些系统的核心。在这个阶段,保皇派工作流 经历了大发展,革新派工作流也风涌而现。

2. 保皇派与革新派

与其它信息技术相比,工作流技术属于较新的 一个, 它现在仍处于标准的制定阶段,目前已有的标准按采用的技术分为两大派别,第一派是保皇派,基于纯XML技术;第二派是革新派,基于Web服务技术。保皇派 与革新派打得难分难解,所以,现在仍有一些工作流引擎没有采用任何标准而独立实现,也就是说,它们不依靠保皇派,也不依靠革新派。

2.1 保皇派分支

﹡ XPDL(Xml Process Definition Language)

XPDL 是保皇派最得人心的一派,因为它是太子。在工作流领域第一个致力于标准化工作的是Workflow Management Coalition (WfMC),它成立于1993年。1994年11月,wfmc发布了工作流管理系统的参考模型。参考模型提出了五类接口,有关过程模型的定义则构成了接 口一的核心内容。接口一早期的标准为WPDL(Workflow Process Definition Language),后来,这一接口的规范变更为XPDL。XPDL是至今工作流领域最为重要的一个标准,目前大多数工作流引擎是依据该标准设计开发的。

﹡ BPML(Business Process Model Language)

因为对太子派的工作方式不满意,BPMI发布BPML规范,成立新的八爷党。由于八爷党发展的非常之快,WfMC和BPMI在2002年6月26日宣布将合作制定业务流程和工作流标准,即采用BPML来描述工作流过程,同时采用XPDL所定义的工作流模型。

﹡ OMG的Workflow Management Facility

四 爷OMG是支持太子的,不过四爷这个人很有报负,在其他领域也做的有声有色。四爷OMG的Workflow Management Facility联合太子的WfMC规范,定义如何将工作流向CORBA转换---要知道, CORBA可是四爷的强项。现在很多老百姓支持四爷OMG的Workflow Management Facility。四爷这招,发展了自己的势力,又不惹太子生气,真是妙啊!

保皇派其它分支势力很小,这里就不多说了。

2.2 革新派分支

﹡ WSCI

2002 年6月26日,BEA,Intalio,SAP,Sun四家公司提出了基于xml的WSCI规范,推动Web服务进入了一个全新的阶段。这个规范主要描述 了一个参与和其它服务进行协作交互的Web服务所交换的消息流。WSCI是第一个革新派,后来又发展了几个新的革新派,

如WSFL(属IBM),Xlang(属MS),因有天生缺陷,均没有很大起色。

﹡ ebXML

ebXML 是一组支持模块化电子商务框架的规范。ebXML支持一个全球化的电子市场,它使得任意规模的企业通过交换基于XML的信息,不受地域限制地接洽和处理生 意。ebXML是联合国(UN/CEFACT,贸易促进和电子商务中心)和OASIS(结构化信息标准发展组织)共同倡导、全球参与开发和使用的规范。由 于现在老百姓都不喜欢抬着羊去换米,而喜欢在网上来个B2C,所以革新派ebXML最近发展很快。

﹡ BPEL

2002 年8月9日,Microsoft, BEA, IBM, SAP & Siebel联合提交发布了BPEL规范。 BPEL联合了一系列革新派/保皇派力量( XLANG, WSFL, BPML)。此规范描述如何处理输入的消息,它不是一个关于业务流程规格化定义的规范。简单的说,可以将它看作XML形式的编程语言,提供将WSDL- Services组合成控制流的能力。顾名思义,此规范重点在(也不只限于)Web Service。

还有其它的革新派如RosettaNet等,因为势力很小,这里也不多说了。

3. 大户人家

这里所谈的大户人家指工作流领域的商业软件供应商,他们都背靠靠山,或穷或富。我们这里按地区来划分他们。

3.1中国之外的大户

﹡ BEA 的WLI

﹡ Fujitsu的 i-Flow

﹡ IBM的 Holosofx

﹡ SAP 的NetWeaver

﹡ Sonic 的Orchestration Server

﹡ Ultimus

﹡ Versata

这些人家大都非常富有;如果需要,请自己搜索他们的信息。

3.2国内的大户人家

﹡ 信雅达的SunFlow 这个公司留给我的印象很好,包括他们的总经理石总。

﹡ 西安协同的协同工作流

﹡ 上海东兰的DLFlo

这里对公司就不多介绍了,否则有做广告的嫌疑J。本人有国内工作流产品的比较报告,大家需要可以给我发邮件索取。

4. 寒门傲骨

这里的寒门傲骨指的是开源工作流引擎的实现,寒门子弟也是有政治取向的,我们还是按保皇派和革新派来区分他们,而自由派指没有按固定的标准来实现的工作流门派。

4.1保皇派寒门

﹡ OFBiz

OFBiz 最主要的特点是OFBiz提供了一整套的开发基于Java的web应用程序的组件和工具。其中包括实体引擎, 服务引擎, 消息引擎, 工作流引擎, 规则引擎等。OFBiz先前的工作流引擎基于WfMC和OMG的规范,使用XPDL作为流程定义语言,也就是说,它是支持太子XPDL的,而且和十三爷 OMG的关系非常之好。OFBiz新版的工作流引擎采用Shark工作流引擎,我们不建议再去学习OFBiz自身的工作流引擎。

﹡ OBE

OBE 是由Adrian Price主持开发的一个开放源码的Java工作流引擎,支持WfMC规范,包括接口1(XPDL)、接口2/3(WAPI)和接口5。OBE主要基于 J2EE实现。OBE的接口1实现得非常好,可惜,OBE的载体公司Zaplet已经于前不久被合并,合并后的公司没有继续发展OBE的打算。 Adrian Price离开了原来的公司,投奔我们前面说过的大户Versata。Versata也不打算继续OBE。OBE至今没有release版,很是可惜。

﹡ Shark

Shark 是完全根据WFMC规范实施的,可扩展功能的工作流引擎,它利用xpdl来定义流程,同时还包括服务器端的用于活动节点执行的WFMC工具代理API。 Shark中的每个组件例如持久层,事物管理器,脚本引擎,流程库,都是可以按照标准实施运用的,而且还可以被具体项目的模块扩展和替换。Shark和 XPDL定义工具的事实标准JAWE同出名门,市场前景被很多人看好。OFBiz新版的工作流引擎采用Shark工作流引擎,OBE的载体公司 Zaplet被合并,对Shark的发展将很有利。2004年9月9日,shark发布1.0版本,对它的发展无疑是一剂强心针。笔者从Shark发展的 早期就在国内力推它,有幸成为Shark工作流引擎在国内的主要推广者之一(http://blog.csdn.net/hongbo781202/), 感到十分荣幸。Shark的讨论请看http://211.95.124.238:22/cgi-bin/forums.cgi?forum=24。

4.2 革新派寒门

﹡ OpenebXML

OpenebXML项目致力于提供一个ebXML框架,主要支持 UN/CEFACT和OASIS发布的ebXML规范2.0版。

﹡ Bonita

Bonita是一个符合WfMC规范、灵活的协同工作流系统。Bonita基于浏览器、使用SOAP和XML数据绑定技术的Web Services封装了已有的工作流业务方法并将它们以基于J2EE的Web Service形式发布。

﹡ Twister

Twister的目标是提供新一代、易集成、应用Java领域中最新成果、面向B2B的工作流解决方案。流程引擎基于BPEL业务流程规范和Web Service标准。

﹡ ActiveBpel

ActiveBPEL 引擎是一个于今年7月发布的健壮的运行时环境,它能执行用户按BPWL4WS规范编写的业务流程。ActiveBPEL引擎由Active Endpoints公司开发和维护,该公司同时在它的多个商业产品中使用了该技术。本人将密切观注ActiveBPEL引擎的技术发展和产品状态。

4.3 自由派寒门

﹡ OSWorkflow

OSWorkflow的最大特点是灵活

﹡ OpenWFE

OpenWFE是一个开放源码的Java工作流引擎。 它的思想来源于 Scheme,包括可升级的三个组件:引擎、工作列表和Web界面。

﹡jBpm

jBpm 是tom baeyens编写的一个灵活可扩展的工作流管理系统。jBmp将工作流应用开发的便利性和杰出的企业应用集成(EAI)能力结合了起来。jBmp包括一 个Web应用程序和一个日程安排程序。jBmp是一组J2SE组件,可以作为J2EE应用集群部署。国内目前有部分人研究jBpm。

5 大局势

目前是封建社会后期,以太子党XPDL为首的保皇派还将辉煌一段时间。我个人认为,在Ofbiz投靠Shark,强势派OBE倒台,自由派不得人心的情况下,Shark工作流引擎依靠与XPDL定义工具JAWE的兄弟关系,将坐上保皇派头把交椅。

目 前,革新派和保皇派的争夺并不激烈。因为在现在的情况下, 革新派的根基Web服务并不劳靠,在老百姓中的影响不太大,所以革新派只求从保皇派嘴边分口食就行了。但随着社会的发展, Web服务将越来越流行,现在,MS/IBM/BEA等跨国巨头越来越主推BPEL4WS标准,并且已经发布基于BPEL4WS标准的系列产品,而且,他 们还主推Integration/Portal的概念,这些概念将把工作流带入资本主义阶段。
posted @ 2006-08-11 18:25 Alex 阅读(615) | 评论 (0)编辑 收藏

Osworkflow完全用java语言编写的开放源代码的工作流引擎,具有显著的灵活性及完全面向有技术 背景的用户的特点。用户可以根据自身的需求利用这款开源软件设计简单或是复杂的工作流。通过使用,用户就可以把工作中心放在业务和规则的定义上,而不需通 过硬编码的方式实现一个Petri网或是一个有穷自动机。用户可以以最小的代价把osworkflow整合到自己的程序中来。Osworkflow几乎提 供了所有用户可能在实际流程定义中需要用到的工作流构成元素,如:环节(step)、条件(conditions)、循环(loops)、分支 (spilts)、合并(joins)、角色(roles)等等。(假如读者对这些概念还不熟悉,笔者将在Osworkflow基本概念一节中进行简单描 述。)

但是,这款开源软件的文档十分匮乏,而且在大多数现实情形中并不适用。本文将尝试为读者填平实际的用例需求与十分简单的说明文档间的鸿沟。

用户可以在OpenSymphony的网站上下载osworkflow的发布。当前的最高版本是2.7(译者注:最新版本为2.8). 解压缩发布的软件包,即得到二进制程序、源代码、API文档、说明文档等。用户可以在软件的论坛和维基上获得进一步的帮助。

版权声明:任何获得Matrix授权的网站,转载时请务必保留以下作者信息和链接
作者:Diego Naya;bugaboo(作者的blog:http://blog.matrix.org.cn/page/bugaboo)
Matrix原文:http://www.matrix.org.cn/resource/article/44/44467_osworkflow.html
关键字:osworkflow;business

什么是工作流?

维基百科(Wikipedia,WP)把工作流定义为“一份工作的操作过程”:任务如何组成、如何操作、相关顺序如何、如何同步、信息如何流动以支持这些任务、以及任务如何被跟踪等。
一个工作流引擎实现了业务的流程处理。用户应可以自动跟踪过程,这将使得引擎更具效率。同时用户可以对工作流进行建模,监控及统计引擎数据等。

示例业务过程:贷款程序

本 文的示例业务过程研究一个贷款应用程序的实例。我们将通过一个利用osworkflow工作流引擎的工作流来实现它。这个过程会在每家银行及金融机构中出 现, 其区别仅仅体现为有更过的部门或更多地文档需要处理. 在本示例中, 我们会用尽量简单的方法来实现这个业务流程以便于用户理解。图一描述了这个业务流程

image
图一 贷款业务流程

过程非常简单,分为如下4步,如下所示:
1)        填写表格:银行客户填写表格申请贷款。
2)        风险分析:一位风险分析家评估不良贷款的风险。
3)        财务历史审查:财务管理官员负责检查客户历史贷款、应付账单、信用卡历史纪录等信息。
4)        最终决定(同意/拒绝):银行部门主管根据风险分析情况及财务历史审查情况最终决定是否贷款给该客户。

正如我们之前看到的,每个工作流都包含角色,每个角色都包含被分配的任务。下文说明了业务流程中涉及到的角色:
1)        前台职员:在某个银行部门向顾客提供信贷申请表的雇员。
2)        财务官员:负责检查申请者历史财务情况(往期贷款、未支付帐单等等)的职员。
3)        风险分析家:负责分析将钱给予借贷申请者的外部因素(比如社会经济情况等)及借贷者本人的个人情况。
4)        银行部门经理:负责最终决定是否给予借贷者贷款的经理。
请记住,“信贷申请表”是一个重要的概念(我们将在系统实现一节看到其重要性)因为它是流经整个工作流的业务数据。

基本工作流概念

笔者在开篇曾介绍osworkflow提供了一些特有的构造,现在笔者将逐一介绍它们。

首 先,在osworkflow中读者需要了解得最重要的概念是环节,每个工作流包含了多个环节,读者可以把环节想象成工作流中每一个重要的活动。每个环节可 以有一些诸如“已完成”、“正在处理”、“已添加至处理队列”、“未处理”等的状态,设计工作流的人可以根据需要自己定义状态。

在每个环节,动作被用户指定为自动或手动地执行。每个动作执行后,都有一个结果(result)。结果决定了工作流的流转方向:可以停留在同一环节,跳转到另一环节,跳转到一个分支,或者汇集到一个合并等。
最后两个概念涉及用户对业务流程的并发执行,分支把工作流分解为两个并行的环节,合并则在用户满足一定条件后,把两个并行的环节合并成一个。

动 作的执行代表了业务流程的执行,每个动作都有一组预处理功能(pre-functions)和一组后处理功能(post-functions)。其作用正 如读者想象的那样,一个在动作触发之前执行,一个在动作触发之后执行。一个简单的例子是:可以在预处理功能中检验申请表格数据的正确性,而后在后处理贡功 能中把经检验的数据保存至数据库。

动作的执行结果可以是有条件的(conditional)或无条件的(unconditional)。 对于有条件的结果,引擎将首先检查是否条件被满足,然后再交给工作流来处理。如果条件不满足的话,引擎将进一步判断下一个有条件结果是否得到满足,以此类 推,直到系统最终执行到无条件结果进行处理。

读者可能会问,如果所有的条件结果都没有得到满足会如何呢?事实上,每个动作都强制要求具有唯一一个无条件结果。与此对应的,可以有多个有条件的结果。

业务规则常常在最终结果中带有条件判断,比如,“如果申请来自于一个老客户,则流转到环节1”或者“如果当前系统的用户的角色是经理的话,直接流转道最后一个环节”。

最 后一个重要的概念是步骤状态(process state),在osworkflow中,当前步骤状态是所有当前环节状态的集合。读者可能会认为工作流在运行过程中只能有一个状态,但现实的情况是:因 为对分支和合并的支持,引擎能够做到对环节的并发控制,因此工作流的当前状态就可能出现:“等待风险分析及已核查财务历史”的情况。
激活动作的用户被顺理成章地称为触发者(caller),每个环节都有一个所有者(owner),以代表在当前环节中负责执行动作的角色或用户。

当用户在环节中运转流程的时候,已完成的环节被保存至历史表中(history),用户当前所处的环节成为当前环节(current steps)。

最 后,读者可能注意到,在osworkflow中并不存在其他工作流引擎中所包含的工作项(workitem)的概念。这是因为osworkflow是“十 分底层”的工作流实现,怎样实现或定义工作项完全交由用户来决定。笔者认为工作项的概念太过抽象,用业务数据来称呼它或许更为贴切一些。

Osworkflow 的文档中介绍了更多的构造元素,如寄存器(Registers),共用方法(common functions)等,但笔者建议在建立好第一个工作流以后再去研究它们。它们是osworkflow基本元素外的高级特性,而我们前面所认识的元素则 是osworkflow的根本所在。


Osworkflow体系结构

我们将在本节分析控制osworkflow的体系结构,我们需要理解它是怎样适用到我们的程序中来的。如我们所猜想的,Osworkflow最主要的接口是Workflow,这个接口也是整个工作流引擎的入口点。是整个系统的门面(facade)。

接口的实现主要关注具体的业务操作能力,这个接口定义了工作流查询,获取当前的可执行动作,执行动作,显示历史环节等。
工作流被持久化在工作流存储体(Workflow Store)中,osworkflow提供了几种持久化的方法,包括Hibernate持久化集成,JDBC持久化集成等。一个存储体包含了环节信息,变量,工作流自身的描述信息等等。

用户可能遇到的最常见的应用模式如下所示:
1)通过给定的状态在工作流存储体中查询工作流信息,通常还根据某一个工作流程中具有需执行动作的用户来进行查询。这种查询时通过WorkflowQuery对象中的Workflow.query()方法实现的。
2)通过getAvailableActions()方法列出所有在满足条件查询结果中可执行的操作。
3)通过doAction()方法执行用户选择的动作。在执行动作的时候一些执行参数可以以java.util.Map的形式传递,以实现在工作流定义的运行期进行信息的传递。
4)用户可以有选择地通过调用initialize()实例化一个工作流。

在理想情况下,由业务逻辑层负责调用osworkflow中的方法,如图二所示:

image
图二 在业务逻辑中集成工作流

在osworkflow 中,业务逻辑描述在一个XML文件中,称为工作流描述符(workflow descriptor.)。我们将在实现小节中建立一个简单的描述符。在工作流描述符中的功能(functions)和条件(conditions)中, 用户可以定义自己的业务逻辑。笔者将在把工作流集成到应用程序中一节中进行论述。

实现

本小节介绍如何把一个业务逻辑抽象成一个工作流。首先我们要在业务流程图中识别出工作流的环节。如图一所示,显然,我们共有四个环节,同时包含一个分支及一个合并。在下面的bank.xml文件中,读者将看到它们在描述符中是如何被表示的。

建立好环节以后,必须在每个环节中添加一些动作以便于工作流运转。每个动作有唯一的无条件结果,条件结果由读者有选择地来实现。
<step id="1" name="Form Filling">
<actions>
  <action id="2" name="Fill Form">
   <results>
        <unconditional-result old-status="Finished" split="1"/>
   </results>
  </action>
</actions>
</step>


接下来要并行地执行风险分析和财务历史核查,这里是放置分支的最理想地点。
<splits>
<split id="1">
  <unconditional-result old-status="Finished"
    status="Underway" owner="Risk Analyst" step="2"/>
  <unconditional-result old-status="Finished"
    status="Underway" owner="Financial Officer" step="3"/>
</split>
</splits>


在 部门经理最终确认以前,并发的工作流环节必须得到合并。我们可以通过应用一个合并(join)来实现它,合并通过一个条件告诉工作流引擎,是否可以合并并 进行到下一环节。在本例中,我们假设这个条件为:前面两个环节都已具有“Finished”的结束状态。即当风险分析或财务审核任何一个未完成前,不能进 行到下一步:
<joins>
<join id="1">
  <conditions type="AND">
   <condition type="beanshell">
    <arg name="script"><![CDATA[
     "Finished".equals(jn.getStep(2).getStatus()) &&
        "Finished".equals(jn.getStep(3).getStatus())
      ]]></arg>
    </condition>
   </conditions>
  <unconditional-result old-status="Finished"
    status="Underway" owner="Manager" step="4"/>
</join>
</joins>


下面在描述符中加入每个环节的所有者,正如我们在基本概念一节看到的那样,所有者通常代表了环节间交互的角色,角色的引入默认情况下通过osuser框架来实现。
用户既可以手写XML描述符文件,也可以通过osworkflow提供的设计器来实现。读者可以在OpenSymphony的网站上试用这个工具。

测试实现
测试时,我们可以应用osworkflow提供的例子,把bank.xml放到WEB-INF/classes文件夹下,在文件workflows.xml中添加一行以使引擎能够识别这个工作流。

把工作流集成到应用程序

在建模及测试工作流之后,我们即可以通过下面的几行代码把osworkflow集成到我们的程序中。
orkflow wf = new BasicWorkflow(username); 
HashMap inputs = new HashMap();
inputs.put("docTitle", request.getParameter("title"));
wf.initialize("workflowName", 1, inputs);


inputs 哈希表包含了初始工作流动作中需要传出的参数,有几个实现了Workflow接口的类,其中BasicWorkflow是不支持事务的简单实现。在工作流 执行过程中可以在流程中调用外部的方法,这种方法应该是实现了FunctionProvider接口的方法类。然后我们就可以用以下的方式调用它:

<action id="1" name="Execute business rule">
<pre-functions>
  <function type="class">
   <arg name="class.name">java.net.DroolsExecutorFunction</arg>
   <arg name="ruleBaseName">BusinessRules.drl</arg>
  </function>
</pre-functions>
...


用户可以通过实现Condition接口,添加自己的条件控制。FunctionProvider及Condition接口可以调用工作流中的已知方法,这两个接口都可以接受来自于XML描述符文件中的参数。

结论

把 一个业务流程抽象成一个工作流的任务并不容易,需要好的方法和合适的工具,osworkflow是一个为我们提供了许多可重用结构的理想工具。希望通过对 本文的阅读,读者能够理解最基本的osworkflow概念。本文论述过程中所采用的方法非常基础和简单,但却值得借鉴。
posted @ 2006-08-11 18:16 Alex 阅读(951) | 评论 (1)编辑 收藏

key words: decorator 装饰模式 jdk中的装饰模式

JDK为程序员提供了大量的类库,而为了保持类库的可重用性,可扩展性和灵活性,其中使用到了大量的设计模式,本文将介绍JDK的I/O包中使用到的Decorator模式,并运用此模式,实现一个新的输出流类。

  Decorator模式简介

  Decorator模式又名包装器(Wrapper),它的主要用途在于给一个对象动态的添加一些额外的职责。与生成子类相比,它更具有灵活性。
有 时候,我们需要为一个对象而不是整个类添加一些新的功能,比如,给一个文本区添加一个滚动条的功能。我们可以使用继承机制来实现这一功能,但是这种方法不 够灵活,我们无法控制文本区加滚动条的方式和时机。而且当文本区需要添加更多的功能时,比如边框等,需要创建新的类,而当需要组合使用这些功能时无疑将会 引起类的爆炸。

  我们可以使用一种更为灵活的方法,就是把文本区嵌入到滚动条中。而这个滚动条的类就相当于对文本区的一个装饰。 这个装饰(滚动条)必须与被装饰的组件(文本区)继承自同一个接口,这样,用户就不必关心装饰的实现,因为这对他们来说是透明的。装饰会将用户的请求转发 给相应的组件(即调用相关的方法),并可能在转发的前后做一些额外的动作(如添加滚动条)。通过这种方法,我们可以根据组合对文本区嵌套不同的装饰,从而 添加任意多的功能。这种动态的对对象添加功能的方法不会引起类的爆炸,也具有了更多的灵活性。

  以上的方法就是Decorator模式,它通过给对象添加装饰来动态的添加新的功能。如下是Decorator模式的UML图:



  Component为组件和装饰的公共父类,它定义了子类必须实现的方法。

  ConcreteComponent是一个具体的组件类,可以通过给它添加装饰来增加新的功能。

  Decorator是所有装饰的公共父类,它定义了所有装饰必须实现的方法,同时,它还保存了一个对于Component的引用,以便将用户的请求转发给Component,并可能在转发请求前后执行一些附加的动作。

  ConcreteDecoratorA和ConcreteDecoratorB是具体的装饰,可以使用它们来装饰具体的Component。

  Java IO包中的Decorator模式

  JDK提供的java.io包中使用了Decorator模式来实现对各种输入输出流的封装。以下将以java.io.OutputStream及其子类为例,讨论一下Decorator模式在IO中的使用。

  首先来看一段用来创建IO流的代码:

以下是代码片段:
try {
 OutputStream out = new DataOutputStream(new FileOutputStream("test.txt"));
} catch (FileNotFoundException e) {
 e.printStackTrace();
}

   这段代码对于使用过JAVA输入输出流的人来说再熟悉不过了,我们使用DataOutputStream封装了一个FileOutputStream。 这是一个典型的Decorator模式的使用,FileOutputStream相当于Component,DataOutputStream就是一个 Decorator。将代码改成如下,将会更容易理解:

以下是代码片段:
try {
 OutputStream out = new FileOutputStream("test.txt");
 out = new DataOutputStream(out);
} catch(FileNotFoundException e) {
 e.printStatckTrace();
}

  由于FileOutputStream和DataOutputStream有公共的父类OutputStream,因此对对象的装饰对于用户来说几乎是透明的。下面就来看看OutputStream及其子类是如何构成Decorator模式的:



OutputStream是一个抽象类,它是所有输出流的公共父类,其源代码如下:

以下是代码片段:
public abstract class OutputStream implements Closeable, Flushable {
public abstract void write(int b) throws IOException;
...
}

它定义了write(int b)的抽象方法。这相当于Decorator模式中的Component类。

ByteArrayOutputStream,FileOutputStream 和 PipedOutputStream 三个类都直接从OutputStream继承,以ByteArrayOutputStream为例:

以下是代码片段:
public class ByteArrayOutputStream extends OutputStream {
protected byte buf[];
protected int count;
public ByteArrayOutputStream() {
this(32);
}
public ByteArrayOutputStream(int size) {
if (size 〈 0) {
throw new IllegalArgumentException("Negative initial size: " + size);
}
buf = new byte[size];
}
public synchronized void write(int b) {
int newcount = count + 1;
if (newcount 〉 buf.length) {
byte newbuf[] = new byte[Math.max(buf.length 〈〈 1, newcount)];
System.arraycopy(buf, 0, newbuf, 0, count);
buf = newbuf;
}
buf[count] = (byte)b;
count = newcount;
}
...
}

它实现了OutputStream中的write(int b)方法,因此我们可以用来创建输出流的对象,并完成特定格式的输出。它相当于Decorator模式中的ConcreteComponent类。

接着来看一下FilterOutputStream,代码如下:

以下是代码片段:
public class FilterOutputStream extends OutputStream {
protected OutputStream out;
public FilterOutputStream(OutputStream out) {
this.out = out;
}
public void write(int b) throws IOException {
out.write(b);
}
...
}

同 样,它也是从OutputStream继承。

但是,它的构造函数很特别,需要传递一个OutputStream的引用给它,并且它将保存对此对象的引用。 而如果没有具体的OutputStream对象存在,我们将无法创建FilterOutputStream。由于out既可以是指向 FilterOutputStream类型的引用,也可以是指向ByteArrayOutputStream等具体输出流类的引用,因此使用多层嵌套的方 式,我们可以为ByteArrayOutputStream添加多种装饰。这个FilterOutputStream类相当于Decorator模式中的 Decorator类,它的write(int b)方法只是简单的调用了传入的流的write(int b)方法,而没有做更多的处理,因此它本质上没有对流进行装饰,所以继承它的子类必须覆盖此方法,以达到装饰的目的。

BufferedOutputStream 和 DataOutputStream是FilterOutputStream的两个子类,它们相当于Decorator模式中的 ConcreteDecorator,并对传入的输出流做了不同的装饰。以BufferedOutputStream类为例:

以下是代码片段:
public class BufferedOutputStream extends FilterOutputStream {
...
private void flushBuffer() throws IOException {
if (count 〉 0) {
out.write(buf, 0, count);
count = 0;
}
}
public synchronized void write(int b) throws IOException {
if (count 〉= buf.length) {
flushBuffer();
}
buf[count++] = (byte)b;
}
...
}


   这个类提供了一个缓存机制,等到缓存的容量达到一定的字节数时才写入输出流。首先它继承了FilterOutputStream,并且覆盖了父类的 write(int b)方法,在调用输出流写出数据前都会检查缓存是否已满,如果未满,则不写。这样就实现了对输出流对象动态的添加新功能的目的。

  下面,将使用Decorator模式,为IO写一个新的输出流。

  自己写一个新的输出流

   了解了OutputStream及其子类的结构原理后,我们可以写一个新的输出流,来添加新的功能。这部分中将给出一个新的输出流的例子,它将过滤待输 出语句中的空格符号。比如需要输出"java io OutputStream",则过滤后的输出为"javaioOutputStream"。以下为SkipSpaceOutputStream类的代码:

以下是代码片段:
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* A new output stream, which will check the space character
* and won’t write it to the output stream.
* @author Magic
*
*/
public class SkipSpaceOutputStream extends FilterOutputStream {
 public SkipSpaceOutputStream(OutputStream out) {
  super(out);
 }
 /**
 * Rewrite the method in the parent class, and
 * skip the space character.
 */
 public void write(int b) throws IOException{
  if(b!=’ ’){
   super.write(b);
  }
 }
}

  它从FilterOutputStream继承,并且重写了它的write(int b)方法。在write(int b)方法中首先对输入字符进行了检查,如果不是空格,则输出。

  以下是一个测试程序:

以下是代码片段:
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Test the SkipSpaceOutputStream.
* @author Magic
*
*/
public class Test {
 public static void main(String[] args){
  byte[] buffer = new byte[1024];

  /**
  * Create input stream from the standard input.
  */
  InputStream in = new BufferedInputStream(new DataInputStream(System.in));

  /**
  * write to the standard output.
  */
  OutputStream out = new SkipSpaceOutputStream(new DataOutputStream(System.out));

  try {
   System.out.println("Please input your words: ");
   int n = in.read(buffer,0,buffer.length);
   for(int i=0;i〈n;i++){
    out.write(buffer[i]);
   }
  } catch (IOException e) {
   e.printStackTrace();
  }
 }
}

  执行以上测试程序,将要求用户在console窗口中输入信息,程序将过滤掉信息中的空格,并将最后的结果输出到console窗口。比如:

以下是引用片段:
Please input your words:
a b c d e f
abcdef

  总 结

   在java.io包中,不仅OutputStream用到了Decorator设计模式,InputStream,Reader,Writer等都用到 了此模式。而作为一个灵活的,可扩展的类库,JDK中使用了大量的设计模式,比如在Swing包中的MVC模式,RMI中的Proxy模式等等。对于 JDK中模式的研究不仅能加深对于模式的理解,而且还有利于更透彻的了解类库的结构和组成

(转载文章请保留出处:Java家(www.javajia.com))

posted @ 2006-08-10 21:04 Alex 阅读(681) | 评论 (0)编辑 收藏

key words : POI java读取Excel  java.io.IOException Unable to read entire block

版本:2.5.1final
错误提示:
java.io.IOException Unable to read entire block

出这个问题具有随机性,有时候没问题,有时候将Excel里的CellType改一下好像就没问题,但也不总是这样,真是莫名其妙.

Google了一下是一个bug,重新下载src文件,将RawDataBlock.java文件的RawDataBlock(final InputStream stream)constructor覆盖:

public RawDataBlock(final InputStream stream) throws IOException
    {
        _data 
= new byte[ POIFSConstants.BIG_BLOCK_SIZE ];
        
int count = 0;
        
int totalBytesRead = 0;
        
while ((totalBytesRead < POIFSConstants.BIG_BLOCK_SIZE) &&
(count 
!= -1)) {
                count 
= stream.read(_data, totalBytesRead,
POIFSConstants.BIG_BLOCK_SIZE 
- totalBytesRead);
                
if (count != -1) {
                        totalBytesRead 
+= count;
                }
        }
          
if (count == -1) {
                _eof 
= true;
          } 
else {
            _eof 
= false;
        } 
          
if ((totalBytesRead != POIFSConstants.BIG_BLOCK_SIZE) && (totalBytesRead != 0)) {
            String type 
= " byte" + ((totalBytesRead == 1? (""): ("s"));
            
throw new IOException("Unable to read entire block; " +
totalBytesRead 
+ type + " read; expected " + POIFSConstants.BIG_BLOCK_SIZE + "bytes");
        }
    }


打包:
ant jar

重启app,OK!

说明:
主要问题出在 InputStream的read上,原来的实现用ReadFully方法:
public static int readFully(InputStream in, byte[] b, int off, int len)
    
throws IOException
    {
        
int total = 0;
        
for (;;) {
            
int got = in.read(b, off + total, len - total);
            
if (got < 0) {
                
return (total == 0? -1 : total;
            } 
else {
                total 
+= got;
                
if (total == len)
                    
return total;
            }
        }
    }

InputStream的read不能确保返回的是最大字节数,但是另一个实现却可以:
ByteInputStream
所以,下面的方法也可以修改这个问题:
 // read entire stream into byte array:
    ByteArrayOutputStream byteOS = new ByteArrayOutputStream();
    
byte[] buffer = new byte[1024];
    
int count;
    
while (count = inputStream.read(buffer)) != -1)
        byteOS.append(buffer, 
0, count);
    byteOS.close();
    
byte[] allBytes = byteOS.betByteArray();

// create workbook from array:
InputStream byteIS = new ByteArrayInputStream(allBytes);
HSSFWorkbook wb 
= new HSSFWorkbook(byteIS);

posted @ 2006-08-10 17:09 Alex 阅读(3571) | 评论 (1)编辑 收藏

By: Wu Yin
Date: 2006-06-30
Email: lazy_fox#msn.com
Link: http://blog.csdn.net/wooin/archive/2006/07/11/903974.aspx
版权信息: 该文章版权由Wu Yin所有。可在非商业目的下任意传播和复制。
对于商业目的下对本文的任何行为需经作者同意。
联系方式:lazy_fox#msn.com

1. 本文需要两个文件,httpd和subversion。
·httpd是web服务器,用来通过web访问subversion,这里是官方网站,这里是它的下载网页,
  这里可以下载到httpd-2.2.2.tar.bz2。
·subversion就是本文的主角,这里是他的官方网站,这里是它的下载网页,
  这里可以下载到subversion-1.3.1.tar.bz2
2. 新建一个用户:svnroot
最好不要让root用户参与到svn的权限管理和日常的运行和维护工作中来,但是下面的一些安装和配置操作还是 需要root用户来完成的,因为有些操作只有root才能做。
3. 编译安装httpd (root用户操作):
//解压apache2安装包
# tar xvzf httpd-2.2.2.tar.gz
//进入解压后的目录
# cd httpd-2.2.2
//配置apache安装,前两个参数是必须要加的,你还可以根据您的需要添加其他的参数。
//后面的参数制定你要把apache安装哪里
# ./configure --enable-dav --enable-so --prefix=/usr/local/apache2/
# make
//安装
# make install
# cd /usr/local/apache2/bin
//启动apache服务
# ./apachectl start
//打开浏览器http://localhost/如果有测试页"It works!"出现则证明已经安装成功。
3. 安装Subversion
//解压SubVersion安装包 (root用户进行下面的操作)
# tar xvzf Subversion-1.3.1.tar.gz
//进入解压后的目录
# cd Subversion-1.3.1
//配置subversion安装
#./configure --with-apxs=/usr/local/apache2/bin/apxs --prefix=/usr/local/subversion
--with-apr=/usr/local/apache2 --with-apr-util=/usr/local/apache2 --with-ssl --with-zlib
--enable-maintainer-mode
# make
//安装
# make install
//创建库文件所在的目录 (svnroot用户进行下面的操作)
# mkdir /home/svnroot/repository
//进入subversion的bin目录
# cd /usr/local/subversion/bin
//创建仓库"test"
# ./svnadmin create /home/svnroot/repository/test
# cd /home/svnroot/repository/test
//看看是不是多了些文件,如果是则说明Subversion安装成功了
# ls –l
# cd /usr/local/subversion/bin
//这条语句将把路径/home/user/import下找到的文件导入到你创建的Subversion 仓库中去,
//提交后的修订版为1。
# ./svn import /home/user/import file:///home/svnroot/repository/test –m "注释"
//不让其他人有该目录的权限
# chmod 700 /home/svnroot/repository
4. 修改Apache配置文件
# cd /usr/local/apadche2/bin
//启动Apache
# ./apachect1 start
# vi /usr/local/apache2/conf/httpd.conf
   //在最下面添加
   LoadModule dav_svn_module modules/mod_dav_svn.so
   LoadModule authz_svn_module modules/mod_authz_svn.so
   <Location /svn>
   DAV svn
   SVNParentPath /home/svnroot/repository/ //svn父目录
   AuthzSVNAccessFile /home/svnroot/repository/authz.conf //权限配置文件
   AuthType Basic //连接类型设置
   AuthName "Subversion.zoneyump" //连接框提示
   AuthUserFile /home/svnroot/repository/authfile //用户配置文件
   Require valid-user //采用何种认证
   </Location>
   //其中authfile是通过"htpasswd [–c] /home/svnroot/repository/authfile username password"来创建的
   //"Require valid-user"告诉apache在authfile中所有的用户都可以访问。如果没有它,
   //则只能第一个用户可以访问新建库

5. 重启apache
# ./usr/local/apache2/bin/apachectl restart
//打开浏览器访问http://localhost/svn/test/,如果有东西显示就说明成功。
6. 权限管理
1)增加用户
# htpasswd [-c] /home/svnroot/repository/authfile wooin
//第一次设置用户时使用-c表示新建一个用户文件。回车后输入用户密码,完成对用户的增加
# htpasswd authfile 用户名(加入新的用户)
2)权限分配
# vi /home/svnroot/repository/authz.conf
   [test:/] //这表示,仓库test的根目录下的访问权限
   wooin = rw //test仓库wooin用户具有读和写权限
   bao = r //test仓库bao用户具有读权限
   [test2:/] //test2仓库根目录下的访问权限
   wooin = r //wooin用户在test2仓库根目录下只有读权限
   bao = //bao用户在 test2仓库根目录下无任何权限
   [/] //这个表示在所有仓库的根目录下
   * = r //这个表示对所有的用户都具有读权限
   #[groups] //这个表示群组设置
   #svn1-developers = wooin, bao //这个表示某群组里的成员
   #svn2-developers = wooin
   #[svn1:/]
   #@svn1-developers = rw //如果在前面加上@符号,则表示这是个群组权限设置
将这个设置完成后。重启Apache,就可以通过
http://localhost/svn/test
这个URL来访问仓库了,当然,受权限的限制,必须是合法用户才能访问且具有相应的权限
7. 一些备忘:
1. svn checkout http://localhost/svn/hello.world

2. svn commit 时的默认编辑器的环境变量$SVN_EDITOR=vi需要手动设定,用kate好像有问题

3. 如果linux的登录用户名密码都和svn的其中一个用户名密码相同时,在checkout的时候不会要求输 入用户名密码直接就可以checkout出来。比如:linux有个用户wooin,svn也有一个用户wooin,并且密码 都是一样的,当用wooin登录linux后,执行checkout,可以直接提取出源码文件,不用输入认证信息。

4. 在svn使用过程中牵扯到几种权限:文件系统的权限,linux系统权限,svn用户的权限,apache进程的权限。

文件系统的权限,linux系统权限:这里相同的意思,就是平时大家使用linux时文件夹和文件的访问权限。在 svn建立仓库,文件夹,配置文件的时候用svnroot用户,并将仓库权限设置为700,不允许其他用户直接通过 文件系统查看,只能由svnroot进行管理。

apache进程的权限:因为所有跟仓库传输的操作都是通过apache进程进行的,所以即使你给svn用户设置了 很大的权限,但是apache进程没有访问仓库或者相关文件的权限也没有用,apache进程的权限设置在 /usr/local/apache2/conf/httpd.conf 文件中配置,找到文件中的这两行:
User daemon # 将daemon改为svnroot,让apache进程以svnroot的身份运行
Group daemon

svn用户的权限:就是在repository/authz.conf文件中设置的权限信息,是svn用来管理仓库访问权限的。

5. svn服务器设置有两种方式:http 和 svnserve。这里介绍的是http方法

6. 在/etc/profile的结尾设置一些svn启动时要做的工作
# start apache server for svn
/usr/sbin/apachectl start
export SVN_EDITOR=vi

7. APR libraries 安装 SVN 的时候最好指定 --with-apr= 和 --with-apr-util= 参数到 Apache 安装的根目录
(ServerRoot)下,而不是使用缺省的 SVN 安装包中自带的 apr 。否则如果你安装的 Apache
版本不同有可能导致 APR 库不匹配,出现类似:
Can't set position pointer in file '/svn/test/db/revs/1': Invalid argument 的错误。
Updated 2006-04-20 16:30 -- 比如说如果你安装的是apache 2.2.0版本,就需要在编译安装svn的时候指定
--with-apxs和--with-apr参数到你的apache2.2.0安装目录下:
./configure --prefix=${subversionInstallFolder} /
--with-apxs=${apacheInstallFolder}/bin/apxs /
--with-apr=${apacheInstallFolder} /
--with-apr-util=${apacheInstallFolder} /
--with-ssl /
--with-zlib /
--enable-maintainer-mode
   
   
   

参考资料:
1. SVN學習筆記
2. Subversion版本管理器简单配置说明
3. 关于Subversion的安装、配置和权限管理
posted @ 2006-08-10 11:07 Alex 阅读(406) | 评论 (0)编辑 收藏

key words: svn subversion安装

一、Subversion的下载与安装
Subversion的官方网站:http://subversion.tigris.org/
可以在官方网站上下载Subversion的最新版本。本说明使用的是1.1.4的Win32版。
下载下来Subversion安装文件后,直接安装就可以了。这里假设系统上已经安装好了Apache了,这样,Subversion安装的时候选择Apache方式,它就会自动的配置相关的模块到Apache里。

二、Subversion的建立与使用仓库使用。
$svnadmin create /path/to/repos
在这里,我们假设设置D盘svn目录下的svn1和svn2两个目录为仓库。
svnadmin create d:/svn/svn1
svnadmin create d:/svn/svn2
这样,我们使用的时候,就可以向里面提交文件。
仓库的使用,必须先把服务搭建起来才行。我们等后面的服务器配置后再讲。

三、Subversion服务器的搭建
我们使用与Apache模块的方式运行。
首先,把我们需要的SVN模块加载上,修改Apache的配置文件httpd.conf:

LoadModule dav_svn_module modules/mod_dav_svn.so
LoadModule dav_module modules/mod_dav.so
LoadModule authz_svn_module modules/mod_authz_svn.so

然后配置仓库:

<Location /svn>
  DAV svn
  SVNParentPath d:/svn
</Location>
然后,配置验证方式:
这里使用旦Apache的验证方式:

htpasswd -cm d:/svn/svn-auth-file user1
然后,会提示输入密码,这里就加入了一个用户了。然后再加一个用户:
htpasswd -m d:/svn/svn-auth-file user2

然后修改配置:
<Location /svn>
DAV svn
SVNParentPath d:/svn
AuthType Basic
AuthName “Subversion repository”
AuthUserFile d:/svn/svn-auth-file
Require valid-user
AuthzSVNAccessFile d:/svn/svn-access-file
</Location>
下面,就要写这个svn-access-file文件了,它是用来控制用户访问仓库的权限的,我们把说明写在后面
[svn1:/]               //这表示,仓库svn1的根目录下的访问权限
harry = rw             // svn1仓库harry用户具有读和写权限
sally = r               // svn1仓库sally用户具有读权限
[svn2:/]               //svn2仓库根目录下的访问权限
harry = r               // harry用户在svn2仓库根目录下只有读权限
sally =               // sally用户在 svn2仓库根目录下无任何权限
#[/]               // 这个表示在所有仓库的根目录下
#* = r               // 这个表示对所有的用户都具有读权限
#[groups]               // 这个表示群组设置
#svn1-developers = harry, sally           // 这个表示某群组里的成员
#svn2-developers = sally
#[svn1:/]            
#@svn1-developers = rw       // 如果在前面加上@符号,则表示这是个群组权限设置

将这个设置完成后。重启Apache,就可以通过
http://localhost/svn/svn1
http://localhost/svn/svn2
这两个URL来访问仓库了,当然,要受权限的限制,必须是合法用户才能访问且具有相应的权限
客户端的使用,可以通过许多客户端GUI软件来用。比如TortoiseSVN就很好用。
官方网址:http://tortoisesvn.tigris.org/
posted @ 2006-08-10 11:05 Alex 阅读(361) | 评论 (0)编辑 收藏

key words : openldap ldap

发布时间:2004年04月07日

最近更新:2005年08月08日

Abstract

LDAP (轻量级目录服务访问协议,Lightweight Directory Access Protocol)基于X.500标准,支持TCP/IP,使用简单方便。现在越来越多的网络应用系统都支持LDAP。OpenLDAP是LDAP的一种 开源实现,本笔记基于OpenLDAP2.1.29。


Chapter 1. 目录服务简介

目 录是一个为查询、浏览和搜索而优化的专业分布式数据库,它成树状结构组织数据,就好象Linux/Unix系统中的文件目录一样。目录数据库和关系数据库 不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好象它的名字一样。目 录服务是由目录数据库和一套访问协议组成的系统。类似以下的信息适合储存在目录中:

  • 企业员工和企业客户之类人员信息;

  • 公用证书和安全密钥;

  • 邮件地址、网址、IP等电脑信息;

  • 电脑配置信息。

  • ...

1.1. X.500和LDAP

现在国际上的目录服务标准有两个,一个是较早的X.500标准,一个是较新的LDAP标准。

  • X.500是一个协议族,由一系列的概念和协议组成,包括:

    • X.501是模型定义,定义目录服务的基本模型和概念;

    • X.509是认证框架,定义如何处理目录服务中客户和服务器认证;

    • X.511是抽象服务定义,定义X.500提供的功能性服务;

    • X.518是分布式操作过程定义,定义如何跨平台处理目录服务;

    • X.519 是协议规范,定义了X.500协议,包括DAP(Directory Access Protocol,目录访问协议)、DSP(Directory System Protocol,目录系统协议)、DOP(Directory Operator Protocol,目录操作绑定协议)、DISP(Directory Information Shadowing Protocol,目录信息阴影协议 );

    • X.520定义属性类型要求;

    • X.521定义对象类型;

    • X.525定义如果在目录服务器间复制内容。

    X.500标准中定义了很多内容,包括:

    • 定义了信息模型,确定目录中信息的格式和字符集,如何在项中表示目录信息(定义对象类、属性等模式);

    • 定义命名空间,确定对信息进行的组织和引用,如何组织和命名项-目录信息树DIT和层次命名模型;

    • 定义功能模型,确定可以在信息上执行的操作;

    • 定义认证框架,保证目录中信息的安全,如何实现目录中信息的授权保护-访问控制模型;

    • 定义分布操作模型,确定数据如何分布和如何对分布数据执行操作,如何将全局目录树划分为管理域,以便管理。

    • 定义客户端与服务器之间的通信的各种协议。

    由于X.500较复杂,且需严格遵照OSI七层协议模型。造成应用开发较困难。所以开发了LDAP,以便在INTERNET上使用。

  • LDAP协议于1993年获批准,产生LDAPv1版,1997年发布最新的LDAPv3版,该版本是LDAP协议发展的一个里程碑,它作为X.500的简化版提供了很多自有的特性,使LDAP功能更为完备,具有更强大的生命力。

    LDAP也是一个协议族,包含以下内容:

    • RFC 2251--LDAPv3核心协议,定义了LDAPv3协议的基本模型和基本操作;

    • RFC 2252--定义LDAPv3基本数据模式(Schema)(包括语法、匹配规则、属性类型和对象类)以及标准的系统数据模式;

    • RFC 2253--定义LDAPv3中的分辩名(DN)表达式;

    • RFC 2254--定义了LDAPv3中的过滤表达式;

    • RFC 2255--定义LDAPv3统一资源地址的格式;

    • RFC 2256--定义LDAPv3中使用X.500的Schema列表;

    • RFC 2829--定义了LDAPv3中的认证方式;

    • RFC 2830--定义了如何通过扩展使用TLS服务;

    • RFC 1823--定义了C的LDAP客户端API开发接口;

    • RFC 2847--定义了LDAP数据导入、导出文件接口LDIF。

    这些协议定义了LDAP的内容,包括:

    • 定义了一个信息模型,确定了LDAP目录中信息的格式和字符集,如何表示目录信息(定义对象类、属性、匹配规则和语法等模式);

    • 定义了命名空间,确定信息的组织方式--目录树DIT,以DN和RDN为基础的命名方式,以及LDAP信息的Internet表示方式;

    • 定义了功能模型,确定在可以在信息上执行的操作及API。

    • 定义了安全框架,保证目录中信息的安全,定义匿名、用户名/密码、SASL等多种认证方式,以及与TLS结合的通讯保护框架;

    • 定义分布式操作模型,基于指引方式的分布式操作框架;

    • 定义了LDAP扩展框架。

1.2. LDAP产品

现 在市场上有关LDAP的产品已有很多,各大软件公司都在他们的产品中集成了LDAP服务,如Microsoft的ActiveDirectory、 Lotus的Domino Directory、IBM的WebSphere中也集成了LDAP服务。LDAP的开源实现是OpenLDAP,它比商业产品一点也不差,而且源码开 放。

Chapter 2. OpenLDAP安装笔记

2.1. 源码安装

我的安装方法是以源码编译的方式进行的,以root用户进行安装。安装所需软件如下:

具体的安装步骤如下:

  1. 由于openldap需要Berkeley DB来存放数据,所以需先安装Berkeley DB 4.2.52,可到它的网站下载,网址见上面。运行下面的命令解压:

    # tar -zxvf db-4.2.52.tar.gz

    解完压后,会生成一个db-4.2.52目录,进行该目录下的build_unix目录。执行以下命令进行配置安装。

    # ../dist/configure
    # make
    # make install

    也是按linux源码安装的三步曲完成,没有什么好说的了。该软件默认是安装在/usr/local/BerkeleyDB.4.2目 录下。安装完成后,要把/usr/local/BerkeleyDB.4.2/lib的库路径加到/etc/ld.so.conf文件内,添加完成后执行 一次ldconfig,使配置文件生效。这样编译openldap时才能找到相应的库文件。这样资料库就安装完成了,接下来可以安装openldap了。 ld.so.conf是什么东西?它就是系统动态链接库的配置文件。此文件内,存放着可被LINUX共享的动态链接库所在目录的名字(系统目录/lib, /usr/lib除外),各个目录名间以空白字符(空格,换行等)或冒号或逗号分隔。一般的LINUX发行版中,此文件均含一个共享目录 /usr/X11R6/lib,为X window窗口系统的动态链接库所在的目录。 ldconfig是它的管理命令,具体操作方法可查询man手册,这里就不细讲了。

  2. 到openldap官方网站下载最新 的稳定版源码,并解压。查看INSTALLT 和README文档,这个很重要,因为安装方法和一些注意事项都在里面有介绍。认真弄明白文档内容能节省你不少的安装调试时间。这也是开源软件的一个特 点,给用户提供了最大的灵活性和可配置性。但也增加了系统安装配置的难度,需要有相关的文档配置说明和指导。在官方网站上还有详细的帮助文件,在整个系统 配置中需要经常查询。

    # tar -zxvf openldap-stable-20040329.tgz

    解压完成后,会生成一个openldap-2.1.29目录。进行该目录,执行以下命令进行配置安装。

    # env CPPFLAGS="-I/usr/local/BerkeleyDB.4.2/include" 
    LDFLAGS="-L/usr/local/BerkeleyDB.4.2/lib" ./configure --prefix=/usr/local/openldap
    --enable-ldbm

    注意以上配置语句,要设置资料库的include和lib路径,否则在配置到资料库相关内容时会提示Berkeley DB版本不兼容,并中断配置。如果没有--enable-ldbm选项,在make test时会提示ldbm找不到。为了减少出错,还是加上为好。

    #make depens
    #make
    #make test

    在make test阶段要花费较长时间进行测试,好像有16项吧。你可以放松一下,上上网,聊聊天,听听歌,呵呵,开玩笑了,这个时间应该是最紧张的了。成与不成就看这下的了,如果没问题就可安装了。

    #make install

    通过配置命令可以看出,我们把openldap安装到/usr/local/openldap目录下。建议以源码安装的软件都放到独立的目录下,不要放到软件默认的目录。好处是方便管理和控制,所有文件在统一的目录下,卸载软件只要删除整个目录就可以了。

  3. 安 装完相关软件后就可以着手配置了。Berkeley DB资料库没什么好配置的。主要是配置openldap 服务。配置文件在软件的安装目录的etc/openldap下,有四个文件,主要的是slapd.conf and ldap.conf,其它两个是backup文件。首先,我们先来配置slapd.conf文档。系统默认的slapd.conf文件如下:

    # $OpenLDAP: pkg/ldap/servers/slapd/slapd.conf,v 1.23.2.8 2003/05/24 23:19:14 kurt Exp $
    #
    # See slapd.conf(5) for details on configuration options.
    # This file should NOT be world readable.
    #
    include /usr/local/openldap/etc/openldap/schema/core.schema
    #设置schema配置文档包含

    # Define global ACLs to disable default read access.

    # Do not enable referrals until AFTER you have a working directory
    # service AND an understanding of referrals.
    #referral ldap://root.openldap.org

    pidfile /usr/local/openldap/var/slapd.pid
    #设置pid和args文档位置
    argsfile /usr/local/openldap/var/slapd.args

    # Load dynamic backend modules:
    # modulepath /usr/local/openldap/libexec/openldap
    # moduleload back_bdb.la
    # moduleload back_ldap.la
    # moduleload back_ldbm.la
    # moduleload back_passwd.la
    # moduleload back_shell.la

    # Sample security restrictions
    # Require integrity protection (prevent hijacking)
    # Require 112-bit (3DES or better) encryption for updates
    # Require 63-bit encryption for simple bind
    # security ssf=1 update_ssf=112 simple_bind=64

    # Sample access control policy:
    # Root DSE: allow anyone to read it
    # Subschema (sub)entry DSE: allow anyone to read it
    # Other DSEs:
    # Subschema (sub)entry DSE: allow anyone to read it
    # Other DSEs:
    # Allow self write access
    # Allow authenticated users read access
    # Allow anonymous users to authenticate
    # Directives needed to implement policy:
    # access to dn.base="" by * read
    # access to dn.base="cn=Subschema" by * read
    # access to *
    # by self write
    # by users read
    # by anonymous auth
    #
    # if no access controls are present, the default policy is:
    # Allow read by all
    #
    # rootdn can always write!

    #######################################################################
    # ldbm database definitions
    #######################################################################

    database bdb
    #设置使用的资料库,也可用lbdm。
    suffix "dc=my-domain,dc=com"
    #设置目录后缀
    rootdn "cn=Manager,dc=my-domain,dc=com"
    #设置目录管理员
    # Cleartext passwords, especially for the rootdn, should
    # be avoid. See slappasswd(8) and slapd.conf(5) for details.
    # Use of strong authentication encouraged.
    rootpw secret
    #设置管理密码,这里用了明文的“secret”密码。这样设置不安全,需使用加密的密码,下面会讲到如何设置加密密码。
    # The database directory MUST exist prior to running slapd AND
    # should only be accessible by the slapd and slap tools.
    # Mode 700 recommended.
    directory /usr/local/openldap/var/openldap-data
    #设置资料库路径
    # Indices to maintain
    index objectClass eq
    #设置目录项索引

    要服务器正常动作,要修改一些始初参数和设置,修改后的配置文档如下:

    # $OpenLDAP: pkg/ldap/servers/slapd/slapd.conf,v 1.23.2.8 2003/05/24 23:19:14 kurt Exp $
    #
    # See slapd.conf(5) for details on configuration options.
    # This file should NOT be world readable.
    #
    #为了有效使用目录服务,包含相关的文件。注意,在包含文件时是要按一定顺序的,因为
    #文件里的属性存在依赖关系。如果顺序不对,服务器启动不了,文档间的依赖关系在文档
    #中都有说明,请仔细查看一下。如果懒得看也可以按我的顺序。
    include /usr/local/openldap/etc/openldap/schema/core.schema
    include /usr/local/openldap/etc/openldap/schema/corba.schema
    include /usr/local/openldap/etc/openldap/schema/cosine.schema
    include /usr/local/openldap/etc/openldap/schema/inetorgperson.schema
    include /usr/local/openldap/etc/openldap/schema/misc.schema
    include /usr/local/openldap/etc/openldap/schema/openldap.schema
    include /usr/local/openldap/etc/openldap/schema/nis.schema
    include /usr/local/openldap/etc/openldap/schema/samba.schema
    # Define global ACLs to disable default read access.

    # Do not enable referrals until AFTER you have a working directory
    # service AND an understanding of referrals.
    #referral ldap://root.openldap.org

    pidfile /usr/local/openldap/var/slapd.pid
    argsfile /usr/local/openldap/var/slapd.args

    loglevel 1
    #增加了日志功能,需修改syslog配置文件,在文件中增加一项:local4.* /var/log/ldap.log日志级别定义可查相官方网站的文档。
    #1级记录的信息很多,可用于调试。
    # Load dynamic backend modules:
    # modulepath /usr/local/openldap/libexec/openldap
    # moduleload back_bdb.la
    # moduleload back_ldap.la
    # moduleload back_ldbm.la
    # moduleload back_passwd.la
    # moduleload back_shell.la

    # Sample security restrictions
    # Require integrity protection (prevent hijacking)
    # Require 112-bit (3DES or better) encryption for updates
    # Require 63-bit encryption for simple bind
    # security ssf=1 update_ssf=112 simple_bind=64

    # Sample access control policy:
    # Root DSE: allow anyone to read it
    # Subschema (sub)entry DSE: allow anyone to read it
    # Other DSEs:
    # Allow self write access
    # Allow authenticated users read access
    # Allow anonymous users to authenticate
    # Directives needed to implement policy:
    # access to dn.base="" by * read
    # access to dn.base="cn=Subschema" by * read
    # access to *
    # by self write
    # by users read
    # by anonymous auth
    #
    # if no access controls are present, the default policy is:
    # Allow read by all
    #
    # rootdn can always write!

    #######################################################################
    # ldbm database definitions
    #######################################################################

    database bdb
    suffix "dc=it,dc=com"
    #改成你自已的目录后缀,
    rootdn "cn=root,dc=it,dc=com"
    #设置root为管理员,与linux的root没有什么关系。
    # Cleartext passwords, especially for the rootdn, should
    # be avoid. See slappasswd(8) and slapd.conf(5) for details.
    # Use of strong authentication encouraged.
    rootpw {MD5}mjkiuPt0wXhpxxkdiOOO+0000000AKq0by
    #设置root密码,用MD5加密。密码串用slappasswd -h {MD5}指令生成
    # The database directory MUST exist prior to running slapd AND
    # should only be accessible by the slapd and slap tools.
    # Mode 700 recommended.
    directory /usr/local/openldap/var/openldap-data
    # Indices to maintain
    index objectClass eq
    #这里可根据你的需要设置相关索引,以加快查询速度。具体内容可查询官方网站管理手册。

    #ACL configure以下内容定义访问控制
    access to attr=userPassworduserPassword
    #只能由自已修改,有效验证用户查询。
    by self write
    by anonymous auth
    access to attr=mail
    by dn="cn=root,dc=it,dc=tigerhead" writemail
    #只能由自已修改,有效验证用户查询。
    by self write
    by anonymous auth
    access to dn=".*,dc=it,dc=tigerhead"
    #允许所有人查询没受控制访问限制的信息。
    by self write
    by * read

    到现在为止,服务器基本就配置完成了,可以启动了,服务器程序是位于安装目录的libexec下的slapd程序。注意,不是 sldap哦。ok,到现在为止,服务器基本就配置完成了,可以启动了,服务器程序是位于安装目录的libexec下的slapd程序。注意,不是 sldap哦。启动服务器执行以下命令:

    # ./slapd

    如果没有提示什么出错信息,直接返回shell状态,就说明服务器正常启动了,你可以查询日志或用ps -aux查看。或用以下命令查询服务器:

    ldapsearch -x -b '' -s base '(objectclass=*)' namingContexts

    如果命令执行成功,返回一些信息,则说明服务器正常运行了。如果启动不成功,它会提示一些出错信息,多数是slapd.conf配置出错。回头仔细核查一下配置文档。

  4. 客户端配置文档是ldap.conf。该文档相当简单,其实不用配置也能正常操作。

    # $OpenLDAP: pkg/ldap/libraries/libldap/ldap.conf,v 1.9 2000/09/04 19:57:01 kurt Exp $
    #
    # LDAP Defaults
    #

    # See ldap.conf(5) for details
    # This file should be world readable but not world writable.

    BASE dc=it, dc=com设置目录起点
    #URI ldap://ldap.example.com ldap://ldap-master.example.com:666

    #SIZELIMIT 12
    #TIMELIMIT 15
    #DEREF never

2.2. 数据录入

服 务器正常运作后,就可以录入信息了。信息的录入有三种方法,一种是手工录入,一种是.ldif文件格式录入,一种是脚本自动录入。我们先从最基础的手工录 入方式开始介绍,了解录入信息的格式。明白了手工录入的格式,其它两种方式都很容易明白。信息录入用到ldapadd这个程序。可在安装目录的bin目录 下找到。

2.2.1. 手动录入方法

  • 第一步是要建立DN:

    # ldapadd -x -D 'cn=root,dc=it,dc=com' -W
    dn: dc=it,dc=com
    objectClass: dcObject
    objectClass: organization
    dc: it
    o: Corporation
    description: d Corporation
    注意:如果你用复制/粘贴功能把以上内容拷贝过去,一定要注意每行后面不要有空格,建议还是手工输入,按Ctrl+d存盘。
  • 第二步是建立RDN:

    # ldapadd -x -D 'cn=root,dc=it,dc=com' -W        
    #-x表示用简单验证,-D表示指定目录,-W表示弹出密码输入提示

    输入密码,这里的密码是在配置文件中rootpw项设置的密码,不是操作系统中root用户的密码。验证通过后就可输入以下内容:

    dn: uid=qq,dc=it,dc=com
    objectClass: person
    objectClass: organizationalPerson
    objectClass: inetOrgPerson
    uid: qq
    cn: qq
    sn: qq
    telephoneNumber: 138888888
    description: openldap test
    telexNumber: tex-8888888
    street: my street
    postOfficeBox: postofficebox
    displayName: qqdisplay
    homePhone: home1111111
    mobile: mobile99999
    mail:qq@qq.com

    输入完所有信息后,按Ctrl+d结束存盘。如果出现出错信息,请查一下对象类和属性的对应关系有没有错或输入失误。初学者就容易出错 的地方是对象类和属性的对应关系没有处理好。对象类和属性是在schema文档中定义的。它们之间的关系是这样的,对象类中有些属性是必选的,有些属性是 可选的。录入信息的属性必须在对象类中有定义才能用。

输入以下命令可查询到刚才输入的信息。

# ldapsearch -x -b 'dc=it,dc=com'            
-b选项是设置目录起点,如果设置了客户端的BASE配置参数,该项可不用。

如果按以上配置文件设置了acl,用上面的查询命令是查询不到受保护的内容的。如上面的userPassword and mail。要查询到这些受限内容,需要通过验证才可以:

# ldapsearch -x -LLL -h it.com -b 'dc=it,dc=com' -D 'uid=qq,dc=it,dc=com' -W 'uid=qq'
接着提示输入密码。输入userPassword的密码回车,所有信息就都出来了。

2.2.2. 文件方式

.ldif文件方式也就是把以上手工输入的内容先写入一个.ldif文件中,然后,用ldapadd命令的-f参数导入。

# ldapadd -x -D "cn=root,dc=it,dc=com" -W -f test.ldif

一个完整的global.ldif文件例子:

dn: dc=info, dc=net
objectClass: top
objectClass: organization
o: info.net

dn: ou=People, dc=info, dc=net
objectClass: top
objectClass: organizationalUnit
ou: People
description: User Info

dn: cn=Admin, dc=info, dc=net
objectClass: top
objectClass: person
objectClass: organizationalPerson
cn: Admin
sn: Admin
userPassword: Admin
description: Administrator for info.net

dn: id=1, ou=People, dc=info, dc=net
objectclass: top
objectclass: InfoPerson
id: 1
username: 张三
tel:021-63138990
card_id:ABC001

我们也可用slapadd命令来导入数据。该命令可以导入包含一些系统信息的ldif文件,如:

dn: dc=it,dc=com
objectClass: top
objectClass: dcObject
objectClass: organization
dc: it
structuralObjectClass: organization
entryUUID: d97b06da-d77e-1028-9866-d4ec7ac00d12
creatorsName: cn=anonymous #系统信息
createTimestamp: 20041201005115Z #系统信息
o:: 5bm/5bee5biC6JmO5aS055S15rGg6ZuG5Zui5pyJ6ZmQ5YWs5Y+4
userPassword:: e01ENX14TXBDT0tDNUk0SU56RkNhYjNXRW13PT0=
entryCSN: 2004120603:50:08Z#0x0001#0#0000 #系统信息
modifiersName: cn=admin,dc=it,dc=com #系统信息
modifyTimestamp: 20041206035008Z #系统信息
[Note]
再次提醒,注意每行后面不要留有空格。

2.2.3. 脚本方式

脚本录入方式需要自已编写脚本,或到网上下载。有一个用PHP写的LDAP管理工具不错,叫phpLDAPadmin。可以到以下网址下载:http://phpldapadmin.sourceforge.net。安装方法也很简单,只要解压出来,拷贝到apache的web目录下,按说明配置一下设定文档,就ok了。

2.3. 常用命令介绍

接着为大家介绍一下几个常用的ldap命令,如果你用了phpLDAPadmin程序,其实它已经有一个很好的图形介面帮你完成这些命令了。但了解一下还是对你还是很有益的,因为命令方法才是最根本的。

  • 删除命令ldapdelete

    # ldapdelete -x -D 'cn=root,dc=it,dc=com' -W 'uid=qq1,dc=it,dc=com'
  • 重新索引ldap数据库命令slapindex

    # slapindex -f slapd.conf
  • 设置使用者密码,当然了,你的用户需要有userPassword项了。

    #ldappasswd -x -D "cn=root,dc=it,dc=com" -W "uid=qq1,dc=it,dc=com" -S 
    New password:
    Re-enter new password:
    Enter bind password:
    Result: Success (0)
    [Note]
    "Enter bind password" 是 "cn=root,dc=it,dc=com"管理员的密码。
  • 管理员密码更改

    #slappasswd 
    New password
    Re-enter new password
    {SSHA}83DJ4KVwqlk1uh9k2uDb8+NT1U4RgkEs

    接下再拷贝到 path/to/sldap.conf 的 rootpw 即可,重启使用配置文件生效

  • 通过ldapmodify修改目录内容

    # ldapmodify -x -D "cn=root,dc=it,dc=com" -W -f modify.ldif

    通过ldif文件修改ldap数据,ldif文件格式如下:

    dn: cn=qq,dc=it,dc=com
    changetype: modify
    replace: mail
    mail: modme@example.com
    -
    add: title
    title: Grand Poobah
    -
    add: jpegPhoto
    jpegPhoto:< file:///tmp/modme.jpeg
    -
    delete: description
    -

2.4. 启用sasl验证

前提是你在系统中安装了sasl认证库,并在编译openldap时支持它,默认就支持了。到http://asg.web.cmu.edu/cyrus下载。安装方法见我写的sendmail安装笔记。安装好之后,需要在sasl中建立相应的帐号,用以下命令可完成。

# saslpasswd2 -c test

接着配置slapd.conf文件,加入以下内容。

sasl-regexp
uid=(.*),cn=.*,cn=auth
uid=$1,dc=it,dc=com

重启服务器使配置文件生效。这个配置是最大权限的配置,如果要细化请查阅相关文档。用以下命令测试。

# ldapsearch -U qq  -b 'uid=qq,dc=it,dc=com' -D 'dc=it,dc=com' -Y DIGEST-MD5

采用digest-md5验证,提示密码,输入saslpasswd2的密码。

2.5. 配置服务器复制

在 一些关键的应用场合,我们需设置多个ldap服务器实例,且数据要保持同步。当一台服务器出现故障或被黑客攻击时,我们就能继续保持应用的正常运行。通过 DNS的轮流查询功能,还能实现服务器的负载均衡,提高响应速度。在openldap中有一个slurpd进程,利用slurpd进程可帮助我们实现多台 ldap服务器数据的同步功能。下面简单介绍一下主、从ldap服务器的配置。

slurpd运行在主服务器上,它能把主服务器上的变化通过 LDAP协议传送到从服务器上。从服务器上的变化不能传送到主服务器上,也就是说是单向同步的。主从服务器的版本最好一样,以减少兼容性问题。主从服务器 的安装方法是一样的,关键是配置文件有所不同。我的操作系统是debian sarge,在确保主从服务器能正常运行的前提下进行以下配置:

  • 首先,把主从服务器关闭。通过以下三步操作静态同步主从服务器上的数据:

    1. 把主服务器上/var/lib/ldap目录下的所有数据库文件全部拷贝到从服务器的同目录中,覆盖原有文件。

    2. 把主服务器上的/etc/ldap/schema目录下的所有schema文件拷贝到从服务器的同目录中,覆盖原有文件。

    3. 把主服务器上/etc/ldap/slapd.conf文件拷贝到从服务器的同目录中,覆盖原有文件。

  • 配置主服务器上的slapd.conf文件,取消replogfile指令前的注释符号,取消后的结果如下:

    # Where to store the replica logs for database #1
    replogfile /var/lib/ldap/replog

    增加replica指令,如:

    #replace config
    replica uri=ldap://192.168.6.195:389 #指定从服务器主机名和端口号
    binddn="cn=admin,dc=com" #指定需同步的DN的管理员
    bindmethod=simple credentials=1 #指定验证方式和需同步的DN的管理员密码
  • 配置从服务器上的slapd.conf文件,增加updatedn指令,如:

    updatedn "cn=admin,dc=com"          #与主服务器的binddn对应

    在从服务器的配置文件中,不要包含replica和replogfile指令。

  • 先启动主服务器的slapd和slurpd,再启动从服务器的slapd。

配置完成后,我们可测试一下,在主服务器上修改一个目录项,在从服务器上可查看目录项的数据已同步。

Chapter 3. 管理工具

开源的目录服务管理工具有很多,包括phpldapadmin,gq,CPU,JXplore等。这些工具可帮助我们方便维护目录服务器上的数据。这些工具各有优缺点,下面简要介绍一下,详细的内容可参考相关的官方网站。

3.1. phpldapadmin

3.2. gq

3.3. CPU

3.4. JXplore

3.5. LAM

官方网站:http://lam.sourceforge.net/index.htm

Chapter 4. HowTo

4.1. 禁止整个服务器的匿名访问

在slapd.conf配置文件中加入disallow bind_anon即可。


posted @ 2006-08-10 09:33 Alex 阅读(1502) | 评论 (0)编辑 收藏

key words : openldap ldap


2006 年 6 月 26 日

使用轻量级目录访问协议(LDAP)构建集中的身份验证系统可以减少管理成 本,增强安全性,避免数据复制的问题,并提高数据的一致性。随着 Linux® 的不断成熟,已经出现了很多工具用来简化用户帐号信息到 LDAP 目录的迁移。还开发了一些工具用来在客户机和目录服务器之间启用加密通信配置,并通过复制提供容错性。本文将向您展示如何配置服务器和客户机在 Red Hat Linux 上使用 OpenLDAP。

简介

Linux 发行版中提供的 OpenLDAP 软件按照一个客户机/服务器模型实现了轻量级目录访问协议(LDAP)。LDAP 的设计目的是提供一种有效的方法来查找和管理信息。OpenLDAP 软件和包提供了创建目录信息树(一个主要进行读操作的数据库)的工具。本文向您展示如何存储用户的帐号信息,并修改身份验证服务来使用 LDAP 获取所需要的信息。内部细节并不重要,因为这些工具可以将数据库的内容以文本格式(LDAP 数据交换格式,LDIF)呈现在您的面前。

LDAP 信息被组织成属性和值的组合,称为 条目(entry)。条目可能会具有必须的属性或可选属性。一个条目的属性必须要遵循 /etc/openldap/schema/ 模式文件中定义的规则。规则包含在条目的 objectclass 属性中。看一下下面的关系,我们可以看出 posixAccount objectclass 中包含了密码文件条目的信息(posixAccount userPassword 是文件条目的 base64 编码)。


图 1. LDAP 目录条目和 Linux 密码文件之间的关系
LDAP 目录条目和 Linux 密码文件之间的关系

文件 /etc/openldap/schema/nis.schema 为 posixAccount 对象类中的条目定义了所有的属性和 objectclass。例如,下面是对 uidNumber 属性的描述:

attributetype ( 1.3.6.1.1.1.1.0 NAME 'uidNumber'
DESC 'An integer uniquely identifying a user in an administrative domain'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )

所有的属性类型都已经定义了,它们被收集到 posixAccount objectclass 中。例如:

objectclass ( 1.3.6.1.1.1.2.0 NAME 'posixAccount' SUP top AUXILIARY
DESC 'Abstraction of an account with POSIX attributes'
MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory )
MAY ( userPassword $ loginShell $ gecos $ description ) )

ldapuser 条目具有一个识别名属性 dn,它用作用户名,并与 userPassword 一起用来在 LDAP 目录中记录信息,或与 LDAP 目录绑定在一起使用。

LDAP 为作为容器使用的特殊条目提供了将这些条目组织成树结构的功能。在这个例子中,我们将使用一个容器 People 保存用户帐号信息,使用另外一个容器 Groups 保存组帐号信息。所生成的目录信息树如图 2 所示。


图 2. 用户帐号信息使用的目录信息树
用户帐号信息使用的目录信息树

让我们来看一下如何配置 OpenLDAP 服务器,如何将信息从系统文件迁移到 LDAP 目录中,如何配置 OpenLDAP 客户机通过 LDAP 对用户进行身份验证。在使用一个集中的身份验证数据库时,应该通过使用复制技术采用第二个 LDAP 服务器提供高可用性,这样在主服务器出现问题时,就可以使用第二个 LDAP 服务器响应客户机的请求。由于诸如密码之类的身份验证数据会通过网络进行传输,因此希望使用 TSL 协议建立加密通信连接。

我们的 OpenLDAP 服务器和客户机都是虚拟机,上面运行的是 Red Hat Enterprise Linux AS release 4(Nahant Update 1)。在我们的例子中使用了 表 1 所列出的系统。如果想模仿这些例子,请使用适合您自己的设置。


表 1. 系统网络信息
角色主机名IP 地址
OpenLDAP 主服务器dhcp64-233.ibm.com9.47.64.233
OpenLDAP 从服务器dhcp64-253.ibm.com9.47.64.253
OpenLDAP 客户机dhcp64-251.ibm.com9.47.64.251





回页首


配置 LDAP 服务器

我们使用 Red Hat Enterprise Linux release 4 Update 1 上的包来构建服务器:

  • openldap-2.2.13-2:包含 OpenLDAP 配置文件、库和文档
  • openldap-servers-2.2.13-2:包含 slapd 和 slurpd 服务器、迁移脚本和相关文件
  • openldap-clients-2.2.13-2:包含客户机程序,用来访问和修改 OpenLDAP 目录

OpenLDAP 包在服务器上安装了很多程序:

  • 守护进程:
    • slapd:主 LDAP 服务器
    • slurpd:负责与复制 LDAP 服务器保持同步的服务器
  • 对网络上的目录进行操作的客户机程序。下面这两个程序是一对儿:
    • ldapadd:打开一个到 LDAP 服务器的连接,绑定、修改或增加条目
    • ldapsearch:打开一个到 LDAP 服务器的连接,绑定并使用指定的参数进行搜索
  • 对本地系统上的数据库进行操作的几个程序:
    • slapadd:将以 LDAP 目录交换格式(LDIF)指定的条目添加到 LDAP 数据库中
    • slapcat:打开 LDAP 数据库,并将对应的条目输出为 LDIF 格式

OpenLDAP 的主要服务器配置文件是 /etc/openldap/slapd.conf。本例所使用的完整 slapd.conf 文件如 清单 18 所示。slapd.conf 文件中包括一系列全局配置选项,它们作为一个整体应用到 slapd 上面,后面是包含数据库特有信息的数据库后端定义。如果一行内容是以空格开始的,就认为它是上一行的延续。空行和以 “#” 字符开头的注释行都会被忽略。

如果您正把本文当作练习来做,那就可以按照下面指定的方式进行修改,从而启动 LDAP 服务器。一旦确认服务器正常工作之后,就可以添加复制功能,然后再添加安全性支持。首先是全局配置信息段的设置。其中每个选项的值都是我们想要的。

正如上面介绍的一样,信息被组织成属性和值的组合,称为条目。条目属性必须遵循的规则是使用 objectclass 专用属性进行组织的,这可以在 /etc/openldap/schema/ 模式文件中找到。对于身份验证来说,需要使用在 nis.schema 中定义的 posixAccountshadowAccount objectclasses:

include /etc/openldap/schema/nis.schema

loglevel 行设置的是日志选项。可以将其设置为这样的级别:调试语句和操作统计信息都会被记录到 /var/log/slapd.log 中。日志级别是累加的:296 = 256 日志连接/操作/结果 + 32 搜索过滤器处理 + 8 连接管理:

loglevel 296

日志信息会被记录到 syslogd LOG_LOCAL4 机制中。还需要将下面的内容添加到 /etc/syslog.conf 中,并让 syslogd 重新读取自己的配置文件:

local4.debug /var/log/slapd.log

access 行定义了谁可以访问目录中的内容。我们希望用户可以修改自己的密码,并更新自己的 shadow 信息来反映密码的变化。希望身份验证程序能够检索用户的密码。还希望用户能够读取所有其他条目。注意密码条目是不可读的,shadow 属性的惟一用处就是管理密码的过期问题。

access to attrs=shadowLastChange,userPassword
by self write
by * auth

access to *
by * read

接下来,在 database 部分,要定义下面的内容。

使用新的 bdb 后端数据库:

database bdb

指定后端数据库需要响应的查询的 DN 前缀。为了确保惟一性,根前缀应该从自己的网络域名构建出来。在本例的情况中,它是 .dc=svc,dc=beaverton,dc=ibm,dc=com.,但是在下面的例子中我们对其进行了简化:

suffix "dc=ibm,dc=com"

指定管理 DN,它不用于访问控制或限制数据库的操作。也不需要在目录中为这个 DN 指定一个条目。为具有 rootpw 密码的管理员使用 DN 可以跳过 ACL 规则中的所有访问控制:

rootdn "cn=Manager,dc=ibm,dc=com"
rootpw {MD5}ijFYNcSNctBYg

这就是我们现在想要设置的选项。稍后将返回 slapd.conf 文件来配置复制,然后在配置安全性。

现在,我们希望将数据添加到目录中,并确认可以访问这些信息。要实现这种功能,需要配置服务器来使用 ldap 客户机工具,例如 ldapadd 和 ldapsearch。ldap 客户机工具的配置文件是 /etc/openldap/ldap.conf。我们使用的这个文件的完整列表如本文末尾的 清单 19 所示。要在 ldap 服务器上运行这些工具,只需要将该行修改成下面的内容:

BASE dc=ibm,dc=com

设置启动脚本在级别 2、3 和 5 时启动 LDAP:


清单 1. 设置启动运行级别
# chkconfig --levels 235 ldap on

从命令行中启动服务:


清单 2. 启动服务
# service ldap start
Starting slapd: [ OK ]

OpenLDAP 守护进程 slapd 应该已经运行了:


清单 3. 检查服务正在运行
# ps -ef | grep slap
ldap 13521 1 0 Oct24 ? 00:00:00 /usr/sbin/slapd -u ldap -h ldap:/// ldaps:///

ldapsearch -x 命令应该可以成功完成,但不会返回任何数据。





回页首


迁移密码和 shadow 信息

Red Hat 所提供的 openldap-servers 包包含 PADL Software Pty Ltd. 公司的 MigrationTools 工具。我们将使用这些工具将数据从 Linux 系统文件(例如 /etc/group 和 /etc/password)转换成 LDAP LDIF 格式,这是数据库信息的一种文本格式的表示。这种格式是行界定、冒号分隔的属性-值对。

有一组 Perl 脚本被安装到 /usr/share/openldap/migration/ 中执行迁移。这些 Perl 脚本的配置信息包含在 migrate_common.ph 文件的开头。对于我们的目的来说,只需要修改命名前缀的变量来使用条目的识别名就足够了,如下所示:

$DEFAULT_BASE = "dc=ibm,dc=com"

在进行这些修改之后,请运行脚本 migrate_base.pl,它会创建根项,并为 Hosts、Networks、Group 和 People 等创建低一级的组织单元:


清单 4. 运行 migrate_base.pl
# migrate_base.pl > base.ldif

编辑 base.ldif,删除除下面之外的所有条目:


清单 5. base.ldif 条目
# cat base.ldif
dn: dc=ibm,dc=com
dc: ibm
objectClass: top
objectClass: domain

dn: ou=People,dc=ibm,dc=com
ou: People
objectClass: top
objectClass: organizationalUnit

dn: ou=Group,dc=ibm,dc=com
ou: Group
objectClass: top
objectClass: organizationalUnit

在 LDAP 服务器上,使用 OpenLDAP 客户机工具 ldapadd 将以下条目插入到数据库中。简单身份验证必须要使用 -x 选项指定。在 slapd.conf 中定义的 rootdn 身份验证识别名是 “cn=Manager,dc=ibm,dc=com”。对于简单身份验证来说,必须使用密码。选项 -W 强制提示输入密码。这个密码就是在 slapd.conf 文件中指定的 rootpw 参数的值。包含这些条目的 LDIF 文件是使用 -f 选项指定的:


清单 6. 使用 ldapadd 插入条目
# ldapadd -x -D "cn=Manager,dc=ibm,dc=com" -W -f base.ldif

接下来,从 /etc/group 中迁移 ldapuser 组:


清单 7. 迁移 ldapuser 组
# grep ldapuser /etc/group > group.in
# ./migrate_group.pl group.in > group.ldif

# cat group.ldif
dn: cn=ldapuser,ou=Group,dc=ibm,dc=com
objectClass: posixGroup
objectClass: top
cn: ldapuser
userPassword: {crypt}x
gidNumber: 500

# ldapadd -x -D "cn=Manager,dc=ibm,dc=com" -W -f group.ldif

最后,从 /etc/passwd 和 /etc/shadow 中迁移 ldapuser 的信息:


清单 8. 迁移 ldapuser 信息
# grep ldapuser /etc/passwd > passwd.in
# ./migrate_passwd.pl passwd.in > passwd.ldif

# cat passwd.ldif
dn: uid=ldapuser,ou=People,dc=ibm,dc=com
uid: ldapuser
cn: ldapuser
objectClass: account
objectClass: posixAccount
objectClass: top
objectClass: shadowAccount
userPassword: {crypt$1$TeOlOcMc$cpQaa0WpLSFRC1HIHW5bt1
shadowLastChange: 13048
shadowMax: 99999
shadowWarning: 7
loginShell: /bin/bash
uidNumber: 500
gidNumber: 500
homeDirectory: /home/ldapuser
gecos: ldapuser

# ldapadd -x -D "cn=Manager,dc=ibm,dc=com" -W -f passwd.ldif

现在检查已经添加到数据库中的信息。清单 9 给出了全部输出结果:


清单 9. 以 LDIF 格式填充的 OpenLDAP 数据库
# ldapsearch -x

# extended LDIF
#
# LDAPv3
# base <> with scope sub
# filter: (objectclass=*)
# requesting: ALL
#

# ibm.com
dn: dc=ibm,dc=com
dc: ibm
objectClass: top
objectClass: domain

# People, ibm.com
dn: ou=People,dc=ibm,dc=com
ou: People
objectClass: top
objectClass: organizationalUnit

# Group, ibm.com
dn: ou=Group,dc=ibm,dc=com
ou: Group
objectClass: top
objectClass: organizationalUnit

# ldapuser, Group, ibm.com
dn: cn=ldapuser,ou=Group,dc=ibm,dc=com
objectClass: posixGroup
objectClass: top
cn: ldapuser
gidNumber: 500

# ldapuser, People, ibm.com
dn: uid=ldapuser,ou=People,dc=ibm,dc=com
uid: ldapuser
cn: ldapuser
objectClass: account
objectClass: posixAccount
objectClass: top
objectClass: shadowAccount
shadowMax: 99999
shadowWarning: 7
loginShell: /bin/bash
uidNumber: 500
gidNumber: 500
homeDirectory: /home/ldapuser
gecos: test2

# search result
search: 2
result: 0 Success

# numResponses: 6
# numEntries: 5





回页首


配置 LDAP 客户机

用来设置客户机的 Red Hat Enterprise Linux release 4 Update 1 包包括:

  • nss_ldap-226-6:包括两个 LDAP 访问客户机:nss_ldap 和 pam_ldap
    • nss_ldap 是一组 C 库扩展,它允许 LDAP 目录服务器用作一个用户和组信息的主源
    • pam_ldap 是一个 Linux-PAM 模块,它支持身份验证功能

LDAP 身份验证要想正确地工作,需要配置两个服务:系统命名服务和身份验证服务。

系统命名服务(NSS)需要配置为使用 LDAP 来解析诸如用户和组帐号之类的资源。例如,在运行命令 ls -l 时,如果某个文件 inode 给出文件的所有者是 “user 501”,那么命名服务就需要将 “uid 501” 解析成用户名,并在 ls 命令输出结果中输出。通常来说,这是通过查找 /etc/passwd 文件中的所有用户帐号实现的。由于用户现在都存储在 LDAP 目录中,因此系统需要配置成同时对 passwd 文件和 LDAP 目录中的帐号进行解析。这种功能是通过 /usr/lib/libnss_ldap.so 库提供的。

身份验证服务是实际向 LDAP 验证用户身份的服务。可插入身份验证模块(PAM)提供了本地 Linux 身份验证服务。下面我们将配置 PAM 先对本地的 /etc/passwd 文件检查用户帐号,然后再对 LDAP 服务器进行检查。PAM LDAP 模块可以用来将身份验证重定向到 LDAP 目录上。/lib/security/pam_ldap.so PAM 模块提供了 LDAP 身份验证功能。

身份验证本身是由 PAM 程序执行的,它从身份验证候选机制中获取用户名,将其绑定到 OpenLDAP 服务器上,检索与这个 uid 条目(用户名条目)相关的 DN;从身份验证候选机制中获取密码,然后使用这个 DN 和密码试图将其绑定到 OpenLDAP 服务器上。如果绑定成功,PAM 会报告说这个用户已经成功通过了 pam_ldap.so 提供的身份验证测试。根据 PAM 的配置不同,在用户看到命令行提示符之前可能会执行其他测试。

我们可以采用两种方法来配置 LDAP 客户机。一种快速而简单的方法是运行 /usr/sbin/authconfig,并在两个屏幕中输入信息。另外一种方法是通过编辑客户机 LDAP 配置文件 /etc/ldap.conf,然后修改 /etc/nsswitch.conf、/etc/sysconfig/authconfig 和 /etc/pam.d/system-auth。首先让我们来看一下如何运行 authconfig。

如图 3 所示进行选择,然后点击 Next


图 3. 执行 authconfig 命令的第一个页面
执行 authconfig 命令的第一个页面

在如图 4 所示的第二个屏幕中输入对应的信息,然后点击 OK


图 4. 执行 authconfig 命令的第二个页面
执行 authconfig 命令的第二个页面

要手工配置 OpenLDAP 客户机,请遵循下面的步骤。

用来跟踪特定身份验证机制是否已经启用的文件是 /etc/sysconfig/。我们可以希望以下条目的值都是 “yes”:

USELDAP=yes
USELDAPAUTH=yes
USEMD5=yes
USESHADOW=yes
USELOCAUTHORIZE=yes

PAM 和 NSS 模块使用的基本配置文件是 /etc/ldap.conf。host 选项指定 LDAP 服务器,base 选项指定这个目录使用的 DN,最初我们希望关闭加密功能:

host dhcp64-233.ibm.com
base dc=ibm,dc=com
ssl off

要让 NSS 服务使用 OpenLDAP 服务器,需要将 “ldap” 添加到 /etc/nsswitch.conf 文件的 passwd、shadow 和 group 行中,如下所示:

passwd: files ldap
shadow: files ldap
group: files ldap

要让 PAM 身份验证服务使用 OpenLDAP 服务器,请将 pam_ldap 行加入到 /etc/pam.d/system-auth 中,位置在对应的标准 pam_unix.so 条目之后。尽管其他设置也可以实现相同的结果,但是我使用下面的文件设置:

auth required /lib/security/$ISA/pam_env.so
auth sufficient /lib/security/$ISA/pam_unix.so likeauth nullok
auth sufficient /lib/security/$ISA/pam_ldap.so use_first_pass
auth required /lib/security/$ISA/pam_deny.so

account required /lib/security/$ISA/pam_unix.so broken_shadow
account sufficient /lib/security/$ISA/pam_localuser.so
account sufficient /lib/security/$ISA/pam_succeed_if.so uid %lt; 100 quiet
account [default=bad success=ok user_unknown=ignore] /lib/security/$ISA/pam_ldap.so
account required /lib/security/$ISA/pam_permit.so

password requisite /lib/security/$ISA/pam_cracklib.so retry=3
password sufficient /lib/security/$ISA/pam_unix.so nullok use_authtok md5 shadow
password sufficient /lib/security/$ISA/pam_ldap.so use_authtok
password required /lib/security/$ISA/pam_deny.so

session required /lib/security/$ISA/pam_limits.so
session required /lib/security/$ISA/pam_unix.so
session optional /lib/security/$ISA/pam_ldap.so

现在,用户帐号信息可以从客户机系统中删除并从 LDAP 目录中进行获取了。当用户试图登录客户机系统时,PAM 身份验证服务就会从用户那里获取用户名,在我们的例子中是 ldapuser。PAM 会从 LDAP 服务器中检索识别名(DN)条目 .dn: uid=ldapuser, ou=People, dc=ibm, dc=com.。PAM 然后会从用户那里获取密码。然后 PAM 试图使用这个 DN 和密码与 LDAP 服务器进行绑定。DN 和密码都以正文文本的格式发送给 LDAP 服务器。在对密码进行散列操作之后,如果服务器可以让用户登录,就会向 PAM 报告说已经成功进行了绑定。成功绑定可以完全满足 PAM 对 pam_ldap 模块汇报成功的标准,如果所有其他 PAM 标准都已经满足了,那么就允许用户登录到系统中。

当 LDAP 服务器对身份验证进行处理时,需要解决另外两个问题才能满足提供可靠安全的身份验证的目标。现在,任何客户机系统不能成功地与 LDAP 服务器进行通信都会阻止用户登录客户机系统。在下一节中我们将看到如何消除这种单点故障,这将显示客户机如何从备份服务器上访问 LDAP 目录。由于用户密码是在网络上以正文文本格式传输的,因此这并不能满足安全身份验证的需求。配置 TLS 安全性 将解决这个问题。





回页首


配置复制

为了防止出现客户机由于 LDAP 服务器的问题而不能登录的情况,我们需要采用复制技术来实现可靠性目标。复制是通过 OpenLDAP 复制进程 slurpd 提供的,它会周期性地唤醒,并检查主服务器上的日志文件,从而确定是否有任何更新。这些更新然后会传播到从服务器上。读请求可以由任何一个服务器进行解 析,而更新请求则只能由主服务器进行解析。客户机需要负责在推荐地址上重试更新操作。

要配置复制,需要停止 OpenLDAP 服务器的 slapd 守护进程:


清单 10. 停止服务
# service ldap stop

将以下内容添加到服务器的 /etc/openldap/slapd.conf 文件中,从而启用对新从服务器的复制。replogfile 行的内容指定类 LDIF 变化应该写入的文件。replica 原语定义了变化应该传播到的主机:

#Replicas of this database
replogfile /var/lib/ldap/replog
replica host=dhcp64-253.ibm.com:389
binddn="cn=Manager,dc=ibm,dc=com"
credentials=secret
bindmethod=simple

在运行从 OpenLDAP 服务器的系统上,请遵循 配置 LDAP 服务器 一节给出的步骤。然后通过将信息导出到一个 ldif 文件中并将其添加到从服务器数据库上,从而将数据库从主服务器拷贝到复制服务器上。

在主服务器上:


清单 11. 将数据导出到 LDIF 文件中
# ldapsearch -x > database.ldif

在复制服务器上:


清单 12. 将数据添加到从服务器数据库中
# ldapadd -x -D "cn=Manager,dc=ibm,dc=com" -W -f database.ldif

将以下内容添加到复制服务器的 /etc/openldap/slapd.conf 文件中。updatedn 指定了在更新从目录时主 slurpd 守护进程使用的 DN。updateref 指定的是主目录服务器。当一个 LDAP 客户机请求从服务器进行更新时,从服务器就将客户机重定向到这个主服务器上。

updatedn "cn=Manager,dc=ibm,dc=com"
updateref ldap://dhcp64-233.ibm.com:389/

启动复制 OpenLDAP 服务器,当它运行之后,再启动主 OpenLDAP 服务器。在主服务器上,slapd 和 slurpd 都会启动。

现在,可以让 OpenLDAP 客户机除了主服务器之外还可以使用复制服务器,这可以通过运行 authconfig 并将复制主机名添加到第二个屏幕中的 Server 行中实现,也可以通过在 /etc/ldap.conf 中修改 host 实现:

host dhcp64-253.ibm.com dhcp64-233.ibm.com

为了确认复制可以正常工作,需要研究一下在更新 gecos 属性时到底发生了什么。复制日志使用了与 LDIF 类似的格式。在读取 replogfile 之后,slurpd 会将这个条目拷贝到自己的重做日志中。实际的变化都以 LDIF 格式保存在主 LDAP 服务器上 /var/lib/ldap/replica/ 中的 slurpd.replog 文件中。

使用文件 user_mod 中的变化,客户机程序 ldapmodify 应用这些变化:


清单 13. 应用 user_mod 的变化
# cat user_mod
dn: uid=ldapuser,ou=People,dc=ibm,dc=com
changetype: modify
replace: gecos
gecos: test2

# tail -f /var/lib/ldap/replog &

# ldapmodify -x -r -f /home/ldapuser/user_mod -D'cn=Manager,dc=ibm,dc=com' -W
Enter LDAP Password:
modifying entry "uid=ldapuser,ou=People,dc=ibm,dc=com"

replica: dhcp64-253.ibm.com:389
time: 1130111686
dn: uid=ldapuser,ou=People,dc=ibm,dc=com
changetype: modify
replace: gecos
gecos: test2
-
replace: entryCSN
entryCSN: 20051023235446Z#000001#00#000000
-
replace: modifiersName
modifiersName: cn=Manager,dc=ibm,dc=com
-
replace: modifyTimestamp
modifyTimestamp: 20051023235446Z
-





回页首


配置 TLS 安全性

LDAP 是以明文的格式通过网络来发送所有信息的,包括密码。我们将采用 TLS 所提供的加密机制(SSL 的后继者)来解决这个问题。在传输层,数据使用 TLS 协议进行加密和封装,然后通过网络进行传输。用来配置加密的工具都是由 OpenSSL 包提供的。

虽然加密是一个复杂的主题,但是要使用 OpenSSL 包,我们依然需要简要介绍一下 TLS 是如何工作的。数据块使用一个对称密钥算法进行加密,它使用一个密钥来实现对数据的加密和解密。我们还有一个问题:如何防止出现以正文文本格式将密钥从 LDAP 服务器发送到 LDAP 客户机上的情况。我们使用公钥算法来解决这个问题,其中客户机可以使用一个自由获取的公钥对自己的密钥进行加密,而只有服务器才可以对这个密钥进行解密。

公钥是作为证书的一部分来创建和分发的,其中包含了一些支持信息,例如 ID、过期日期、提供这个证书的 LDAP 服务器的完整域名(FQDN)。在 LDAP 客户机使用证书进行加密之前,它会验证自己正在与之进行交谈的服务器拥有这个证书,这是通过加密一个挑战并验证服务器可以对其进行解密实现的。

要验证发行这个证书的服务器是一个已经批准过的 LDAP 服务器,客户机被配置为只接受本地证书机构(CA)所签署的证书。它使用 CA 所生成的证书中的公钥,这个公钥保存到客户机中以验证这个 LDAP 所产生的证书是有效的。

在这个例子中,我们将自己的 LDAP 服务器设置为证书机构,并创建一个自签署的证书供 LDAP 客户机和服务器在加密信息中使用。

用来构建 TLS 服务器的 Red Hat Enterprise Linux release 4 Update 1 包是:

  • openssl-0.9.7a-43.1:包括一个证书管理工具和提供各种加密算法和协议的共享库

要构建证书机构的工作环境并生成自己的自签署证书,需要运行 /usr/share/ssl/misc/CA shell 脚本,这是一个对 openssl 命令的封装程序。私密性增强邮件(PEM)是一种用来对数据进行加密和编码的格式:


清单 14. 运行 CA shell 脚本
# cd /usr/share/ssl/misc
# ./CA -newca
CA certificate filename (or enter to create)

Making CA certificate ...
Generating a 1024 bit RSA private key
.........++++++
......++++++
writing new private key to './demoCA/private/./cakey.pem'
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----
You are about to be asked to enter information that will be incorporated into
your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [GB]:US
State or Province Name (full name) [Berkshire]:Oregon
Locality Name (eg, city) [Newbury]:Beaverton
Organization Name (eg, company) [My Company Ltd]:IBM
Organizational Unit Name (eg, section) []:its
Common Name (eg, your name or your server's hostname) []:dhcp64-233.ibm.com
Email Address []:root@dhcp64-233.ibm.com

接下来,要生成由证书机构进行签署的服务器证书。其余的步骤只会对主 LDAP 服务器执行一次,此时指定的是 CN=dhcp64-233.ibm.com;然后对从服务器执行一次,此时指定的是 CN=dhcp64-253.ibm.com。还要使用 nodes 选项,这样就不用在每次启动 OpenLDAP 服务器守护进程 slapd 时都需要输入密码了。签署后的公钥被嵌入到证书请求 slapd-req.pem 中,与之匹配的私钥嵌入在 slapd-key.pem 中:


清单 15. 生成服务器证书
# openssl req -new -nodes -subj
'/CN=dhcp64-233.ibm.com/O=IBM/C=US/ST=Oregon/L=Beaverton'
-keyout slapd-key.pem -out slapd-req.pem -days 365
Generating a 1024 bit RSA private key
...............++++++
.....................................++++++
writing new private key to 'slapd-key.pem'
-----

使用在第一个步骤中创建的 CA 证书对这个证书进行签署:


清单 16. 对证书进行签署
# openssl ca -out slapd-cert.pem -infiles slapd-req.pem
Using configuration from /usr/share/ssl/openssl.cnf
Enter pass phrase for ./demoCA/private/cakey.pem:
Check that the request matches the signature
Signature ok
Certificate Details:
Serial Number: 1 (0x1)
Validity
Not Before: Oct 25 02:50:05 2005 GMT
Not After : Oct 25 02:50:05 2006 GMT
Subject:
countryName = US
stateOrProvinceName = Oregon
organizationName = IBM
commonName = dhcp64-233.ibm.com
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
Netscape Comment:
OpenSSL Generated Certificate
X509v3 Subject Key Identifier:
11:A2:FB:59:42:A4:B3:26:73:1D:6D:F5:4D:2F:80:F0:FA:10:38:F5
X509v3 Authority Key Identifier:
keyid:F7:6A:25:F5:76:BE:20:E7:8D:0F:51:EF:D8:86:7B:AF:2C:74:2F:80
DirName:/C=US/ST=Oregon/L=Beaverton/O=IBM/OU=its/CN=dhcp64-233.ibm.com
/emailAddress=root@dhcp64-233.ibm.com
serial:00

Certificate is to be certified until Oct 25 02:50:05 2006 GMT (365 days)
Sign the certificate? [y/n]:y

1 out of 1 certificate requests certified, commit? [y/n]y
Write out database with 1 new entries
Data Base Updated

下一个步骤将所有需要的证书拷贝到 slapd 可以找到的地方。另外,还要对每个文件强制采用正确的权限:


清单 17. 拷贝证书并强制设置权限
# cp -p slapd-key.pem /etc/openldap/slapdkey.pem
# cp -p slapd-cert.pem /etc/openldap/slapdcert.pem
# chown ldap:ldap /etc/openldap/slapdcert.pem
# chmod 644 /etc/openldap/slapdcert.pem
# chown ldap:ldap /etc/openldap/slapdkey.pem
# chmod 400 /etc/openldap/slapdkey.pem

# mkdir /etc/openldap/cacerts/
# cp /usr/share/ssl/misc/demoCA/cacert.pem /etc/openldap/cacerts/cacert.pem
# chown ldap:ldap /etc/openldap/cacerts cacert.pem
# chmod 644 /etc/openldap/cacerts cacert.pem

在 OpenLDAP 服务器上,将以下内容添加到 /etc/openldap/slapd.conf 文件的 global 段下面。TLSCertificateFileTLSCertificateKeyFile 指定了证书文件和私钥文件的路径。TLSCipherSuite 指定了一个 OpenSSL 密码的列表,slapd 在与 TLS 协商建立连接时可以从中按照降序顺序依次选择。HIGH 的意思是 “所有密钥的长度都大于 128 位”;MEDIUM 表示 “所有密钥的长度都等于 128 位”;+SSLv2 表示 “不管密钥强度如何,所有密码都是以 SSL 协议版本 2 的形式指定的”。

TLSCipherSuite HIGH:MEDIUM:+SSLv2
TLSCACertificateFile /etc/openldap/cacerts/cacert.pem
TLSCertificateFile /etc/openldap/slapdcert.pem
TLSCertificateKeyFile /etc/openldap/slapdkey.pem

将以下内容添加到 LDAP 服务器的第二个配置文件 /etc/openldap/ldap.conf 中:

TLS_CACERTDIR /etc/openldap/cacerts
TLS_REQCERT allow

为了允许从 OpenLDAP 客户机上使用安全连接,需要将以下内容添加到 /etc/openldap/ldap.conf 文件中:

ssl start_tls
tls_checkpeer yes
tls_cacertfile /etc/openldap/cacerts/cacert.pem





回页首


结束语

按照本文给出的提示,我们现在已经使用轻量级目录访问协议(LDAP)构建了一个集中的身份验证系统。我们最初是通过配置 LDAP 服务器来响应对 “dc=ibm,dc=com” 的基本请求开始入手的。我们使用了一组 Perl 脚本来将用户帐号信息迁移到 LDAP 目录中。我们对 Linux 用户帐号服务、PAM 和 NSS 服务进行了修改,从而可以从 LDAP 服务器中检索用户信息。还设置了一个 LDAP 服务器副本作为备用服务器来响应客户机的请求。然后,使用 TLS 协议在 LDAP 客户机和服务器之间对通信进行安全加密。恭喜!

为了参考方便,下面给出了本文中使用的配置文件的完整清单。


清单 18. 本文例子中使用的服务器 /etc/openldap/slapd.conf 文件
#
# See slapd.conf (5) for details on configuration options.
#
include /etc/openldap/schema/core.schema
include /etc/openldap/schema/cosine.schema
include /etc/openldap/schema/inetorgperson.schema
include /etc/openldap/schema/nis.schema

loglevel 256
pidfile /var/run/slapd.pid
argsfile /var/run/slapd.args

# The next three lines allow use of TLS for encrypting connections.
TLSCipherSuite HIGH:MEDIUM:+SSLv2
TLSCACertificateFile /etc/openldap/cacerts/cacert.pem
TLSCertificateFile /etc/openldap/slapdcert.pem
TLSCertificateKeyFile /etc/openldap/slapdkey.pem

# access control policy:
# Restrict password access to change by owner and authentication.
# Allow read access by everyone to all other attributes.

access to attrs=shadowLastChange,userPassword
by self write
by * auth

access to *
by * read

#######################################################################
# database definition
#######################################################################

database bdb
suffix "dc=ibm,dc=com"

rootdn "cn=Manager,dc=ibm,dc=com"
rootpw {MD5}ijFYNcSNctBYg

directory /var/lib/ldap

# Indices to maintain for this database
index objectClass eq,pres
index ou,cn,mail,surname,givenname eq,pres,sub
index uidNumber,gidNumber,loginShell eq,pres
index uid,memberUid eq,pres,sub
index nisMapName,nisMapEntry eq,pres,sub

#Replicas of this database
replica host=dhcp64-253.ibm.com:389
binddn="cn=Manager,dc=ibm,dc=com"
credentials=secret
bindmethod=simple
replogfile /var/lib/ldap/replog


清单 19. 本文例子中使用的服务器 /etc/openldap/ldap.conf 文件
#
# LDAP Defaults
#

# See ldap.conf(5) for details

HOST 127.0.0.1
BASE dc=ibm,dc=com

TLS_CACERTDIR /etc/openldap/cacerts
TLS_REQCERT allow


清单 20. 本文例子中使用的客户机 /etc/ldap.conf 文件
a
# @(#)$Id: ldap.conf,v 1.34 2004/09/16 23:32:02 lukeh Exp $
#
# This is the configuration file for the LDAP nameservice
# switch library and the LDAP PAM module.
#
# PADL Software
# http://www.padl.com
#

# Your LDAP server.
# Multiple hosts may be specified, each separated by a
# space.

host dhcp64-233.ibm.com dhcp64-233.ibm.com

# The distinguished name of the search base.

base dc=ibm,dc=com

# OpenLDAP SSL mechanism, start_tls mechanism uses the normal LDAP port 389
ssl start_tls

#Require and verify server certificate
tls_checkpeer yes

# CA certificates for server certificate verification
tls_cacertfile /etc/openldap/cacerts/cacert.pem

pam_password md5





回页首


参考资料

学习

获得产品和技术
  • 索取免费的 SEK for Linux,这有两张 DVD,包括最新的 IBM for Linux 的试用版软件,包括 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere®。

  • 在您的下一个开发项目中采用 IBM 试用版软件,这可以从 developerWorks 上直接下载。


讨论




回页首


关于作者

Mike O'Reilly

Mike O'Reilly 是 IBM Linux 和 VMware ESX 产品支持小组的一员,他为 Linux 产品提供支持已经有 5 年的时间了。另外,他支持 VMware 超


posted @ 2006-08-10 09:19 Alex 阅读(850) | 评论 (0)编辑 收藏

key words : 热部署 动态读取配置文件 动态读取properties文件

come from here

package com.javaeye.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.Properties;

/**
@author Robbin Fan

*/
public class ConfigUtil {

    
private static Properties props = null;   
    
private static File configFile = null;
    
private static long fileLastModified = 0L;
   
    
private static void init() {
        URL url 
= ConfigUtil.class.getClassLoader().getResource("global.properties");
        configFile 
= new File(url.getFile());
        fileLastModified 
= configFile.lastModified();     
        props 
= new Properties();
        load();
    }
   
    
private static void load() {
        
try {
            props.load(
new FileInputStream(configFile));
            fileLastModified 
= configFile.lastModified();
        } 
catch (IOException e) {           
            
throw new RuntimeException(e);
        }
    }

    
public static String getConfig(String key) {
        
if ((configFile == null|| (props == null)) init();
        
if (configFile.lastModified() > fileLastModified) load();
        
return props.getProperty(key);
    }

}

posted @ 2006-08-08 19:08 Alex 阅读(347) | 评论 (0)编辑 收藏

key words: 观察者模式 Observer模式

definition:
defines a one-to-many dependency between objects so that when one object changes state,all of its dependents are notified and updated automatically.

物理模型:
观察者注册
observerpattern_02.gif

观察者通知:
observerpattern_03.gif
观察者撤销注册
observerpattern_04.gif

business: weather station
天气预报,在预报的数据更新后自动通知到各类不同显示类型的终端,前提是这些终端向预报中心注册了预定服务(register the service),这一点类似于订阅报纸,你'订阅'了以后就可以呆在家等邮递员给你送过来.
看一下类图:weather.png


implement:
public interface Subject {
    
public void registerObserver(Observer o);
    
public void removeObserver(Observer o);
    
public void notifyObservers();
}

public class WeatherData implements Subject {
    
private ArrayList observers;
    
private float temperature;
    
private float humidity;
    
private float pressure;
    
    
public WeatherData() {
        observers 
= new ArrayList();
    }
    
    
public void registerObserver(Observer o) {
        observers.add(o);
    }
    
    
public void removeObserver(Observer o) {
        
int i = observers.indexOf(o);
        
if (i >= 0) {
            observers.remove(i);
        }
    }
    
    
public void notifyObservers() {
        
for (int i = 0; i < observers.size(); i++) {
            Observer observer 
= (Observer)observers.get(i);
            observer.update(temperature, humidity, pressure);
        }
    }
    
    
public void measurementsChanged() {
        notifyObservers();
    }
    
    
public void setMeasurements(float temperature, float humidity, float pressure) {
        
this.temperature = temperature;
        
this.humidity = humidity;
        
this.pressure = pressure;
        measurementsChanged();
    }
    
    
// other WeatherData methods here
    
    
public float getTemperature() {
        
return temperature;
    }
    
    
public float getHumidity() {
        
return humidity;
    }
    
    
public float getPressure() {
        
return pressure;
    }
}

public interface Observer {
    
public void update(float temp, float humidity, float pressure);
}


public interface DisplayElement {
    
public void display();
}

public class CurrentConditionsDisplay implements Observer, DisplayElement {
    
private float temperature;
    
private float humidity;
    
private Subject weatherData;
    
    
public CurrentConditionsDisplay(Subject weatherData) {
        
this.weatherData = weatherData;
        weatherData.registerObserver(
this);
    }
    
    
public void update(float temperature, float humidity, float pressure) {
        
this.temperature = temperature;
        
this.humidity = humidity;
        display();
    }
    
    
public void display() {
        System.out.println(
"Current conditions: " + temperature 
            
+ "F degrees and " + humidity + "% humidity");
    }
}

public class WeatherStation {

    
public static void main(String[] args) {
        WeatherData weatherData 
= new WeatherData();
   
        //register to weatherData(subscribe to weatherData)
        CurrentConditionsDisplay currentDisplay 
=new CurrentConditionsDisplay(weatherData);
        StatisticsDisplay statisticsDisplay 
= new StatisticsDisplay(weatherData);
        ForecastDisplay forecastDisplay 
= new ForecastDisplay(weatherData);

        weatherData.setMeasurements(
806530.4f);
        weatherData.setMeasurements(
827029.2f);
        weatherData.setMeasurements(
789029.2f);
    }
}


public class WeatherStationHeatIndex {

    
public static void main(String[] args) {
        WeatherData weatherData 
= new WeatherData();
        CurrentConditionsDisplay currentDisplay 
= new CurrentConditionsDisplay(weatherData);
        StatisticsDisplay statisticsDisplay 
= new StatisticsDisplay(weatherData);
        ForecastDisplay forecastDisplay 
= new ForecastDisplay(weatherData);
        HeatIndexDisplay heatIndexDisplay 
= new HeatIndexDisplay(weatherData);

        weatherData.setMeasurements(
806530.4f);
        weatherData.setMeasurements(
827029.2f);
        weatherData.setMeasurements(
789029.2f);
    }
}


other places to use Observe-Pattern in JDK:

public class SwingObserverExample {
    JFrame frame;
    
    
public static void main(String[] args) {
        SwingObserverExample example 
= new SwingObserverExample();
        example.go();
    }
    
    
public void go() {
        frame 
= new JFrame();

        JButton button 
= new JButton("Should I do it?");
        //register to AngelListener and DevilListener

        button.addActionListener(
new AngelListener());
        button.addActionListener(
new DevilListener());
        frame.getContentPane().add(BorderLayout.CENTER, button);

        
// Set frame properties 
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(BorderLayout.CENTER, button);
        frame.setSize(
300,300);
        frame.setVisible(
true);
    }
    
    
class AngelListener implements ActionListener {
        
public void actionPerformed(ActionEvent event) {
            System.out.println(
"Don't do it, you might regret it!");
        }
    }

    
class DevilListener implements ActionListener {
        
public void actionPerformed(ActionEvent event) {
            System.out.println(
"Come on, do it!");
        }
    }
}



implement it with java built-in Observe-Pattern
import java.util.Observable;
import java.util.Observer;
    
public class WeatherData extends Observable {
    
private float temperature;
    
private float humidity;
    
private float pressure;
    
    
public WeatherData() { }
    
    
public void measurementsChanged() {
        setChanged();
        notifyObservers();
    }
    
    
public void setMeasurements(float temperature, float humidity, float pressure) {
        
this.temperature = temperature;
        
this.humidity = humidity;
        
this.pressure = pressure;
        measurementsChanged();
    }
    
    
public float getTemperature() {
        
return temperature;
    }
    
    
public float getHumidity() {
        
return humidity;
    }
    
    
public float getPressure() {
        
return pressure;
    }
}






增加一个看到的通俗版的解说:

观察者模式 Observer Pattern — 三国演义之超级间谍战 — 美女貂蝉的故事

说明:我也是初学者,希望大家能提出宝贵意见。另外转载请注明作者左光和出处博客园,毕竟花费了很长时间才完成。

情节:

这一次讲的故事情节很简单,但是充满了谋略和斗争。大体意思就是三国初期,曹刘孙三家在徐州联手消灭了吕布,但是自己也伤了元气。而此时袁术得了传国玉玺,在淮南称帝,兵精将广,图谋不轨,对三家威胁都很大。于是曹刘孙三家在一起开了个会,决定派遣一名超级间谍打入到袁术身旁,监视他的一举一动,这样的话一旦袁术想干什么坏事,他们就可以立刻知道并做出相应的调整,知己知彼百战百胜嘛。

计是好计,问题是派谁去当这个超级间谍呢?正当大家愁眉苦脸的时候,美女貂蝉自告奋勇,想当年董卓那么厉害都让 我灭了,何况一个小小的袁术呢?大家一听,说的有理,于是就把貂蝉献给了袁术当了妃子。另外三家还各派出一名小奸细常住在淮南城内,他们的任务是当联络 员,貂蝉有什么情报总不能自己曹刘孙三家挨个跑着送吧?直接丢给各国联络员,然后让他们通知各自的主公就 OK 了!而三家只要一接到各自奸细的通知,就会立即做出反应。

还有一个小插曲,袁术虽然收下了貂蝉,但是对她看管很严,大大方方地把情报送出去不太可能,逼不得以,貂蝉自己设计了一套密码来传递情报,虽然加密 方法没有公开,但是她想总有人可以破解了吧?没错,对诸葛亮来说就是小菜一碟,从此袁术穿什么内裤曹刘孙三家都知道得清清楚楚。

分析:

下面我们用 观察者模式  来分析一下上面这个故事

1、美女貂蝉:貂蝉在观察者模式中叫做被观察者(Subject),主要任务是独立的管理后台数据和业务逻辑,同时尽可能不受前台客户端界面变化的影响。当然,还要负责登记或者注销各个观察者。

在这个故事里,貂蝉仅仅维护了一个数据 ,就是情报 —  私有变量 info ;另外还拥有一个业务逻辑,是用来加密 info 的方法 Reverse(string str) 。每次得到新的情报,她就会先加密,然后立刻找到在自己这登记过的联络员,让这些联络员通知自己的主公应变。

情报一旦发送出去, 貂蝉的任务就算完成了,具体曹刘孙三家怎么应对,是打是降还是另有其他方法,那是三家自己的事情,和她貂蝉就没什么关系了。

2、曹刘孙三家:曹刘孙三家在观察者模式里叫做观察者,主要任务就是从界面上用“各种方式”即时的反映出 被观察者,所谓“各种方式”就是说用字符、图形、声音都可以表示同样数据,外在表现不同而已,本质都是一些数据。所谓“即时”,就是说只要 被观察者 发生变化, 观察者 也会立刻跟着变化,用行话应该叫做刷新 Update吧。

在这个故事里,我们可以看到运行结果中,每次貂蝉有什么新的情报,三家都会在屏幕上显示出来,虽然很简单,但 他们确实是用各自不同的方式表示了同样的数据








posted @ 2006-08-07 20:07 Alex 阅读(431) | 评论 (0)编辑 收藏

先转一篇 jdon的文章:

Strategy策略模式是属于设计模式中 对象行为型模式,主要是定义一系列的算法,把这些算法一个个封装成单独的类.

Stratrgy应用比较广泛,比如, 公司经营业务变化图, 可能有两种实现方式,一个是线条曲线,一个是框图(bar),这是两种算法,可以使用Strategy实现.

这里以字符串替代为例, 有一个文件,我们需要读取后,希望替代其中相应的变量,然后输出.关于替代其中变量的方法可能有多种方法,这取决于用户的要求,所以我们要准备几套变量字符替代方案.

 

首先,我们建立一个抽象类RepTempRule 定义一些公用变量和方法:

public abstract class RepTempRule{

protected String oldString="";
public void setOldString(String oldString){
  this.oldString=oldString;
}

protected String newString="";
public String getNewString(){
  return newString;
}



public abstract void replace() throws Exception;


}

在RepTempRule中 有一个抽象方法abstract需要继承明确,这个replace里其实是替代的具体方法.
我们现在有两个字符替代方案,
1.将文本中aaa替代成bbb;
2.将文本中aaa替代成ccc;

对应的类分别是RepTempRuleOne RepTempRuleTwo

public class RepTempRuleOne extends RepTempRule{


public void replace() throws Exception{

  //replaceFirst是jdk1.4新特性
  newString=oldString.replaceFirst("aaa", "bbbb")
  System.out.println("this is replace one");
  
}


}

public class RepTempRuleTwo extends RepTempRule{


public void replace() throws Exception{

  newString=oldString.replaceFirst("aaa", "ccc")
  System.out.println("this is replace Two");
  
}


}

第二步:我们要建立一个算法解决类,用来提供客户端可以自由选择算法。

public class RepTempRuleSolve {

  private RepTempRule strategy;

  public RepTempRuleSolve(RepTempRule rule){
    this.strategy=rule;
  }

  public String getNewContext(Site site,String oldString) {
    return strategy.replace(site,oldString);
  }

  public void changeAlgorithm(RepTempRule newAlgorithm) {
    strategy = newAlgorithm;
  }

}

 

 

调用如下:

public class test{

......

  public void testReplace(){

  //使用第一套替代方案
  RepTempRuleSolve solver=new RepTempRuleSolve(new RepTempRuleSimple());
  solver.getNewContext(site,context);

  //使用第二套

  solver=new RepTempRuleSolve(new RepTempRuleTwo());
  solver.getNewContext(site,context);

  }

.....

}

我们达到了在运行期间,可以自由切换算法的目的。

实际整个Strategy的核心部分就是抽象类的使用,使用Strategy模式可以在用户需要变化时,修改量很少,而且快速.

Strategy和Factory有一定的类似,Strategy相对简单容易理解,并且可以在运行时刻自由切换。Factory重点是用来创建对象。

Strategy适合下列场合:

1.以不同的格式保存文件;

2.以不同的算法压缩文件;

3.以不同的算法截获图象;

4.以不同的格式输出同样数据的图形,比如曲线 或框图bar等


另:

ahead first design 中的第一篇举例的模型如下


strategy.png
posted @ 2006-08-01 18:26 Alex 阅读(300) | 评论 (0)编辑 收藏

Oracle常用的函数:

sysdate
systimestamp
to_char
to_date:
select to_date('06-5月-1957','DD-Mon-YY'from dual;

nvl:

select nvl(sal,''from emp;


decode:
decode(条件,值1,翻译值1,值2,翻译值2,...值n,翻译值n,缺省值)

该函数的含义如下:
IF 条件=值1 THEN
    RETURN(翻译值1)
ELSIF 条件=值2 THEN
    RETURN(翻译值2)
    ......
ELSIF 条件=值n THEN
    RETURN(翻译值n)

ELSE
    RETURN(缺省值)
END IF
String sql =
        "
select product_name,sum(decode(bill_month,'"+chargePeriod+"',charge))/100 as cur_charge,
sum(decode(bill_month,'"+lastPeriod+"',charge))/100 as last_charge, "
        
+
        " 
sum(decode(bill_month,'"+chargePeriod+"',bill_time_len))/60 as cur_time,
sum(decode(bill_month,'"+lastPeriod+"',bill_time_len))/60 as last_time "
        
+
        " 
from tl_acc_report "
        
+
        " 
group by product_name ";


SELECT sid,serial#,username,

DECODE(command

,
0,’None’

,
2,’Insert

,
3,’Select

,
6,’Update

,
7,’Delete

,
8,’Drop

,’Other’) cmd 
from des

posted @ 2006-07-31 16:34 Alex 阅读(384) | 评论 (0)编辑 收藏

熟练人员经过多年的积累加上自己的CodeSnip的总结,基本不用额外再查找资料。而一般的开发人员在开发过程中会花掉10-20%时间去查找资料。

熟练人员注意代码复用,并且时刻注意重构和抽取公用代码。一般开发人员是代码拷来拷去完成功能。

熟练人员非常注意查找,定位,标签等各种快捷键的使用,定位查找方便快捷,IDE环境也根据习惯定义到最方便状态。

熟练人员编码前先思考清楚整个流程,在头脑或纸张上规划好整个实现方式和方法函数的划分。一般人员想到哪里写到哪里。

熟练人员写了50行以上或更多代码才Debug一两次,一般人员写了几行代码就要Debug多次,完全通过Debug来验证代码正确性。

熟练人员注重代码的质量,单元测试和可维护性,注重各种业务逻辑的验证和边界条件的校验。一般人员只注重简单功能的简单完成。

熟练人员提交测试的代码BUG很少,返工工作量很小。一般开发人员由于自测不完善BUG较多,造成大量的返工工作量。

熟练人员合理分配自己的时间,规划好每天工作任务,开发过程各位专注。一般开发人员一心多用,边开发边聊Q。

熟练人员善于知识的总结和积累,形成自我的知识库和经验库

熟练人员善于发现问题,分析不足而自我持续改进。一般人员在外力干预侠被动改进。

熟练开发人员开发重点已经专业到对业务的深刻理解,一般开发人员考虑的是开发上编程的语言和工具。

熟练人员善于从各种影响自己开发效率的因素中挤时间,善于使用各种辅助开发工具。而一般人员则不善于这种总结。
posted @ 2006-07-31 11:21 Alex 阅读(428) | 评论 (0)编辑 收藏

级别: 初级

易立, IBM 中国软件开发实验室 SOA设计中心 高级软件工程师
赵勇, IBM 中国软件开发实验室 SOA设计中心 高级软件工程师

2006 年 4 月 20 日

在本文中,作者通过一个Web Service访问的实例,具体描述了SOA应用中所遇到的一系列具体问题,并描述如何利用IoC和AOP等技术进行代码重构,从而构建结构更加良好、灵活的SOA应用。

1.引言

SOA是一种构造分布式系统的方法,它将业务应用功能以服务的形式提供出来,以便更好的复用、组装和与外部系统集成,从而降低开发成本,提高开发效率。SOA的目标是为企业构建一个灵活,可扩展的IT基础架构来更好地支持随需应变的商务应用。

随着SOA技术和产品的不断成熟,现在越来越多的用户开始了解并认同SOA的理念,但对SOA项目的实施还缺乏信心。其主要原因是:SOA应用开发还相对比较复杂。

一年多来,本文作者所在的部门已经从事了许多国内外的SOA项目的实施和支持工作,积累了许多SOA应用开发经验。我们希望能够通过一系列的文章与读者分享这些想法,帮助您更好地构建SOA应用。

本 文将从Web Service调用入手,在解决一系列具体问题的过程中,使用IoC (Inversion of Control) 和AOP (Aspect- Oriented Programming) 等方法重构Web Service的访问代码,使得业务逻辑与Web Service访问解耦,为您提供一个更加灵活和易于扩展的访问模式。

Spring是一个流行的轻量级容器,对IoC和AOP提供了良好的 支持。本文为您提供了一个基于Spring的实现供您下载学习。示例代码工程使用Eclipse3.1/3.02和JDK1.4开发, 您还需要Spring 1.2.5和Axis1.3提供的支持。详细的下载信息请参见参考资源部分。





回页首


2.Web Service调用

Web Service是目前实现SOA应用的一项基本的,适用的技术,它为服务的访问提供了一个被广泛接受的开放标准。为了便于说明问题,我们将使用XMethods 网站(http://www.xmethods.net/)发布的货币兑换服务作为示例。并针对JAX-RPC 1.1,说明如何编写Web Service 的调用代码。

2.1 示例说明

http://xmethods.net 作为最早推出Web Service实际示例的网站,提供了很多优秀的Web Service 样例。其中有一个汇率计算服务,可以返回两个国家之间的货币兑换比例。获取该服务的详细信息,请参考该服务的服务描述文档(获取WSDL 文档) 。在此就不具体解析该服务描述文档了。读者可以从WSDL2Java生成的接口中了解该服务的用法:


												
														
public interface CurrencyExchangePortType extends java.rmi.Remote {
public float getRate(String country1, String country2) throws java.rmi.RemoteException;
}

2.2 客户端调用方法

JAX-RPC作为Java平台的RPC服务调用标准接口,为Web Service客户端调用提供了3种方法,分别是DII,动态代理,和静态Stub。 DII(Dynamic Invocation Interface)采用直接调用方式,可以在程序中设置诸多的调用属性,使用较为灵活,但是调用过程却相对繁琐复杂,易造成代码膨胀且可重用性低,每次调用不同的Web Service都要重复进行大量编码。

JAX-RPC中动态代理(Dynamic Proxy)的方法实现对Web Service的动态调用,可以在运行时根据用户定义的Client端接口创建适配对象。从而避免了直接操作底层的接口,减少了客户端的冗余,屏蔽了调用相关的复杂性。

使 用静态Stub和Service Locator是目前最常用的调用方式。JAX-RPC使用静态的Stub方式包装对底层接口的调用,从而提供一种更为简便的调用方式。使用该方式需要利 用支持环境(比如Axis)所提供的工具根据WSDL预生成Web Service客户端的实现代码。因此如果服务的WSDL发生变化,就必须重新生成新的客户端代码并进行重新部署。

为了更详细的了解静态Stub的调用方式,您可以将示例代码的WebServiceClient.jar导入到您现有Eclipse工作区之中。

客户端生成代码包括如下4个类:如图 1 所示:


图 1: 客户端代码类图
图 1: 客户端代码类图

在上图中包括的几个类中:

CurrencyExchangePortType:服务端点接口,定义了Web Service的方法签名。

CurrencyExchangeService:Service接口,定义了获取服务端点接口的方法。

CurrencyExchangeServiceLocator:ServiceLocator类,实现了Service接口。

CurrencyExchangeBindingStub: Stub实现类,实现了服务端点接口,封装了对Web Service访问的底层逻辑。

使用Stub调用Web Service的过程也非常简单,读者可以参考清单 1:


清单 1:Web Service 调用代码示例
												
														
try {
//创建ServiceLocator
CurrencyExchangeServiceLocator locator = new
CurrencyExchangeServiceLocator();
//设定端点地址
URL endPointAddress = new URL("http://services.xmethods.net:80/soap");
//创建Stub实例
CurrencyExchangePortType stub =
locator.getCurrencyExchangePort(endPointAddress);
//设定超时为120秒
((CurrencyExchangeBindingStub)stub).setTimeout(120000);
//调用Web Service计算人民币与美元的汇率
float newPrice = stub.getRate("China", "USA") * 100;
} catch (MalformedURLException mex) {
//...
} catch (ServiceException sex) {
//...
} catch (RemoteException rex) {
//...
}





回页首


3.重构Web Service调用代码

3.1 实例代码中的"坏味道"

上面的基于Service Locator的Web Service访问代码虽然简单但暴露出以下几个问题:

1.访问Web Service所需的配置代码被嵌入应用逻辑之中
在Web Service调用中,我们需要设定一系列必要的参数。比如:服务端点地址、用户名/密码、超时设定等等。这些参数在开发和运行环境中都有可能发生变化。我们必须提供一种机制:在环境变化时,不必修改源代码就可以改变Web Service的访问配置。

2 客户端代码与Web Service访问代码绑定
在上面的代码中,业务逻辑与Web Service的Stub创建和配置代码绑定在一起。这也不是一种良好的编程方式。客户端代码只应关心服务的接口,而不应关心服务的实现和访问细节。比 如,我们既可以通过Web Service的方式访问远程服务,也可以通过EJB的方式进行访问。访问方式对业务逻辑应该是透明的。

这 种分离客户端代码与服务访问代码的方式也有利于测试。这样在开发过程中,负责集成的程序员就可能在远程服务还未完全实现的情况下,基于服务接口编写集成代 码,并通过编写POJO(Plain Old Java Object)构建伪服务实现来进行单元测试和模拟运行。这种开发方式对于保证分布式系统代码质量具有重要意义。

因此,为了解决上面的问题我们需要:

1、将Web Service访问的配置管理与代码分离;

2、解除客户端代码与远程服务之间的依赖关系;

3.2 利用IoC模式进行重构代码

我 们先介绍在Core J2EE Patterns一书中提到的一种业务层模式:Business Delegate。它所要解决的问题是屏蔽远程服务访问的复杂性。它的主要思想就是将Business Delegate作为远程服务的客户端抽象,隐藏服务访问细节。Business Delegate还可以封装并改变服务调用过程,比如将远程服务调用抛出的异常(例如RemoteException)转换为应用级别的异常类型。

其类图如图 2 所示:


图 2:Business Delegate 模式的类图图解
图 2:Business Delegate 模式的类图图解

Business Delegate模式实现很好地实现了客户端与远程访问代码的解耦,但它并不关注Delegate与远程服务之间的解耦。为了更好解决Business Delegate和远程服务之间的依赖关系,并更好地进行配置管理,我们可以用IoC模式来加以解决。

IoC(Inversion of Contro)l意为控制反转,其背后的概念常被表述为"好莱坞法则":"Don't call me, I'll call you." IoC将一部分责任从应用代码交给framework(或者控制器)来做。通过IoC可以实现接口和具体实现的高度分离,降低对象之间的耦合程度。 Spring是一个非常流行的IoC容器,它通过配置文件来定义对象的生命周期和依赖关系,并提供了良好的配置管理能力。

现在我们来重构我们的Web Service应用程序,我们首先为Business Delegate定义一个接口类型,它提供了一个应用级组件接口,所有客户端都应通过它来执行汇率计算,而不必关心实现细节,如清单 2 所示:


清单 2:接口定义的代码示例
												
														

Public interface CurrencyExchangeManager {
//货币兑换计算
//新价格 = 汇率 * 价格
public float calculate(String country1, String country2, float price)
throws CurrencyExchangeException;
}

Business Delegate的实现非常简单,主要工作是包装汇率计算 Web Service的调用,如清单 3 所示。


清单 3:Business Delegate的代码示例
												
														
public class CurrencyExchangeManagerImpl implements CurrencyExchangeManager {
//服务实例
private CurrencyExchangePortType stub;
//获取服务实例
public CurrencyExchangePortType getStub() {
return stub;
}
//设定服务实例
public void setStub(CurrencyExchangePortType stub) {
this.stub = stub;
}
//实现货币兑换
public float calculate(String country1, String country2, float price)
throws CurrencyExchangeException {
try {
//通过Stub调用WebService
float rate = stub.getRate(country1, country2);
return rate * price;
} catch (RemoteException rex) {
throw new CurrencyExchangeException(
"Failed to get exchange rate!", rex);
}
}
}

下面我们需要讨论如何利用Spring的IoC机制,来创建和配置对象,并定义它们的依赖关系。

Spring 利用类工厂来创建和配置对象。在Spring框架中,已经为基于JAX-RPC的Web Service调用提供了一个客户端代理的类工厂实现:JaxRpcPortProxyFactoryBean。在配置文件bean.xml中,我们将使 用JaxRpcPortProxyFactoryBean来创建和配置Web Service的客户端代理"CurrencyExchangeService",如清单 5 所示。我们还将定义一个名为"CurrencyExchangeManager"的CurrencyExchangeManagerImpl实例,并建立 它与CurrencyExchangeService之间的依赖关系。有关Spring 配置和JaxRpcPortProxyFactoryBean的使用细节请参见参考资料。


清单 5:bean.xml的配置文件
												
														

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="CurrencyExchangeService"
class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean">
<property name="serviceInterface">
<value>net.xmethods.www.sd.CurrencyExchangeService_wsdl.
CurrencyExchangePortType</value>
</property>
<property name="wsdlDocumentUrl">
<value>http://www.xmethods.net/sd/2001/CurrencyExchangeService.
wsdl</value>
</property>
<property name="namespaceUri">
<value>http://www.xmethods.net/sd/CurrencyExchangeService.
wsdl</value>
</property>
<property name="serviceName">
<value>CurrencyExchangeService</value>
</property>
<property name="portName">
<value>CurrencyExchangePort</value>
</property>
<property name="endpointAddress">
<value>http://services.xmethods.net:80/soap</value>
</property>
</bean>
<bean id="CurrencyExchangeManager"
class="test.ws.CurrencyExchangeManagerImpl">
<property name="stub">
<ref bean="CurrencyExchangeService"/>
</property>
</bean>
</beans>

最后我们创建一个测试程序来验证我们的代码,如清单6 所示:


清单 6:测试代码
												
														

public class Main {
// For test only
public static void main(String[] args) {
// Spring Framework将根据配置文件创建并配置CurrencyExchangeManager实例
ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml");
// 获取CurrencyExchangeManager实例
CurrencyExchangeManager manager = (CurrencyExchangeManager) ctx
.getBean("CurrencyExchangeManager");
try {
System.out.println(manager.calculate("China", "USA", 100));
System.out.println(manager.calculate("China", "Japan", 200));
System.out.println(manager.calculate("China", "USA", 200));
} catch (Exception ex) {
ex.printStackTrace();
}
}
}

此时运行测试客户端,等待片刻将会看见测试结果,如清单 7 所示:


清单 7:测试结果。
												
														
12.34
2853.26
24.68

注:该结果会随着汇率的变化而出现不同的值。

该程序的类图和顺序图如图3及图4所示:


图 3:示例程序的类图
图 3:示例程序的类图

从 上面的类图我们可以看到,我们的测试程序(Main.java)通过Spring框架获取了BusinessDelegate的实例。而且Spring 框架还会根据配置中的依赖关系,在运行时将Web Service的客户端代理" 注射"到CurrencyExchangeManagerImpl实例中,这就是依赖注入(Dependency Injection)。通过这种方式解决了应用逻辑和BusinessDelegate之间的依赖关系,以及BusinessDelegate的实现与远程服务之间的依赖关系,如图 4 所示。


图 4: 示例程序的顺序图
图 4:  示例程序的顺序图

Spring 框架提供的ApplicationContext实现会根据配置文件中的描述信息来实现对象生命周期管理,配置管理以及依赖管理等功能。这一切对于应用程 序是透明的,应用程序代码只依赖接口进行编程,而无需考虑其它复杂问题。无论是Web Service的配置发生变化,或是改用不同的服务实现时,都不会对客户端应用代码的产生影响。这很好地实现了业务逻辑与Web Service调用之间的解耦。

3.3 构建自己的 Web Service代理工厂

Spring 所提供的JaxRpcPortProxyFactoryBean封装了构造Web Service客户端代理的细节,可以通过参数配置来创建Dynamic Proxy和DII类型的Web Service客户端代理。(如果您希望深入了解其实现细节可以参考org.springframework.remoting.jaxrpc包下的源代 码。)但由于JaxRpcPortProxyFactoryBean需要使用者对WSDL中Port,Service,名空间等概念有深入的了解;而且如 果Web Service使用了复杂数据类型,开发人员需要手工定义类型映射代码。所以JaxRpcPortProxyFactoryBean并不适合Web Service的初学者来使用。

为了进一步简化Web Service代理的创建,并帮助读者更好地理解类工厂在Spring框架下的作用。我们提供了一个基于静态Stub的Web Service客户端代理工厂实现。其核心代码非常简单,就是通过ServiceLocator提供的方法来创建Web Service客户端代理。

其主要代码如清单8所示:


清单8:静态代理工厂的代码
												
														
public class WebServiceStubFactoryBean implements FactoryBean,
InitializingBean {
private Class serviceInterface;
private Class serviceLocator;
private Object stub;

public void afterPropertiesSet() throws Exception {
//利用serviceLocator和服务接口创建Web Service客户端代理
stub = ((javax.xml.rpc.Service)
serviceLocator.newInstance()).getPort(serviceInterface);
//为Stub设定endpointAddress,usernam, 超时等参数
preparePortStub((javax.xml.rpc.Stub) stub);
}
public Object getObject() {
// 返回客户端代理
return stub;
}
public Class getObjectType() {
// 返回服务接口
return serviceInterface;
}
public boolean isSingleton() {
return true;
}
}

我们需要修改配置文件bean.xml中有关Web Service代理创建的部分,让新的Web Service 代理工厂发挥作用。如清单9所示:


清单9:修改后的bean.xml的配置文件
												
														

<bean id="CurrencyExchangeService" class="test.ws.WebServiceStubFactoryBean">
<property name="serviceInterface">
<value>net.xmethods.www.sd.CurrencyExchangeService_wsdl.CurrencyExchangePortType</value>
</property>
<property name="serviceLocator">
<value>net.xmethods.www.sd.CurrencyExchangeService_wsdl.CurrencyExchangeServiceLocator</value>
</property>
<property name="endpointAddress2">
<value>http://services.xmethods.net:80/soap</value>
</property>
<property name="timeout">
<value>120000</value>
</property>
</bean>

得益于Spring框架,虽然我们已经替换了对象的类工厂,却并不需要更改应用代码。通过Spring框架的IoC机制,我们可以完全使用面向接口的编程方式,而将实现的创建、配置和依赖管理交由Spring在运行时完成。即使实现发生了变化,也不需要改变应用程序结构。





回页首


4.新的思考

故事并没有结束,在开发过程中,我们又遇到了一系列关于Web Service调用的问题。

4.1性能

系 统性能是分布式应用中的一个重要问题。许多用户都担心由Web Service技术所引入的额外开销是否会影响到产品的性能。随着技术的不断发展,Web Service引擎性能已经有了很大提高,一般来说使用Web Service的系统的性能可以满足绝大部分应用的需求。但在特定情况下,如果系统性能无法满足客户需求,我们首先需要对系统性能进行科学地分析和测定才 能定位真正的性能瓶颈。这个问题在上文简单的示例中并不难解决,只需要在Web Service调用前后加入日志代码记录调用时间即可实现。但在实际系统中,比如一个产品目录的Web Service可能提供数十种查询方法,而程序中很多组件都会依赖于该服务提供的查询功能。如果在系统中所有的地方加入性能测定代码,这个工作就变得非常 繁琐和困难。我们需要用一种更加优雅的解决方式,在增添新功能的同时并不影响系统代码或结构。

4.2缓存

在 项目实践中,一个有效的改善Web Service系统性能的方法就是利用缓存来减少Web Service的重复调用。在具体实现中我们可以采用客户端缓存和服务器端缓存等不同方式,他们具有不同的特点和适用范围。在本文例子中,我们希望实现客 户端缓存来提高系统性能。但由于Web Service业务逻辑的差别,我们希望能够为特定的Web Service提供特定的缓存策略,而且这些策略应该是能够被灵活配置的,它们不应于应用程序的逻辑代码耦合在一起。

4.3故障恢复:

对 于Web Service应用,系统的可用性也是一个需要考虑的重要问题。在运行时由于网络运行环境的复杂性和不确定性,用户希望能够对Web Service访问提供一定的故障恢复机制:比如重试或者访问备份服务(当系统在调用Web Service失败后,使用备份Web Service的服务地址来继续访问)。这些故障恢复策略应该是可配置的,对应用逻辑透明的。





回页首


5.使用AOP解决SOA应用中的Crosscutting Concern

通过对上边一系列问题的分析,读者也许会发现这些问题并不是Web Service访问的核心问题,但会影响系统中许多不同的组件。而且其中一些问题需要我们能够灵活配置不同的实现策略,因此我们不应该将处理这些问题的代码与应用代码混合。

下 面我们将利用AOP(Aspect-Oriented Programming)提供的方法来解决上述的问题。AOP是一种新兴的方法学,它最基本的概念就是关注隔离(Separation of Concern)。AOP提供了一系列的技术使得我们能够从代码中分离那些影响到许多系统模块的crosscutting concerns,并将他们模块化为Aspects。AOP的主要目的仍然是解耦,在分离关注点后,才能将关注点的变更控制一定范围内,增加程序的灵活 性,才能使得关注能够根据需求和环境作出随时调整。

我们将利用Spring所提供的AOP功能支持来解决以上问题。这里我们只简单地介绍涉及到的AOP基本概念以及实现,如果您希望更好地了解AOP的概念以及Spring AOP支持的细节请参见参考资料。

  • Joinpoint 是程序的运行点。在Spring AOP中,一个Joinpoint对应着一个方法调用。
  • Advice 定义了AOP框架在特定的Joinpoint的处理逻辑。Spring AOP框架通过interceptor方式实现了advice,并且提供了多种advice类型。其中最基本的"around advice"会在一个方法调用之前和之后被执行。

下面我们将利用Spring提供的MethodInterceptor来为Web Service调用实现我们的定义的处理逻辑。

5.1 PerformanceMonitorInterceptor

性能测量是AOP最简单的例子之一,我们可以直接利用Spring提供的实现在bean.xml中声明我们的WebServicePerformanceMonitorInterceptor。

5.2 CacheInterceptor

为 了不引入缓存策略的复杂性,我们只提供了一个利用HashMap的简单实现:它利用 Web Service的调用参数列表作为HashMap键值。在Web Service调用之前,首先检查缓存中是否拥有与现在参数列表相同的项,如果有则返回缓存的结果,否则调用Web Service并将<参数列表,结果>记录在HashMap中。在实际应用中,您应该根据具体情况来选择、构造适合Web Service的业务特性的Cache实现,也可以采用成熟的Cache实现。

在下面代码实现中有一个生成Web Service调用主键的小技巧。因为Web Service引擎要求所有调用参数必须是可序列化的,所以我们可以利用Java提供的序列化功能来实现对象的克隆。如清单10所示:


清单10:SimpleCacheInterceptor的代码示例
												
														

public class SimpleCacheInterceptor implements MethodInterceptor {
private Map cache = new HashMap();
private Object cloneObject(Object obj) throws Exception {
Object newObj = null;
if (obj != null) {
// 通过序列化/反序列化来克隆对象
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
out.writeObject(obj);
out.flush();
out.close();
ObjectInputStream in = new ObjectInputStream(
new ByteArrayInputStream(bos.toByteArray()));
newObj = in.readObject();
}
return newObj;
}
//基于参数列表数组,生成用于HashMap的键值
public Object generateKey(Object[] args) throws Exception {
Object[] newArgs = (Object[]) cloneObject(args);
List key = Arrays.asList(newArgs);
return key;
}
//实现使用缓存技术的invoke方法
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Object result = null;
Object data = null;
Object key = null;

try {
key = generateKey(methodInvocation.getArguments());
data = cache.get(key);
} catch (Exception ex) {
logger.error("Failed to find from the cache", ex);
}

if (data == null) {
//如果Cache中没有缓存结果,调用服务执行生成用于HashMap的键值
result = methodInvocation.proceed();
try {
data = cloneObject(result);
cache.put(key, data);
} catch (Exception ex) {
logger.error("Failed to cache the result!", ex);
}
} else {
result = data;
}
return result;
}
}

5.3 FailoverInterceptor

下面代码提供了一个基于服务备份切换的故障恢复实现,在运行时,如果Interceptor检测到服务调用由于网络故障抛出异常时,它将使用备份服务的端点地址并重新调用。如清单11所示:


清单 11: SimpleFailoverInterceptor的代码示例
												
														

public class SimpleFailoverInterceptor implements MethodInterceptor { …

//实现支持端点运行时切换的invoke方法
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Object result = null;
try {
result = methodInvocation.proceed();
} catch (Throwable ex) {
if (isNetworkFailure(ex)) {
//切换服务端点地址
switchEndPointAddress((Stub) methodInvocation.getThis());
result = methodInvocation.proceed();
} else {
throw ex;
}
}
return result;
}
}

为了支持备份服务切换的功能,我们在WebServicePortProxyFactoryBean中为填加了配置参数"endpointAddress2",它会在创建的Web Service客户端代理对象中记录备份URL。

我 们可以在CurrencyExchangeService加入下列参数来试验SimpleFailoverInterceptor的功能。其中第一个端点 地址为一个错误的URL。在第一次调用服务时,SimpleFailoverInterceptor会侦测到网络故障的发生,并自动切换使用第二个端点地 址继续访问。如清单12所示:


清单12:配置文件种增加的属性
												
														


<property name="endpointAddress">
<value>http://localhost/wrong_endpoint_address</value>
</property>
<property name="endpointAddress2">
<value>http://services.xmethods.net:80/soap</value>
</property>

5.4配置文件和运行结果

现 在我们需要在Spring配置文件中,为所有interceptor添加定义,并描述如何为CurrencyExchangeService构建AOP Proxy。需要指出的是,我们要在interceptorName列表中声明interceptor链的调用顺序,还要将原有 CurrencyExchangeManager引用的stub对象替换为新AOP Proxy。如清单13所示:


清单13:修改后的配置文件片段
												
														


<bean id="WebServicePerformanceMonitorInterceptor"
class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor">
<property name="prefix">
<value>Web Service </value>
</property>
<property name="suffix">
<value></value>
</property>
</bean>
<bean id="CacheInterceptor" class="test.ws.SimpleCacheInterceptor"/>
<bean id="FailoverInterceptor" class="test.ws.SimpleFailoverInterceptor"/>
<bean id="CurrencyExchangeProxy"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>net.xmethods.www.sd.CurrencyExchangeService_wsdl.
CurrencyExchangePortType</value>
</property>
<property name="target">
<ref local="CurrencyExchangeService"/>
</property>
<property name="interceptorNames">
<list>
<value>WebServicePerformanceMonitorInterceptor</value>
<value>CacheInterceptor</value>
<value>FailoverInterceptor</value>
</list>
</property>
</bean>
<bean id="CurrencyExchangeManager"
class="test.ws.CurrencyExchangeManagerImpl">
<property name="stub">
<ref bean="CurrencyExchangeProxy"/>
</property>
</bean>

这里我们通过为AOP 的ProxyFactoryBean为 Web Service Stub创建了一个AOP代理,并且建立了一个Interceptor链。这样在调用Web Service时,Spring框架会依次调用Interceptor执行。实例执行的顺序图将如图5所示:


图5系统运行顺序图
图5系统运行顺序图

5.5 Interceptor与JAX-RPC Handler的关系与区别

SOAP Message Handler是JAX-RPC为用户自定义Web Service处理过程提供的一种扩展机制。在处理Web Service请求/响应过程中,Web Service 引擎会根据部署描述中的定义,按照一定的次序调用Handler的处理代码。用户编写的Handler实现可以截获并修改Web Service消息和处理流程,从而实现对Web Service引擎处理行为的定制和增强。

比如,我们可以实现一个服务器端Handler,记录Web Service在受到请求消息和发出响应消息之间的时间间隔来实现对服务器端业务性能的测定。而且我们只需在部署描述中增加Handler声明即可,无需修改任何服务器端代码。

从 此可以看出,JAX-RPC Handler与我们在上文中所提供的AOP Interceptor都可以帮助我们的SOA应用程序实现关注分离(Separate Concern)的目标,在不改变应用代码的同时,增强或改变Web Service服务访问的功能。虽然我们可以利用它们实现一些类似的功能,但它们具有着不同的特点和适用范围。

JAX-RPC Handler是Web Service引擎的扩展机制。如果我们需要实现对SOAP消息进行的修改和处理,加入自定义的SOAP Header或对消息内容进行加密,Handler是我们的最佳选择。而AOP是针对对象级别的扩展机制,它更适合对应用层逻辑进行操作。

比 如,我们在上文展示的利用AOP实现的CacheInterceptor,它缓存的是Web Service调用参数和结果。而我们也可以通过JAX-RPC Handler实现一个面向SOAP消息的实现,它将缓存Web Service的请求消息和响应消息。这两个实现相比,基于AOP的实现更加简单、直观、快速、对资源消耗也比较小。而面向SOAP消息的实现则更加灵 活,对于不采用RPC方式的Web Service访问也能提供支持。

所以在具体的实践过程中,开发人员应该根据具体的需求选择合适的技术,也可以将这两种技术结合使用。





回页首


6.总结

"分而治之"的方法是人们解决复杂问题的一种常见做法。而IoC、AOP等技术都体现了这种思想。通过更好的切分程序逻辑,使得程序结构更加良好,更加富有弹性,易于变化。也使得开发人员可以更加专注于业务逻辑本身,而将一部分其他逻辑交给容器和框架进行处理。

在本文中,我们通过一个Web Service访问的实例,具体描述了SOA应用中所遇到的一系列具体问题,并描述如何利用IoC和AOP等技术进行代码重构,构建更加结构良好、灵活的SOA应用。综上所述,我们可以看到:

1使用IoC框架来实现对象的生命周期管理、配置管理和依赖管理,可以解除业务逻辑对服务调用的依赖关系;

2 使用AOP方法来解决Web Service调用中的crosscutting concerns,将为系统增加新的功能而不必更改应用程序。

3通过IoC和AOP来屏蔽Web Service访问的复杂性,使得开发人员可以更加专注于业务逻辑本身,也使得系统更加稳定和富有弹性。






回页首


下载

描述 名字 大小 下载方法
code sample code.zip 27 KB HTTP
关于下载方法的信息 Get Adobe® Reader®




回页首


参考资料





回页首


作者简介


易立 IBM 中国软件开发实验室 SOA设计中心 高级软件工程师。



赵勇 IBM 中国软件开发实验室 SOA设计中心 软件工程师。

posted @ 2006-07-28 13:29 Alex 阅读(467) | 评论 (1)编辑 收藏

key words: 真正的测试先行开发 测试驱动 Fitnesse


摘要
本文描述了如何使用开源的FitNesse来实现真正的测试先行开发过程,并让客户、需求提报工程师、开发人员、以及测试人员进行协同工作,达到需求更精准、减少需求更改、测试数据与Junit单元测试代码分离的目的,让这一切更简洁、更易于维护。
作者:Stephan Wiesner
译者:陈海青(joson)

在过去的几年里,我在开发测试工作中担任过各种角色,使用过服务器端的JavaScript,Perl,PHP,Struts,Swing以及模型驱动架构等各类技术。尽管项目不同,但是他们有一些共同点:项目结束的时间越晚,就越难以达到客户的真正需求。
每个项目都有一些需求,有的非常详细,有的却只有几页纸,这些需求一般要经历以下三个阶段:
---由客户或者承包人来书写或采纳一些官方的验收标准
---测试者试图根据需求来找出软件中不符合要求的地方
---项目开发完毕进入验收测试, 可是客户突然又提出对软件需求进行补充或变更的要求

最后一个阶段将导致项目发生变化,开发期甚至要超出最后期限,使开发人员的工作压力剧增,从而导致发生更多的错误。Bug的数量将快速增长,系统的质量将下降。听上去是不是很熟悉?

现 在让我们看一下在上述的项目开发中发生了哪些错误:客户、开发、测试人员没能协同工作;需求已经确认通过,但是在不同位置的人可能还用不同的需要未考虑。 另外,开发者一般会写一些自动测试代码,测试人员也试图进行自动化测试,但是他们往往不能充分协同,许多项目被重复测试,但另一些(经常是更困难的部分) 却没能被测试,客户也没参与到测试工作中。本文所介绍的就是通过自动测试与项目需求相结合的方式来解决这些问题的一种方案。

版权声明:任何获得Matrix授权的网站,转载时请务必保留以下作者信息和链接
作者:Stephan Wiesner ;joson(作者的blog:http://blog.matrix.org.cn/page/joson)
原文:http://www.javaworld.com/javaworld/jw-02-2006/jw-0220-fitnesse.html
Matrix:http://www.matrix.org.cn/resource/article/44/44507_FitNesse.html
关键字:FitNesse;Test

开始FitNesse

FitNesse 是一个增加了可触发Junit 测试等附加功能的wiki程序。如果这些测试能够与业务需求结合起来,就会使业务需求更加清晰。而且,测试数据的组织更有逻辑性。使用FitNesse更 重要的是学习隐含在其中的一些思想,某些部分需求可以作为测试的一部分,这意味着,这些需求是可以测试的,或者说是可以进行校验的。

利用 FitNesse,开发的工作过程可以这样描述:需求工程师使用FitNesse书写业务需求(取代了一般文档)。他试图尽可能让客户参与其中,当然这并 不是每天都能做到的。而测试者在反复研究这些文档,并从第一天起就开始提问各种问题,因为他们考虑问题的方式不同,不是在考虑“软件应该实现些什么”?而 是在考虑“怎样才能让软件出错?如何让软件中断运行?”等。开发者更象一个需求工程师,他更想知道“软件必须要完成它的功能是什么”?

测试人员可以更早地开始测试,甚至在需求没有全部完成前,而且可以把测试写进业务需求中,这些测试不仅仅成为需求的一部分,而且也将成为需求评审和验收的重要过程,并具有以下几方面的重要优点:

---客户也会被吸引来开始考虑关于测试的事情,通常他们还会参与到建立测试的工作中来(你也许会吃惊,他们怎么对这些这么感兴趣了。)
---相关规范将更详细、更周密,因为测试总比单纯的文字要准确.
---通过这种方式,可以更清晰明确地了解软件(象一个软件原形,但是功能更多),因此可以更早地考虑真实的运行场景,提供测试数据和测算结果。

最后,需求将提交给开发人员,他的工作要比以前要更容易些,因为需求都附带具体的实例,因而更贴近实际需求,因此减少了被突然改变的机会。下面,就让我们看一下这个过程是如何使开发者工作更轻松的吧。

测试先行的实现

通 常情况下,测试先行开发中最困难的是没人愿意花费那么多的时间来写测试,而更愿意去考虑如何让软件工作起来。按照上述的过程,开发者把功能测试看作合同的 一部分,他的任务要从“按要求编程,检测并修改”转变为“让测试运行起来”。现在,在确定应该做什么,何时完成,项目的定位等方面,我们有了更好的方法。

并非所有的测试都可以自动进行,并非所有的部分都可进行单元测试。我们通常将测试划分为以下几种类别:
---数据驱动的测试,需要通过单元测试来完成,如计算就是一种典型的例子.
---关键字驱动 (Keyword-driven) 测试,常自动化进行。这是一些系统测试,要求应用程序能够运行,按钮能够被点击,数据可以被键入,而输出的结果中包含规定的值。测试团队一般都能实现这种测试,但是可能开发者更容易完成这些工作。
---手工测试。这类测试适用于或者实现自动化测试的代价太昂贵并且是对出错的要求不高的情况,或者是一些基础功能(如,起始页面不能显示),可以很容易地发现错误的情况。

我是在2004年首次接触FitNesse,我曾经嘲笑过它并扬言它是不可能工作的。把测试写入wiki并自动进行测试,这个主意看起来太荒唐。但是,我错了,FitNesse真的象看起来得那样简单高效。

FitNesse的简单是从安装时就开始体现了,只要完全下载了FitNesse的发行包,并解压缩就可以了。在以下的讨论中假设解压到了c:\fitnesse目录中。

运 行C:\fitnesse 下的run.bat(linux 下运行run.sh)来启动FitNesse,FitNesse作为一个web服务运行在80端口上,当然你可以指定端口,如设定81,在运行脚本的首行 加上 –p 81 即可,这就是设置的全部工作。现在你可以用http://locahost:81来访问你的FitNesse了。

在本文,我使用windows平台的java版FitNesse,当然,这些例子也可以用于其它版本和其它平台(如Python,.net等)

一些测试

你 可以从FitNesse的在线文档提供了一些例子(可以与有名的Junit的货币用例相提并论)开始做起,这些例子非常适合学习如何使用 FitNesse,但是它们还不算是解决复杂问题的例子。因此,我将使用一些在我最近项目中的真实的用例。我已经简化了问题、代码,而不是直接取自项目 中,并写了一些说明。尽管如此,这些例子还是足够复杂,能够充分展示FitNesse简洁直观的威力
现在,假设我们正在从事一个为一个大型的保险公司开发开发项目,这是一个复杂的基于java的企业级应用。产品将涵盖公司的全部业务,包括客户和合同管理,以及支付业务,在我们的例子里,我们只关注其中的极小的一部分。

在瑞士,父母有权利获得给每个孩子的儿童津贴,经过确认家庭环境后,将根据情况得到相应的津贴。以下是这个需求的简化版本,我们将从一个传统的需求开始,并把它迁移到FitNesse中。

儿童津贴的发放存在几种状态。有关条款规定在孩子出生月份的第一天开始生效,在孩子达到规定年龄、就业或死亡的所在月份的最后一天失效。

按规定在到了12岁所在月份的第一天,就可以领到190瑞士法郎(瑞士官方货币)的补贴。
根据父母全职或兼职工作等情况,依据不同的条款,如表1所示进行分类.

image
图 1. 儿童津贴资格表.

就业率要根据工作合同来计算,合同需要被确认是有效的,如果有终止日期,还要确认是否在“有效期”内。表2显示了根据孩子年龄的不同等条件,父母可以得到的补贴数量。

image
图 2. 年龄相关条款

正常情况下,每两年要对款项进行有规律地调整。

第一次看到这些需求,也许会认为这些需求是很明确的,开发人员有能力轻松地实现它,但是,我们真的确定了这些边界条件了吗?该如何测试呢?

边界条件
边界条件是指达到、超过、低于某个给定的输入输出之的情况。经验表明,在边界条件附近的测试比其他测试更有价值,典型的例子就是著名的“只执行一次”的条件跳转和程序段.


场景对于查找例外条件和边界条件有很大帮助,因为这是得到关于商业规则的行业经验的好方法。

场景

对 大部分项目而言,是由需求工程师来提供给开发人员的需求规格说明书,然后开始学习了解需求,提问,然后开始设计、编码、测试。再后来,开发人员把软件提供 给测试团队,在经过几番改写和修补后,最后交付给客户(客户也许更喜欢考虑提出一些意外的需求)。如果采用了FitNesse,将不会改变这一过程,只是 要增加一些测试用例、场景,和测试意愿。

场景在启动测试过程中具有特别的的帮助。以下是一些例子,在回答将支付多少儿童津贴等为题上,就要分清多种情况:
---玛丽亚是单亲家庭,她又两个儿子 (鲍勃, 2岁,  彼得 15岁) ,从事兼职秘书工作 (每周工作20个小时).
---玛丽亚失业了,后来她找到一个每周工作10个小时的商店助手和每周工作5个小时的临时照顾幼儿的工作
---保罗和拉瑞(Lara)有一个17岁的女儿丽莎(Lisa),她是一个残疾人,还有一个儿子弗兰克,18岁,还在上大学。
即使是仅谈论一下这些场景,也有助于测试工作的开展,哪怕是软件中手工运行这些例子,也几乎可以确信会发现程序的遗漏,难道因为没有原型就不做这些吗?为什么不做呢?

关键字驱动(Keyword-driven)的测试

关 键字驱动的测试常用于模拟原型,FirNesse允许定义关键字驱动的测试类型(详见“完全的数据驱动自动化测试”(“Totally Data-Driven Automated Testing”))甚至在没有软件支持的情况下(不能自动运行),运行基于关键字驱动的测试也会很有好处。

image
图 3. ActionFixture 测试

图3 展示的是关键自驱动测试的示例。第一列描述的是来自FitNesse的关键字,第二列描述的是java类的方法(开发者根据这里的描述,在Java程序使 用这些名字来命名)。第三列描述的是来自第二列的方法所产生的数据。最后一行演示了测试失败的情形(测试通过为绿色)。正如你所看见的那样,找出错误是很 容易的。

这会使人很容易甚至很乐意去建立这些测试。测试者不必具备编程技能,经过简短介绍后,客户也会很容易的读懂。
用这种方法定义的测试,几乎就是业务需求,并具备传统测试用例没有的重要优势, 甚至不必去自动生成也有很多优势:
---测试内容很容易获得,不必特别训练就可以轻松完成。
---与其改变需求,到不如先改变测试,这样反而更直观(这与使用某些工具的情况完全不同)
---在确定新需求或修改需求时,可以随时运行测试来看看需要修正哪些内容。

为自动运行测试,需要建立一个简单的软件层,用于代表真实的测试代码。这些测试在自动化测试GUI操作时特别有用。我曾开发过一个基于HTTPUnit的自动测试web页

这里就是FitNesse自动运行的代码:
package stephanwiesner.javaworld;

import fit.ColumnFixture;

public class ChildAllowanceFixture extends ColumnFixture
{
   public void personButton() {
      System.out.println("pressing person button");
   }
   public void securityNumber(int number) {
      System.out.println("entering securityNumber " + number);
   }
   public int childAllowance() {
      System.out.println("calculating child allowance");
      return 190;  
   }
   [...]
  }


在FitNesse中的运行结果如图4所示,很有助于调试。与Junit相比,Junit抑制了调试信息的输出,但我认为在自动Web测试中这些输出绝对是有必要的。

image
图 4. 标准输出

在测试基于Web的应用时,错误也将被包含在FitNesse页面里,并被显示出来,这是的条施工作比使用log文件更容易。

数据驱动的测试

关 键字驱动的测试是GUI自动测试的最佳选择,同样,数据驱动测试也是各类计算类项的首选测试方式。如果你曾经写过单元测试,那么测试中什么是最令人厌倦的 呢?也许,你会认为是数据。你的测试要进行数据填充,但是数据经常改变,使维护工作变成了可怕的恶梦,测试不同的组合,需要不同的数据,这也许会使你的测 试工作变得日益复杂,变成了”丑陋的怪兽”。

使用数据驱动的测试,数据将从代码中分离。一般情况下,建立几种类型的表格,存储为CSV文件,由测试代码来读取。如果再使用FitNesse,我们就可以更便利地进行存储、改变,以及访问这些测试数据。

现 在,让我们把图2种的表格转变成一个测试吧。首先,将表格拷贝到Excel中,进行修改,然后使用FitNesse的EXCEL导入功能输入。这是一个有 用的功能,因为在FitNesse中,表越大越难以维护。输入的结果如图5所示。每一行表示一个测试,有问号的列表示需要写Java方法,没有问号的行表 示输入的测试数据。

image
图 5. 数据驱动测试表Data-driven test table.

再 重复一次,在阅读了业务需求后,这种测试就非常容易理解了。如果你曾经担心如何在你的测试用例里的进行等值测试的话,那就看看这里吧。最后一行大概是最重 要的,因为需求并没有说明如何处理非法数据,或者处理20岁以上的孩子但仍在上学等情况,这里的关键字“error”表明需要处理异常了。

常见问题解答

这里讨论使用FitNesse在需求、测试和开发过程中遇到的未解决的问题的解决办法,以下是部分答案。

手工测试会遇到什么问题?
手 工测试者最常做的就是重复的手工回归测试,不但代价昂贵,而且容易出错。自动化测试可以减少但不能消除这种工作的工作量。测试者可以有更多的时间去从事更 有趣的测试,例如在应用程序在复杂的场景下的不同处理等,尽管测试就是要花费更长的时间找到错误,但比不意味着因此而要付出更高的代价.

用户验收测试不再有必要了吗?
也许有人会这样认为。如果客户在需求中定义了所有的测试,当所有测试测试都变绿(通过了)时,软件就“完工”了。我的实践经验是,客户仍会提出附加的需求来,尽管如此,他们的需求变化会更少,并更容易地结合实际变化进行分类(同时也会带来追加的款项)。

在FitNesse书写比Word documents更有优势吗?
甚至在没用测试的情况下,使用wiki 来写业务需求也能带给你更多好处。文档可以自动用在Web 服务器上,可以被多人并发访问,可以全文检索,可以被链接。我发现还有以下两个特点:
---更容易建立数据字典,并链接上明细内容。更重要的是,具有查找被引用位置("where-used")的功能。 如果修改某个细目,就会立即找到是否被引用过,以避免发生冲突.
---由于Bugzilla (或者 Jira,二者均为缺陷跟踪系统,译者注) 是基于Web的, 我们可以方便地在两个系统之间互相链接文档(artifacts) (bugs, 任务, 讨论等),便于提高工作效率。

FitNesse 允许按照确定的顺序运行测试,而JUnit 禁止这样做,你有何体验?
使 用JUnit,需要对同一个对象分别建立许多setUp() 和tearDown() 方法. 在某些情况下,可以在一个FitNesse页面里放上所有的相关测试,并能够给定执行顺序. 因此,第一个测试可以建立一个”人”,下一个测试可以对它进行修改,最后一个可以进行删除操作.如果第一个测试失败了,而且那个”人”没有被建立,这该怎 么办?没问题,因为第一个测试失败了,随意在把它修正好之前,我是不会继续的. 我一般不会经常使用这种顺序执行方式,但有时它会给我带来很大方便.

FitNesse 可以用于大项目吗?
大 部分开发者会明白,FitNesse的优势就是简单易用。但没有技术背景的人,如果不使用自动检查特性以及所见即所得风格的编辑器,工作起来会很困难。一 个简单的例子就是FitNesse 只有允许使用三种类型的标题, 但现在我们需要五种,这就需要我们就可以对FitNesse 进行扩充.

FitNesse 不包含输出为类似PDF文件的功能。我曾经使用过长度达到60页的Word文档,尽管编辑起来有些困难,但是可以轻松打印出来. 但是当我把它转到FitNesse里后,我就难以打印了,因为被分解为几个不同的页面,最后不得不通过编程解决.

FitNesse 具有一个简单的机制可以显示那些文档被修改了,被谁修改了,但不能显示修改了什么, 权限管理有局限。使用FitNesse重开一个项目要比改写一个运行的项目更容易.对中小型项目而言,我极力推荐FitNesse. 大项目也可以使用,但是进行更仔细的评估后再实施会更好.

结论
我认为FitNesse是需求分析的好工具,对测试优先开发很有帮助,它是很有利用价值的。我被迫花费几个周来适应FitNesse及其处理过程,我的项目目前尚未完成,但是我已经看到了需求质量和软件质量均得到明显提高,而对测试者而言,工作起来会获得更多的乐趣 。

关于作者
Stephan Wiesner 是瑞士卢赛恩L&ouml;wenfels Partner AG的测试管理人员。他是“Learning Jakarta Struts 1.2”(伽利略出版社,德语版,ISBN:3898426130;英语版:ISBN: 190481154X)的作者,” Java der Code”(由德国MITP出版社发行,ISBN: 3826614623)。另外还在德文杂志上写过大量的与java相关的文章。他有着丰富的开发和测试经验,是一位ISTQB认证的测试管理员和认证的 Scrum主管(ScrumMaster,Scrum是一种敏捷开发方式,译者注)。

陈海青(joson),本文译者,生活在中国的山东省烟台市,先后从事软件开发、数据库管理、系统管理等工作,2001年获得高级程序员资格。

相关资源
---Matrix-Java和中间件社区:http://www.matrix.org.cn
---“完全的数据驱动自动测试”("Totally Data-Driven Automated Testing" ),作者Keith Zambelich ,详细阐述了数据驱动和关键字驱动的测试: http://www.sqa-test.com/w_paper1.html
---以下站点提供了更多的关于优化FitNesse的内容: http://fitnesse.testmanager.info
---官方的FitNesse网站,经常更新: http://fitnesse.org/
---下在完整的FitNesse发行版: http://fitnesse.org/FitNesse.DownLoad
---部分资料可以在译者的站点找到:http://www.chq.name/

作者的其他文章

其他相关文章

posted @ 2006-07-28 11:43 Alex 阅读(2949) | 评论 (0)编辑 收藏

key words: SOA

come from here

2004512据业内分析,面向服务的架构(SOA)的基本概念--重用性和互用性--已经提出了大约20年。那么SOA具有哪些新的特色呢?为什么其他技术和标准都惨遭失败,而SOA却能够成功呢?BEA的首席信息官Rhonda Hocker回答了有关SOA如何发挥IT潜力方面的问题。


问:您认为SOA的哪些方面在其成功中起到了至关重要的作用?

答:第一点就是灵活性。就长期以来在广大公司中的知名度而言,SOA可能名列IT架构第一,而内容一直在变化。一个SOA实质上就是一套松散耦合的服务。在必要的情况下,每一项服务都可以进行构造和替换,而相关的费用很低。松散耦合甚至还可以让架构适应一些改变,并不像传统的紧耦合架构表现得那么脆弱;在一个SOA中,您能够使用一种服务替换另一种服务,无需考虑下列技术:接口问题,它是否在Web服务和XML的通用标准中已经定义。这就是通过互用性所体现出来的灵活性。灵活性还表现为利用现有资产、遗留应用程序和数据库的能力,通过将他们扩展到SOA中,而非进行替换,使其成为整个企业解决方案的组成部分。最终结果就是具备快速高效发展的能力,换句话说,就是按照业务需求"有机地"进行适应。这就是真正的新特色。

第二点就是"业务相关性"SOA就是最终表现为对业务人员意义重大这一层面上的IT架构。如果您也相信IT架构的核心问题就是业务和IT专家的联盟和协作,那么这就是关键。今天的SOA服务能够完成映射为业务流程活动的各部分工作:例如,想起一个命名为"更新客户订单状态"的服务。这种服务与那些能够参与创造和使用这些服务定义新流程的业务分析人员密切相关,因而能够形成那种服务驱动型的企业。因为Web服务已将其大部分技术作了摘要,所以几乎不再需要技术说明。公司和IT业能够将关注的重点转移到业务逻辑和通讯上。他们最终共享"服务"的通用语言。也就是说,是真正的新特色,在IT架构的交付中具有深刻的蕴涵。

问:您认为什么是SOA成功的最大障碍呢?
答:SOA是新型IT架构的蓝图。由于总是伴随着大的变化发生,因此最大的障碍就是组织,而非技术。主要包括以下几个方面:

管理:共享服务是SOA方法的核心。这种快速组装应用或编排流程的能力是在一些现有的能被共享的服务的基础上实现的。共享资源需要进行管理。

开发文化:切换到SOA要求开发风格发生极大的变化。大多数开发人员仍然适应那种将每一个应用程序作为一个独立的问题解决的方式。目前可以重用的代码还非常少。在SOA中,开发人员需要编写自己的应用程序,同时还要留意要让自己编写的代码可以重用,不仅包括使用现有代码,还要包括计划在未来的应用程序中重用他们的代码。

业务流程架构技能:SOA方法让公司和IT合作伙伴能够在业务流程的创造中完成更高效率的协同工作。他们的成功将有赖于其实施业务流程架构的技能。也需要他们灵活应对业务流程并且要将自己看作是业务流程架构设计师。

这些方面的确都具有极大的挑战性,但是便于管理。最后,那些善于管理,IT和业务人员知道如何有效合作,流程和架构技能受到重视的公司将会从自身的SOA中得到更多的回报。这几乎是最好的方式,它有助于解决IT 问题。

问:现在的SOA与以前的集成/连通标准,如CORBA有哪些不同之处?

答:很好,我将会用案例说明以上我所描述的两个SOA有别于其他标准的优点。问题是为什么分布式架构中的CORBA和其他方法无法表现出这些优点呢?
高度概括性的回答就是:CORBASOA具有更大的技术难度,在其执行过程中需要强大的技能和知识支持。那些技能十分贫乏,如果没有真正的CORBA标准将会无济于事。而比较而言,SOA简单,基于真正通用的标准。这就确保了构造它们的这些技能是广泛可用的。

更为详细的回答请您查看他们的架构方法和原理的基本差别。

在一个SOA中,分布式资产就是"粗粒度"服务,它可以完成一些非常有用的功能,如"更新客户订单状态"。使用CORBA,分布式资产就是一些对象,每个对象都拥有自己的属性和方法。例如,订单对象具有"状态"属性和"更新"方法。这样对架构设计师而言是相当繁琐的,它需要具有很高水平的知识和技能。在这种细粒度级别之下很难保证一致性。而使用SOA,会控制少和动力少,却易于管理。这种方法在技术上并不是非常强大,但在IT成功方面的组织和人员角色上却体现出了相当的敏捷度。

问:为什么SOA会成功而CORBA会导致失败呢?

答:SOA会成功主要取决于合作伙伴的帮助。就重用效率或企业广泛一致性而言,由中心IT组织独立推动的架构都无法在长时间的运行中获得成功。我认为使用SOA,我们将会拥有第一个业务合作伙伴帮助推动企业架构的实例。这不是因为他们喜欢架构本身,而是因为他们的支持是基于SOA的业务相关性的,很快就可以从结果上看出,开发生命周期改变了它的重点,由原来较长的交付周期应用程序的交付转变成小单元代码--服务的交付和集成。持续的结果将使业务合作伙伴效忠于这种方法。

问:JavaSOA的潜在成功中起到了什么作用?

答:Java作为实现服务的最流行的编程标准,是非常重要的。Java社区的规模和技能保证大量高质量的技能可以用于构造SOA。这就是Java实现帮助SOA成功的方式。也就是说,Java只是实现服务的一种方式。没有一个大的IT组织会只运作一个单一的编程标准。有利的方面就是使用SOA您不需要一个单一的编程标准。服务范例的定义只能以Web服务和XML的接口标准识别出这种内在的多相性并设立需求。


posted @ 2006-07-28 11:29 Alex 阅读(1126) | 评论 (0)编辑 收藏

key words:soa
转自这里

最近,SOA成为跨技术平台(特别是J2EE和.Net)软件开发中的热门话题。然而,如果我们比较一下围绕着SOA的宣传和90年代后期EJB和服务件 的宣传,你会发现这没有什么区别。1998年,EJB带领互联网的潮流并推翻了以CORBA的统治和由PB/Oracle Forms和其他主导的CS架构标准。SOA,作为一种新技术的术语,还不具有那么大的破坏性。SOA只是一种想法/概念和一组构建应用功能的最佳实践。 相反地,J2EE是一套完整地开发技术,可以用来设计所有的东西。

  我对SOA的主要关注在于企业级Java应用通用的问题:复杂性。 次要关注的是SOA通常作为一种解决方案被用来跨越J2EE应用各层,虽然这好像没有什么意义。本文提取出SOA的基本元素并介绍他们。一旦我们理解这 些,就可以理解SOA系统中的更复杂的组件了。最后,我们可以了解一下SOA给J2EE应用带来的实际价值,同时并不增加无用的复杂性。
本文分为个部分:首先,提出了我对SOA作为一种标准参考点的定义。其次,检查那些主要的软件工种问题通过SOA可以解决而不是用SOA来检查。再次,会给出基于复杂需求的SOA的建议分类。最后,给出三种主要SOA分类的建议实现。

  SOA是什么?

  SOA有很多定义。下面是我的定义:
  SOA是宏级别的应用到应用架构级的设计模式:
  1、可选地暴露应用的功能作为一组离散的组件。
  2、使这些组件能被用来构建更复杂的组件和应用。
  3、仅包含基于消息的组件内部通讯。

  我还遗漏了什么呢?还有一些方面,包括:
  1、安全性
  2、事务
  3、状态或无状态会话
  4、消息无数据
  5、 消息特性
  6、 消息协议
  7、 消息内容
  8、  具体技术实现

  这些方面也是重要的,但不是主要的。我的定义提取了SOA的核心规则,但没有抛弃概念本身。
注意我在定义中引用了设计模式。我认为这是关键。SOA不是什么新技术,事实上,其最吸引人的一个地方是可以利用现有的技术并使其泛出新的光芒。对我来说,SOA更像是一幅蓝图,一组最佳实践,或者说是一个定义下一代的软件应用应该如何设计和实现的规范。

  基础SOA方法

  从上面的定义,我们应该可以标识出组成SOA应用的必须提供的软件服务的最小集合。简洁地说,这些服务是:

  1、消息层,允许消息通过特定的协议传输和接收。用SOA的说法,这一层称为企业服务母线或简写为ESB。
  2、一个组件模型,如应用必须遵循的发送和接收来消息母线的消息的最小约定。

  取决于你自己的业务需求,这两种服务可以极度的扩大,但在核心来说,消息层和通用组件模型就代表了SOA。

   注意,我没有在SOA的定义中包含自动定位和发现服务(在大部分JEE场景中,这是很有杀伤力的)。在UDDI(通用描述/发现/集成协议)后的原始想 法是认为企业最终会使用软件服务(通过一个大的基于元数据搜索服务仓库)来购买和销售。这个美梦至少也得十年后,也许永远不会实现,因为人们是需要做的实 际的业务而不是软件。

  JEE应用不需要自动发现服务,例如登录或支付服务,这些服务应该在初始化时设置。不要误导我,如果这些服务的实现不应该硬编码到应用中,那么你也不需要SOA来解决这些问题了。

  下一节,我们会来考虑一下究竟需要SOA来解决什么,或者他能替代什么。

  为什么要SOA?

   最近的两拨企业级软件开发的主浪潮是C/S架构和多层架构。虽然多层架构提供了C/S架构中布署/平台支持/性能/伸缩性上更好的效果,但两者都没有解 决一个关键的企业级计算机领域的软件工程问题:如何重用软件功能。作为软件开发人员和架构师,我们始终没有完全解决软件重用的问题。再往下看,你会看到我 也不认为SOA能解决这个问题。然而,我认为软件重用是SOA出现的最重要原因(至少在JEE应用中是这样)。

  其他SOA使用现有的Jini和风格计算。基于Jini环境的特点如下:
  1、自动发现组件/服务
  2、自愈的

   然而,这些特性并没有与JEE应用等同的重要性。使用JDBC配置数据库的位置只需要一次。我期望数据库来提供容错和除错功能,而且我不需要JEE应用 来尝试当产品实例当机时自动发现其他的数据库实例。另一方面,对一个有2000个工作站的办公室来说自动发现一个彩色打印机是一件好事,这也是符合 Jini硬件的一个关键好处。

  平等地主,在一个真实的全球网格计算环境中,自动发现和枚举计算资源来解决问题是基础框架的关键部分,但这不是一个JEE环境,那儿硬件预先计算的以便在定义用户数据和服务性能之间平衡。

  我的观点是,SOA对不同的需求需要不同对待。在本文中,我只关心JEE架构方面的SOA,而我认为这意味着功能重用。其他从JEE观点来看SOA的优点还有:
  1、松耦合的组件,这是软件设计中重要的部分
  2、引入ESB作为消息层意味着强制“面向接口编程,而不是实现”
  3、异步消息增加了应用的伸缩性

  让我们通过问三个特定的问题来看一下软件重用中更细节的问题:
  1、为什么重用软件是重要的?
  2、SOA是如何提出解决软件重用问题的?
  3、是否SOA的允诺能够使软件重用应用到现实中?

  首先,软件重用是重要的原因如下:
  1、时间和花费上的效率—能够重用已经的组件来满足陈述的业务需求将节省大量的时间和金钱。
  2、重要的特性包括但不限于如稳定性/性能/可管理性/文档/可配置性。因为一个组件被重用的次数越多,对这个组件的投资也越多,他的优势也越多。
  3、 良好设计的可重用框架无论在哪里被使用都拥有正面的效果,而且你愿意的话可以封装更好的想法来解决通用问题。

   因此我们需要重用性。那么最简单的方法是什么呢?就是打包软件作为一组良好定义的组件来满足离散的功能需求。然后,如果其他应用需要相同的组件,他就可 以重用了。还有些细节需要考虑,如如何配置,但这些细节已经偏离了主题:重用任何语言编写的代码,那些代码必须被设计成一组离散的组件或重构为集合。

  可以参考我在JavaWorld上的第一篇文章,“节省时间的框架”(2000.9),有更多细节善于JEE项目的软件重用。

   其次,SOA是如何解决软件重用的问题呢?是通过基于组件模型来构建和引入一个重要的强制约定:组件间的通讯要通过下发到ESB的消息来进行,而这就确 保了松耦合。实际上,最广泛布署的SOA实现—Web services可以通过使消息层技术中性来缝合用不同语言开发的组件。

  最 后,SOA对软件重用的允诺真有实际意义吗?不,我想念如果SOA在1945(大概是和ENIAC同时代吧)被发明的话确实可以解决软件重用的问题。但没 有,现存的大量代码是用不同的开发语言编写的,有COBOL/C/C++http://java.chinaitlab.com/C#和其他语言。这些代 码没有作为离散的组件来编写,因此也没有SOA魔法来解决。事实上,我认为有大量的SOA项目的工作是花费在重构相同的代码库。

  现在,让我们来看一下对于JEE应用SOA可以解决的一些问题。

  SOA缺点

  SOA缺点包括下面三方面:
  1、 SOA自身的缺点,主要当前还没有成熟的实现
  2、 SOA的复杂性
  3、  厂商对SOA在更广泛的JEE产品和方案中的位置

  那么我们就心批判的眼光来看一下:

  ·并没有像JEE规范那样有自己的正式规范。虽然有一个发布的规范,但那个太复杂了并且没有遵循80:20法则(80%的应用需要简单的SOA,只有20%的应用需要更强大而复杂的功能)
  ·有状态会话依然存在广泛争议而且现在还没有被SOA的缺省实现(Web services)所解决。而无状态会话已经是完全支持了。
  ·由于缺省正式或推荐的规范,Web services已经成为许多人眼里SOA的代名词了,但Web services通常是过于强大了。
  ·SOA增加了复杂性。可能你更喜欢硬编码和紧耦合,而不需要XML配置文件来运行简单的应用。
   ·SOA兼容的应用对本身来说没有什么意义。其商业价值来自于能够提供离散的功能块通过SOA被用于其他的应用和模块。例如,如果你对订单的较验规则是 通过JSP页面中的Java代码来实现的,那么你还需要重构代码将其放到服务端对象中以便于SOA调用—但很多厂商并没有提及这一点。
  ·在某些情况下,厂商将SOA作为网页应用框架的替代者!我认为,WAF是SOA定义功能中的消费者,只是作为一种补充,而不存在竟争关系。
  · 与厂商提供的相反,一些应用根本不需要SOA而只需要简单使用MVC框架就可以了。这很短视吗?我不这么认为,即使SOA的特性是需要的,在上面的情况下,最重要的部分是用来服务于企业服务总线的良好定义的业务逻辑层,而不是ESB自身。

  虽然我不认为SOA是一颗解决现有和新建应用中问题的银弹,便我相信SOA在他相应的位置上还是有其内在的价值的。现在让我们来看一下在应用中增加有效的SOA解决方案是如何提供体现其商业价值的。

  建议的SOA分类

   现在,你应该对我保持事物的简单性的热忱表示感激吧。但我本质上并不是简单论者,我是一个实用主义者。对软件项目来说,我认为实用主义是一方面要平衡项 目的商业和实际价值,另一方面是使用软件设计上的最佳实践。简单的说,就是在我们现有条件下构建我们所能创建的最好的系统。

  一个实用主义的好例子来自于民间的工程历史。在修铁路时常修木桥,而我们知道用铁桥会更好。当铁路公司的股东想使用铁路尽快开工而且初始投资要有限制时,他就是这是最好的工程方案了。是否听起来耳熟?同样的原则可以应用于软件工程。

  根据实用主义的精神,我建议将SOA分为三个级别:简单/中等/复杂,衡量标准是需要满足的业务需求。如果你需要简单的SOA,那么不要浪费时间和金钱在复杂的SOA上。

  级别1:简单的SOA

  样例实现:
  1、使用自己的POJO队列实现来发送和接收消息。
  2、带有MDB(消息驱动Bean)的JMS队列/主题作为消息的消费者。

  这里涵盖的关键SOA概念有:
  1、企业服务总线
  2、生产者/消费者的组件模型。

resized image


  Figure 1. Schematic illustrating the core components of the simple SOA. Click on thumbnail to view full-sized image.

  级别2:中等的SOA

  样例实现:
  1、带有MDB的JMS队列/主题作为消息的消费者,并附加其他特性如安全性/事务/JMS元数据属性等
  2、 Web services,例如Apache Axis

  这里涵盖的关键SOA概念在包含简单SOA外还有:
  1、用来增加健壮性和可靠性的错误/重试队列。
  2、引入XML作为消息的有效负载内容来代替序列化Java对象,从而支持其他技术如.Net

resized image


   Figure 2. Schematic illustrating the core components of the medium-complexity SOA. Click on thumbnail to view full-sized image.

  级别3:复杂的SOA

  样例实现:
  1、带有MDB的JMS队列/主题作为消息的消费者,并附加其他特性如安全性/事务/JMS元数据属性等
  2、Web services
  3、厂商/标准相关的SOA兼容工具包(如专门的金融服务)

  这里涵盖的关键SOA概念在包含中等SOA外还有:
  1、良好定义而且严格的组件模型(例如Java业务集成/服务组件架构及其他)
  2、增强的厂商支持,如可插拔的新生产者/消费者组件创建
  3、 详细枚举特定SOA实现上可用服务的组件注册表。

resized image


  Figure 3. Schematic illustrating the core components of the complex SOA. Click on thumbnail to view full-sized image.

  小结

   目前SOA是作为一种架构体现,也将会成为与C/S或多层架构一样存在。但是,他目前还是不够成熟而且只是作为厂商利用的工具。我对SOA的建议是,从 简单的做起并保持SOA尽可能的简单。不要将SOA与Web services等同起来,也不要强制使用SOA的设计模式在JEE应用的各层上,告别是网页层。

  那么我会为大多数JEE应用推荐哪 一个SOA实现呢?级别2上的SOA实现如带有MDB的JMS队列作为消费者,而POJO或无状态的会话Bean作为消息生产者。当然,如果你确信你需要 集成非Java应用,那么考虑一下Web services实现。还要考虑你现在采用的解决方案在以后要有足够的扩展空间。虽然预测多久通常都有争议的,但我还是建议最远不超过36个月。如果你预 见到那个时间段内有额外的SOA需求,那么现在就来构建吧。

  关于作者

  Humphrey Sheil是英国服务业企业级应用供应商CedaropenAccounts的首席技术架构师。特别擅长于集成领域。拥有爱尔兰都柏林大学的计算机科学硕士学位。点击这里进入他的博客。

  资源

  ·Jini技术,最早的SOA实现之一:http://www.jini.org
  ·JEE规范:http://java.sun.com/j2ee/download.html#platformspec
  ·学习.Net的的入门点:http://msdn2.microsoft.com/en-us/library/ms310245(en-us,MSDN.10).aspx
  ·http://www.uddi.org UDDI协议
  ·创建SOA的准备:http://weblog.infoworld.com/techwatch/archives/004644.html
   ·Java业务集成,用来为Java应用(特别指基于SOA的应用)定义组件模型的规范。这更正规些,因此允许厂商根据标准提供工具和框架以实现最终的 交互性。目前许多失败就是因为缺少这些支持:http://www.jcp.org/en/jsr/detail?id=208
  ·http://ws.apache.org/axis/ 开源的JEE网页服务实现- Apache Axis

  版权声明:任何获得Matrix授权的网站,转载时请务必保留以下作者信息和链接
  原文:http://www.javaworld.com/
  译文:http://www.matrix.org.cn/

posted @ 2006-07-28 11:03 Alex 阅读(330) | 评论 (0)编辑 收藏

key words: 表连接
内连接

select statement from table1 join table2 on table1.fieled1 = table2.field2

select statement from table1 ,table2 where table1.field1 = table2.field2


左连接

select .....table1 left outer join table2 on table1....= table2.field

select .... table1 ,table2 where table1.field1= table2.field(+)

合并查询
union:获得并集,并自动去掉重复行,并且会以第一列的结果进行排序
union all: 获得并集,但不去掉重复行,也不排序

子查询:
对于多行子查询,必须要用多行运算符(IN,NOT IN,EXISTS,NOT EXISTS,ALL,ANY)

posted @ 2006-07-27 21:21 Alex 阅读(937) | 评论 (0)编辑 收藏

key words: 数据库复制 
come from here

一、背景
  DB2 联合数据库是分布式数据库管理的特殊形式。在联合数据库系统中,可以通过一个 SQL 命令来发出对多个数据源的命令请求。DB2 与非 DB2 数据库之间进行复制之前,首先需要保证非 DB2 数据源可以被 DB2 ESE Version 8 federated database访问。对于DB2 Replication Version 8 所需的联合数据库功能可以在现有发布的 DB2 ESE Version 8 和 DB2 Connect Enterprise Edition Version 8 中提供。

  "SQL复制"又称为"DB2复制",是为 DB2 开发的两种数据复制类型中的一种,它是通过 SQL 进行的复制。在这里简单提一下,DB2 复制中的另一种"Q复制"是通过 Websphere MQ 消息队列进行的。在进行 SQL 复制时,Capture 程序读取 DB2 恢复日志以获取对指定源表的更改。该程序将更改保存到传输表格中,也称作变化数据表(changed data table),Apply 程序并行读取更改并应用于目标事务,见图1。




  图1:SQL复制的结构

  WebSphere II 全球信息集成复制,通过不同数据库之间的复制,有效的利用了数据资源,为提高效率提供了良好的平台。

  DB2 与非 DB2 数据库之间的复制需要用到 WebSphere II。本文力争通过复制实例让读者对不同数据库之间的复制有一个整体的概念。

二、动机

  商业上出于很多原因使用复制,可以归纳为:

分散:把数据分散到各个位置;
整合:把其他位置的数据联合起来;
交换:与其他位置进行双向的数据交换;
灵活应用:对上面提到的方式进行一些改变或者结合。

   联合 (Federated) 数据库系统的诞生,利用了现有的数据资源,把不同商业数据库软件的数据整合到一起,很大程度的提高了数据利用率。联合数据库可以用一个SQL语句对分布在 不同地点的多种数据源发出请求。联合数据库系统可以把本地表和远程数据源联接起来,就像数据都在本地一样,并且可以通过对数据源进行分布请求来提高数据源 处理能力,还可以通过在联合服务器处理部分分布请求来补充数据源的 SQL 限制。

  联合数据库具有两个与其他应用服务器不同的特点:

联合服务器可以被配置为接收全部或接收部分针对数据源的请求。联合服务器把这些请求分散到数据源。
与其他应用服务器一样,一个联合服务器用 DRDA 通信协议(例如 SNA 和 TCP/IP)与 DB2 家族实例通信。然而,与其他应用服务器不同的是,与非 DB2 家族实例通信时用其他协议。

  图2描述了联合数据库系统的设置流程:


  图2:联合数据库系统的设置流程


   WebSphere II 包括两种包装器(Wrapper),一种为关系型包装器,负责DB2 UDB, Informix, Oracle, Microsoft SQL Server, Sybase, ODBC, OLE DB 等数据的复制。另一种为非关系型包装器,负责 Flatfile, Excel, XML 等非关系型数据的复制。

  包装器定义了一个负责本地数据库与远程数据库通信的库。包装器执 行很多任务,比如:它可以连接到数据源,包装器应用了数据源的标准连接API。它还可以给数据源提交请求。联合数据库系统可以操作远程联合系统的表。远程 表在本地联合数据库中虚拟存在,客户应用程序可以操作这些虚拟表,但是它们真正存在于远端数据库中。每个远程虚拟数据库,把联合数据库当作数据库客户端, 他们只对数据库客户端的请求有回应。因此联合数据库需要下载各种远程数据库的客户端。

一个联合系统的构造,需要一个作为联合服务器的 DB2 实例,一个作为联合数据库的数据库,一个或多个数据源,和可以存取数据库和数据源的客户(用户和应用)。如果要完成远程不同数据库之间的复制,还需要应用DB2的数据复制功能。

IBM DB2 复制(在一些平台上被称为数据传播)是一个从一个位置到另一个位置复制 DB2 和/或其他数据库厂商数据的强大的,灵活的工具。IBM的复制支持数据转换,数据连接和过滤数据。可以在不同的平台之间搬运数据,也可以把数据分散到不同 的地点或从分散的地方把数据聚合到一个地方。可以在不同的系统之间交换数据。

  IBM复制由四个主要部分组成:管理 (Administrator),Capture,Apply,警报监视器 (Alert Monitor)。

  管理的部分主要通过复制中心的图形界面来实现。通过复制中心可以定义复制源,定义从数据源到目标数据的地图。它也用来管理和监控本地和远程的 Capture 和 Apply 进程。从图3中可以看出复制中心图形界面对其他几个部分的支持关系。


  图3:复制中心的应用


   在源数据服务器上运行的 Capture 程序可以获取 DB2 源数据表中的变化。DB2 的源数据服务器可以为 DB2 在 z/os, os/390 上的版本 6,7和8,也可以是 iseries 在 os/400 V5R2,或 DB2 在 Windows, Unix 系统中的版本 8。当定义数据源的时候会自动生成相应的触发器 (Triggers),可以用来捕获数据源的变化。要复制的数据可以在 Capture 进程中通过选择列来进行过滤。被捕获的更改信息首先存放到本地的源数据所在的数据库的表中并且当更改应用到目标数据中之后会自动删除。

   当对源表进行改动时,DB2 把相关的记录写入日志。这些日志服务于数据库发现和复制。Capture 程序通过数据库自动连接并获取日志记录。每个源表都有相应的 CD (change data) 表来获取数据的变化。当定义一个复制数据源时,复制中心自动生成 CD 表。

对于 Apply 部分,捕获的改变通过 Apply 程序应用到目标表中。Apply 程序可以在任何服务器上运行并且必须对所用到的源服务器和目标服务器都有连通性。数据可以通过列,行进行过滤,可以进行合并(例如通过视图),也可以在 Apply 过程中通过 SQL 表达式进行传送。DB2 与其他相关的数据间进行复制的时候,必须通过联合数据库系统来进行昵称的创建。在本地机器上需要安装关系型包装器和非关系型包装器。对于本例中 db2<->ORACLE之间的复制,需要安装关系型包装器。见图4。

  图4:进行远程复制关系图



  报警监视器用来进行对Capture和Apply部分的错误监控。





posted @ 2006-07-27 19:54 Alex 阅读(440) | 评论 (0)编辑 收藏

如何在Oracle里设置访问多个SQL Server数据库?假设我们要在ORACLE里同时能访问SQL Server里默认的pubs和Northwind两个数据库。

1、 在安装了ORACLE9i Standard Edition或者ORACLE9i Enterprise Edition的windows机器上(IP:192.168.0.2), 产品要选了透明网关(Oracle Transparent Gateway)里访问Microsoft SQL Server数据库

$ORACLE9I_HOME\tg4msql\admin下新写initpubs.ora和initnorthwind.ora配置文件.
initpubs.ora内容如下:
HS_FDS_CONNECT_INFO="SERVER=SQLSERVER_HOSTNMAE;DATABASE=pubs"
HS_DB_NAME=pubs
HS_FDS_TRACE_LEVEL=OFF
HS_FDS_RECOVERY_ACCOUNT=RECOVER
HS_FDS_RECOVERY_PWD=RECOVER
initnorthwind.ora内容如下:
HS_FDS_CONNECT_INFO="SERVER=sqlserver_hostname;DATABASE=Northwind"
HS_DB_NAME=Northwind
HS_FDS_TRACE_LEVEL=OFF
HS_FDS_RECOVERY_ACCOUNT=RECOVER
HS_FDS_RECOVERY_PWD=RECOVER

$ORACLE9I_HOME\network\admin 下listener.ora内容如下:
LISTENER =
(DESCRIPTION_LIST =
(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.0.2)(PORT = 1521))
)
)
)

SID_LIST_LISTENER =
(SID_LIST =
(SID_DESC =
(GLOBAL_DBNAME = test9)
(ORACLE_HOME = d:\oracle\ora92)
(SID_NAME = test9)
)
(SID_DESC=
(SID_NAME=pubs)
(ORACLE_HOME=d:\Oracle\Ora92)
(PROGRAM=tg4msql)
)
(SID_DESC=
(SID_NAME=northwind)
(ORACLE_HOME=d:\Oracle\Ora92)
(PROGRAM=tg4msql)
)
)


重启动这台做gateway的windows机器上(IP:192.168.0.2)TNSListener服务.

(凡是按此步骤新增可访问的SQL Server数据库时,TNSListener服务都要重启动)

2、ORACLE8I,ORACLE9I的服务器端配置tnsnames.ora, 添加下面的内容:

pubs =
(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.0.2)(PORT = 1521))
)
(CONNECT_DATA =
(SID = pubs)
)
(HS = pubs)
)

northwind =
(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.0.2)(PORT = 1521))
)
(CONNECT_DATA =
(SID = northwind)
)
(HS = northwind)
)
保存tnsnames.ora后,在命令行下
tnsping pubs
tnsping northwind


出现类似提示,即为成功

Attempting to contact (DESCRIPTION = (ADDRESS_LIST = 
(ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.0.2)
(PORT = 1521))) (CONNECT_DATA = (SID = pubs)) (HS = pubs))
OK(20毫秒)
Attempting to contact (DESCRIPTION = (ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.0.2)
(PORT = 1521))) (CONNECT_DATA = (SID = northwind)) (HS = northwind))
OK(20毫秒)


设置数据库参数global_names=false。

设置global_names=false不要求建立的数据库链接和目的数据库的全局名称一致。global_names=true则要求, 多少有些不方便。

oracle9i和oracle8i都可以在DBA用户下用SQL命令改变global_names参数

alter system set global_names=false;


建立公有的数据库链接:

create public database link pubs 
connect to testuser identified by testuser_pwd using 'pubs';
create public database link northwind
connect to testuser identified by testuser_pwd using 'northwind';
(假设SQL Server下pubs和northwind已有足够权限的用户登陆testuser,
密码为testuser_pwd)


访问SQL Server下数据库里的数据:

select * from stores@pubs;
...... ......
select * from region@northwind;
...... ......


3、使用时的注意事项

ORACLE通过访问SQL Server的数据库链接时,用select * 的时候字段名是用双引号引起来的。

例如:

create table stores as select * from stores@pubs;
select zip from stores;
ERROR 位于第 1 行:
ORA-00904: 无效列名
select "zip" from stores;
zip
-----
98056
92789
96745
98014
90019
89076


已选择6行。

用SQL Navigator或Toad看从SQL Server转移到ORACLE里的表的建表语句为:

CREATE TABLE stores
("stor_id" CHAR(4) NOT NULL,
"stor_name" VARCHAR2(40),
"stor_address" VARCHAR2(40),
"city" VARCHAR2(20),
"state" CHAR(2),
"zip" CHAR(5))
PCTFREE 10
PCTUSED 40
INITRANS 1
MAXTRANS 255
TABLESPACE users
STORAGE (
INITIAL 131072
NEXT 131072
PCTINCREASE 0
MINEXTENTS 1
MAXEXTENTS 2147483645
)
/


总结:

WINDOWS下ORACLE9i网关服务器在$ORACLE9I_HOME\tg4msql\admin目录下的initsqlserver_databaseid.ora

WINDOWS下ORACLE9i网关服务器listener.ora里面

(SID_DESC=
(SID_NAME=sqlserver_databaseid)
(ORACLE_HOME=d:\Oracle\Ora92)
(PROGRAM=tg4msql)
)
UNIX或WINDOWS下ORACLE8I,ORACLE9I服务器tnsnames.ora里面
northwind =
(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.0.2)(PORT = 1521))
)
(CONNECT_DATA =
(SID = sqlserver_databaseid)
)
(HS = sqlserver_databaseid)
)


sqlserver_databaseid一致才行.
posted @ 2006-07-27 19:49 Alex 阅读(327) | 评论 (0)编辑 收藏

key words:自动脚本 代理脚本 代理 冲出窗子

本文献给那些对自动代理脚本有兴趣、想自己写的朋友。

1、什么是代理脚本(PAC)
 一个PAC文件其实就是一个文本文件,最简单的格式就是包含一个叫FindProxyForURL的
 JScript函数,IE通过传入两个变量来调用这个函数,一个是用户浏览的地址URL全路经,
 一个是这个URL中的主机名部分(host)。这个FindProxyForURL函数有三种可能的字符串
 返回值,一是"DIRECT",就是直接连接,不通过代理;二是"PROXY proxyaddr:port",
 其中proxyaddr和port分别是代理的地址和代理的端口;三是"SOCKS socksaddr:port",
 其中socksaddr和port分别是socks代理的地址和端口,一个自动代理文件可以是多个
 选择的组合,其中用分号(;)隔开,如:
   
   function FindProxyForURL(url,host)
   {
     if (host == "www.mydomain.com")
         return "DIRECT";
 
         return "PROXY myproxy:80;
                 PROXY myotherproxy:8080; 
                 DIRECT";
   }
   
   
2、下面是代理脚本可能用到的函数和说明:
PAC Helper Functions

dnsDomainIs(host, domain)              Returns true if the host is part of the
                                       specified domain, false otherwise.
 
isInNet(hostname,                      Resolves the hostname and subnet IP,
                                       subnet mask) returns true if the
                                       hostname is within the subnet specified
                                       by the IP address and the subnet mask,
                                       false otherwise.
 
isPlainHostName(host)                  Returns true if there are no dots in the
                                       hostname, false otherwise.
 
isResolvable(host)                     Internet Explorer tries to resolve the
                                       hostname through DNS and returns true if
                                       successful, false otherwise.
 
localHostOrDomainIs                    Returns true if the host matches (host,
                                       domain) the host portion of the domain,
                                       or if the host matches the host and
                                       domain portions of the domain, false
                                       otherwise. (Executed only for URLs in
                                       the local domain.)
 
dnsDomainLevels(host)                  Returns the number of dots in the
                                       hostname.
 
dnsResolve(host)                       Returns a string containing the IP
                                       address of the specified host.
 
myIPAddress( )                         Returns a string containing the local
                                       machine’s IP address.
 
shExpMatch(url, shexp)                 Returns true if the supplied URL matches
                                       the specified shell expression, false
                                       otherwise. 
 
dateRange(parmList)                    Returns true if the current date falls
                                       within the dates specified in parmList,
                                       false otherwise. 
 
timeRange(parmList)                    Returns true if the current time falls
                                       within the times specified in parmList,
                                       false otherwise. 
 
weekdayRange(parmList)                 Returns true if today is within the days
                                       of the week specified in parmList, false
                                       otherwise. 

3、下面是各个函数应用的例子:
  a、isPlainHostName(host),本例演示判断是否为本地主机,如http://myservername/
  的方式访问,如果是直接连接,否则使用代理
  function FindProxyForURL(url, host)
  {
    if (isPlainHostName(host))
      return "DIRECT";
    else
      return "PROXY proxy:80";
  }
  
  b、dnsDomainIs(host, "")、localHostOrDomainIs(host, ""),本例演示判断访问主机
  是否属于某个域和某个域名,如果属于.company.com域的主机名,而域名不是
  www.company.com和home.company.com的直接连接,否则使用代理访问。
  function FindProxyForURL(url, host)
  {
    if ((isPlainHostName(host) ||
       dnsDomainIs(host, ".company.com")) &&
      !localHostOrDomainIs(host, "www.company.com") &&
      !localHostOrDomainIs(host, "home.company.com"))

      return "DIRECT";
    else
      return "PROXY proxy:80";
  }
  
  c、isResolvable(host),本例演示主机名能否被dns服务器解析,如果能直接访问,否
  则就通过代理访问。
  function FindProxyForURL(url, host)
  {
    if (isResolvable(host))
      return "DIRECT";
    else
      return "PROXY proxy:80";
  }
  
  d、isInNet(host, "", ""),本例演示访问IP是否在某个子网内,如果是就直接访问,
  否则就通过代理,例子演示访问清华IP段的主页不用代理。
  function FindProxyForURL(url, host)
  {
    if (isInNet(host, "166.111.0.0", "255.255.0.0"))
      return "DIRECT";
    else
      return "PROXY proxy:80";
  }
  
 e、shExpMatch(host, ""),本例演示根据主机域名来改变连接类型,本地主机、*.edu、
  *.com分别用不同的连接方式。
  function FindProxyForURL(url, host)
  {
    if (isPlainHostName(host))
      return "DIRECT";
    else if (shExpMatch(host, "*.com"))
      return "PROXY comproxy:80";
    else if (shExpMatch(host, "*.edu"))
      return "PROXY eduproxy:80";
    else
      return "PROXY proxy:80";
  }
  
 f、url.substring(),本例演示根据不同的协议来选择不同的代理,http、https、ftp、
  gopher分别使用不同的代理。
  function FindProxyForURL(url, host)
  {
      if (url.substring(0, 5) == "http:") {
        return "PROXY proxy:80";
      }
      else if (url.substring(0, 4) == "ftp:") {
        return "PROXY fproxy:80";
      }
      else if (url.substring(0, 7) == "gopher:") {
        return "PROXY gproxy";
      }
      else if (url.substring(0, 6) == "https:") {
        return "PROXY secproxy:8080";
      }
      else {
        return "DIRECT";
      }
  }
  
  g、dnsResolve(host),本例演示判断访问主机是否某个IP,如果是就使用代理,否则直
  接连接。
  unction FindProxyForURL(url, host)
  {
      if (dnsResolve(host) == "166.111.8.237") {
        return "PROXY secproxy:8080";
      }
      else {
        return "PROXY proxy:80";
      }
  }
  
  h、myIpAddress(),本例演示判断本地IP是否某个IP,如果是就使用代理,否则直接使
  用连接。
  function FindProxyForURL(url, host)
  {
      if (myIpAddress() == "166.111.8.238") { 
        return "PROXY proxy:80";
      }
      else {
        return "DIRECT";
      }
  }
  
  i、dnsDomainLevels(host),本例演示访问主机的域名级数是几级,就是域名有几个点
  如果域名中有点,就通过代理访问,否则直接连接。
  function FindProxyForURL(url, host)
  {
      if (dnsDomainLevels(host) > 0) { // if number of dots in host > 0
        return "PROXY proxy:80";
      }
        return "DIRECT";
  }
  
  j、weekdayRange(),本例演示当前日期的范围来改变使用代理,如果是GMT时间周三
  到周六,使用代理连接,否则直接连接。
  function FindProxyForURL(url, host)
  {
    if(weekdayRange("WED", "SAT", "GMT")) 
     return "PROXY proxy:80";
    else 
     return "DIRECT";
  }
  
  k、最后一个例子是演示随机使用代理,这样可以好好利用代理服务器。
 function FindProxyForURL(url,host)
 {
      return randomProxy();
 }
 
 function randomProxy()
 {
     switch( Math.floor( Math.random() * 5 ) )
     {
         case 0:
             return "PROXY proxy1:80";
             break;
         case 1:
             return "PROXY proxy2:80"; 
             break;
         case 2:
             return "PROXY proxy3:80";
             break;
         case 3:
             return "PROXY proxy4:80";
             break;
         case 4:
             return "PROXY proxy5:80";
             break;
     }    
 }
 
利用上面的函数和例子说明,大家就可以写出比较复杂有效的自动代理脚本。
posted @ 2006-07-21 14:57 Alex 阅读(963) | 评论 (0)编辑 收藏

key words:js捕捉浏览器退出 关闭浏览器 刷新浏览器

有时在用户session管理时,需要跟踪会话状态,比如当前在线用户,如果用户非正常退出,需要知道。

用js控制浏览器的关闭是一个辅助的手段,当然也可以设置session的有效期,不过不够及时。

js代码如下:

<html>
<script language="javascript"> 
<!-- 
var s="close"
function window.onunload(){ 
    
if(s=="fresh"
        
if(window.screenLeft>10000){alert('¹Ø±Õ');}
        
else{alert('Ë¢ÐÂ');} 
    
else alert('¹Ø±Õ'); 

function window.onbeforeunload()

    s
="fresh"

--> 
</script>
</html>

posted @ 2006-07-20 10:50 Alex 阅读(1204) | 评论 (0)编辑 收藏

key words:20% 80*法则

一个人在人力市场的价值,是从两种截然不同的因素衍生出来的──「潜力价值」以及「经验价值」。我们基本上是带着满满一袋潜力和空空一袋经验开始职业生涯,诀窍就在于,在潜力袋被掏空之前,先把经验袋装满。


你现在正处在事业曲线的哪个位置?你的价值是属于潜力型,还是经验型?
回答之前,你必须了解到,你的价值会随着事业进展不断改变,从潜力价值转到经验价值,再转回潜力价值。事业成功的第 1 个法则,就是建立并管理你的潜力与经验价值,在事业生涯的每个阶段为自己创造最大价值。

事业生涯 3 阶段

事实上,在你的职业生涯中,不管上升速度如何,不论你所处的职业、产业或是地理位置,你都可能经历 3 个不同的阶段:潜力阶段、动能阶段与收割阶段(见图)。

图中那条稳定向上攀升的事业线就表示成功的事业。成功人士和泛泛之辈通常就在事业生涯的动能阶段分道扬镳

走对第 1 步,非凡主管的黄金 5 年:毕业后~ 30

潜力阶段是指正式教育结束到 30 岁左右这个阶段,你的已知价值主要取决于潜力。在这个时期,如果加入绩优公司、接触各种不同的经验、找到自己想做的事情、发现自己擅长的工作,对于形塑未来的事业将有重大影响。我们建议,年轻上班族在这个阶段最好早一点到绩优公司历练。

在这个阶段,你的报酬相对较低,你和其它同僚的工作经验差别也相去无几。但是,这个阶段的确很重要,是导正事业方向最好的时机。根据我们调查,成功非凡的高阶主管,认为他们职业生涯最初 5 年很成功的比率,是一般职员的两倍。

在潜力阶段必须完成两项目标:至少到一家备受推崇的公司或机构工作,取得背书;同时必须更了解自己,引导自己走向一家能够在未来几年发挥个人优势与兴趣的企业。完成这两个目标,未来表现将会维持一定水平,也能让你找到一个未来几年都乐在工作的环境。

被挖角的黄金期: 38 40

事业起步后的 5 7 年,你经历了一个个不同职位,也换过几家公司,慢慢累积了经验;此时,你的个人价值,是以技能及经历的不同处境为基础。当你能够驾驭专业技能、不断写下成功纪录、承担更广的责任、管理其它人,并且培养事业关系的网络,你的潜力价值就稳定地转为经验价值。人通常是在 35 岁左右,进入事业的动能阶段。

这个阶段是专业生涯往上走或往下滑的关键,动能阶段也是工作人将经验价值发挥极致的阶段。

你 的潜力价值并没有消失,毕竟未来的路还很长。但是到了这个时点,你已经累积足够经验,可以往组织高层职位突破,或是引起其它公司注意。这个阶段,最容易根 据过往战功升迁。因为你有成功纪录可考,也蓄积了足够技能与经验,足堪承担更大责任。这个阶段的主管,通常是猎人头公司捕猎的对象,有兴趣挖角的公司也最 多。我们的研究显示,被挖角的黄金高峰期,平均在 38 岁到 40 岁之间。

如 果好好管理这个阶段,通常会有很多选择。你的表现大家有目共睹,尤其是你能直通消费者、客户与供货商,并且有机会在产业研讨会上演讲。相反的,没有好好把 握这个阶段的人,就会看到事业停滞不前。如果你无法发挥本身优势与热情,或是创造影响力吸引最重要机会,就错失了动能阶段的大好良机。因为在这个阶段,是 你和其它同辈展开不同事业轨道的重要分水岭。

收割期:事业倒吃甘蔗

工作了 25 年或 30 年,来到 45 50 岁 时,多数人也进入了事业的收割阶段。每个人的事业轨道在动能阶段的中期开始分岔,成功的高阶主管不断被赋予最重要职务与责任,经验较浅、但才华洋溢的经理 人,在这段时期还需要多加磨练。有些人的事业会继续进展,有些人则已达高原。相对而言,在动能阶段,高阶主管还有相当大的进步空间。毕竟,在这个阶段,大 多数的工作人还是处在受雇的状态。

事业最明显的差异就出现在收割阶段,在这个阶段,事业成功的高阶主管有许多工作机会供其挑选:经营公司、加入董事会、联盟投资银行、当顾问、写书或发表文章、上电视、演讲、领导非营利的计划等。

为什么有些人可以平步青云、进入成熟阶段呢?

80
20 法则

成功事业的第 2 个法则,就是善用 80 20 表现法则,让你的老板记得你最擅长的 20 %。成功运用这项法则,你就能赢得职业生涯中的大赛。

2002
年,阿姆斯特朗( Lance Armstrong )连续第 4 度赢得环法自行车大赛( Tour de France )的冠军。他决定除了一、两场特别的赛事外,不再在美国出赛。

为什么?对世界第一流的运动员而言,阿姆斯特朗的决定并不意外,因为他们把有限的精力留给最重要的比赛。高尔夫球迷绝对希望老虎伍兹( Tiger Woods )参加每一场比赛,电视制作人更乐见此事。但是伍兹一年参加的赛事比其它职业选手,或是之前的球王都要少。他的目光只放在高球 4 大赛。他并未因此道歉,反而被视为传奇。

多数人都知道 80 20 法则,这是意大利经济学家帕雷图( Vilfredo Paredo )所提出的理论。前 20 %最有价值的客户,贡献 80 %的利润。伍兹和阿姆斯特朗都决定专注最重要的比赛,因为这些比赛会带来最大的效益。我们得到一个简单有力的教训:做对的事,比你做多少事重要得多。这条法则同样适用你的职业生涯。

但不像职业运动员,工作生涯中所要完成的任务和目标,并非总是充满荣耀。

事实上,我们的工作中有 80 %的内容,是单调狭隘、一成不变的,实在很难靠这些事让自己与众不同。但是,我们要与伍兹和阿姆斯特朗同行,就必须赢得关键战役,才能真正与众不同。

赢得重要比赛

要更了解这个观念,我们可以反过来,先看看失败的例子。有一个失意的专家马丁,他因为没有应用 80 20 法则,使自己的职场生涯陷入平庸的困境。

和数百万的企业人一样,在 2001 2002 年间,马丁遭遇了几次挫败。最初,他将责任归咎于经济不景气。但是,随着时间过去,他愈来愈觉得自己的失败,也许是某些行为的结果。

毕业后,马丁进入一家大型制造公司当会计,但很快就升到中阶管理职位。他总是准时结帐,面对询问,也都能立刻拿出正确的财务报表。但是慢慢的,马丁觉得公司一些重大的策略决定,好像愈来愈不需要他,渐渐消失在升迁名单中。

马丁上班、做该做的事,是个扎实的员工。但从某种角度看,也不会有太大的突破。他和某些运动员一样,参加所有比赛,但从未赢过重要大赛。马丁工作努力但和别人没什么不同,最后,变成公司的消耗品。

相较下,本书共同作者瑞克.史密斯( Rick Smith )就掌握了 80 20 法则。他早期曾是知名软件服务商 EDS 的市场分析师。负责搜集、分析市场信息,提供给 EDS 的高阶经理人。

做「多」不如做「精」

有一天,他发现一份档案,内容是 EDS 过去 15 年来所有重要案子的财务表现,每一笔都写得非常详细。他很好奇,不知大案子的利润是否比小案子高。他快速分析,发现在许多方面都有很大的差异。他兴奋不已,于是在办公室待到午夜,思考如何运用这些分析结果。

隔天,他对主管做了简报,并提议:「如果我们不只帮公司赢得更多案子,而是赢得对的案子呢?」一个星期内,他的报告被送到执行长办公室。

从此,事情开始不一样了。瑞克除了做日常的市场研究外,他还被要求对公司的事业做更深入的分析。有 1 年半的时间,瑞克的分析报告都会交给高层主管,使他比别人有更多机会,让研究成果发挥最大影响,最后导致公司大规模地改变市场策略。

在这段期间,瑞克主要的工作还是市场分析,但他重新归类自己的例行公事,想办法挤出额外的时间来做企业绩效分析。

他一方面训练自己的小组成员,逐步将非关键性的工作交给他们。另一方面与信息部合作,将某些研究报告的产出流程自动化,以挤出珍贵的时间用在他认为能增加价值的事上。

事实上,他已经重新定义自己的角色,从「协助赢得生意」到「协助赢得对的生意」。因为这样,瑞克常受邀参与许多超越他权责范围、但有助于事业开展的活动,得到别人没有的机会和经验。

80
20 法则的重点在于,清楚什么该做与什么不该做。卓越的经理人正是懂得把这 20 %放在最前面。瑞克善用此一法则,最后让他发挥正面影响力。
posted @ 2006-07-20 09:41 Alex 阅读(360) | 评论 (0)编辑 收藏

key words: oracle优化 优化oracle  表设计优化 powerdesigner


前言

绝 大多数的Oracle数据库性能问题都是由于数据库设计不合理造成的,只有少部分问题根植于Database Buffer、Share Pool、Redo Log Buffer等内存模块配置不合理,I/O争用,CPU争用等DBA职责范围上。所以除非是面对一个业已完成不可变更的系统,否则我们不应过多地将关注点 投向内存、I/O、CPU等性能调整项目上,而应关注数据库表本身的设计是否合理,库表设计的合理性才是程序性能的真正执牛耳者。

合理的数据库设计需要考虑以下的方面:

·业务数据以何种方式表达。如一个员工有多个Email,你可以在T_EMPLOYEE表中建立多个Email字段如email_1、email_2、 email_3,也可以创建一个T_EMAIL子表来存储,甚至可以用逗号分隔开多个Email地址存放在一个字段中。

·数据以何种方式物理存储。如大表的分区,表空间的合理设计等。

·如何建立合理的数据表索引。表索引几乎是提高数据表查询性能最有效的方法,Oracle拥有类型丰富的数据表索引类型,如何取舍选择显得特别重要。

本 文我们将目光主要聚焦于数据表的索引上,同时也将提及其他两点的内容。通过对一个简单的库表设计实例的分析引出设计中的不足,并逐一改正。考虑到手工编写 库表的SQL脚本原始且低效,我们将用目前最流行的库表设计工具PowerDesigner 10来讲述表设计的过程,所以在本文中你还会了解到一些相关的PowerDesigner的使用技巧。

一个简单的例子

某个开发人员着手设计一个订单的系统,这个系统中有两个主要的业务表,分别是订单基本信息表和订单条目表,这两张表具有主从关系的表,其中T_ORDER是订单主表,而T_ORDER_ITEM是订单条目表。数据库设计人员的设计成果如图 1所示:

合理设计优化Oracle库表设计的若干方法

图 1 订单主从表


ORDER_ID 是订单号,为T_ORDER的主键,通过名为SEQ_ORDER_ID的序列产生键值,而ITEM_ID是T_ORDER_ITEM表的主键,通过名为 SEQ_ORDER_ITEM的序列产生键值,T_ORDER_ITEM通过ORDER_ID外键关联到T_ORDER表。

需求文档指出订单记录将通过以下两种方式来查询数据:

·CLIENT + ORDER_DATE+IS_SHPPED:根据"客户+订货日期+是否发货"条件查询订单及订单条目。

·ORDER_DATE+IS_SHIPPED:根据"订货日期+是否发货"条件查询订单及订单条目。

数 据库设计人员根据这个要求,在T_ORDER表的CLIENT、 ORDER_DATE及IS_SHPPED三字段上建立了一个复合索引IDX_ORDER_COMPOSITE;在T_ORDER_ITEM为外键 ORDER_ID建立IDX_ORDER_ITEM_ORDER_ID索引。

让我们看一下该份设计的最终SQL脚本:

/*订单表*/

create table T_ORDER (

ORDER_ID NUMBER(10) not null,

ADDRESS VARCHAR2(100),

CLIENT VARCHAR2(60),

ORDER_DATE CHAR(8),

IS_SHIPPED CHAR(1),

constraint PK_T_ORDER primary key (ORDER_ID)

);

create index IDX_CLIENT on T_ORDER (

 CLIENT ASC,

 ORDER_DATE ASC,

 IS_SHIPPED ASC);

/*订单条目子表*/

create table T_ORDER_ITEM (

 ITEM_ID NUMBER(10) not null,

 ORDER_ID NUMBER(10),

 ITEM VARCHAR2(20),

 COUNT NUMBER(10),

 constraint PK_T_ORDER_ITEM primary key (ITEM_ID)

);

create index IDX_ORDER_ITEM_ORDER_ID on T_ORDER_ITEM (

 ORDER_ID ASC);

 alter table T_ORDER_ITEM add constraint FK_T_ORDER__REFERENCE_T_ORDER foreign key (ORDER_ID) references T_ORDER (ORDER_ID);



我们承认在ER关系上,这份设计并不存在的缺陷,但却存在以下有待优化的地方:

·没有将表数据和索引数据存储到不同的表空间中,而不加区别地将它们存储到同一表空间里。这样,不但会造成I/O竞争,也为数据库的维护工作带来不便。

·ORACLE会自动为表的主键列创建一个普通B-Tree索引,但由于这两张表的主键值都通过序列提供,具有严格的顺序性(升序或降序),此时手工为其指定一个反键索引(reverse key index)将更加合理。

· 在子表T_ORDER_ITEM外键列ORDER_ID上建立的IDX_ORDER_ITEM_ORDER_ID的普通B-Tree索引非常适合设置为压 缩型索引,即建立一个压缩型的B-Tree索引。因为一份订单会对应多个订单条目,这就意味着T_ORDER_ITEM表存在许多同值的 ORDER_ID列值,通过将其索引指定为压缩型的B-Tree索引,不但可以减少IDX_ORDER_ITEM_ORDER_ID所需的存储空间,还将 提高表操作的性能。

·企图仅通过建立一个包含3字段IDX_ORDER_COMPOSITE复合索引满足如前所述的两种查询条件方式的索引是有问题的,事实上使用ORDER_DATE+IS_SHIPPED复合条件的查询将利用不到IDX_ORDER_COMPOSITE索引。

 优化设计

1、将表数据和索引数据分开表空间存储

1.1 表数据和索引为何需要使用独立的表空间

Oracle 强烈建立,任何一个应用程序的库表至少需要创建两个表空间,其中之一用于存储表数据,而另一个用于存储表索引数据。因为如果将表数据和索引数据放在一起, 表数据的I/O操作和索引的I/O操作将产生影响系统性能的I/O竞争,降低系统的响应效率。将表数据和索引数据存放在不同的表空间中(如一个为 APP_DATA,另一个为APP_IDX),并在物理层面将这两个表空间的数据文件放在不同的物理磁盘上,就可以避免这种竞争了。

拥有独立的表空间,就意味着可以独立地为表数据和索引数据提供独立的物理存储参数,而不会发生相互影响,毕竟表数据和索引数据拥有不同的特性,而这些特性又直接影响了物理存储参数的设定。

此外,表数据和索引数据独立存储,还会带来数据管理和维护上的方面。如你在迁移一个业务数据库时,为了降低数据大小,可以只迁出表数据的表空间,在目标数据库中通过重建索引的方式就可以生成索引数据了。

1.2 表数据和索引使用不同表空间的SQL语法

指定表数据及索引数据存储表空间语句最简单的形式如下。

将表数据存储在APP_DATA表空间里:

create table T_ORDER ( ORDER_ID NUMBER(10) not null, …)tablespace APP_DATA;

将索引数据存储在APP_IDX表空间里:

create index IDX_ORDER_ITEM_ORDER_ID on T_ORDER_ITEM ( ORDER_ID ASC)tablespace APP_IDX;

1.3 PowerDesigner中如何操作

1) 首先,必须创建两个表空间。通过Model->Tablespace...在List of Tablespaces中创建两个表空间:

合理设计优化Oracle库表设计的若干方法(2)

图 2 创建表空间



2) 为每张表指定表数据存储的表空间。在设计区中双击表,打开Table Properties设计窗口,切换到options 页,按图 3所示指定表数据的存储表空间。

合理设计优化Oracle库表设计的若干方法(2)

图 3 指定表数据的存储表空间



3) 为每个索引指定索引数据的存储表空间。在Table Properties中切换到Indexes页,在这里列出了表的所有索引,双击需设置表空间的索引,在弹出的Index Properties窗口中切换到Options页,按如下方式指定索引的存储表空间。



合理设计优化Oracle库表设计的若干方法(2)

图 4 指定索引数据的存储表空间



将表空间的问题延展一下:一个应用系统库表的表空间可以进行更精细的划分。

首先,如果表中存在LOB类型的字段,有为其指定一个特定的表空间,因为LOB类型的数据在物理存储结构的管理上和一般数据的策略有很大的不同,将其放在一个独立的表空间中,就可方便地设置其物理存储参数了。

其 次,需要考虑库表数据的DML操作特性:根据DML(INSERT,UPDATE,DELETE)操作频繁程度,将几乎不发生任何DML操作的数据放在独 立的表空间中,因为极少DML操作的表可设置符合其特性的物理参数:如PCTFREE可置为0,其BUFFER_POOL指定为KEEP,以便将数据缓存 在KEEP数据缓存区中等等,不一而足。

此外,还可以考虑按业务需要将不同的业务模块分开存放,这主要是考虑到备份问题。假设我们有一部分业务数据重要性很强,而其他的业务数据重要性相对较弱,这样就可以将两者分开存储,以便设置不同的备份策略。

当然,无节制的细化表空间也将带来管理上和部署上的复杂,根据业务需求合理地规划表空间以达到管理和性能上的最佳往往需要更多的权衡。

 2、显式为主键列建立反向键索引

2.1 反向键索引的原理和用途

我 们知道Oracle会自动为表的主键列建立索引,这个默认的索引是普通的B-Tree索引。对于主键值是按顺序(递增或递减)加入的情况,默认的B- Tree索引并不理想。这是因为如果索引列的值具有严格顺序时,随着数据行的插入,索引树的层级增长很快。搜索索引发生的I/O读写次数和索引树的层级数 成正比,也就是说,一棵具有5个层级的B-Tree索引,在最终读取到索引数据时最多可能发生多达5次I/O操作。因而,减少索引的层级数是索引性能调整 的一个重要方法。

如果索引列的数据以严格的有序的方式插入,那么B-Tree索引树将变成一棵不对称的"歪树",如图 5所示:

合理设计优化Oracle库表设计的若干方法(3)

图 5不对称的B-Tree索引


而如果索引列的数据以随机值的方式插入,我们将得到一棵趋向对称的索引树,如图 6所示:

合理设计优化Oracle库表设计的若干方法(3)

图 6对称的B-Tree索引


比较图 5和图 6,在图 5中搜索到A块需要进行5次I/O操作,而图 6仅需要3次I/O操作。

既 然索引列数据从序列中获取,其有序性无法规避,但在建立索引时,Oracle允许对索引列的值进行反向,即预先对列值进行比特位的反向,如 1000,10001,10011,10111,1100经过反向后的值将是0001,1001,1101,0011。显然经过位反向处理的有序数据变得 比较随机了,这样所得到的索引树就比较对称,从而提高表的查询性能。

但反向键索引也有它局限性:如果在WHERE语句中,需要对索引列的 值进行范围性的搜索,如BETWEEN、<、>等,其反向键索引无法使用,此时,Oracle将执行全表扫描;只有对反向键索引列进行 <> 和 = 的比较操作时,其反向键索引才会得到使用。

2.2 反向键索引的SQL语句

回到我们上面的例子,由于T_ORDER和T_ORDER_ITEM的主键值来源于序列,主键值是有严格顺序的,所以我们应该摒弃默认的Oracle所提供的索引,而采取显式为主键指定一个反向键索引的方式。

ORDER_ID为T_ORDER表的主键,主键名为PK_ORDER,我们为ORDER_ID列上建立一个反向键索引IDX_ORDER_ID,并使PK_ORDER_ID使用这个索引,其SQL语句如下:

create table T_ORDER (

 ORDER_ID NUMBER(10) not null,

 CLIENT VARCHAR2(60),

 ADDRESS VARCHAR2(100),

 ORDER_DATE CHAR(8));

create unique index IDX_ORDER_ID on T_ORDER ( ORDER_ID ASC) reverse;alter table T_ORDER add constraint PK_ORDER primary key (ORDER_ID) using index IDX_ORDER_ID;


要保证创建IDX_ORDER_ID的SQL语句在创建PK_ORDER主键的SQL语句之前,因为主键需要引用到这个反向键索引。

由于主键列的数据是唯一的,所以为IDX_ORDER_ID加上unique限定,使其成为唯一型的索引。

2.3 PowerdDesigner如何操作

1) 首先,需要为ORDER_ID列建立一个反向键索引。打开T_ORDER的Table Properties的窗口,切换到Indexes页,新建一个名为IDX_ORDER_ID的索引。填写完索引的名称后,双击这个索引,弹出Index Properties窗口,在这个窗口的Columns中选择ORDER_ID列。然后,切换到Options页,按图 7的方式将其设置为反向键索引。

合理设计优化Oracle库表设计的若干方法(3)

图 7 设置反向键索引
 

2) 显式指定主键PK_ORDER使用这个索引。在Table Properties窗口中切换到Keys页,默认情况下,PowerDesigner为T_ORDER所指定的主键名为Key1,我们将其更名为 PK_ORDER,双击这个主键,弹出Key Properties窗口,切换到Options页,按图 8的方式为PK_ORDER指定IDX_ORDER_ID。

合理设计优化Oracle库表设计的若干方法(4)

图 8 为主键指定特定的索引


不可否认PowerDesigner确实是目前业界最强大易用的数据库设计工具,但很遗憾,当我们为表主键指定一个索引时,其产生的语句在顺序上有问题:即创建主键的语句位于创建索引语句之前:

create table T_ORDER (…);alter table T_ORDER add constraint PK_T_ORDER primary key (ORDER_ID) using index IDX_ORDER_ID;create unique index IDX_ORDER_ID on T_ORDER ( ORDER_ID ASC) reverse;


我们可以通过对PowerDesigner生成SQL语句的设置进行调整,先生成创建表和索引的SQL语句,再创建为表添加主键和外键的SQL语句来达到曲线救国的目的,请看下一步。

3)通过菜单Database->Generate Database...调出Database Configuration窗口,切换到Keys&Indexes页,按图 9设置:



合理设计优化Oracle库表设计的若干方法(4)

图 9 设置生成键和索引SQL的选项


这里,我们将Primary Keys和Foreign keys的选项都取消,而将Indexes勾选,以达到只生成表的索引SQL语句的目的。

点击"确定"后,生成创建数据库表及其索引的SQL语句,运行该SQL创建数据库后,再按图 10设置生成为表添加主键和外键的SQL语句:

合理设计优化Oracle库表设计的若干方法(4)

图 10 生成创建表主键和外键的SQL语句



除此设置外,还必须切换到Tables & Views页下,取消所有选项,避免重新生成创建表的语句。

 3、将子表的外键列的索引改为压缩型

3.1 压缩型索引的原理和用途

在前面的例子中,由于一条订单会对应多条订单条目,所以T_ORDER_ITEM的ORDER_ID字段总会出现重复的值,如:

ITEM_ID ORDER_ID ITEM COUNT

1 100 101 1

2 100 104 2

3 100 201 3

4 200 301 2

5 200 401 1

6 200 205 3


在ORDER_ID列上创建一个普通未压缩的B-Tree索引,则索引数据的物理上的存储形式如下:

合理设计优化Oracle库表设计的若干方法(5)

图 11 未进行压缩的索引存储


ORDER_ID的重复值在索引块中重复出现,这样不但增加了存储空间的需求,而且因为查询时需要读取更多的索引数据块,所以查询性能也会降低=。让我们来看一下经过压缩后索引数据的存储方式:

合理设计优化Oracle库表设计的若干方法(5)

图 12 进行压缩的索引存储


压缩型的索引消除了重复的索引值,将相同索引列值所关联的ROWID存储在一起。这样,不但节省了存储空间,查询效率也提高了,真可谓两全齐美了。

对象T_ORDER和T_ORDER_ITEM这样的主从表进行查询时,一般情况下,我们都必须通过外键查询出子表所有关联的记录,所以在子表的外键上建立压缩型的索引是非常适合的。

3.2 压缩型索引的SQL语句

创建压缩型索引的SQL语句非常简单,在T_ORDER_ITEM的ORDER_ID上创建压缩型索引的SQL如下所示:

create index IDX_ORDER_ITEM_ORDER_ID on T_ORDER_ITEM ( ORDER_ID ASC) compress;


需要在创建索引的语句后附上compress关键字就可以了。

3.3 PowerDesigner如何创建压缩型索引

1) 打开T_ORDER_ITEM表的Table Properties的窗口,切换到Indexes页,为ORDER_ID列创建一个名为IDX_ORDER_ITEM_ORDER_ID的索引。

2) 双击IDX_ORDER_ITEM_ORDER_ID弹出Index Properties窗口,切换到Options页,按图 13将索引设置为压缩型:

合理设计优化Oracle库表设计的若干方法(5)

图 13 将索引指定为压缩型


4、建立满足需求的复合键索引

设计人员希望通过T_ORDER表上的IDX_ORDER_COMPOSITE复合索引满足以下两种组合条件的查询:

·CLIENT + ORDER_DATE + IS_SHIPPED

·ORDER_DATE + IS_SHIPPED

为方便阐述,我们特地将IDX_ORDER_COMPOSITE的创建SQL语句再次列出:

create index IDX_ORDER_COMPOSITE on T_ORDER ( CLIENT ASC, ORDER_DATE ASC, IS_SHIPPED ASC);


事实上,在CLIENT + ORDER_DATE + IS_SHIPPED 三列上所执行的复合条件查询会应用到这个索引,而在ORDER_DATE + IS_SHIPPED列上所执行的复合查询不会使用这个索引,因而将导致一个全表扫描的操作。

可以用许多工具来了解查询语句的执行计划,通过SET AUTOTRACE ON来查询以上两个复合查询的执行计划:

打开SQL/Plus,输入以下的语句:

SQL> set autotrace on

SQL> select * from t_order where CLIENT = '1' and ORDER_DATE='1' and IS_SHIPPED='1';


分析得到的执行计划为:

SELECT STATEMENT Optimizer=CHOOSETABLE ACCESS (BY INDEX ROWID) OF 'T_ORDER' INDEX (RANGE SCAN) OF 'IDX_ORDER_COMPOSITE' (NON-UNIQUE)


可见Oracle先利用IDX_ORDER_COMPOSITE得到满足条件的记录ROWID,再通过ROWID返回记录。

而下面查询语句:

SQL> select * from t_order where ORDER_DATE='1' and IS_SHIPPED='1'


的执行计划则为:

SELECT STATEMENT Optimizer=CHOOSE TABLE ACCESS (FULL) OF 'T_ORDER'


很明显,Oracle在T_ORDER表上执行了一个全表扫描的操作,没有用到IDX_ORDER_COMPOSITE索引。

对复合列索引,我们得出这个结论:

假设在COL_1,COL_2,…,COL_n这些列上建立了一个复合索引:

create index IDX _COMPOSITE on TABLE1

{

COL_1,

COL_2,

…,

COL_n

}


则只有WHERE语句上包含COL_1(复合索引的第一个字段)的查询才会使用这个复合索引,而未包含COL_1的查询则不会使用这个复合索引。

回到我们的例子,如何建立满足CLIENT + ORDER_DATE + IS_SHIPPED和ORDER_DATE + IS_SHIPPED两种查询的索引呢?

考 虑到IS_SHIPPED列基数很小,只有两个可能的值:0,1。在这种情况下,有两种方案:第一,分别为CLIENT + ORDER_DATE + IS_SHIPPED和ORDER_DATE + IS_SHIPPED建立一个复合索引;第二,分别在CLIENT和ORDER_DATE列上建立一个索引,而IS_SHIPEED列不建立索引。

第一种方案的查询效率最快,但因为CLIENT和ORDER_DATE在索引中会重复出现两次,占用较大的存储空间。第二种方案CLIENT和ORDER_DATE不会在索引存储出现两次,较为节省空间,查询效率比之于第一种方案会稍低一些,但影响不大。

我们采用第二种方案为CLIENT和ORDER_DATE分别创建索引IDX_CLIENT和IDX_ORDER_DATE,组合查询条件为CLIENT + ORDER_DATE + IS_SHIPPED时的执行计划为:

SELECT STATEMENT Optimizer=CHOOSE TABLE ACCESS (BY INDEX ROWID) OF 'T_ORDER' AND-EQUAL INDEX (RANGE SCAN) OF 'IDX_CLIENT' (NON-UNIQUE) INDEX (RANGE SCAN) OF 'IDX_ORDER_DATE' (NON-UNIQUE)


而组合条件为ORDER_DATE + IS_SHIPPED时的执行计划为:

SELECT STATEMENT Optimizer=CHOOSE TABLE ACCESS (BY INDEX ROWID) OF 'T_ORDER' INDEX (RANGE SCAN) OF 'IDX_ORDER_DATE' (NON-UNIQUE)


通过这样的改造,我们得到了一个满足两种组合查询的执行计划。

总结

贯穿本文的订单主从表实例结构上很简单,但是其粗糙的设计包含了许多问题,这也是许多对Oracle物理存储结构没有很好理解的数据库设计师容易忽视的地方。

在一般情况下,这样的设计并不会导致严重系统的性能问题,但是精益求精是每一位优秀软件设计师的品质,此外,对于设计师,一定要清楚这样一条规律:对于等质的性能提升,在编码层面往往需要比设计层面付出更多的艰辛。

在Oracle中提高数据库的性能需要考虑的问题,注意的误区还很多,本文涵盖是一些最常见的问题。下面,我们将提高数据库操作性能方法及一些误区作个小结:

·对于大表,可以考虑创建分区表,分区表有范围分区、散列分区、列表分区和散列分区几种,通过它可以达到化大表为小表的目的。

·考虑适量的数据冗余,如一个业务表有一个审批状态,审批需要经过多步,每一步对应审批表的一条记录,最后审批的那条记录决定了业务的状态。我们大可在业务表中存放一个审批状态的标志,以取消每次需要通过关联审批表获取业务审批状态的复杂的关联表查询。

· 不要做太多的关联表查询,一些几乎不发生数据变动的表码表,如性别,学历,婚姻状态等表码表,可以考虑在应用程序启动时一次性地下载到应用程序的内存中缓 存起来,在从数据库获取结果集后,再由程序利用这些缓存的表码表数据来翻译这些表码字段,而不要在数据库中通过表间的关联查询方式来翻译这些字段。

· 常看到一些令我瞠目的设计:在需要进行频繁DML(INSERT,UPDATE,DELETE)操作的表的某些基数低的字段(如性别,婚姻状态)上创建位 图索引。位图索引是好东西,但它是有使用范围的,在OLTP系统中,需要进行频繁DML操作的表中不应该出现位图索引,位图索引只适用于几乎不进行DML 操作,只进行查询的DSS系统中。此外,聚簇和索引组织表也都更适合DSS系统,而非OLTP系统。
(完)

posted @ 2006-07-19 18:16 Alex 阅读(362) | 评论 (0)编辑 收藏

key words: Oracle监控  shell脚本

前言

这篇文章介绍了DBA每天在监控Oracle数 据库方面的职责,讲述了如何通过shell脚本来完成这些重复的监控工作。本文首先回顾了一些DBA常用的Unix命令,以及解释了如何通过Unix Cron来定时执行DBA脚本。同时文章还介绍了8个重要的脚本来监控Oracle数据库:

检查实例的可用性

检查监听器的可用性

检查alert日志文件中的错误信息

在存放log文件的地方满以前清空旧的log文件

分析table和index以获得更好的性能

检查表空间的使用情况

找出无效的对象

监控用户和事务

DBA需要的Unix基本知识

基本的UNIX命令

以下是一些常用的Unix命令:

ps--显示进程 grep--搜索文件中的某种文本模式 mailx--读取或者发送mail cat--连接文件或者显示它们 cut--选择显示的列 awk--模式匹配语言 df--显示剩余的磁盘空间

以下是DBA如何使用这些命令的一些例子:

显示服务器上的可用实例:

$ ps -ef | grep smon

oracle 21832 1 0 Feb 24 ? 19:05 ora_smon_oradb1

oracle 898 1 0 Feb 15 ? 0:00 ora_smon_oradb2

dliu 25199 19038 0 10:48:57 pts/6 0:00 grep smon

oracle 27798 1 0 05:43:54 ? 0:00 ora_smon_oradb3

oracle 28781 1 0 Mar 03 ? 0:01 ora_smon_oradb4、
显示服务器上的可用监听器:

$ ps -ef | grep listener | grep -v grep

(译者注:grep命令应该加上-i参数,即grep -i listener,

该参数的作用是忽略大小写,因为有些时候listener是大写的,这时就会看不到结果)

oracle 23879 1 0 Feb 24 ? 33:36 /8.1.7/bin/tnslsnr listener_db1 -inherit

oracle 27939 1 0 05:44:02 ? 0:00 /8.1.7/bin/tnslsnr listener_db2 -inherit

oracle 23536 1 0 Feb 12 ? 4:19 /8.1.7/bin/tnslsnr listener_db3 -inherit

oracle 28891 1 0 Mar 03 ? 0:01 /8.1.7/bin/tnslsnr listener_db4 -inherit
查看Oracle存档目录的文件系统使用情况

$ df -k | grep oraarch

/dev/vx/dsk/proddg/oraarch 71123968 4754872 65850768 7% /u09/oraarch
统计alter.log文件中的行数:

$ cat alert.log | wc -l

2984

列出alert.log文件中的全部Oracle错误信息:

$ grep ORA- alert.log

ORA-00600: internal error code, arguments: [kcrrrfswda.1], [], [], [], [], []

ORA-00600: internal error code, arguments: [1881], [25860496], [25857716], []

CRONTAB基本

一个crontab文件中包含有六个字段:

分钟 0-59

小时 0-23

月中的第几天 1-31

月份 1 - 12

星期几 0 - 6, with 0 = Sunday

Unix命令或者Shell脚本

要编辑一个crontab文件,输入:Crontab -e

要查看一个crontab文件,输入:

Crontab -l

0 4 * * 5 /dba/admin/analyze_table.ksh

30 3 * * 3,6 /dba/admin/hotbackup.ksh /dev/null 2>&1

在上面的例子中,第一行显示了一个分析表的脚本在每个星期5的4:00am运行。第二行显示了一个执行热备份的脚本在每个周三和周六的3:00a.m.运行。

监控数据库的常用Shell脚本

以下提供的8个shell脚本覆盖了DBA每日监控工作的90%,你可能还需要修改UNIX的环境变量。

检查Oracle实例的可用性

oratab文件中列出了服务器上的所有数据库

$ cat /var/opt/oracle/oratab


############################################################


## /var/opt/oracle/oratab##


############################################################

oradb1:/u01/app/oracle/product/8.1.7:Y

oradb2:/u01/app/oracle/product/8.1.7:Y

oradb3:/u01/app/oracle/product/8.1.7:N

oradb4:/u01/app/oracle/product/8.1.7:Y
以下的脚本检查oratab文件中列出的所有数据库,并且找出该数据库的状态(启动还是关闭)

##############################################################


## ckinstance.ksh ## ###################################################################

ORATAB=/var/opt/oracle/oratab

echo `date`

echo Oracle Database(s) Status `hostname` :

db=`egrep -i :Y|:N $ORATAB | cut -d: -f1 | grep -v # | grep -v *`

pslist=`ps -ef | grep pmon`

for i in $db ; do

echo $pslist | grep ora_pmon_$i > /dev/null 2>$1

if (( $? )); then

echo Oracle Instance - $i: Down

else

echo Oracle Instance - $i: Up

fi

done

使用以下的命令来确认该脚本是可以执行的:

$ chmod 744 ckinstance.ksh

$ ls -l ckinstance.ksh

-rwxr--r-- 1 oracle dba 657 Mar 5 22:59 ckinstance.ksh*
以下是实例可用性的报表:

$ ckinstance.ksh

Mon Mar 4 10:44:12 PST 2002

Oracle Database(s) Status for DBHOST server:

Oracle Instance - oradb1: Up

Oracle Instance - oradb2: Up

Oracle Instance - oradb3: Down

Oracle Instance - oradb4: Up

检查Oracle监听器的可用性

以下有一个类似的脚本检查Oracle监听器。如果监听器停了,该脚本将会重新启动监听器:

#######################################################################

## cklsnr.sh ##

#######################################################################

#!/bin/ksh

DBALIST=primary.dba@company.com,another.dba@company.com;export DBALIST

cd /var/opt/oracle

rm -f lsnr.exist

ps -ef | grep mylsnr | grep -v grep > lsnr.exist

if [ -s lsnr.exist ]

then

echo

else

echo Alert | mailx -s Listener ‘mylsnr‘ on `hostname` is down $DBALIST

TNS_ADMIN=/var/opt/oracle; export TNS_ADMIN

ORACLE_SID=db1; export ORACLE_SID

ORAENV_ASK=NO; export ORAENV_ASK

PATH=$PATH:/bin:/usr/local/bin; export PATH

. oraenv

LD_LIBRARY_PATH=${ORACLE_HOME}/lib;export LD_LIBRARY_PATH

lsnrctl start mylsnr

fi

检查Alert日志(ORA-XXXXX)

每个脚本所使用的一些环境变量可以放到一个profile中:

####################################################################### 

## oracle.profile ##

#######################################################################

EDITOR=vi;export EDITOR ORACLE_BASE=/u01/app/oracle; export

ORACLE_BASE ORACLE_HOME=$ORACLE_BASE/product/8.1.7; export

ORACLE_HOME LD_LIBRARY_PATH=$ORACLE_HOME/lib; export

LD_LIBRARY_PATH TNS_ADMIN=/var/opt/oracle;export

TNS_ADMIN NLS_LANG=american; export

NLS_LANG NLS_DATE_FORMAT=‘Mon DD YYYY HH24:MI:SS‘; export

NLS_DATE_FORMAT ORATAB=/var/opt/oracle/oratab;export

ORATAB PATH=$PATH:$ORACLE_HOME:$ORACLE_HOME/bin:/usr/ccs/bin:/bin:/usr/bin:/usr/sbin:/

sbin:/usr/openwin/bin:/opt/bin:.; export

PATH DBALIST=primary.dba@company.com,another.dba@company.com;export

DBALIST
以下的脚本首先调用oracle.profile来设置全部的环境变量。如果发现任何的Oracle错误,该脚本还会给DBA发送一个警告的email。

####################################################################

## ckalertlog.sh ##

####################################################################

#!/bin/ksh

.. /etc/oracle.profile

for SID in `cat $ORACLE_HOME/sidlist`

do

cd $ORACLE_BASE/admin/$SID/bdump

if [ -f alert_${SID}.log ]

then

mv alert_${SID}.log alert_work.log

touch alert_${SID}.log

cat alert_work.log >> alert_${SID}.hist

grep ORA- alert_work.log > alert.err

fi

if [ `cat alert.err|wc -l` -gt 0 ]

then

mailx -s ${SID} ORACLE ALERT ERRORS $DBALIST < alert.err

fi

rm -f alert.err

rm -f alert_work.log

done

清除旧的归档文件

以下的脚本将会在log文件达到90%容量的时候清空旧的归档文件:

$ df -k | grep arch

Filesystem kbytes used avail capacity Mounted on

/dev/vx/dsk/proddg/archive 71123968 30210248 40594232 43% /u08/archive

#######################################################################

## clean_arch.ksh ##

#######################################################################

#!/bin/ksh

df -k | grep arch > dfk.result

archive_filesystem=`awk -F ‘{ print $6 }‘ dfk.result`

archive_capacity=`awk -F ‘{ print $5 }‘ dfk.result`

if [[ $archive_capacity > 90% ]]

then

echo Filesystem ${archive_filesystem} is ${archive_capacity} filled

# try one of the following option depend on your need

find $archive_filesystem -type f -mtime +2 -exec rm -r {} ;

tar

rman

fi

分析表和索引(以得到更好的性能)

以下我将展示如果传送参数到一个脚本中:

#################################################################### 

## analyze_table.sh ##

####################################################################

#!/bin/ksh

# input parameter: 1: password # 2: SID

if (($#<1)) then echo "Please enter oracle user password as the first parameter !" exit 0

fi

if (($#<2)) then echo "Please enter instance name as the second parameter!" exit 0

fi

要传入参数以执行该脚本,输入:

$ analyze_table.sh manager oradb1 
脚本的第一部分产生了一个analyze.sql文件,里面包含了分析表用的语句。脚本的第二部分分析全部的表:

#####################################################################

## analyze_table.sh ##

#####################################################################

sqlplus -s <
oracle/$1@$2

set heading off

set feed off

set pagesize 200

set linesize 100

spool analyze_table.sql

select ANALYZE TABLE || owner || . || segment_name ||

ESTIMATE STATISTICS SAMPLE 10 PERCENT;

from dba_segments

where segment_type = TABLE

and owner not in (SYS, SYSTEM);

spool off

exit

!

sqlplus -s <
oracle/$1@$2

@./analyze_table.sql

exit

!
以下是analyze.sql的一个例子:

$ cat analyze.sql

ANALYZE TABLE HIRWIN.JANUSAGE_SUMMARY ESTIMATE STATISTICS SAMPLE 10 PERCENT;

ANALYZE TABLE HIRWIN.JANUSER_PROFILE ESTIMATE STATISTICS SAMPLE 10 PERCENT;

ANALYZE TABLE APPSSYS.HIST_SYSTEM_ACTIVITY ESTIMATE STATISTICS SAMPLE 10 PERCENT;

ANALYZE TABLE HTOMEH.QUEST_IM_VERSION ESTIMATE STATISTICS SAMPLE 10 PERCENT;

ANALYZE TABLE JSTENZEL.HIST_SYS_ACT_0615 ESTIMATE STATISTICS SAMPLE 10 PERCENT;

ANALYZE TABLE JSTENZEL.HISTORY_SYSTEM_0614 ESTIMATE STATISTICS SAMPLE 10 PERCENT;

ANALYZE TABLE JSTENZEL.CALC_SUMMARY3 ESTIMATE STATISTICS SAMPLE 10 PERCENT;

ANALYZE TABLE IMON.QUEST_IM_LOCK_TREE ESTIMATE STATISTICS SAMPLE 10 PERCENT;

ANALYZE TABLE APPSSYS.HIST_USAGE_SUMMARY ESTIMATE STATISTICS SAMPLE 10 PERCENT;

ANALYZE TABLE PATROL.P$LOCKCONFLICTTX ESTIMATE STATISTICS SAMPLE 10 PERCENT;

检查表空间的使用

以下的脚本检测表空间的使用。如果表空间只剩下10%,它将会发送一个警告email。

#####################################################################

## ck_tbsp.sh ##

#####################################################################

#!/bin/ksh

sqlplus -s <
oracle/$1@$2

set feed off

set linesize 100

set pagesize 200

spool tablespace.alert

SELECT F.TABLESPACE_NAME,

TO_CHAR ((T.TOTAL_SPACE - F.FREE_SPACE),999,999) "USED (MB)",

TO_CHAR (F.FREE_SPACE, 999,999) "FREE (MB)",

TO_CHAR (T.TOTAL_SPACE, 999,999) "TOTAL (MB)",

TO_CHAR ((ROUND ((F.FREE_SPACE/T.TOTAL_SPACE)*100)),999)|| % PER_FREE

FROM (

SELECT TABLESPACE_NAME,

ROUND (SUM (BLOCKS*(SELECT VALUE/1024

FROM V\$PARAMETER

WHERE NAME = db_block_size)/1024)

) FREE_SPACE

FROM DBA_FREE_SPACE

GROUP BY TABLESPACE_NAME

) F,

(

SELECT TABLESPACE_NAME,

ROUND (SUM (BYTES/1048576)) TOTAL_SPACE

FROM DBA_DATA_FILES

GROUP BY TABLESPACE_NAME

) T

WHERE F.TABLESPACE_NAME = T.TABLESPACE_NAME

AND (ROUND ((F.FREE_SPACE/T.TOTAL_SPACE)*100)) < 10;

spool off

exit

!

if [ `cat tablespace.alert|wc -l` -gt 0 ]

then

cat tablespace.alert -l tablespace.alert > tablespace.tmp

mailx -s "TABLESPACE ALERT for ${2}" $DBALIST < tablespace.tmp

fi

警告email输出的例子如下:

TABLESPACE_NAME USED (MB) FREE (MB) TOTAL (MB) PER_FREE 

------------------- --------- ----------- ------------------- ------------------

SYSTEM 2,047 203 2,250 9 %

STBS01 302 25 327 8 %

STBS02 241 11 252 4 %

STBS03 233 19 252 8 %
查找出无效的数据库对象

以下查找出无效的数据库对象:

#####################################################################
## invalid_object_alert.sh ##
##################################################################### 
#!/bin/ksh . /etc/oracle.profile 

sqlplus -s <
oracle/$1@$2

set feed off

set heading off column object_name format a30

spool invalid_object.alert

SELECT OWNER, OBJECT_NAME, OBJECT_TYPE,
STATUS FROM DBA_OBJECTS WHERE STATUS = 
INVALID ORDER BY OWNER, OBJECT_TYPE, OBJECT_NAME;

spool off

exit ! if [ `cat invalid_object.alert|wc -l` -gt 0 ] then

mailx -s "INVALID OBJECTS for ${2}" $DBALIST < invalid_object.alert

fi$ cat invalid_object.alert

OWNER OBJECT_NAME OBJECT_TYPE STATUS

--------------------------------------------
HTOMEH DBMS_SHARED_POOL PACKAGE BODY INVALID

HTOMEH X_$KCBFWAIT VIEW INVALID

IMON IW_MON PACKAGE INVALID

IMON IW_MON PACKAGE BODY INVALID

IMON IW_ARCHIVED_LOG VIEW INVALID

IMON IW_FILESTAT VIEW INVALID

IMON IW_SQL_FULL_TEXT VIEW INVALID

IMON IW_SYSTEM_EVENT1 VIEW INVALID

IMON IW_SYSTEM_EVENT_CAT VIEW INVALIDLBAILEY CHECK_TABLESPACE_USAGE PROCEDURE INVALID

PATROL P$AUTO_EXTEND_TBSP VIEW INVALID

SYS DBMS_CRYPTO_TOOLKIT PACKAGE INVALID

SYS DBMS_CRYPTO_TOOLKIT PACKAGE BODY INVALID

SYS UPGRADE_SYSTEM_TYPES_TO_816 PROCEDURE INVALID

SYS AQ$_DEQUEUE_HISTORY_T TYPE INVALID

SYS HS_CLASS_CAPS VIEW INVALID SYS HS_CLASS_DD VIEW INVALID
监视用户和事务(死锁等)

以下的脚本在死锁发生的时候发送一个警告e-mail:

###################################################################

## deadlock_alert.sh ##

##################################################################
##!/bin/ksh

.. /etc/oracle.profile

sqlplus -s <
oracle/$1@$2

set feed off

set heading off

spool deadlock.alert

SELECT SID, DECODE(BLOCK, 0, NO, YES ) BLOCKER,

DECODE(REQUEST, 0, NO,YES ) WAITER

FROM V$LOCK

WHERE REQUEST > 0 OR BLOCK > 0

ORDER BY block DESC;

spool off

exit

!

if [ `cat deadlock.alert|wc -l` -gt 0 ]

then

mailx -s "DEADLOCK ALERT for ${2}" $DBALIST < deadlock.alert

fi


结论

0,20,40 7-17 * * 1-5 /dba/scripts/ckinstance.sh > /dev/null 2>&1

0,20,40 7-17 * * 1-5 /dba/scripts/cklsnr.sh > /dev/null 2>&1

0,20,40 7-17 * * 1-5 /dba/scripts/ckalertlog.sh > /dev/null 2>&1

30 * * * 0-6 /dba/scripts/clean_arch.sh > /dev/null 2>&1

* 5 * * 1,3 /dba/scripts/analyze_table.sh > /dev/null 2>&1

* 5 * * 0-6 /dba/scripts/ck_tbsp.sh > /dev/null 2>&1

* 5 * * 0-6 /dba/scripts/invalid_object_alert.sh > /dev/null 2>&1

0,20,40 7-17 * * 1-5 /dba/scripts/deadlock_alert.sh > /dev/null 2>&1

通过以上的脚本,可大大减轻你的工作。你可以使用这些是来做更重要的工作,例如性能调整。

posted @ 2006-07-18 16:08 Alex 阅读(653) | 评论 (0)编辑 收藏


									2003-05 余枫

ORACLE客户端连服务器的注意事项:

1. 通过SQL*NET协议,ORACLE客户端连服务器时一般需要配置sqlnet.ora和tnsnames.ora。
它们默认的目录在$ORACLE_HOME/network/admin 目录下

也可以设置环境变量TNS_ADMIN指向你想用的sqlnet.ora和tnsnames.ora目录
例如:
TNS_ADMIN=/home/oracle/config/9.0.1;export TNS_ADMIN

sqlnet.ora文件决定找数据库服务器别名的方式

默认的参数有
NAMES.DEFAULT_DOMAIN = WORLD
NAMES.DIRECTORY_PATH = (TNSNAMES, ONAMES, HOSTNAME)

如果你的ORACLE客户端和服务器默认的域名不一样,需要用#号注释第一行
#NAMES.DEFAULT_DOMAIN = WORLD
使它不起作用。

NAMES.DIRECTORY_PATH指定找服务器别名的顺序 (本地的tnsnames.ora文件, 命名服务器, 主机名方式)

服务器的sqlnet.ora里可以设置检查客户端是否alive的时间间隔
sqlnet.expire_time = 10

tnsnames.ora文件里写数据库服务器别名的详细内容,有以下几种写法:

# 一般的写法
APPDB =
(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.0.35)(PORT = 1521))
)
(CONNECT_DATA =
(SERVICE_NAME = appdb)
)
)

# 明确标明用dedicated方式连接数据库
APPD=
(DESCRIPTION=
(ADDRESS=(PROTOCOL=TCP)(HOST=192.168.0.35)(PORT=1521))
(CONNECT_DATA=
(SERVICE_NAME=appdb)
(SERVER=DEDICATED)))

# 对多个listener端口做均衡负载方式连接数据库
APPS =
(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.0.35)(PORT = 1521))
(ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.0.35)(PORT = 1856))
)
(CONNECT_DATA =
(SERVICE_NAME = appdb)
)
)

# 注意:如果数据库服务器用MTS,客户端程序需要用database link时最好明确指明客户端用dedicated直连方式,
# 不然会遇到很多跟分布式环境有关的ORACLE BUG。
# 一般情况下数据库服务器用直接的连接会好一些,除非你的实时数据库连接数接近1000。

2. /etc/hosts (UNIX)
或者windows\hosts(WIN98) winnt\system32\drivers\etc\hosts (WIN2000)
客户端需要写入数据库服务器IP地址和主机名的对应关系。

127.0.0.1 localhost
192.168.0.35 oracledb oracledb
192.168.0.45 tomcat tomcat
202.84.10.193 bj_db bj_db

有些时候我们配置好第一步后,tnsping 数据库服务器别名显示是成功的,
但是sqlplus username/password@servicename不通,jdbc thin link 也不通的时候,
一定不要忘了在客户端做这一步,原因可能是DNS服务器里没有设置这个服务器IP地址和主机名的对应关系。

如果同时有私有IP和Internet上公有IP,私有IP写在前面,公有IP写在后面。

编辑前最好留一个备份,增加一行时也最好用复制粘贴,避免编辑hosts时空格或者tab字符错误。

3. UNIX下ORACLE多数据库的环境,OS客户端需要配置下面两个环境变量

ORACLE_SID=appdb;export ORACLE_SID
TWO_TASK=appdb;export TWO_TASK

来指定默认的目标数据库。
posted @ 2006-07-17 22:31 Alex 阅读(250) | 评论 (0)编辑 收藏

Windows

  • 下载并安装 Tor & Privoxy
    http://www.vidalia-project.net/dist/vidalia-bundle-0.1.1.21-0.0.5.exe
    安装完之后,在localhost:8118有http代理(privoxy提供),localhost:9050有socks5代理(tor提供)
    如果这时候可以配置Firefox使用8118和9050这两个端口,那就所有的Internet访问都将通过tor访问,会很慢。所以这种方式不推荐,要自己编辑一下代理配置脚本
    Proxy settings in Firefox
  • 编辑proxy.pac
    可以在某个地方创建代理配置脚本, 比如说c:\proxy.pac, 下面是我用的内容, 可以上一些比较常用的网站, 比如wikipedia和google网页快照. 如果查询google也经常有问题, 可以把第一条 nosite.google.com 改成 .google.com 记得google.com前面有个点, 这样所有访问google的请求都会通过代理,不过做好心理准备,会很慢
    function FindProxyForURL(url, host)
    {
    url = url.toLowerCase();
    host = host.toLowerCase();

    if(dnsDomainIs(host,"nosite.google.com")) return "PROXY localhost:8118";
    else if(dnsDomainIs(host,".blogspot.com")) return "PROXY localhost:8118";
    else if(dnsDomainIs(host,".wordpress.com")) return "PROXY localhost:8118";
    else if(dnsDomainIs(host,"wikipedia.org")) return "PROXY localhost:8118";
    else if(shExpMatch(url,"*q=cache:*")) return "PROXY localhost:8118";
    else return "DIRECT";
    }
  • 配置IE使用代理配置脚本
    上面那个Firefox配置界面中,选择Aotumatic proxy configuration URL, 填入
    file://c:/proxy.pac

    IE的配置类似.每次重新修改proxy.pac,都应该到上面的界面Reload代理配置脚本(比较讨厌,好在这个文件修改次数不会很多)

Debian Linux

这里主要讲Debian下的安装配置,其他版本的Linux可以参考tor的官方说明

  • 安装 tor privoxy
  •  apt-get install tor privoxy
  • 配置privoxy
  • Debian下面的privoxy需要额外的配置,因为privoxy需要使用tor提供的9050 socks5端口.具体是配置/etc/privoxy/config,加一行

    forward-socks4a / localhost:9050 .

    不要漏掉最后的点.然后重起一下privoxy

    /etc/init.d/tor restart
  • 如果使用Firefox,剩下的配置可以参考Windows下面的配置方法

参考

http://www.linuxsir.org/bbs/showthread.php?t=232436
http://tor.eff.org/docs/tor-doc-win32.html.en

Technorati Tags: , ,

posted @ 2006-07-17 11:01 Alex 阅读(3117) | 评论 (0)编辑 收藏

     摘要: 作者:John Smiley 学习在 RHEL 2.1、RHEL 3 或 SLES 8 上从头安装 Oracle 数据库 10g 的基础知识(仅用于评估) ...  阅读全文
posted @ 2006-07-13 13:57 Alex 阅读(629) | 评论 (0)编辑 收藏

key words: Oracle闪回 flash

  1、Oracle 9i的闪回查询功能

   在Oracle 9i之前,如果用户错误操作数据后,除了不完全恢复外,没有好的解决办法。Oracle 9i中提供闪回查询,由一个新的包DBMS_FLASH来实现。用户使用闪回查询可以及时取得误操作DML(Delete、Update、Insert) 前某一时间点数据库的映像视图,用户可以利用系统时间或系统改变号(SCN:System Change Number)来指定这个只读视图,并可以针对错误进行相应的恢复措施。闪回查询功能完全依赖于自动回滚段管理(AUM),对于Drop等误操作不能恢 复。闪回特性可应用在以下方面:

  (1)自我维护过程中的修复:当一些重要的记录被意外删除,用户可以向后移动到一个时间点,查看丢失的行并把它们重新插入现在的表内恢复。

  (2)恢复Email和声音Email:当用户意外删除了Email或者声音信息时,可以通过移回到固定时间点来恢复删除。

  (3)账号平衡状况:可以查看以前的历史数据。如银行外币管理中用于记录特定时间的汇率。在以前,汇率变更被记录在一个历史表中,现在就可以通过闪回功能进行查询。

  (4)用于趋势分析的决策支持系统:决策支持系统和联机分析应用必须执行一个长时间的事务。使用闪回查询,这些应用可以对历史数据执行分析和建模。例如,特定产品如矿泉水随季节变化需求情况的变化。

  2、回滚段概述

  回滚段用于存放数据修改之前的位置和值,回滚段的头部包含正在使用的该回滚段事务的信息。回滚段的作用如下:

  (1)事务回滚:当事务修改表中数据的时候,该数据修改前的值(即前影像)会存放在回滚段中,当用户回滚事务时,Oracle将会利用回滚段中的数据前影像来将修改的数据恢复到原来的值。

  (2)事务恢复:当事务正在处理的时候,例程失败,回滚段的信息保存在重做日志文件中,Oracle将在下次打开数据库时利用回滚来恢复未提交的数据。

  (3)读一致性:当一个会话正在修改数据时,其它的会话将看不到该会话未提交的修改。而且,当一个语句正在执行时,该语句将看不到从该语句开始执行后的未提交的修改(语句级读一致性)。

  3、Oracle中Delete和Commit操作的流程分析

  (1)删除(Delete)流程

  ·Oracle读Block(数据块)到Buffer Cache(缓冲区)(如果该Block在Buffer中不存在);

  ·在Redo Log Buffer(重做日志缓冲区)中记录Delete操作的细节;

  ·在相应回滚段段头的事物表中创建一个Undo(回滚)条目;

  ·把将要删除的记录创建前镜像,存放到Undo Block(回滚块)中;

  ·在Buffer Cache中的相应数据块上删除记录,并且标记相应的数据块为Dirty(脏)。

  (2)提交(Commit)流程

  ·Oracle产生一个SCN;

  ·在回滚段事物表中标记该事物状态为Commited;

  ·LGWR(日志读写进程) Flush Log Buffer到日志文件;

  ·如果此时数据块仍然在Buffer Cache中,那么SCN将被记录到Block Header上,这被称为快速提交;

  ·如果Dirty Block已经被写回到磁盘,那么下一个访问这个Block的进程将会自回滚段中获取该事物的状态,确认该事物被提交。然后这个进程获得提交SCN并写回到Block Header上,这被称为延迟块清除。

  4、Oracle 9i中闪回查询操作实例

  进行闪回查询必须设置自动回滚段管理,在init.ora设置参数UNDO_MANAGEMENT=AUTO,参数UNDO_RETENTION=n,决定了能往前闪回的最大时间,值越大就需要越多Undo空间。

  例:Oracle 9i的Flashback Query操作。

  (1)创建闪回查询用户

SQL> create user flashtest identified by flashtest;
SQL> grant connect, resource to flashtest;
SQL> grant execute on dbms_flashback to flashtest;
SQL> connect flashtest/flashtest;

  (2)创建测试表,插入测试记录

SQL> create table test(id number(3));
SQL> insert into test values (1);
SQL> insert into test values(2);
SQL> commit;
SQL> create table rec_date(date_scn);

  注意:在执行步骤3或者步骤4之前,等待5分钟。

  (3)删除记录

SQL> execute dbms_flashback.disable;
SQL> insert into rec_date select sysdate from dual;
SQL> commit;
SQL> delete from test where id=1;
SQL> commit;

  通过以上的操作,我们插入了两条记录,并删除了其中一条记录。在以下的操作中,我们将通过flashback query找到删除的记录

  (4)闪回查询

SQL> DECLARE
Restore_scn date;
BEGIN
Select date_scn into restore_scn from rec_date;
Dbms_flashback.enable_at_time (restore_scn);
END;
SQL> select * from test;
ID
1
2


  可以看出,虽然删除记录并提交,但是通过闪回操作,仍能查询到删除前 的两条记录。需要注意Oracle每5分钟记录一次SCN,并将SCN和对应时间的映射进行纪录。如果原来插入的记录到做闪回操作的时间在5分钟之内,用 基于时间的闪回查询可能得不到记录,因为基于时间点的查询实际上是转化为最近的一次SCN,然后从这个SCN开始进行恢复。因此,如果需要精确的查询可以 采用基于SCN的闪回查询,可精确闪回到需要恢复的时间。可以通过DBMS_FLASHBACK.GET_SYSTEM_CHANGE_NUMBER语句 获取SCN。

  Oracle 10g的闪回查询概述

  与Oracle 9i相比Oracle 10g的
Flash back有了非常大的改进,从普通的Flashback Query发展到了多种形式,主要表现在如下几方面新特性:

  1、Flashback Database

  Oracle Flashback Database特性允许通过SQL语句Flashback Database语句,让
数据库 前 滚到当前的前一个时间点或者SCN,而不需要做时间点的恢复。闪回数据库可以迅速将数据库回到误操作或人为错误的前一个时间点,如Word中的"撤消"操 作,可以不利用备份就快速的实现基于时间点的恢复。Oracle通过创建新的Flashback Logs(闪回日志),记录数据库的闪回操作。如果希望能闪回数据库,需要设置如下参数:DB_RECOVER_FILE_DEST日志的存放位置, DB_RECOVER_FILE_DEST_SIZE恢复区的大小。在创建数据库的时候,Oracle将自动创建恢复区,但默认是关闭的,需要执行 alter database flashback on命令。

  例:执行Flashback Database命令格式。

SQL>flashback database to time to_timestamp(xxx);
SQL>flashback database to scn xxx

  2、Flashback Table

  Oracle Flashback Table特性允许利用Flashback Table语句,确保闪回到表的前一个时间点。与Oracle 9i中的Flashback Query相似,利用回滚段信息来恢复一个或一些表到以前的一个时间点(一个快照)。要注意的是,Flashback Table不等于Flashback Query,Flashback Query仅仅是查询以前的一个快照点而已,并不改变当前表的状态,而Flashback Table将改变当前表及附属对象一起回到以前的时间点。

  语法:

flashback table tablename to timestamp xxx或
flashback table tablename to scn xxx

  注意:如果需要闪回一个表,需要以下条件:

  ·需要有flashback any table的系统权限或者是该表的flashback对象权限;

  ·需要有该表的select,insert,delete,alter权限;

  ·必须保证该表row movement。

  例:执行将test表闪回到2005年5月7日下午3点。

SQL>flashback table test to timestamp to_timestamp(’2005-05-07 15:00:00’,’yyyy-mm-dd hh24:mi:ss’);

  3、Flashback Drop

  Oracle Flashback Drop特性提供一个类似回收站的功能,用来恢复不小心被删除的表。当删除表时,Oracle 10g并不立刻释放被删除的表所占用的空间,而是将这个被删除的表进行自动重命名(为了避免同类对象名称的重复)并放进回收站中。所谓的回收站类似于
Windows 系统中的回收站,是一个虚拟的容器,用于存放所有被删除的对象,在回收站中被删除的对象将占用创建时的同样的空间。如果这个被删除的表需要进行恢复,就可利用Flashback Drop功能。

  例:进行一个删除表后恢复的简单测试。

  (1)显示回收站信息

SQL>show recyclebin;

  可以看到,回收站中是没有任何结果的,表示没有任何表在回收站中。

  (2)创建一个表,并删除,再次显示回收站信息

SQL>create table test_drop(name varchar2(10));
SQL>drop table test_drop;
SQL>show recyclebin;
ORIGINAL NAME RECYCLEBIN NAME OBJECT TYPE DROP TIME
TEST_DROP BIN$b+XkkO1RS5K10uKo9BfmuA==$0 TABLE 2005-05-07:14:30:47

  (3)对被删除的表进行恢复

SQL>flashback table test_drop to before drop;或
SQL>flashback table "BIN$b+XkkO1RS5K10uKo9BfmuA==$0" to before drop;

  (4)管理回收站

  清除回收站中的单个表:purge table test_drop

  清除整个回收站:purge recyclebin

  清除不同的对象回收站:purge user_recyclebin或purge dba_recyclebin

  (5)确认删除一个表

SQL>drop table test_drop purge;

  如果删除一个表且不放到回收站中不能进行恢复,在drop语句中可以利用purge选项。

  4、Flash Version Query

   Oracle Flashback Version Query特性,利用保存的回滚信息,可以看到特定的表在时间段内的任何修改,如电影的回放一样,可以了解表在该期间的任何变化。Flashback version query一样依赖于AUM,提供了一个查看行改变的功能,能找到所有已经提交了的行的记录,分析出过去时间都执行了什么操作。Flashback version query采用VERSIONS BETWEEN语句来进行查询,常用的方法:

  ·VERSIONS_SCN - 系统改变号

  ·VERSIONS_TIMESTAMP - 时间

   例如:在test表中,时间1插入一条记录,时间2删除了这条记录,对于时间3执行select * from test当然查询不到这条记录,只能看到该表最后的提交记录。这时如果利用Flash Table或者是Flash Query,只能看到过去的某一时间点的一个快照,而利用Flashback Version Query,能够把时间1、时间2的操作给记录下来,并详细的查询出对表进行的任何操作。

SQL>select versions_starttime,versions_endtime, versions_xid,versions_operation,id
from test versions
between timestamp minvalue and maxvalue
order by versions_starttime;

  在上述查询中,列 versions_starttime、versions_endtime、versions_xid、versions_operation是伪列,还 有一些伪列,如versions_startscn和versions_endscn显示了该时刻的系统更改号。列versions_xid显示了更改该 行的事务标识符。

  当然,除了分析以上所有的变更之外,可以根据需要指定时间段,如显示在2005-05-07时间在15:30到16:30之间test表的所有变更。

SQL>select id from test
versions between timestamp to_date(’2005-05-07 15:30:00’,’yyyy-mm-dd hh24:mi:ss’) and to_date(’2005-05-07 16:30:00’,’yyyy-mm-dd hh24:mi:ss’)

  5、Flashback Transaction Query

  Oracle Flashback Transaction Query特性确保检查数据库的任何改变在一个事务级别,可以利用此功能进行诊断
问题 、 性能分析和审计事务。它其实是Flashback Version Query查询的一个扩充,Flashback Version Query说明了可以审计一段时间内表的所有改变,但是也仅仅是能发现问题,对于错误的事务,没有好的处理办法。而Flashback Transaction Query提供了从FLASHBACK_TRANSACTION_QUERY视图中获得事务的历史以及Undo_sql(回滚事务对应的sql语句),也 就是说审计一个事务到底做了什么,甚至可以回滚一个已经提交的事务。

  例:Flashback Transaction Query的操作实例。

  (1)在test表中删除记录,获得事务的标识XID,然后提交。

SQL>delete from test where id=2;
SQL>select xid from v$transaction;
XID
----------------
04001200AE010000
SQL>commit;

  在测试中方便起见,在事务没有提交的时候,获得事务的XID为04001F0035000000。实际情况下,不可能去跟踪每个事务,想要获得已提交事务的XID,就必须通过上面的Flashback Version Query。

  (2)进行Flashback Transaction Query

SQL>select * from FLASHBACK_TRANSACTION_QUERY
where xid=’04001F0035000000’;
UNDO_SQL
insert into "FLASHTEST"."TEST"("ID") values (’2’);

  注意:这个删除语句对应的是1个Insert语句,如果想回滚这个事务,执行这个Insert语句即可。

   可以看到,Flashback Transaction Query主要用于审计一个事务,并可以回滚一个已经提交的事务。如果确定出错的事务是最后一个事务,我们利用Flashback Table或者Flashback Query就可以解决问题。但是,如果执行了一个错误的事务之后,又执行了一系列正确的事务,那么上面的方法就无能为力,利用Flashback Transaction Query可以查看或回滚这个错误的事务。

  结束语

  通过上面的描述,可以看出闪回功能使用户恢复偶然的错误删除更加容易,增强了系统的可用性与读一致性。
posted @ 2006-07-13 10:26 Alex 阅读(1340) | 评论 (0)编辑 收藏

start the oem service:
emctl start dbconsole

then type following addrese in browser:

http://yoursrv:1561/em


it's convient.

It's B/S model ,and it's C/S model at Oracle9i.


posted @ 2006-07-11 16:18 Alex 阅读(254) | 评论 (0)编辑 收藏

key words: cookie 保留历史记录 登陆记录

很多时候用session觉得挺方便的,今天突然发现自己竟然几乎没用过cookie,呵呵,有点意思。正好碰到一个登陆页面,需要用户选择站点类型,觉得每次都让用户选择有点不合理,毕竟一个用户常用的就一个,所以决定用cookie记录下这个站点,下次登陆的时候可以直接显示.

效果如下:
站点cookie

 /**
     * 从cookie里读取指定Name 对应的值
     * 如果没有返回空 null
     * 
@param cookieName
     * 
@param request
     *
@param decode :编码
     * @return  String
     
*/
    
public static String getCookieValue(String cookieName, HttpServletRequest request,String decode) {
        
if(null == cookieName || cookieName.trim().length() ==0return "";
        Cookie cookies[] 
= request.getCookies();
        Cookie sCookie 
= null;
        String sname 
= null;
        String svalue 
= null;
        
if (null != cookies && cookies.length > 0) {
            
for (int i = 0; i < cookies.length; i++) {
                sCookie 
= cookies[i];
                sname 
= sCookie.getName();
                
if (cookieName.equals(sname)) {
                    
try {
                        svalue 
= java.net.URLDecoder.decode(sCookie.getValue(),decode);
                    } 
catch (UnsupportedEncodingException e) {
                        e.printStackTrace();  
                    }
                    
break;
                }
            }
        }
        
return svalue ;
    }

    
/**
     * 设置cookie
     * 
@param cookieName
     * 
@param cookieValue
     * 
@param maxAge
     * 
@param response
     * @paramencode :编码
     */
    
public static void setCookieValue(String cookieName,String cookieValue,
int maxAge,HttpServletResponse response,String encode) {
        
if(null == cookieName || cookieName.trim().length() ==0return ;
        Cookie cookie 
= null;
        
try {
            cookie 
= new Cookie(cookieName, java.net.URLEncoder.encode(cookieValue,encode));
        } 
catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        cookie.setMaxAge(maxAge);
        response.addCookie(cookie);
    }

posted @ 2006-07-05 17:39 Alex 阅读(543) | 评论 (1)编辑 收藏

key words: 切割字符串 切割中文字符 DecimalFormat 格式化字符 科学计数法

一.切割字符串的前几个字符
在首页,有时候会因为table列表里的某个内容比较长而使得页面撑得很难看,一般做法就是截取前几个字符
/**
     * 截取前几个字符串
     * 
@param src  被截取的字符
     * 
@param num  截取的长度
     * 
@param append 附加的字符
     * 
@return String
     
*/
    
public static String splitStr(String src, int num, String append) {
        
if (null == src || num < 0return "";
        
if (src.length() < num) return src;
        
char[] rtnChar = src.toCharArray();
        StringBuffer sb 
= new StringBuffer();
        
for (int i = 0; i < num; i++) {
            sb.append(rtnChar[i]);
        }
        sb.append(append);
        
return sb.toString();
    }

toCharArray会把一个汉字当作一个char(java中一个char两个字节)

二.用DecimalFormat格式化字符
这次用POI读取Excel碰到的一个问题,即,如果Excel里的格式不同,比如同样的20060623,有可能是字符型的格式,也可能是普通数字型的,而且在数字型的时候如果比较长会给你返回科学计数法的格式,如:2.002623E7,而这个不是我希望出现的,所以后来只好碰到这种格式的就自己给转换一下:

if (null != row.getCell((short) i)) {
                    
switch (row.getCell((short) i).getCellType()) {
                        
case HSSFCell.CELL_TYPE_FORMULA :
                            strExcelLine[i] 
= "FORMULA ";
                            
break;
                        
case HSSFCell.CELL_TYPE_NUMERIC :
                            strExcelLine[i] 
= String.valueOf(row.getCell((short) i).getNumericCellValue());
                            
break;
                        
case HSSFCell.CELL_TYPE_STRING :
                            strExcelLine[i] 
= row.getCell((short) i).getStringCellValue();
                            
break;
                        
case HSSFCell.CELL_TYPE_BLANK :
                            strExcelLine[i] 
= "";
                            
break;
                        
default :
                            strExcelLine[i] 
= "";
                            
break;
                    }
                    
//如果读取的是科学计数法的格式,则转换为普通格式
                    
//added by Alex at 20060626
                    if(null != strExcelLine[i] &&
                            strExcelLine[i].indexOf(
"."!= -1 &&
                            strExcelLine[i].indexOf(
"E"!= -1){
                        DecimalFormat df 
= new DecimalFormat();
                        strExcelLine[i] 
= df.parse(strExcelLine[i]).toString();
                    }
                }



posted @ 2006-07-03 18:31 Alex 阅读(922) | 评论 (0)编辑 收藏

原文地址 :这里

 怎样成为优秀的软件模型设计者?

                    来自:blog   雪茶技术

我们期待自己成为一个优秀的软件模型设计者,但是,要怎样做,又从哪里开始呢?

将下列原则应用到你的软件工程中,你会获得立杆见影的成果。

1. 人远比技术重要

你开发软件是为了供别人使用,没有人使用的软件只是没有意义的数据的集合而已。许多在软件方面很有成就的行家在他们事业的初期却表现平平,因为他们 那时侯将主要精力都集中在技术上。显然,构件(components),EJB(Enterprise Java Beans)和代理(agent)是很有趣的东西。但是对于用户来说,如果你设计的软件很难使用或者不能满足他们的需求,后台用再好的技术也于事无补。多 花点时间到软件需求和设计一个使用户能很容易理解的界面上。

2. 理解你要实现的东西

好的软件设计人员把大多数时间花费在建立系统模型上,偶尔写一些源代码,但那只不过是为了验证设计过程中所遇到的问题。这将使他们的设计方案更加可行。

3. 谦虚是必须的品格

你不可能知道一切,你甚至要很努力才能获得足够用的知识。软件开发是一项复杂而艰巨的工作,因为软件开发所用到的工具和技术是在不断更新的。而且, 一个人也不可能了解软件开发的所有过程。在日常生活中你每天接触到的新鲜事物可能不会太多。但是对于从事软件开发的人来说,每天可以学习很多新东西(如果 愿意的话)。

4. 需求就是需求

如果你没有任何需求,你就不要动手开发任何软件。成功的软件取决于时间(在用户要求的时间内完成)、预算和是否满足用户的需求。如果你不能确切知道用户需要的是什么,或者软件的需求定义,那么你的工程注定会失败。

5. 需求其实很少改变,改变的是你对需求的理解

Object ToolSmiths公司(www.objecttoolsmiths.com) 的Doug Smith常喜欢说:“分析是一门科学,设计是一门艺术”。他的意思是说在众多的“正确”分析模型中只存在一个最“正确”分析模型可以完全满足解决某个具 体问题的需要(我理解的意思是需求分析需要一丝不苟、精确的完成,而设计的时候反而可以发挥创造力和想象力 - 译者注)。

如果需求经常改动,很可能是你没有作好需求分析,并不是需求真的改变了。

你可以抱怨用户不能告诉你他们想得到什么,但是不要忘记,收集需求信息是你工作。

你可以说是新来的开发人员把事情搞得一团糟,但是,你应该确定在工程的第一天就告诉他们应该做什么和怎样去做。

如果你觉得公司不让你与用户充分接触,那只能说明公司的管理层并不是真正支持你的项目。

你可以抱怨公司有关软件工程的管理制度不合理,但你必须了解大多同行公司是怎么做的。

你可以借口说你们的竞争对手的成功是因为他们有了一个新的理念,但是为什么你没先想到呢?

需求真正改变的情况很少,但是没有做好需求分析工作的理由却很多。

6. 经常阅读

在这个每日都在发生变化的产业中,你不可能在已取得的成就上陶醉太久。

每个月至少读2、3本专业杂志或者1本专业书籍。保持不落伍需要付出很多的时间和金钱,但会使你成为一个很有实力的竞争者。

7. 降低软件模块间的耦合度

高耦合度的系统是很难维护的。一处的修改引起另一处甚至更多处的变动。

你可以通过以下方法降低程序的耦合度:隐藏实现细节,强制构件接口定义,不使用公用数据结构,不让应用程序直接操作数据库(我的经验法则是:当应用程序员在写SQL代码的时候,你的程序的耦合度就已经很高了)。

耦合度低的软件可以很容易被重用、维护和扩充。

8. 提高软件的内聚性

如果一个软件的模块只实现一个功能,那么该模块具有高内聚性。高内聚性的软件更容易维护和改进。

判断一个模块是否有高的内聚性,看一看你是否能够用一个简单的句子描述它的功能就行了。如果你用了一段话或者你需要使用类似“和”、“或”等连词,则说明你需要将该模块细化。[注:only one]

只有高内聚性的模块才可能被重用。

9. 考虑软件的移植性

移植是软件开发中一项具体而又实际的工作,不要相信某些软件工具的广告宣传(比如java 的宣传口号write once run many ? 译者注)。

即使仅仅对软件进行常规升级,也要把这看得和向另一个操作系统或数据库移植一样重要。

记得从16位Windows移植到32位windows的“乐趣”吗 ?当你使用了某个操作系统的特性,如它的进程间通信(IPC)策略,或用某数据库专有语言写了存储过程。你的软件和那个特定的产品结合度就已经很高了。

好的软件设计者把那些特有的实现细节打包隐藏起来,所以,当那些特性该变的时候,你的仅仅需要更新那个包就可以了。

10. 接受变化

这是一句老话了:唯一不变的只有变化。

你应该将所有系统将可能发生的变化以及潜在需求记录下来,以便将来能够实现(参见“Architecting for Change”,Thinking Objectively, May 1999)

通过在建模期间考虑这些假设的情况,你就有可能开发出足够强壮且容易维护的软件。设计强壮的软件是你最基本的目标。

11. 不要低估对软件规模的需求

Internet 带给我们的最大的教训是你必须在软件开发的最初阶段就考虑软件规模的可扩充性。

今天只有100人的部门使用的应用程序,明天可能会被有好几万人的组织使用,下月,通过因特网可能会有几百万人使用它。

在软件设计的初期,根据在用例模型中定义的必须支持的基本事务处理,确定软件的基本功能。然后,在建造系统的时候再逐步加入比较常用的功能。

在设计的开始考虑软件的规模需求,避免在用户群突然增大的情况下,重写软件。

12. 性能仅仅是很多设计因素之一

关注软件设计中的一个重要因素--性能,这好象也是用户最关心的事情。一个性能不佳的软件将不可避免被重写。

但是你的设计还必须具有可靠性,可用性,便携性和可扩展性。你应该在工程开始就应该定义并区分好这些因素,以便在工作中恰当使用。性能可以是,也可以不是优先级最高的因素,我的观点是,给每个设计因素应有的考虑。

13. 管理接口

“UML User Guide”(Grady Booch,Ivar Jacobson和Jim Rumbaugh ,Addison Wesley, 1999)中指出,你应该在开发阶段的早期就定义软件模块之间的接口。

这有助于你的开发人员全面理解软件的设计结构并取得一致意见,让各模块开发小组相对独立的工作。一旦模块的接口确定之后,模块怎样实现就不是很重要了。

从根本上说,如果你不能够定义你的模块“从外部看上去会是什么样子”,你肯定也不清楚模块内要实现什么。

14. 走近路需要更长的时间

在软件开发中没有捷径可以走。

缩短你的在需求分析上花的时间,结果只能是开发出来的软件不能满足用户的需求,必须被重写。

在软件建模上每节省一周,在将来的编码阶段可能会多花几周时间,因为你在全面思考之前就动手写程序。

你为了节省一天的测试时间而漏掉了一个bug,在将来的维护阶段,可能需要花几周甚至几个月的时间去修复。与其如此,还不如重新安排一下项目计划。

避免走捷径,只做一次但要做对(do it once by doing it right)。

15. 别信赖任何人

产品和服务销售公司不是你的朋友,你的大部分员工和高层管理人员也不是。

大部分产品供应商希望把你牢牢绑在他们的产品上,可能是操作系统,数据库或者某个开发工具。

大部分的顾问和承包商只关心你的钱并不是你的工程(停止向他们付款,看一看他们会在周围呆多长时间)。

大部分程序员认为他们自己比其他人更优秀,他们可能抛弃你设计的模型而用自己认为更好的

只有良好的沟通才能解决这些问题

要明确的是,不要只依靠一家产品或服务提供商,即使你的公司(或组织)已经在建模、文档和过程等方面向那个公司投入了很多钱。

16. 证明你的设计在实践中可行

在设计的时候应当先建立一个技术原型, 或者称为“端到端”原型。以证明你的设计是能够工作的。

你应该在开发工作的早期做这些事情,因为,如果软件的设计方案是不可行的,在编码实现阶段无论采取什么措施都于事无补。技术原型将证明你的设计的可行性,从而,你的设计将更容易获得支持。

17. 应用已知的模式

目前,我们有大量现成的分析和设计模式以及问题的解决方案可以使用。

一般来说,好的模型设计和开发人员,都会避免重新设计已经成熟的并被广泛应用的东西。
http://www.ambysoft.com/processPatternsPage.html 收藏了许多开发模式的信息。

18. 研究每个模型的长处和弱点

目前有很多种类的模型可以使用,如下图所示。用例捕获的是系统行为需求,数据模型则描述支持一个系统运行所需要的数据构成。你可能会试图在用例中加 入实际数据描述,但是,这对开发者不是非常有用。同样,数据模型对描述软件需求来说是无用的。每个模型在你建模过程中有其相应的位置,但是,你需要明白在 什么地方,什么时候使用它们。

19. 在现有任务中应用多个模型

当你收集需求的时候,考虑使用用例模型,用户界面模型和领域级的类模型。

当你设计软件的时候,应该考虑制作类模型,顺序图、状态图、协作图和最终的软件实际物理模型。

程序设计人员应该慢慢意识到,仅仅使用一个模型而实现的软件要么不能够很好地满足用户的需求,要么很难扩展。

20. 教育你的听众

你花了很大力气建立一个很成熟的系统模型,而你的听众却不能理解它们,甚至更糟-连为什么要先建立模型都不知道。那么你的工作是毫无意义的。

教给你开发人员基本的建模知识;否则,他们会只看看你画的漂亮图表,然后继续编写不规范的程序。

另外, 你还需要告诉你的用户一些需求建模的基础知识。给他们解释你的用例(uses case)和用户界面模型,以使他们能够明白你要表达地东西。当每个人都能使用一个通用的设计语言的时候(比如UML-译者注),你的团队才能实现真正的合作。

21. 带工具的傻瓜还是傻瓜

你给我CAD/CAM工具,请我设计一座桥。但是,如果那座桥建成的话,我肯定不想当第一个从桥上过的人,因为我对建筑一窍不通。

使用一个很优秀的CASE工具并不能使你成为一个建模专家,只能使你成为一个优秀CASE工具的使用者。成为一个优秀的建模专家需要多年的积累,不 会是一周针对某个价值几千美元工具的培训。一个优秀的CASE工具是很重要,但你必须学习使用它,并能够使用它设计它支持的模型。

22. 理解完整的过程

好的设计人员应该理解整个软件过程,尽管他们可能不是精通全部实现细节。

软件开发是一个很复杂的过程,还记得《object-oriented software process》第36页的内容吗?除了编程、建模、测试等你擅长工作外,还有很多工作要做。

好的设计者需要考虑全局。必须从长远考虑如何使软件满足用户需要,如何提供维护和技术支持等。

23. 常做测试,早做测试

如果测试对你的软件来说是无所谓的,那么你的软件多半也没什么必要被开发出来。

建立一个技术原型供技术评审使用,以检验你的软件模型。

在软件生命周期中,越晚发现的错误越难修改,修改成本越昂贵。尽可能早的做测试是很值得的。

24. 把你的工作归档

不值得归档的工作往往也不值得做。归档你的设想,以及根据设想做出的决定;归档软件模型中很重要但不很明显的部分。 给每个模型一些概要描述以使别人很快明白模型所表达的内容

25. 技术会变,基本原理不会

如果有人说“使用某种开发语言、某个工具或某某技术,我们就不需要再做需求分析,建模,编码或测试”。不要相信,这只说明他还缺乏经验。抛开技术和 人的因素,实际上软件开发的基本原理自20世纪70年代以来就没有改变过。你必须还定义需求,建模,编码,测试,配置,面对风险,发布产品,管理工作人员 等等。

软件建模技术是需要多年的实际工作才能完全掌握的。好在你可以从我的建议开始,完善你们自己的软件开发经验。

以鸡汤开始,加入自己的蔬菜。然后,开始享受你自己的丰盛晚餐吧。

posted @ 2006-06-20 14:28 Alex 阅读(387) | 评论 (1)编辑 收藏

mark下资源,有空再来慢慢研究。

Prototype.js--Javascript编写者的小军刀


posted @ 2006-06-18 22:22 Alex 阅读(304) | 评论 (0)编辑 收藏

key words: bash编程

转自 这里

另一篇 详解Bash命令行处理

bash编程教学实例

bash编程

--------------------------------------------------------------------------------
時間:2004/03/02    來源:不详

  Shell Script(bash)简介

  众所皆知地,UNIX上以小工具著名,利用许多简单的小工具,来完成原本需要大量软体开发的工作,这一点特色,使得UNIX成为许多人心目中理想的系统平台。 
  在众多的小工具中,Shell Script算得上是最基本、最强大、运用最广泛的一个。它运用围之广,不但从系统启动、程式编译、定期作业、上网连线,甚至安装整个Linux系统,都可以用它来完成。 

  因为Shell Script是利用您平日在使用的一些指令,将之组合起来,成为一个"程式"。如果您平日某些序列的指令下得特别频繁,便可以将这些指令组合起来,成为另 一个新的指令。这样,不但可以简化并加速操作速度,甚至还可以干脆自动定期执行,大大简化系统管理工作。 

  *************************
  Bash(GNU Bourne-Again SHell)是许多Linux平台的内定Shell,事实上,还有许多传统UNIX上用的Shell,像tcsh、csh、ash、bsh、ksh等等, Shell Script大致都类同,当您学会一种Shell以后,其它的Shell会很快就上手,大多数的时候,一个Shell Script通常可以在很多种Shell上使用。 
  这里我介绍您bash的使用方法。事实上,当您"man bash"时,就可以看到bash的说明书,不过对许多人来说,这份说明书犹如"无字天书"一样难懂。这份文件,主要资料来源为"man bash",我加上一些实际日常的应用例来说明。希望这样能让那些始终不得其门而入的人们,多多少少能有点概念。 


  教学例子

  "Hello world" Shell Script 
  照传统程式教学例,这一节介绍Shell Script的"Hello World"如何撰写。 

  *************************

  #!/bin/sh 
  # Filename : hello 
  echo "Hello world!" 

  *************************

  大家应该会注意到第一行的"#!/bin/sh"。在UNIX下,所有的可执行Script,不管是那一种语言,其开头都是"#!",例如Perl是 "#!/usr/bin/perl",tcl/tk是"#!/usr/bin/wish",看您要执行的Script程式位置在那里。您也可以用"#! /bin/bash"、"#!/bin/tcsh"等等,来指定使用特定的Shell。 
  echo是个bash的内建指令。 

  *************************

  接下来,执行hello这个script: 
  要执行一个Script的方式有很多种。 

  *************************

  第一种 : 将hello这个档案的权限设定为可执行。 
  [foxman@foxman bash]# chmod 755 hello 
  执行 
  [foxman@foxman bash]# ./hello 
  hello world 

  *************************

  第二种 : 使用bash内建指令"source"或"."。 
  [foxman@foxman bash]# source hello 
  hello world 
  或 
  [foxman@foxman bash]# . hello 
  hello world 

  *************************

  第三种 : 直接使用sh/bash/tcsh指令来执行。 
  [foxman@foxman bash]# sh hello 
  hello world 
  或 
  [foxman@foxman bash]# bash hello 
  hello world 

  *************************

  Bash执行选项 

  *************************

  -c string : 读取string来当命令。 
  -i : 互动介面。 
  -s : 由stdin读取命令。 
  - : 取消往后选项的读取。 
  -norc : 不要读~/.bashrc来执行。 
  -noprofile : 不要读/etc/profile、~/.bash_profile、~/.bash_login、~/.profile等等来执行。 
  -rcfile filename : 执行filename,而非~/.bashrc 
  -version : 显示版本。 
  -quiet : 启动时不要哩唆。 
  -login : 确保bash是个login shell。 
  -nobraceexpansion : 不要用curly brace expansion({}符号展开)。 
  -nolineediting : 不用readline来读取命令列。 
  -posix : 改采Posix 1003.2标准。 


  用于自动备份的Shell Script


  一个用于自动备份的Shell Script
  我们先前提到,可利用Shell Script搭配crond来作定期的工作。要作定期性的工作,在UNIX上,就是与crond的搭配运用。 

  *************************

  首先我们先来研究如何对系统进行备份。 
  要对系统进行备份,不外乎便是利用一些压缩工具。在许多UNIX系统上,tar及gzip是de facto的资料交换标准。我们经常可以看见一些tar.gz或tgz档,这些档案,被称为tarball。当然了,您也可以用bzip2、zip等等压 缩工具来进行压缩,不必限定于gzip。但tar配合gzip是最普遍的,也是最方便的方式。 

  要将我们想要的资料压缩起来,进行备份,可以结合tar及gzip一起进行。方式有很多种,最常用的指令是以下这一种: 

  tar -c file/dir ... | gzip -9 > xxxx.tar.gz 

  您也可以分开来做: 

  tar -r file/dir ... -f xxxx.tar 
  gzip -9 xxxx.tar 

  或 

  tar -r file/dir ... -f xxxx.tar 
  gzip -9 < xxxx.tar > xxxx.tar.gz 

  *************************

  在解过Linux下档案备份的基本知识后,我们来写一个将档案备份的Script。 
  #!/bin/sh 
  # Filename : backup 

  DIRS="/etc /var /your_directories_or_files" 
  BACKUP="/tmp/backup.tgz" 

  tar -c $DIRS | gzip -9 > $BACKUP 

  其中DIRS放的是您要备份的档案及目录,BACKUP是您的备份档。可不要将/tmp放进DIRS中,那样做,您是在做备份的备份,可能将您的硬碟塞爆。 


  *************************

  接下来测试 
  [foxman@foxman bash]# chmod 755 backup 
  [foxman@foxman bash]# ./backup 

  执行完成后在/tmp就会有一个backup.tgz,里面储存了您重要的资料。您可用 

  gzip -dc /tmp/backup.tgz | tar -vt 
  或 
  tar vtfz /tmp/backup.tgz 

  来看看里面的档案列表。 

  要解开时,可用以下指令来完成复原: 

  gzip -dc /tmp/backup.tgz | tar -xv 
  或 
  tar xvfz /tmp/backup.tgz 

  备份通常是仅备份系统通常最重要的部份,/etc可说是不可缺少的一部份。另外,看您系统中有那些重要的资料需要备份。通常来说,您没有必要备份 /bin、/sbin、/usr/bin、/usr/sbin、/usr/X11R6/bin等等这些执行档目录。只要备份您重要的档案即可,别把整个硬 碟备份,那是蛮呆的动作。 

  *************************

  如果您有许多台机器,可利用其中一台任务较轻的内部网路主机,做为主要备份主机。将所有机器都自动执行备份,然后利用NFS/Coda/Samba等网路档案系统,将备份的资料放到该备份机器中,该机器则定时收取备份资料,然后您再由该机器中进行一次备份。 
  这里是整个系统备份方案的图示。 

  在您进行之前,先解一下,系统中那些是要备份的,那些是不需要的。 

  *************************

  新的backup
  #!/bin/sh 
  HOSTNAME=`hostname` 
  DIRS="/etc /var /your_important_directory" 
  BACKUP="/tmp/$HOSTNAME.tgz" 
  NFS="/mnt/nfs" 

  tar -c $DIRS | gzip -9 > $BACKUP 
  mv -f $BACKUP $NFS 


  *************************

  备份主机内的Script : collect_backup
  #!/bin/sh 
  NFS="/mnt/nfs" 
  BACKUP="/backup" 

  mv -f $NFS/*.tgz $BACKUP 


  在此,您不能够将所有备份都直接放在/mnt/nfs,这是危险的。万一任一台机器不小心将/mnt/nfs所有内容删除,那么备份就会消失。因此,您需要将/mnt/nfs移到一个只有该备份主机可存取的目录中。 


  *************************

  当这些个别的Script都测试好以后,接下来我们将他们放到crontab里面。找到您的crontab,它的位置可能在/var/spool/cron/crontabs/root、/etc/crontab、/var/cron/tabs/root。 
  在crontab中选择以下之一加入(看您定期的时间): 

  Slackware : /var/spool/cron/crontabs/root
  01 * * * * /full_backup_script_path/backup 1> /dev/null 2> /dev/null # 每小时(太过火一点) 
  30 16 * * * /full_backup_script_path/backup 1> /dev/null 2> /dev/null # 每日16:30,下班前备份 
  30 16 * * 0 /full_backup_script_path/backup 1> /dev/null 2> /dev/null # 每周一16:30 
  0 5 1 * * /full_backup_script_path/backup 1> /dev/null 2> /dev/null # 每月一号5:0 
  RedHat/Debian : /etc/crontab
  RedHat可直接将backup放入/etc/cron.hourly, /etc/cron.daily, /etc/cron.weekly, /etc/cron.monthly。或采用如上加入/etc/crontab的方式: 
  有关crontab的用法,可查"man 5 crontab",在此不详述。 

  备份主机的设定类同。 

  注意: 所有机器不要同时进行备份,否则网路会大塞车。备份主机收取备份的时间要设为最后,否则会收不到备份资料。您可以在实作后,将时间间隔调整一下。 


  *************************

  看看,两个小小不到三行的Shell Script,配合cron这个定时工具。可以让原本需要耗时多个小时的人工备份工作,简化到不到十分钟。善用您的想像力,多加一点变化,可你让您的生活变得轻松异常,快乐悠哉。


  档案系统检查

  系统安全一向是大多数电脑用户关心的事,在UNIX系统中,最重视的事,即系统中有没有"木马"(Trojan horse)。不管Trojan horse如何放进来的,有一点始终会不变,即被放置木马的档案,其档案日期一定会被改变,甚至会有其它的状态改变。此外,许多状况下,系统会多出一些不 知名的档案。因此,平日检查整个档案系统的状态是否有被改变,将所有状态有改变的档案,以及目前有那些程式正在执行,自动报告给系统管理员,是个避免坐上 "木马"的良方。 

  *************************

  #!/bin/sh 
  # Filename : whatever_you_name_it 
  DIRS="/etc /home /bin /sbin /usr/bin /usr/sbin /usr/local /var /your_directory" 
  ADMIN="email@your.domain.com" 
  FROM="admin@your.domain.com" 
  # 写入Sendmail的标头 
  echo "Subject: $HOSTNAME filesystem check" > /tmp/today.mail 
  echo "From: $FROM" >> /tmp/today.mail 
  echo "To: $ADMIN" >> /tmp/today.mail 
  echo "This is filesystem report comes from $HOSTNAME" >> /tmp/today.mail 
  # 报告目前正在执行的程式 
  ps axf >> /tmp/today.mail 
  # 档案系统检查 
  echo "File System Check" >> /tmp/today.mail 
  ls -alR $DIRS | gzip -9 > /tmp/today.gz 
  zdiff /tmp/today.gz /tmp/yesterday.gz >> /tmp/today.mail 
  mv -f /tmp/today.gz /tmp/yesterday.gz 
  # 寄出信件 
  sendmail -t < /tmp/today.mail 

  然后把它放到一个不显眼的地方去,让别人找不到。 

  把它加入crontab中。 

  30 7 * * * /full_check_script_path/whatever_you_name_it 1> /dev/null 2> /dev/null #上班前检查 

  有些档案是固定会更动的,像/var/log/messages、/var/log/syslog、/dev/ttyX等等,不要太大惊小怪。


  控制圈for

  演示了几个简单的Shell Script,相信您应该对Shell Script有点概念了。现在我们开始来仔细研究一些较高等的Shell Script写作。一些进一步的说明,例如"$"、">"、"<"、">>"、"1>"、"2>"符号的使用,会在 稍后解释。 

  *************************

  for name [ in word; ] do list ; done
  控制圈。 
  word是一序列的字,for会将word中的个别字展开,然后设定到name上面。list是一序列的工作。如果[in word;]省略掉,那么name将会被设定为Script后面所加的参数。 


  *************************

  例一: 
  #!/bin/sh 

  for i in a b c d e f ; do 
  echo $i 
  done 

  它将会显示出a到f。 


  *************************

  例二: 另一种用法,A-Z
  #!/bin/sh 
  WORD="a b c d e f g h i j l m n o p q r s t u v w x y z" 

  for i in $WORD ; do 
  echo $i 
  done 

  这个Script将会显示a到z。 


  *************************

  例三 : 修改副档名
  如果您有许多的.txt档想要改名成.doc档,您不需要一个一个来。 
  #!/bin/sh 

  FILES=`ls /txt/*.txt` 

  for txt in $FILES ; do 
  doc=`echo $txt | sed "s/.txt/.doc/"` 
  mv $txt $doc 
  done 

  这样可以将*.txt档修改成*.doc档。 


  *************************

  例四 : meow
  #!/bin/sh 
  # Filename : meow 
  for i ; do 
  cat $i 
  done 

  当您输入"meow file1 file2 ..."时,其作用就跟"cat file1 file2 ..."一样。 


  *************************

  例五 : listbin 
  #!/bin/sh 
  # Filename : listbin 

  for i in /bin/* ; do 
  echo $i 
  done 

  当您输入"listbin"时,其作用就跟"ls /bin/*"一样。 


  *************************

  例六 : /etc/rc.d/rc 
  拿一个实际的例来说,Red Hat的/etc/rc.d/rc的启动程式中的一个片断。 

  for i in /etc/rc.d/rc$runlevel.d/S*; do 
  # Check if the script is there. 
  [ ! -f $i ] && continue 

  # Check if the subsystem is already up. 
  subsys=${i#/etc/rc.d/rc$runlevel.d/S??} 
  [ -f /var/lock/subsys/$subsys ] || \ 
  [ -f /var/lock/subsys/${subsys}.init ] && continue 

  # Bring the subsystem up. 
  $i start 
  done 

  这个例中,它找出/etc/rc.d/rcX.d/S*所有档案,检查它是否存在,然后一一执行。 


  流程控制case

  case word in [ pattern [ | pattern ] ... ) list ;; ] ... esac
  case/esac的标准用法大致如下: 
case $arg in 
  pattern | sample) # arg in pattern or sample 
  ;; 
  pattern1) # arg in pattern1 
  ;; 
  *) #default 
  ;; 
esac 
  arg是您所引入的参数,如果arg内容符合pattern项目的话,那么便会执行pattern以下的程式码,而该段程式码则以两个分号";;"做结尾。 

  可以注意到"case"及"esac"是对称的,如果记不起来的话,把"case"颠倒过来即可。 


*************************

  例一 : paranoia
#!/bin/sh 
case $1 in 
    start | begin) 
     echo "start something" 
    ;; 
    stop | end) 
     echo "stop something" 
    ;; 
    *) 
     echo "Ignorant" 
    ;; 
esac 

  执行
  [foxman@foxman bash]# chmod 755 paranoia 
  [foxman@foxman bash]# ./paranoia 
  Ignorant 
  [foxman@foxman bash]# ./paranoia start 
  start something 
  [foxman@foxman bash]# ./paranoia begin 
  start something 
  [foxman@foxman bash]# ./paranoia stop 
  stop something 
  [foxman@foxman bash]# ./paranoia end 
  stop something 

*************************

  例二 : inetpanel
  许多的daemon都会附上一个管理用的Shell Script,像BIND就附上ndc,Apache就附上apachectl。这些管理程式都是用shell script来写的,以下示一个管理inetd的shell script。 
#!/bin/sh 

case $1 in 
  start | begin | commence) 
    /usr/sbin/inetd 
  ;; 
  stop | end | destroy) 
    killall inetd 
  ;; 
  restart | again) 
    killall -HUP inetd 
  ;; 
  *) 
    echo "usage: inetpanel [start | begin | commence | stop | end | destory | restart | again]" 
  ;; 
esac 


*************************

  例三 : 判断系统
  有时候,您所写的Script可能会跨越好几种平台,如Linux、FreeBSD、Solaris等等,而各平台之间,多多少少都有不同之处,有时候需要判断目前正在那一种平台上执行。此时,我们可以利用uname来找出系统资讯。 
#!/bin/sh 

SYSTEM=`uname -s` 

case $SYSTEM in 
  Linux) 
    echo "My system is Linux" 
    echo "Do Linux stuff here..." 
  ;; 
  FreeBSD) 
    echo "My system is FreeBSD" 
    echo "Do FreeBSD stuff here..." 
  ;; 
  *) 
    echo "Unknown system : $SYSTEM" 
    echo "I don't what to do..." 
  ;; 
esac 


  流程控制select

  select name [ in word; ] do list ; done
  select顾名思义就是在word中选择一项。与for相同,如果[in word;]省略,将会使用Script后面所加的参数。 
  例:
#!/bin/sh 
WORD="a b c" 

select i in $WORD ; do 
 case $i in 
  a) 
   echo "I am A" 
  ;; 
  b) 
   echo "I am B" 
  ;; 
  c) 
   echo "I am C" 
  ;; 
  *) 
   break; 
  ;; 
 esac 
done 

  执行结果
  [foxman@foxman bash]# ./select_demo 
1) a 
2) b 
3) c 
#? 1 
I am A 
1) a 
2) b 
3) c 
#? 2 
I am B 
1) a 
2) b 
3) c 
#? 3 
I am C 
1) a 
2) b 
3) c 
#? 4 


  返回状态Exit

  在继续下去之前,我们必须要切入另一个话题,即返回状态值 - Exit Status。因为if/while/until都迁涉到了使用Exit Status来控制程式流程的问题。 

  *************************

  许多人都知道,在许多语言中(C/C++/Perl....),都有一个exit的函数,甚至连Bash自己都有个exit的内建命令。而exit后面所带的数字,便是返回状态值 - Exit Status。 
  返回状态值可以使得程式与程式之间,利用Shell script来结合的可能性大增,利用小程式,透过Shell script,来完成很杂的工作。 

  在shell中,返回值为零表示成功(True),非零值为失败(False)。 


  *************************

  举例来说,以下这个两个小程式yes/no分别会返回0/1(成功/失败): 
  /* yes.c */ 
  void main(void) { exit(0); } 
  /* no.c */ 
  void main(void) { exit(1); } 
  那么以下这个"YES"的shell script便会显示"YES"。 
  #!/bin/sh 
  # YES 
  if yes ; then 
  echo "YES" 
  fi 
  而"NO"不会显示任何东西。 
  #!/bin/sh 
  # NO 
  if no ; then 
  echo "YES" 
  fi 

  *************************

  test express 
  [ express ] 
  在Shell script中,test express/[ express ]这个语法被大量地使用,它是个非常实用的指令。由于它的返回值即Exit Status,经常被运用在if/while/until的场合中。而在后面,我们也会大量运用到,在进入介绍if/while/until之前,有必要 先解一下。 

  其返回值为0(True)或1(False),要看表述(express)的结果为何。 

  express格式 

  -b file : 当档案存在并且属性是Block special(通常是/dev/xxx)时,返回True。 
  -c file : 当档案存在并且属性是character special(通常是/dev/xxx)时,返回True。 
  -d file : 当档案存在并且属性是目录时,返回True。 
  -e file : 当档案存在时,返回True。 
  -f file : 当档案存在并且是正常档案时,返回True。 
  -g file : 当档案存在并且是set-group-id时,返回True。 
  -k file : 当档案存在并且有"sticky" bit被设定时,返回True。 
  -L file : 当档案存在并且是symbolic link时,返回True。 
  -p file : 当档案存在并且是name pipe时,返回True。 
  -r file : 当档案存在并且可读取时,返回True。 
  -s file : 当档案存在并且档案大小大于零时,返回True。 
  -S file : 当档案存在并且是socket时,返回True。 
  -t fd : 当fd被开启为terminal时,返回True。 
  -u file : 当档案存在并且set-user-id bit被设定时,返回True。 
  -w file : 当档案存在并且可写入时,返回True。 
  -x file : 当档案存在并且可执行时,返回True。 
  -O file : 当档案存在并且是被执行的user id所拥有时,返回True。 
  -G file : 当档案存在并且是被执行的group id所拥有时,返回True。 
  file1 -nt file2 : 当file1比file2新时(根据修改时间),返回True。 
  file1 -ot file2 : 当file1比file2旧时(根据修改时间),返回True。 
  file1 -ef file2 : 当file1与file2有相同的device及inode number时,返回True。 
  -z string : 当string的长度为零时,返回True。 
  -n string : 当string的长度不为零时,返回True。 
  string1 = string2 : string1与string2相等时,返回True。 
  string1 != string2 : string1与string2不相等时,返回True。 
  ! express : express为False时,返回True。 
  expr1 -a expr2 : expr1及expr2为True。 
  expr1 -o expr2 : expr1或expr2其中之一为True。 
  arg1 OP arg2 : OP是-eq[equal]、-ne[not-equal]、-lt[less-than]、-le[less-than-or-equal]、-gt [greater-than]、-ge[greater-than-or-equal]的其中之一。 



  *************************

  在Bash中,当错误发生在致命信号时,bash会返回128+signal number做为返回值。如果找不到命令,将会返回127。如果命令找到了,但该命令是不可执行的,将返回126。除此以外,Bash本身会返回最后一个 指令的返回值。若是执行中发生错误,将会返回一个非零的值。 
  Fatal Signal : 128 + signo 
  Can't not find command : 127 
  Can't not execute : 126 
  Shell script successfully executed : return the last command exit status 
  Fatal during execution : return non-zero

  流程控制if

  if list then list [ elif list then list ] ... [ else list ] fi
  几种可能的写法 

*************************

第一种 
if list then 
 do something here 
fi 
当list表述返回值为True(0)时,将会执行"do something here"。 

例一 : 当我们要执行一个命令或程式之前,有时候需要检查该命令是否存在,然后才执行。 
if [ -x /sbin/quotaon ] ; then 
  echo "Turning on Quota for root filesystem" 
  /sbin/quotaon / 
fi 

例二 : 当我们将某个档案做为设定档时,可先检查是否存在,然后将该档案设定值载入。 
# Filename : /etc/ppp/settings 
PHONE=1-800-COLLECT 

#!/bin/sh 
# Filename : phonebill 
if [ -f /etc/ppp/settings ] ; then 
  source /etc/ppp/settings 
  echo $PHONE 
fi 
执行 
[foxman@foxman ppp]# ./phonebill 
1-800-COLLECT 


*************************

第二种 
if list then 
 do something here 
else 
 do something else here 
fi 
例三 : Hostname 
#!/bin/sh 
if [ -f /etc/HOSTNAME ] ; then 
  HOSTNAME=`cat /etc/HOSTNAME` 
else 
  HOSTNAME=localhost 
fi 


*************************

第三种 
if list then 
 do something here 
elif list then 
 do another thing here 
fi 
例四 : 如果某个设定档允许有好几个位置的话,例如crontab,可利用if then elif fi来找寻。 
#!/bin/sh 

if [ -f /etc/crontab ] ; then 
  CRONTAB="/etc/crontab" 
elif [ -f /var/spool/cron/crontabs/root ] ; then 
  CRONTAB="/var/spool/cron/crontabs/root" 
elif [ -f /var/cron/tabs/root ] ; then 
  CRONTAB="/var/cron/tabs/root" 
fi 
export CRONTAB 


*************************

第四种 
if list then 
 do something here 
elif list then 
 do another thing here 
else 
 do something else here 
fi 
例五 : 我们可利用uname来判断目前系统,并分别做各系统状况不同的事。 
#!/bin/sh 

SYSTEM=`uname -s` 

if [ $SYSTEM = "Linux" ] ; then 
 echo "Linux" 
elif [ $SYSTEM = "FreeBSD" ] ; then 
 echo "FreeBSD" 
elif [ $SYSTEM = "Solaris" ] ; then 
 echo "Solaris" 
else 
 echo "What?" 
fi 

控制圈while/until

while list do list done
当list为True时,该圈会不停地执行。 
例一 : 无限回圈写法 
#!/bin/sh 

while : ; do 
 echo "do something forever here" 
 sleep 5 
done 

例二 : 强迫把pppd杀掉。 
#!/bin/sh 

while [ -f /var/run/ppp0.pid ] ; do 
  killall pppd 
done 


*************************

until list do list done
当list为False(non-zero)时,该圈会不停地执行。 
例一 : 等待pppd上线。 
#!/bin/sh 
until [ -f /var/run/ppp0.pid ] ; do 
  sleep 1 
done 


  参数与变数

  在继续下去介绍function之前,我们必须停下来介绍"参数与变数"。 

  *************************

  参数(Parameters)是用来储存"值"的资料型态,有点像是一般语言中的变数。它可以是个名称(name)、数字(number)、或者是以下所列出来一些特殊符号(Special Parameters)。 
  在shell中,变数是由name形式的参数所构成的。 


  *************************

  在前面的许多例中,我们事实上已经看到许多参数的运用。要设定一个Parameter实际很简单: 
  name=value 

  例如说: 

  MYHOST="foxman" 

  而要使用它时,则是加个"$"符号。 

  echo $MYHOST 

  *************************

  位置参数(Positional Parameters) 

  *************************

  所谓的位置参数便是0,1,2,3,4,5,6,7,8,9...。使用时,用$0,$1,$2...。 
  位置参数是当script被载入时,后面所附加的参数。$0是本身,$1则为第一个参数,$2为第二个,依此类推。而当Positional Parameters被function所使用时,它们会被暂时取代(下一节会介绍function)。 

  例如以下这个script: 
  #!/bin/sh 
  # Filename : position 
  echo $0 
  echo $1 

  执行时: 
  [foxman@foxman bash]# ./position abc 
  ./position 
  abc 

  当位置参数超过两位数时,有特别的方法来展开,称为Expansion。 

  *************************

  特殊参数(Speical Parameters) 
  这些符号,非常不人性,对新手来说很困扰。但上手后,会觉得方便无比,有些如果您看不懂的话,就--算了,不用浪费太多时间在上面。 

  *************************

  * 星号 
  将Positional Parameters合成一个参数,其间隔为IFS内定参数的第一个字元(见内建变数一节)。 
  例: 
  #!/bin/sh 
  # starsig 
  echo $* 

  执行: 
  [foxman@foxman bash]# starsig a b c d e f g 
  a b c d e f g 

  *************************

  @ at符号 
  与*星号类同。不同之处在于不参照IFS。 

  例: 
  #!/bin/sh 
  # atsig 
  echo $@ 

  执行: 
  [foxman@foxman bash]# atsig a b c d e f g 
  a b c d e f g 


  *************************

  # 井字号 
  展开Positional parameters的数量。 

  例: 
  #!/bin/sh 
  # poundsig 
  echo $# 

  执行 
  [foxman@foxman bash]# poundsig a b c d e f g 
  7 

  *************************

  ? 问号 
  最近执行的foreground pipeline的状态。 


  *************************

  - 减号 
  最近执行的foreground pipeline的选项参数。 

  *************************

  $ 钱钱钱 
  本身的Process ID。 

  [foxman@foxman bash]# ps ax | grep bash 
  1635 p1 S  0:00 /bin/bash 

  [foxman@foxman bash]# echo $$ 
  1635 

  *************************

  ! 惊号 
  最近执行背景命令的Process ID。 

  *************************

  0 零 
  在Positional Parameters一部份已经说明过了,是执行的shell script本身。但如果是用"bash -c",则$0被设为第一个参数。 

  [foxman@foxman bash]# echo $0 
  /bin/bash 

  *************************

  _ 底线符号 
  显示出最后一个执行的命令。 

  [foxman@foxman bash]# echo $_ 
  bash 


  *************************

  内建变数(Shell Variables) 
  Bash有许多内建变数,像PATH、HOME、ENV......等等。这些内建变数将在另一节中,专门一一说明。

  函数function

  [ function ] name () { list; }
  function的参数是Positional Paraments。 

  例 
#!/bin/sh 

function func() { 
 echo $1 
 echo $2 
 return 1 
} 

func "Hello" "function" 

  局部变数可用local来宣告。 

  函数可export,使用下一层的shell可以使用。 

  函数可递,没有递层数的限制。

  Bash内建指令集 

  以下的命令,大部份都没有使用例,您可能会看不出所以然,摸不著头脑。在我加入例说明前,建议您"man bash",然后自己实际操作一次。 

  *************************
  : [arguments] 
  不做任何事,除了[arguments]一些参数展开及一些特定重导向的作业外。 

  永远返回零。它的用法跟true一样。 

  *************************
  . filename [arguments] 
  source filename [arguments] 
  由filename中读取命令,并执行。 
  您会在/etc/rc.d/*中发现很多 
  . /xxxx 
  的指令,而xxxx的permission都不是可执行的。事实上,在tcsh中,需要用 
  source /xxxx 
  来做同样的指令。 
  注意到"."的后面是有空格的(比较一下". /"跟"./",不一样)。filename是内含指令的纯文字档即可,无须chmod 755 filename。 

  例
  filename : my_source 
  DEV=lo 
  IP=127.0.0.1 
  NETMASK=255.0.0.0 
  BROADCAST=127.255.255.255 

  ifconfig $IP netmask $NETMASK broadcast $BROADCAST dev $DEV 

  接下来 
  . my_source 
  或 
  source my_source 

  便可执行该script,而不需要"chmod 755 my_source" 

  *************************
  alias [name[=value] ...] 
  昵称命令 
  例如您如果来自DOS的世界,对UNIX的指令不习惯,可用alias来修改,以符合您的习惯。 

  例
  alias ls="ls --color" 
  alias dir="ls" 
  alias cd..="cd .." 
  alias copy="cp -f" # dangerous, recommend, "cp -i" 
  alias del="rm -f" # dangerous, recommend, "rm -i" 
  alias move="mv -f" # dangerous, recommend, "mv -i" 
  alias md="mkdir" 
  alias rd="rmdir" 

  *************************
  unalias [-a] [name ...] 
  unalias取消alias的设定。"unalias -a"将全部alias取消。 

  例
  unalias copy 

  *************************
  bg [jobspec] 
  将指定任务放到背景中,如果jobspec未指定,内定为目前的。 

  *************************
  fg [jobspec] 
  将指定任务放到前景中,如果jobsepc没有指定,那么内定为目前的。 

  *************************
  jobs [-lnp] [ jobspec ... ] 
  第一种形式列出目前正在工作的任务。 
  -l : 除了列出一般资讯外,还列出Process IDs。 
  -p : 仅列出该工作群"首脑"(Process group leader)的Process ID. 
  -n : 则仅列出有改变的jobs的状态。 
  如果给定jobspec,输出资讯则只有该jobspec。 

  返回值为零,除非有非法的选项发生。 

  jobs -x command [ args ... ] 

  如果使用第二种形式(-x),jobs取代指定的command及args,并执行返回其Exit Status。 

  *************************
  kill [-s sigspec | -sigspec] [pid | jobspec] ... 
  将sigspec的信号送到pid或jobspec。 
  sigspec可以是SIGKILL/KILL这种形式或是信号号码。如果sigspec是signal name,则大小写无关,而且可以没有SIG。 
  kill -l [signum] 
  列出信号名称。 

  [foxman@foxman bash]# kill -l 
  1) SIGHUP    2) SIGINT    3) SIGQUIT   4) SIGILL 
  5) SIGTRAP   6) SIGIOT    7) SIGBUS    8) SIGFPE 
  9) SIGKILL   10) SIGUSR1   11) SIGSEGV   12) SIGUSR2 
  13) SIGPIPE   14) SIGALRM   15) SIGTERM   17) SIGCHLD 
  18) SIGCONT   19) SIGSTOP   20) SIGTSTP   21) SIGTTIN 
  22) SIGTTOU   23) SIGURG   24) SIGXCPU   25) SIGXFSZ 
  26) SIGVTALRM  27) SIGPROF   28) SIGWINCH  29) SIGIO 
  30) SIGPWR 

  *************************
  wait [n] 
  等待指定的行程,并返回其结束状态。n可以是个jobspec或Process ID。如果n未指定,则等待所有的子行程,及返回值为零。若n为不存在的job或process,则返回127。否则,返回值为最后一个 job/process的Exit Status。 

  *************************
  bind [-m keymap] [-lvd] [-q name] 
  bind [-m keymap] -f filename 
  bind [-m keymap] keyseq:function-name 
  显示出目前readline的按键及链结函数设定或是巨集。 

  -m keymap : 设定keymap binding。 
  -l : 显示出所有readline function的名称。 
  -v : 显示出目前的function name及bindings。 
  -d : 显示出function name及bindings。 
  -f filename : 从filename读取key bindings。 
  -q function : 询问那个按键触发function。 

  *************************
  break [n] 
  跳出控制回圈for/while/until中使用。如果有指定n,则跳出n层。n必须是大于等于1。若n大于巢状圈数,则所有的圈都会跳离。返回值回零。 

  *************************
  continue [n] 
  还原控制回圈for/while/until中使用。如果有指定n,则返回n层。n必须是大于等于1。若n大于巢状圈数,则还原到最上层。返回值回零。 

  *************************
  exit [n] 
  离开程式。n是Exit Status。 

  *************************
  return [n] 
  在function中使用。n为返回值,其作用与Exit Status一样。 

  *************************
  builtin shell-builtin [arguments] 
  执行内建函数。当您定义了与内建函数相同的指令时,可用此命令来执行内建函数。 

  *************************
  cd [dir] 
  更换目录到dir。如果没有指定,内定为HOME所指定的目录。 

  *************************
  command [-pVv] command [arg ...] 
  用command指定可取消正常的shell function寻找。只有内建命令及在PATH中找得到的才会被执行。"-p"选项,搜寻命令的方式是用PATH来找。"-V"或"-v"选项,会显示出该命令的一些简约描述。 

  *************************
  declare [-frxi] [name[=value]] 
  typeset [-frxi] [name[=value]] 
  宣告参数并给它们设定属性。如果没有给定名称,将会显示各参数值。 

  -f : 仅使用函数名称。 
  -r : 将name设为readonly。 
  -x : 将name输出给后续环境使用。 
  -i : 该参数被设为integer来使用,可用于算术表述。 

  用"+"时,关闭该属性。 

  *************************
  dirs [-l] [+/-n] 
  显示目前记忆的目录。目录可透过pushd/popd来操作。 

  +n : 显示开始的记录n个。 
  -n : 显示结尾的记录n个。 
  -l : 显示较多的资讯。 

  *************************
  echo [-neE] [arg ...] 
  输出显示args,由空白分隔。返回值永为零。 

  -n : 不跳行。 
  -e : 启动"\"符号的解译。 
  -E : 将ESC解译功能取消。 

  "\a" : alert(bell),发出声响。 
  "\b" : backspace,倒退。 
  "\c" : suppress trailing newline,不跳行。 
  "\f" : form feed,跳行跳格。 
  "\n" : new line,新行。 
  "\r" : carriage return,回到行起点。 
  "\t" : horizontal tab,水平跳位。 
  "\v" : vertical tab,垂直跳位。 
  "\\" : 输出"\"。 
  "\nnn" : 输出ASCII Code号码nnn(八进位)。 

  *************************
  enable [-n] [-all] [name ...] 
  启动或关闭内建函数命令。使用"-n"将所有指定命令皆关闭,否则都是启动的。如果只有"-n"参数,它将会显示所有关闭的函数。如果只有"-all",它将会显示所有内建命令。 

  *************************
  eval [arg ...] 
  读取args,并将args合为一个命令,然后执行。其返回值成为eval的返回值。如果没有参数,eval返回True。 

  *************************
  exec [[-] command [arguments]] 
  当命令执行时,该命令取代shell,没有新的process产生。如果第一个参数是"-",shell会将"-"放入第零个参数,传给command。 

  *************************
  export [-nf] [name[=word]] ... 
  export -p 
  将name输出给环境,给往后的命令使用。"-f"选项表示name是函数。"-p"显示出所有export的名称。"-n"移除name。 

  *************************
  set [--abefhkmnptuvxldCHP] [-o option] [arg ...] 
  -a : 自动将变数标记为可让后面环境所使用。 
  -b : 立即报告被终结的背景程式状态。 
  -e : 当命令(simple-command,见后面)返回非零值时,立即跳出。 
  -f : 取消pathname expansion。 
  -h : 找出所记忆的函数命令位置。 
  -k : 所有keyword参数都放到环境中。 
  -m : 监督模式。 
  -n : 读取命令,但不要执行。可用于语法检查。 
  -p : 打开privileged模式。 
  -t : 当读取一个命令并执行后,立即离开。 
  -u : 当参数展开时,把unset参数当成是错误。 
  -v : 列出shell input lines。 
  -x : 在展开每个simple-command后,bash显示展开值在PS4上。 
  -l : 储存并还原name binding在for语法中。 
  -d : 关闭hasing command搜寻。 
  -C : 跟`noclobber=`一样。请见内定参数一节。 
  -H : 启动! style history substitution。 
  -P : 在使用像cd这种指令时,不要跟随symbolic links。 
  -- : "--"之后,没有参数跟在后面。 
  - : 指定将所有后面的参数当成是位置参数。 
  -o option-name : option-name可以是以下之一 
  allexport : 与"-a"相同。 
  braceexpand : 启动Brace Expansion。这是内定设定。 
  emacs : 使用emacs-style命令列编辑界面。 
  errexit : 与"-e"相同。 
  histexpand : 与"-H"相同。 
  ignoreeof : 效果跟`IGNOREEOF=10`一样。 
  interactive-commands : 允许#做为解。 
  monitor : 与"-m"相同。 
  noclobber : 与"-C"相同。 
  noexec : 与"-n"相同。 
  noglob : 与"-f"相同。 
  nohash : 与"-d"相同。 
  notify : 与"-b"相同。 
  nounset : 与"-u"相同。 
  physical : 与"-P"相同。 
  posix : Bash行为修改为Posix 1003.2标准。 
  privileged : 与"-p"相同。 
  verbose : 与"-v"相同。 
  vi : 使用vi-style命令列编辑程式。 
  xtrace : 与"-x"相同。 

  *************************
  unset [-fv] [name ...] 
  移除对映于name的参数。要注意PATH、IFS、PPID、PS1、PS2、UID、EUID不能unset。若RANDOM、SECONDS、 LINENO、HISTCMD被unset,它们会丧失原有意义,既始它们后来被重设也一样。返回值为True,除非name是不能被unset的。 

  *************************
  fc [-e ename] [-nlr] [first] [last] 
  fc -s [pat=rep] [cmd] 
  修正命令。 

  *************************
  getopts optstring name [args] 
  解析位置参数。 

  *************************
  help [pattern] 
  显示协助资讯。 

  *************************
  history [n] 
  history -rwan [filename] 
  没有参数时,会显示所下命令的历史记录。带有参数"n"则显示最后n个。 

  其它参数如下: 
  -a : 新增"新历史"到历史档中。 
  -n : 读取尚未读到历史中的记录。 
  -r : 读取filename做为历史档,并用它为目前历史记录。 
  -w : 将现有历史记录写到filename中。 

  *************************
  let arg [arg ...] 
  算术表述。请参考算术表述一节。 

  *************************
  local [name[=value] ...] 
  产生一个局部参数。如果用于function,则其作用围在function内及其子函数。 

  *************************
  logout 
  离开login shell。 

  *************************
  popd [+/-n] 
  移除目录堆叠。"+n"移除上面n个,"-n"移除下面n个。 


  *************************
  pushd [dir] 
  pushd +/-n 
  将目录新增到目录堆叠的最上面。"+n"旋转该堆叠,使第n个目录变成最上面。"-n"旋转该堆叠,使倒数第n个目录变成最上面。 

  *************************
  pwd 
  列出目前工作目录的绝对路径。 

  *************************
  read [-r] [name ...] 
  读进一行,然后第一个字设到第一个name,第二个设到第二个name,依此类推。如果没有name在参数中,则read会将值设到REPLY。返回值为零,除非遇到End-Of-File。若有"-r"选项,则"\n"被考虑为该行的一部份。 

  *************************
  readonly [-f] [name ...] 
  readonly -p 
  将给定的name标记为readonly。如果是"-f"选项,则函数也一样被标记为readonly。"-p"会列出所有readonly的name。"--"取消检查剩余的参数。 

  *************************
  shift [n] 
  Positional Parameters从n+1...开始,会被改为$1...。n若为零,则没有改变。n若未给定,则内定为1。n必须是非负数,并且小于或等于$#。若n大于$#,则没有改变。返回值为零,除非n大于$#或小于零。 

  *************************
  suspend [-f] 
  暂停这个shell的执行,直到它收到SIGCONT信号。"-f"选项则是叫login shell不要抱怨,不过还是一样暂停。返回状态零,除非该shell是个login shell,而且没有"-f"选项。 

  *************************
  test expr 
  [ expr ] 
  我们在Exit Status的部份已经说过了,不再重。 

  *************************
  times 
  列出该shell的累积的使用者及系统时间及从shell执行的process时间,返回值为零。 

  ------------------------------------------------------------------------------
  trap [-l] [arg] [sigspec] 
  当收到sigspec信号时,执行arg命令。"-l"显示出信号名称及号码。 

  *************************
  type [-all] [-type | -path] name [name ...] 
  没有参数的状况下,它会显示出shell如何解译name做为命令。如果有"-type",它将会显示alias、keyword、 function、builtin或file。如果有"-path"的参数,它将会显示该命令的路径,找不到的话,不显示任何东西。如果有"-all"的 参数,它将会显示所有可执行name的可能路径。type接受"-a"、"-t"、"-p"做为缩写。 

  *************************
  ulimit [-SHacdfmstpnuv [limit]] 
  ulimit提供了对shell的可获取资源控制的功能。 

  -a : 报告目前所有限制。 
  -c : 设定最大可产生的core档案。 
  -d : 行程资料段(process's data segment)最大值。 
  -f : 可被这个shell产生的最大档案。 
  -m : resident set size最大值。 
  -s : 堆叠最大值。 
  -t : CPU TIME最大值(以秒计算)。 
  -p : pipe size in 512-byte blocks的最大值。 
  -n : 可开启的file descriptors最大值。 
  -u : 单一使用者可使用的最大process数。 
  -v : 该shell最大虚拟记忆体可用值。 

  所有项目是以1024做为单位。 

  *************************
  umask [-S] [mode] 
  将使用者的file-creation mask设为mode。"-S"选项将mask印成符号形式。


  Bash内建参数

  PPID : 该bash的呼叫者process ID. 

  PWD : 目前的工作目录。 

  OLDPWD : 上一个工作目录。 

  REPLY : 当read命令没有参数时,直接设在REPLY上。 

  UID : User ID。 

  EUID : Effective User ID。 

  BASH : Bash的完整路径。 

  BASH_VERSION : Bash版本。 

  SHLVL : 每次有Bash执行时,数字加一。 

  RANDOM : 每次这个参数被用到时,就会产生一个乱数在RANDOM上。 

  SECONDS : 从这个Shell一开始启动后的时间。 

  LINENO : Script的行数。 

  HISTCMD : 历史记录数。 

  OPTARG : getopts处理的最后一个选项参数。 

  OPTIND : 下一个要由getopts所处理的参数号码。 

  HOSTTYPE : 机器种类。 

  OSTYPE : 作业系统名称。 

  IFS : Internal Field Separator。 

  PATH : 命令搜寻路径。 
  PATH="/usr/gnu/bin:/usr/local/bin:/usr/ucb:/bin:/usr/bin:." 

  HOME : 目前使用者的home directory; 

  CDPATH : cd命令的搜寻路径。 

  ENV : 如果这个参数被设定,每次有shell script被执行时,将会执行它所设定的档名做为环境设定。 

  MAIL : 如果这个参数被设定,而且MAILPATH没有被设定,那么有信件进来时,bash会通知使用者。 

  MAILCHECK : 设定多久时间检查邮件一次。 

  MAILPATH : 一串的邮件检查路径。 

  MAIL_WARNING : 如果有设定的话,邮件被读取后,将会显示讯息。 

  PS1 : 提示讯息设定,内定为"bash\$ "。(请详见提示讯息一节。) 

  PS2 : 第二提示讯息设定,内定为"> "。 

  PS3 : select命令所使用的提示讯息。 

  PS4 : 执行追踪时用的提示讯息设定,内定为"+ "。 

  HISTSIZE : 命令历史记录量,内定为500。 

  HISTFILE : 历史记录档,内定~/.bash_history。 

  HISTFILESIZE : 历史记录档行数最大值,内定500。 

  OPTERR : 如果设为1,bash会显示getopts的错误。 

  PROMPT_COMMAND : 如果设定的话,该值会在每次执行命令前都显示。 

  IGNOREEOF : 将EOF值当成输入,内定为10。 

  TMOUT : 如果设为大于零,该值被解译为输入等待秒数。若无输入,当成没有输入。 

  FCEDIT : fc命令的内定编辑器。 

  FIGNORE : 请详见READLINE。 

  INPUTRC : readline的startup file,内定~/.inputrc 

  notify : 如果设定了,bash立即报告被终结的背景程式。 

  history_control, HISTCONTROL : history使用。 

  command_oriented_history : 存入多行指令。 

  glob_dot_filenames : 如果设定了,bash将会把"."包含入档案路径中。 

  allow_null_glob_expansion : 如果设定了,bash允许路径明称为null string。 

  histchars : history使用。 

  nolinks : 如果设定了,执行指令时,不会跟随symbolic links。 

  hostname_completion_file, HOSTFILE : 包含与/etc/hosts相同格式的档名。 

  noclobber : 如果设定了,Bash不会覆写任何由">"、">&"及"<>"所操作的档案。 

  auto_resume : 请见任务控制一节。 

  no_exit_on_failed_exec : 如果该值存在,非互动的shell不会因为exec失败而跳出。 

  cdable_vars : 如果启动,而cd命令找不到目录,可切换到参数形态指定的目录下。


  提示符号

  Bash使用PS1~PS4来显示提示符号,其格式如下: 

  *************************

  \t : 现在时间。 
  \d : 现在日期。 
  \n : 新行。 
  \s : shell的名称。 
  \w : 目前工作目录。 
  \W : 目前工作目录完整路径。 
  \u : 使用者名称。 
  \h : Hostname。 
  \# : 这个命令的号码。 
  \! : 历史号码。 
  \$ : 如果EUID是0,则#,否则为$。 
  \nnn : 八进位的字元。 
  \\ : "\"符号。 
  \[ : 开始一序列不可列印的字元。 
  \] : 结束一序列不可列印的字元。


  算术表述

  - + 
  ! ~ 
  * / % 
  + - 
  << >> 
  <= >= < > 
  == != 
  & 
  ^ 
  | 
  && 
  || 
  = *= /= %= += -= <<= >>= &= ^= |= 

  重导Redirection

  > 
  >> 
  1> 
  . 
  . 


  语法

  Simple Command 


  Pipelines 


  Lists 

  (list) 
  { list; }
posted @ 2006-06-16 14:20 Alex 阅读(340) | 评论 (0)编辑 收藏

key words: css  隔行换色 控制表格  交替颜色

最近用到不少css的东西,发现用css是可以省不少麻烦,特别更改效果。

google了一篇 利用CSS控制表格的交替颜色

下面是实现的代码:

<style type="text/css">
<!--
tr 
{background-color:expression((this.sectionRowIndex==0)?"":
(this.sectionRowIndex%2==0)?"red":"blue")
}
-->
</style>
</HEAD>
<table>
<tr>
<td>第1行</td><td>第1行</td>
</tr>
<tr>
<td>第2行</td><td>第2行</td>
</tr>
<tr>
<td>第3行</td><td>第3行</td>
</tr>
<tr>
<td>第4行</td><td>第4行</td>
</tr>
<tr>
<td>第5行</td><td>第5行</td>
</tr>
</table>

关键就是这一句:
tr {background-color:expression((this.sectionRowIndex==0)?"":
(this.sectionRowIndex%2==0)?"red":"blue")}


对于td的控制如下:
<style type="text/css">
<!--
tr 
{background-color:expression((this.sectionRowIndex%2==0)?"red":"blue")}
td 
{background-color:expression((this.cellIndex%2==0)?"":((this.parentElement.sectionRowIndex%2==0)?"green":"yellow"))}
-->
</style>
</HEAD>
<table>
<tr><td>第1行</td><td>第1行</td><td>第1行< /td><td>第1行</td><td>第1行</td></tr>
<tr><td>第2行</td><td>第2行</td><td>第2行< /td><td>第2行</td><td>第2行</td></tr>
<tr><td>第3行</td><td>第3行</td><td>第3行< /td><td>第3行</td><td>第3行</td></tr>
<tr><td>第4行</td><td>第4行</td><td>第4行< /td><td>第4行</td><td>第4行</td></tr>
<tr><td>第5行</td><td>第5行</td><td>第5行< /td><td>第5行</td><td>第5行</td></tr>
</table>

cellIndex 

另:和数组一样,第一行从0开始



但愿对你有用.  :)



posted @ 2006-06-15 18:14 Alex 阅读(2974) | 评论 (2)编辑 收藏