2005年12月6日
#
中国人民可以说是被盗版和Windows惯坏了。在Linux默认环境下的中文显示至今惨不忍睹。
看看现在一些主要的发行版本,默认设置下的日语的显示已经相当不错了。
作为汉字发祥地且拥有众多人口的中国,实在是有些悲哀。
文泉驿计划正在为实现这个目标努力。作为一个非盈利性组织,他们的贡献的确值得赞赏。
最近方正正在为字体的事情打官司,并提出了高额赔偿的要求。建议方正要是能够胜诉的话,贡献一些字体给开源组织,为弘扬汉语言文化多做点贡献。
- surefire-report 生成单元测试报告
- jdepend 生成包的度量报告
- findbugs 检查潜在的缺陷
- cobertura 生成测试覆盖率报告
以上是一些常用的用于品质管理的插件。默认情况下都不用配置,相当方便。如果需要手动配置的话,根据网上的文档也相当容易配置。
apache的maven plugin页面:
http://maven.apache.org/plugins/codehaus mojo 页面:
http://mojo.codehaus.org
2006 Sun Techdays Shanghai 的第2天下午有一个名为《
Java Scripting: One VM, Many Languages》的Session。
Rags为大家展示了Mustang的一个新特性,Scripting in Java——脚本语言支持。
通过加入脚本引擎的支持,就能够在Java中解释Javascript,python,ruby等诸多脚本语言。
对于这个特性,想到的一个可能的应用就是在annotation中写脚本语言,然后在代码中用相应的脚本语言引擎解释执行。
保留到运行时的annotation可以用实现aop的功能,使用非inline的脚本就可以更灵活地控制aspect的行为。
比如:
//inline scripting
@ScriptBefore(script="",language="javascript" )
public void foo() {
}
//non-inline scripting
@ScriptBefore(file="scripts/logging.js",language="javascript")
public void bar() {
}
前些天,和一位XPer进行了一次愉快的谈话。他向我讲述了一些感觉很有效的实践。
关于过程和迭代
他曾经参与过的项目的迭代是以月为迭代单位的,但事实上每周都会重复一个简单的过程。
在迭代过程中,他非常推崇Burn-Down Charts。这是一个Scrum的工具。通过Burn-Down Charts,能够把过程中间的变化记录下来,使过程高度可视化。等到一次迭代完成,回顾一下所有的Burn-Down Charts就能作为改进的判断依据。
KPT Meeting。所谓KPT Meeting就是 Keep-Prevent-Try metting。小组定期举行KPT会议(基本上是每周一次)。在KTP会议上,通过头脑风暴的方式每个人(不是某几个人)把各自认为前一阶段里做得好的方面写在Keep一栏里;做得不好的方面写在Prevent一栏里;希望尝试的写在Try一栏里。然后大家对这些项目进行评估和筛选。下一阶段中,Keep的项目继续保持,Prevent的项目应该杜绝,Try的项目进行尝试。
工具
在开展这些实践的时候,交流比较频繁。首推的工具是Mini white board和DC。
选择Mini white board的原因并不是因为带有"mini"听上去会像 Mini Cooper 或者 iPod mini 那么cool。因为一块A3左右大小的白板非常适合个人或者结对使用,而且环保(省去了草稿纸)。虽然整个团队也有用于大规模交流的更大的白板,但那属于“竞争资源”,各自使用自己的白板更为方便。
交流结果产生后,为了不花不必要的时间去做精美的文档,一台轻便的DC往往是最合适的选择。当然,如果足够,手机上的照相功能也可以完成同样的任务。相比偷拍街上的MM,这些电子产品能够实现更大的价值。
关于结对
每天进行6小时的结对编程,分3次,每次2小时。每次和不同的成员组队。在结队的时候充分利用了上面提到的工具进行交流。如果出现两个人不能解决的问题的时候,会立即向整个团队提出,这样可能导致一次stand-up meeting。即使问题不能马上解决,至少也能确保每个人都知道这个问题。
locale是国际化与本土化过程中的一个非常重要的概念,个人认为,对于中文用户来说,通常会涉及到的国际化或者本土化,大致包含三个方面:看中文,写中文,与window中文系统的兼容和通信。从实际经验上看来,locale的设定与看中文关系不大,但是与写中文,及window分区的挂载方式有很密切的关系。本人认为就像一个纯英文的Windows能够浏览中文,日文或者意大利文网页一样,你不需要设定locale就可以看中文。那么,为什么要设定locale呢?什么时候会用到locale呢?
一、为什么要设定locale
正如前面我所讲的,设定locale与你能否浏览中文的网页没有直接的关系,即便你把locale设置成en_US.ISO-8859-1这样一个标准的英文locale你照样可以浏览中文的网页,只要你的系统里面有相应的字符集(这个都不一定需要)和合适的字体(如simsun),浏览器就可以把网页翻译成中文给你看。具体的过程是网络把网页传送到你的机器上之后,浏览器会判断相应的编码的字符集,根据网页采用的字符集,去字体库里面找合适的字体,然后由文字渲染工具把相应的文字在屏幕上显示出来。
在下文本人会偶尔把字符集比喻成密码本,个人觉得对于一些东西比较容易理解,假如你不习惯的话,把全文copy到任何文本编辑器,用字符集替换密码本即可。
那有时候网页显示乱码或者都是方框是怎么回事呢?个人认为,显示乱码是因为设定的字符集不对(或者没有相应的字符集),例如网页是用UTF-8编码的,你非要用GB2312去看,而系统根据GB2312去找字体,然后在屏幕上显示,当然是一堆的乱码,也就是说你用一个错误的密码本去翻译发给你的电报,当然内容那叫一个乱;至于有些时候浏览的网页能显示一部分汉字,但有很多的地方是方框,能够显示汉字说明浏览器已经正确的判断出了网页的编码,并在字体库里面找到了相应的文字,但是并不是每个字体库都包含某个字符集全部的字体的缘故,有些时候会显示不完全,找一个比较全的支持较多字符集的字体就可以了。
既然我能够浏览中文网页,那为什么我还要设定locale呢?
其实你有没有想过这么一个问题,为什么gentoo官方论坛上中文论坛的网页是用UTF-8编码的(虽然大家一直强烈建议用GB2312编码),但是新浪网就是用GB2312编码的呢?而Xorg的官方网页竟然是ISO-8859-15编码的,我没有设定这个locale怎么一样的能浏览呢?这个问题就像是你有所有的密码本,不论某个网站是用什么字符集编码的,你都可以用你手里的密码本把他们翻译过来,但问题是虽然你能浏览中文网页,但是在整个操作系统里面流动的还是英文字符。所以,就像你能听懂英语,也能听懂中文。
最根本的问题是:你不可以写中文。
当你决定要写什么东西的时候,首先要决定的一件事情是用那种语言,对于计算机来说就是你要是用哪一种字符集,你就必须告诉你的linux系统,你想用那一本密码本去写你想要写的东西。知道为什么需要用GB2312字符集去浏览新浪了吧,因为新浪的网页是用GB2312写的。
为了让你的Linux能够输入中文,就需要把系统的locale设定成中文的(严格说来是locale中的语言类别LC_CTYPE ),例如zh_CN.GB2312、zh_CN.GB18030或者zh_CN.UTF-8。很多人都不明白这些古里古怪的表达方式。这个外星表达式规定了什么东西呢?这个问题稍后详述,现在只需要知道,这是locale的表达方式就可以了。
二、到底什么是locale?
locale这个单词中文翻译成地区或者地域,其实这个单词包含的意义要宽泛很多。Locale是根据计算机用户所使用的语言,所在国家或者地区,以及当地的文化传统所定义的一个软件运行时的语言环境。
这个用户环境可以按照所涉及到的文化传统的各个方面分成几个大类,通常包括用户所使用的语言符号及其分类(LC_CTYPE),数字(LC_NUMERIC),比较和排序习惯(LC_COLLATE),时间显示格式(LC_TIME),货币单位(LC_MONETARY),信息主要是提示信息,错误信息, 状态信息, 标题, 标签, 按钮和菜单等(LC_MESSAGES),姓名书写方式(LC_NAME),地址书写方式(LC_ADDRESS),电话号码书写方式(LC_TELEPHONE),度量衡表达方式(LC_MEASUREMENT),默认纸张尺寸大小(LC_PAPER)和locale对自身包含信息的概述(LC_IDENTIFICATION)。
所以说,locale就是某一个地域内的人们的语言习惯和文化传统和生活习惯。一个地区的locale就是根据这几大类的习惯定义的,这些locale定义文件放在/usr/share/i18n/locales目录下面,例如en_US, zh_CN and
de_DE@euro都是locale的定义文件,这些文件都是用文本格式书写的,你可以用写字板打开,看看里边的内容,当然出了有限的注释以外,大部分东西可能你都看不懂,因为是用的Unicode的字符索引方式。
上面我们说到了zh_CN.GB18030的前半部分,后半部分是什么呢?大部分Linux用户都知道是系统采用的字符集。
三、什么是字符集?
字符集就是字符,尤其是非英语字符在系统内的编码方式,也就是通常所说的内码,所有的字符集都放在/usr/share/i18n/charmaps,所有的字符集也都是用Unicode编号索引的。Unicode用统一的编号来索引目前已知的全部的符号。而字符集则是这些符号的编码方式,或者说是在网络传输,计算机内部通信的时候,对于不同字符的表达方式,Unicode是一个静态的概念,字符集是一个动态的概念,是每一个字符传递或传输的具体形式。就像Unicode编号U59D0是代表姐姐的“姐”字,但是具体的这个字是用两个字节表示,三个字节,还是四个字节表示,是字符集的问题。例如:UTF-8字符集就是目前流行的对字符的编码方式,UTF-8用一个字节表示常用的拉丁字母,用两个字节表示常用的符号,包括常用的中文字符,用三个表示不常用的字符,用四个字节表示其他的古灵精怪的字符。而GB2312字符集就是用两个字节表示所有的字符。需要提到一点的是Unicode除了用编号索引全部字符以外,本身是用四个字节存储全部字符,这一点在谈到挂载windows分区的时候是非常重要的一个概念。所以说你也可以把Unicode看作是一种字符集(我不知道它和UTF-32的关系,反正UTF-32就是用四个字节表示所有的字符的),但是这样表述符号是非常浪费资源的,因为在计算机世界绝大部分时候用到的是一个字节就可以搞定的26个字母而已。所以才会有UTF-8,UTF-16等等,要不然大同世界多好,省了这许多麻烦。
四、zh_CN.GB2312到底是在说什么?
Locale 是软件在运行时的语言环境, 它包括语言(Language), 地域 (Territory) 和字符集(Codeset)。一个locale的书写格式为: 语言[_地域[.字符集]]. 所以说呢,locale总是和一定的字符集相联系的。下面举几个例子:
1、我说中文,身处中华人民共和国,使用国标2312字符集来表达字符。
zh_CN.GB2312=中文_中华人民共和国+国标2312字符集。
2、我说中文,身处中华人民共和国,使用国标18030字符集来表达字符。
zh_CN.GB18030=中文_中华人民共和国+国标18030字符集。
3、我说中文,身处中华人民共和国台湾省,使用国标Big5字符集来表达字符。
zh_TW.BIG5=中文_台湾.大五码字符集
4、我说英文,身处大不列颠,使用ISO-8859-1字符集来表达字符。
en_GB.ISO-8859-1=英文_大不列颠.ISO-8859-1字符集
五、怎样去自定义locale
在gentoo生成locale还是很容易的,首先要在USE里面加入userlocales支持,然后编辑locales.build文件,这个文件用来指示glibc生成locale文件。
很多人不明白每一个条目是什么意思。 其实根据上面的说明现在应该很明确了。
File: /etc/locales.build
en_US/ISO-8859-1
en_US.UTF-8/UTF-8
zh_CN/GB18030
zh_CN.GBK/GBK
zh_CN.GB2312/GB2312
zh_CN.UTF-8/UTF-8
上面是我的locales.build文件,依次的说明是这样的:
en_US/ISO-8859-1:生成名为en_US的locale,采用ISO-8859-1字符集,并且把这个locale作为英文_美国locale类的默认值,其实它和en_US.ISO-8859-1/ISO-8859-1没有任何区别。
en_US.UTF-8/UTF-8:生成名为en_US.UTF-8的locale,采用UTF-8字符集。
zh_CN/GB18030:生成名为zh_CN的locale,采用GB18030字符集,并且把这个locale作为中文_中国locale类的默认值,其实它和zh_CN.GB18030/GB18030没有任何区别。
zh_CN.GBK/GBK:生成名为zh_CN.GBK的locale,采用GBK字符集。
zh_CN.GB2312/GB2312:生成名为zh_CN.GB2312的locale,采用GB2312字符集。
zh_CN.UTF-8/UTF-8:生成名为zh_CN.UTF-8的locale,采用UTF-8字符集。
关于默认locale,默认locale可以简写成en_US或者zh_CN的形式,只是为了表达简单而已没有特别的意义。
Gentoo在locale定义的时候掩盖了一些东西,也就是locale的生成工具:localedef。
在编译完glibc之后你可以用这个localedef 再补充一些locale,就会更加理解locale了。具体的可以看 localedef 的manpage。
$localedef -f 字符集 -i locale定义文件 生成的locale的名称
例如
$localedef -f UTF-8 -i zh_CN zh_CN.UTF-8
上面的定义方法和在locales.build中设定zh_CN.UTF-8/UTF-8的结果是一样一样的。
六、locale的五脏六腑
刚刚生成了几个locale,但是为了让它们生效,必须告诉Linux系统使用那(几)个locale。这就需要对locale的内部机制有一点点的了解。在前面我已经提到过,locale把按照所涉及到的文化传统的各个方面分成12个大类,这12个大类分别是:
1、语言符号及其分类(LC_CTYPE)
2、数字(LC_NUMERIC)
3、比较和排序习惯(LC_COLLATE)
4、时间显示格式(LC_TIME)
5、货币单位(LC_MONETARY)
6、信息主要是提示信息,错误信息, 状态信息, 标题, 标签, 按钮和菜单等(LC_MESSAGES)
7、姓名书写方式(LC_NAME)
8、地址书写方式(LC_ADDRESS)
9、电话号码书写方式(LC_TELEPHONE)
10、度量衡表达方式(LC_MEASUREMENT)
11、默认纸张尺寸大小(LC_PAPER)
12、对locale自身包含信息的概述(LC_IDENTIFICATION)。
其中,与中文输入关系最密切的就是 LC_CTYPE, LC_CTYPE 规定了系统内有效的字符以及这些字符的分类,诸如什么是大写字母,小写字母,大小写转换,标点符号、可打印字符和其他的字符属性等方面。而locale定义zh_CN中最最重要的一项就是定义了汉字(Class “hanzi”)这一个大类,当然也是用Unicode描述的,这就让中文字符在Linux系统中成为合法的有效字符,而且不论它们是用什么字符集编码的。
LC_CTYPE
% This is a copy of the "i18n" LC_CTYPE with the following modifications: - Additional classes: hanzi
copy "i18n"
class "hanzi"; /
% <U3400>..<U4DBF>;/
<U4E00>..<U9FA5>;/
<UF92C>;<UF979>;<UF995>;<UF9E7>;<UF9F1>;<UFA0C>;<UFA0D>;<UFA0E>;/
<UFA0F>;<UFA11>;<UFA13>;<UFA14>;<UFA18>;<UFA1F>;<UFA20>;<UFA21>;/
<UFA23>;<UFA24>;<UFA27>;<UFA28>;<UFA29>
END LC_CTYPE
在en_US的locale定义中,并没有定义汉字,所以汉字不是有效字符。所以如果要输入中文必须使用支持中文的locale,也就是zh_XX,如zh_CN,zh_TW,zh_HK等等。
另外非常重要的一点就是这些分类是彼此独立的,也就是说LC_CTYPE,LC_COLLATE和 LC_MESSAGES等等分类彼此之间是独立的,可以根据用户的需要设定成不同的值。这一点对很多用户是有利的,甚至是必须的。例如,我就需要一个能够输入中文的英文环境,所以我可以把LC_CTYPE设定成zh_CN.GB18030,而其他所有的项都是en_US.UTF-8。
七、怎样设定locale呢?
设定locale就是设定12大类的locale分类属性,即 12个LC_*。除了这12个变量可以设定以外,为了简便起见,还有两个变量:LC_ALL和LANG。它们之间有一个优先级的关系:
LC_ALL>LC_*>LANG
可以这么说,LC_ALL是最上级设定或者强制设定,而LANG是默认设定值。
1、如果你设定了LC_ALL=zh_CN.UTF-8,那么不管LC_*和LANG设定成什么值,它们都会被强制服从LC_ALL的设定,成为 zh_CN.UTF-8。
2、假如你设定了LANG=zh_CN.UTF-8,而其他的LC_*=en_US.UTF-8,并且没有设定LC_ALL的话,那么系统的locale设定以LC_*=en_US.UTF-8。
3、假如你设定了LANG=zh_CN.UTF-8,而其他的LC_*,和LC_ALL均未设定的话,系统会将LC_*设定成默认值,也就是LANG的值 zh_CN.UTF-8 。
4、假如你设定了LANG=zh_CN.UTF-8,而其他的LC_CTYPE=en_US.UTF-8,其他的LC_*,和LC_ALL均未设定的话,那么系统的locale设定将是:LC_CTYPE=en_US.UTF-8,其余的 LC_COLLATE,LC_MESSAGES等等均会采用默认值,也就是LANG的值,也就是LC_COLLATE=LC_MESSAGES=……= LC_PAPER=LANG=zh_CN.UTF-8。
所以,locale是这样设定的:
1、如果你需要一个纯中文的系统的话,设定LC_ALL= zh_CN.XXXX,或者LANG= zh_CN.XXXX都可以,当然你可以两个都设定,但正如上面所讲,LC_ALL的值将覆盖所有其他的locale设定,不要作无用功。
2、如果你只想要一个可以输入中文的环境,而保持菜单、标题,系统信息等等为英文界面,那么只需要设定LC_CTYPE=zh_CN.XXXX,LANG=en_US.XXXX就可以了。这样LC_CTYPE=zh_CN.XXXX,而LC_COLLATE=LC_MESSAGES=……= LC_PAPER=LANG=en_US.XXXX。
3、假如你高兴的话,可以把12个LC_*一一设定成你需要的值,打造一个古灵精怪的系统:
LC_CTYPE=zh_CN.GBK/GBK(使用中文编码内码GBK字符集);
LC_NUMERIC=en_GB.ISO-8859-1(使用大不列颠的数字系统)
LC_MEASUREMEN=de_DE@euro.ISO-8859-15(德国的度量衡使用ISO-8859-15字符集)
罗马的地址书写方式,美国的纸张设定……。估计没人这么干吧。
4、假如你什么也不做的话,也就是LC_ALL,LANG和LC_*均不指定特定值的话,系统将采用POSIX作为lcoale,也就是C locale。
转自
http://www.linuxsir.org/bbs/showthread.php?t=32116
pg_hba.conf 文件
客户端认证是由 $PGDATA 目录里的文件pg_hba.conf 控制的,也就是说, /usr/local/pgsql/data/pg_hba.conf. (HBA 的意思是 host-based authentication:基于主机的认证.) 在initdb初始化数据区的时候,它会 安装一个缺省的文件.
文件 pg_hba.conf 的常用格式是一套记录, 每行一条。空白行或者井号(“#”)开头的行被忽略。一条记录 是由若干用空格和/或 tab 分隔的字段组成。
每条记录可以下面三种格式之一
local database authentication-method [ authentication-option ]
host database IP-address IP-mask authentication-method [ authentication-option ]
hostssl database IP-address IP-mask authentication-method [ authentication-option ]
各个字段的含义如下:
local
这条记录适用于通过 Unix 域套接字的联接.
host
这条记录适用于通过 TCP/IP 网络的联接.请注意,除非服务器是 带着 -i 选项或者等效的配置参数集启动的,否则 TCP/IP 联接将完全被禁止掉.
hostssl
这条记录适用于试图建立在 TCP/IP 上的 SSL 之上的联接. 要使用这个选项,服务器必须带着 SSL 支持编译.而且在服务器启动的时候, 必须用 -l 选项 或等效的配置设置打开 SSL.
database
声明记录所适用的数据库。值 all 表明该记录应用于所有数据库, 值 sameuser 表示于正在联接的用户同名的数据库。 否则它就是某个具体的 Postgres 数据库名字.
IP address, IP mask
这两个字段以各主机的 IP 地址为基础, 控制一条 host 记录应用于哪个主机. (当然,IP 地址可能会被欺骗(spoofed),但是这个考虑 超过了 Postgres 的考虑范围.) 准确的逻辑是,对于匹配的记录
(actual-IP-address xor IP-address-field) and IP-mask-field
必需为零.
authentication method(认证方法)
声明一个用户在与该数据库联接的时候必须使用的认证方法. 可能的选择如下,详细情况在 Section 4.2.
trust
无条件地允许联接.这个方法允许任何有登录客户机权限的用户以任意 Postgres 数据库用户身份进行联接.
reject
联接无条件拒绝.常用于从组中“过滤”某些主机.
password
要求客户端在尝试联接的时候提供一个口令, 这个口令与为该用户设置的口令必须匹配.
在 password 关键字后面可以声明一个可选的文件名. 这个文件包含一个用户列表,列表记录的是那些适用口令认证记录的用户, 以及可选的候选口令.
口令是以明文的方式在线路上传输的.如果要更好的保护,请使用 crypt 方法.
crypt
类似 password 方法,但是口令是用一种简单的 口令对应协议加密后在线路上传送的.这么做在密码学理论上是不安全的, 但可以防止偶然的线路侦听.在 crypt 关键字后面 可以有一个文件,文件里包含适用口令认证记录的用户列表.
krb4
用 Kerberos V4 认证用户.只有在进行 TCP/IP 联接的时候才能用. (译注:Kerberos,"克尔波洛斯",故希腊神话冥王哈得斯的多头看门狗. Kerberos 是 MIT 开发出来的基与对称加密算法的认证协议和/或密钥 交换方法.其特点是需要两个不同用途的服务器,一个用于认证身份, 一个用于通道两端用户的密钥交换.同时 Kerberos 对网络时间同步 要求比较高,以防止回放攻击,因此通常伴随 NTP 服务.)
krb5
用 Kerberos V5 认证用户.只有在进行 TCP/IP 联接的时候才能用. (译注:Kerberos V5 是上面 V4 的改良,主要是不再依赖 DES 算法, 同时增加了一些新特性.)
ident
服务器将询问客户机上的 ident 服务器以核实正在联接的用户身份. 然后 Postgres 核实该操作系统用户是否被允许以其请求的数据库用户身份与数据库联接. 只有在使用 TCP/IP 联接的时候才能用这个选项. 跟在 ident 关键字后面的 authentication option 声明一个 ident map(身份映射), 该文件声明那些操作系统用户等效于数据库用户.见下文获取详细信息.
authentication option(认证选项)
这个字段根据不同的认证方法(authentication method)有不同的 解释.
认证时使用与联接请求的客户端 IP 地址和所要求的 数据库名字匹配的第一条记录. 请注意这里没有 “fall-through(越过)” 或者 “backup(备份)”:如果选定了一条记录但认证失败, 那么将不会继续考虑下面的记录.如果没有匹配的记录,则拒绝访问.
在每次联接的请求时,文件 pg_hba.conf 都会被重新读取.因此很容易就能在服务器运行的时候修改访问权限.
在 Example 4-1 里是 pg_hba.conf 的一个例子. 阅读下文理解不同认证方法的细节.
Example 4-1. 一个 pg_hba.conf 文件的例子
# TYPE DATABASE IP_ADDRESS MASK AUTHTYPE MAP
# 允许在本机上的任何用户以任何身份联接任何数据库
# 但必须是通过 IP 进行联接
host all 127.0.0.1 255.255.255.255 trust
# 同样,但用的是 Unix-套接字联接
local all trust
# 允许 IP 地址为 192.168.93.x 的任何主机与数据库
# "template1" 相连,用与他们在自己的主机上相同 ident 的用户名标识他自己
# (通常是他的 Unix 用户名)
host template1 192.168.93.0 255.255.255.0 ident sameuser
# 允许来自主机 192.168.12.10 的用户与 "template1" 数据库联接,
# 只要该用户提供了在 pg_shadow 里正确的口令.
host template1 192.168.12.10 255.255.255.255 crypt
# 如果前面没有其它 "host" 行,那么下面两行将拒绝所有来自
# 192.168.54.1 的联接请求 (因为前面的记录先匹配
# 但是允许来自互联网上其它任何地方有效的 Kerberos V5 认证的联接
# 零掩码表示不考虑主机 IP 的任何位.因此它匹配任何主机:
host all 192.168.54.1 255.255.255.255 reject
host all 0.0.0.0 0.0.0.0 krb5
# 允许来自 192.168.x.x 的任何用户与任意数据库联接,只要他们通过 ident 检查
# 但如果 ident 说该用户是 "bryanh" 而他要求以 PostgreSQL 用户 "guest1" 联接,
# 那么只有在 `pg_ident.conf' 里有 "omicron" 的映射,说 "bryanh" 允许以
# "guest1" 进行联接时才真正可以进行联接.
host all 192.168.0.0 255.255.0.0 ident omicron
虽然我没能去参加BEA的活动,但是相关的资料已经下载并且浏览过了,确实收获不少。所以,对于庄兄的这些想法我很理解。
相信不只你我,大部分的人都比较认同敏捷化的过程,希望使过程变得敏捷。的确,这是个好东西,之前我也说过“敏捷过程是三赢的”这样的话。
我所关心的问题是“如何能够用好XP?”。
庄兄认为“汤的味道,不需要什么过程控制”,我也会认同。为什么?因为你我都是中国人。大部分中国人不会认为汤的味道需要什么过程控制。但是想想看,如果你在不同地方买到的肯德基炸鸡味道各异;同一批次生产的同型号的汽车形状各异;银行里取出来的一叠百元大钞大小不一,你不会觉得奇怪么或是有那么一点点愤怒么?
西方人(甚至学习西方的日本人)对品质的重视程度却完全不同。他们不允许肯德基炸鸡的味道有很大偏差(即便你觉得无所谓);“2毫米工程”不允许整车的总装长度发生2毫米以上的偏差(即便你觉得无所谓);百元大钞……(我想谁都不会无所谓)。
所以,一切质量都有标准,一切标准都应该被度量!这就是工程学的目标之一,为了实现更严格的质量标准,就需要过程控制和度量。
庄兄所说,用测试用例保证代码的质量其实还是采用了“测试用例”作为度量的标准。唯一的问题是:“如何确保测试用例的质量”。显然,我们不能把一把不直的尺子度量出来的结果作为可靠的参考依据。怎么解决呢?“结对编程”么?嗯,这是一个不错的方式,那么最终该信赖谁呢?是Pair中的A还是B呢?或者,是Leader么?那么又是谁提出的要求呢?是老板么?还是客户?政府?法规?市场?……问题没有终结了。
不要学习哲学家的方法,提出一层又一层无法解决的问题。我们是工程师,应该试图解决问题才对!解决问题的关键在于,XP同样需要标准!为了制定标准,必要的文档是不可以少的。而且,标准本身的质量是严苛的。因为,作为标准,他不可以含糊其辞、模棱两可。在标准的基础之上,我们才可以谈什么TDD、Pair Programming之类的实践。
回到争论的开端。我引用了林先生的话“UP是正楷;XP是草书。要先学好UP才能学好XP,先学XP会乱套。”我对这句话的理解如下:这句话并没有批判UP或是XP,只是指出了一个学习的顺序。我认为这句话是有实践依据的,因为UP强调的是一种经典的工程方法。软件工程本来就源于其他行业的工程实践经验。UP利用大量的文档对开发活动进行约束和记录。正是这种重量级的过程规范了规范了从PM到Coder的所有活动,有问题可以参照文档,看看自己应该怎么做。文档也可以作为日后评估这个过程的依据。随着整个团队和每个个人的经验不断积累,开发活动中的日常行为渐渐形成了一种职业习惯。然后可以通过对UP的配置,逐渐减少文档的使用量,一些没有必要的文档就可以省去,更具团队的实际能力调整过程。UP是可配置的,不必要的文档没有存在的理由,这一点UP和XP没有什么两样。当然,随着大家的职业习惯越来越好,经验越来越丰富,个人和团队就可以采用更敏捷更轻便的过程,逐渐过渡到XP上去。
反过来,如果一开始就没有详尽的文档,很多活动(比如设计、版本控制)往往会脱离控制,进入一种无序的、混乱的状态。没有文档可参考,就意味着很多问题只能问人,而不同人的回答可能各异,同一个人对同一个问题的两次回答也可能不同!当然,如果整个团队的工程素养和个体的职业习惯都比较好的情况下可能不会发生类似的情况。但是这种工程素养和职业习惯从哪里来,可能单靠的XP是不足以培养出来的。
“UP是正楷;XP是草书。要先学好UP才能学好XP,先学XP会乱套。”这句话表明了UP和XP在一定程度上是存在冲突的,并且提出了一条路线去降低和避免这个冲突。
再次需要强调的是庄兄所提到的“XP是一种思想”,这点我认同。但是我认为这个除了思想之外,还是一种“文化”。这种思想和文化也是出于软件工程多年来的实践,其中也不免有UP等其他过程。不能简单地认为“我们只要吸取历史的教训,提出新的思想和文化就不会再犯同样的错误了。”很多时候历史总是一次又一次地重演着。新的思想和文化如果不能被准确地理解和运用,它所带来的可能仍然是它原本想解决的问题。只有我们具备了引入这种文化的基础,才能把它变成自己的文化,否则这仍然是挂在嘴边行于表面的一种不求精髓只求模仿的伪文化、伪思想。这一点对于UP和XP的实践者来说没有什么两样。
不光是做软件,凡是做产品,最后关注的总是产品的质量。
举个例子,比如你做一锅汤:
今天你状态很好,做完后尝了尝,感觉很美味,你的家人尝了以后也有同感,喝完后感觉心情舒畅、意犹未尽。
隔了一个礼拜,你做同样的汤给家里人喝。做完后你尝了尝,感觉依然美味,盼望着得到家人的赏识,然而他们却说味道咸了点。你很奇怪,为什么同样自己尝过了,家里人却感觉不一样呢?是不是最近加班多了,休息不好,味觉不准了?
一个月过后,你要去国外出差,给家里请了个临时保姆。一天,他也做了这么个汤,做完后,他也尝了尝,感觉口味很不错,可是端上桌,家里人说这汤太辣了。原来这保姆才从湖南老家出来不久……
因此,只把焦点放在最后的产品上往往是不够的。需要对“做汤的过程”加以控制。所以工程界会比较关注过程的管理,在软件领域也称作“软件生命周期管理”。
再来看看UP和XP。它们都属于软件过程,只不过各有特色。
再拿刚才那个做汤的例子:
大家都听说过德国人的厨房像化学实验室,天平、计时器、量杯……装备齐全,再配上精确的菜谱,严谨的德国人能够确保不用尝那最后一口都做出口味基本一致的汤。
换了中国人,大部分人都不会模仿德国人做菜的方式。解决方案很简单,让你的太太和孩子都尝那最后一口,再根据反馈调整几次,同样能做出全家人满意的汤。
这个例子也许不太贴切,但是可以联想一下:德国人做汤倾向于UP;中国人做汤倾向于XP。
UP和XP最终目的都是为了保证产品的质量,不同的是,两个过程所强调的方法不同。我想,没有人会说“UP的目的在于变态地追求文档的完美”、“UP是为了要程序员学会写各种各样文档”……之类的话。同时,也没人会说“XP就是不要文档只要代码”、“XP就是要变态地追求完美的代码”……这样的话。
这些不正确的看法,只是人们对于这两种过程的误解。或许是来自于开发人员和项目经理的那些“不堪回首的经历”。
“UP害惨了整个软件行业,让开发人员没完没了地写文档而忽略了代码,XP才是王道”这样的话,我不敢苟同,仍然有很多企业使用着UP这样的重型软件工程,就好比德国人依然喜欢把厨房弄得像个实验室。
XP固然是个好东西。但是,不知道大多数人对于XP的热衷是出于对XP文化的理解,还是国人惯有的“一窝蜂”似的行为。不晓得一个“能够熟练阅读代码的Leader”是不是能够真正运用好XP,确保他的团队能够尽可能少地出现"Over engineering"这种违背Agile精神的东西,或是能够让他的团队保证“每周只工作40小时”这样的基本实践?
对于不同的技术和过程,应该给予冷静的分析和慎重的选择。每个过程和技术都不能以“正确”或“不正确”来定性,只能以“合适”和“不合适”来定性。因为正确或不正确是要严格证明的,而合适不合适是来源于工程实践的结果。所以,COBOL依然在金融领域起着举足轻重的作用,科学家们仍不忘Fortran,汇编和C仍然健在……
另外不得不提的是文化上的差异。为什么很多时候,我们学习国外的先进技术,购买了整套生产线,引进了全套图纸,请国外专家做了详细的全程化培训,国人生产出的产品品质依然不如国外原产的?这是每个中国人都应该思考的问题……
“UP是正楷,XP是草书。先学好了UP,才能学好XP;先学XP再学UP就会乱套。 ”
老师曾这么说。最近,对这句话有了深刻的体会。
软件过程是一个以人为中心的活动。人是项目中最难确定和控制的因素。休息的质量、情绪的起伏都会影响整个活动。为了尽可能地约束这种个体的不确定行为和减少开发过程中不必要的误会。"UP"采用了大量的文档来对整个开发过程进行控制。这些文档主要分为以下几类:
- 计划文档——项目的开发计划、迭代计划、测试计划等。
- 技术文档——项目的设计文档、某个操作的说明文档等。
- 记录文档——日常的会议纪要、每日进度反馈、评估报告等。
文档成了UP活动的主要部分。在UP中,往往大量的资源用于文档的制作。这些文档的目的是为了尽可能减少不必要的沟通成本和误会,也为了在发生问题的时候能够尽快确定原因找到解决方法。
而正是因为如此繁重的资源消耗,导致真正的设计和代码只占到了总开销的很少部分。这对很多人来说不可理解,甚至觉得本末倒置。于是很多敏捷方法诞生了,最具代表性也是对UP思想最具颠覆性的就属XP了。
对外,XP以快速的反应速度来响应客户的需求;对内,XP以高质量的代码和设计来确保尽可能不产生不必要的文档和资源开销。
从表面上看,在当今,XP确实是一种非常理想的开发过程。
但是,从没有过程到XP往往会非常失败。这是为什么?问题的关键还在于人。
UP利用文档来约束和规范人们的开发活动。当一个没有经验的团队经历UP后,就等于把性格各异、习惯差别不同的人统一成了“相对较一致的开发人员”。
他们有一致的编码习惯,有共同的用语,有严格的规则。随着经验的积累,这个团队间的默契越来越高。此时,如果过程由UP向XP切换,付出的代价就会相对较低。
XP主张快速反应。如果一个没有经验的团队在一开始就尝试XP,那么后果可能是惨痛的。因为一个没有经验的团队其成员间的相互了解颇少,对于一件事,往往十个人有十种想法。当缺少文档约束时,在以代码和设计为中心的活动中,成员之间往往因为水平的参差不齐导致无休止的讨论甚至争论,代码被不必要地频繁改动。这是因为,在团队建设早期,成员之间往往连最基本的尊重和信任都不存在。 这种无意义的活动往往会严重影响项目的正常进行。
所以,学习和应用过程不仅仅是个体的事,而是整个团队的事。只有当团队采用严格文档化的过程并且经过磨合后,才能渐渐向轻量级的过程迁移,逐渐将不必要的文档删减掉,采用更灵活的过程。但是,此时并不是“没有文档”而是“心中有文档”。
URL url = this.getClass().getResource("EJBConfig.xml");
try {
File xmlFile = new File(URLDecoder.decode(url.getFile(),"UTF-8"));
if(xmlFile.exists())
System.out.println("OK");
} catch (UnsupportedEncodingException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
}
开闭原则很简单,一句话:“Closed for Modification; Open for Extension”——“对变更关闭;对扩展开放”。开闭原则其实没什么好讲的,我将其归结为一个高层次的设计总则。就这一点来讲,OCP的地位应该比SRP优先。
OCP的动机很简单:软件是变化的。不论是优质的设计还是低劣的设计都无法回避这一问题。OCP说明了软件设计应该尽可能地使架构稳定而又容易满足不同的需求。
为什么要OCP?答案也很简单——重用。
“重用”,并不是什么软件工程的专业词汇,它是工程界所共用的词汇。早在软件出现前,工程师们就在实践“重用”了。比如机械产品,通过零部件的组装得到最终的能够使用的工具。由于机械部件的设计和制造过程是极其复杂的,所以互换性是一个重要的特性。一辆车可以用不同的发动机、不同的变速箱、不同的轮胎……很多东西我们直接买来装上就可以了。这也是一个OCP的例子。(可能是由于我是搞机械出身的吧,所以就举些机械方面的例子^_^)。
如何在OO中引入OCP原则?把对实体的依赖改为对抽象的依赖就行了。下面的例子说明了这个过程:
05赛季的时候,一辆F1赛车有一台V10引擎。但是到了06赛季,国际汽联修改了规则,一辆F1赛车只能安装一台V8引擎。车队很快投入了新赛车的研发,不幸的是,从工程师那里得到消息,旧车身的设计不能够装进新研发的引擎。我们不得不为新的引擎重新打造车身,于是一辆新的赛车诞生了。但是,麻烦的事接踵而来,国际汽联频频修改规则,搞得设计师在“赛车”上改了又改,最终变得不成样子,只能把它废弃。
为了能够重用这辆昂贵的赛车,工程师们提出了解决方案:首先,在车身的设计上预留出安装引擎的位置和管线。然后,根据这些设计好的规范设计引擎(或是引擎的适配器)。于是,新的赛车设计方案就这样诞生了。
显然,通过重构,这里应用的是一个典型的Bridge模式。这个实现的关键之处在于我们预先给引擎留出了位置!我们不必因为对引擎的规则的频频变更而制造相当多的车身,而是尽可能地沿用和改良现有的车身。
说到这里,想说一说OO设计的一个误区。
学习OO语言的时候,为了能够说明“继承”(或者说“is-a”)这个概念,教科书上经常用实际生活中的例子来解释。比如汽车是车,电车是车,F1赛车是汽车,所以车是汽车、电车、F1赛车的上层抽象。这个例子并没有错。问题是,这样的例子过于“形象”了!如果OO设计直接就可以将现实生活中的概念引用过来,那也就不需要什么软件工程师了!OO设计的关键概念是抽象。如果没有抽象,那所有的软件工程师的努力都是徒劳的。因为如果没有抽象,我们只能去构造世界中每一个对象。上面这个例子中,我们应该看到“引擎”这个抽象的存在,因为车队的工程师们为它预留了位置,为它制定了设计规范。
上面这个设计也实现了后面要说的DIP(依赖倒置原则)。但是请记住,OCP是OO设计原则中高层次的原则,其余的原则对OCP提供了不同程度的支持。为了实现OCP,我们会自觉或者不自觉地用到其它原则或是诸如Bridge、Decorator等设计模式。然而,对于一个应用系统而言,实现OCP并不是设计目的,我们所希望的只是一个稳定的架构。所以对OCP的追求也应该适可而止,不要陷入过渡设计。正如Martin本人所说:“No significant program can be 100% closed.”“Closure not complete but strategic”
(下一篇就要讲LSP了,我觉得这是意义最为重要的OO设计原则,它直指当今主流OO语言的软肋,点出了OO设计的精髓。)
from http://blog.csdn.net/wangyihust/archive/2006/01/02/568616.aspx
JUnit是由 Erich Gamma 和 Kent Beck 编写的一个回归测试框架(regression testing framework)。Junit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。Junit是一套框架,继承TestCase类,就可以用Junit进行自动测试了。
http://www.junit.org/
Cactus是一个基于JUnit框架的简单测试框架,用来单元测试服务端Java代码。Cactus框架的主要目标是能够单元测试服务端的使用Servlet对象的Java方法如HttpServletRequest,HttpServletResponse,HttpSession等。
http://jakarta.apache.org/cactus/
Abbot是一个用来测试Java GUIs的框架。用简单的基于XML的脚本或者Java代码,你就可以开始一个GUI。
http://abbot.sourceforge.net/
Junitperf实际是junit的一个decorator,通过编写用于junitperf的单元测试,我们也可使测试过程自动化。
http://www.clarkware.com/software/JUnitPerf.html
DbUnit是为数据库驱动的项目提供的一个对JUnit 的扩展,除了提供一些常用功能,它可以将你的数据库置于一个测试轮回之间的状态。
http://dbunit.sourceforge.net/
Mockrunner用在J2EE环境中进行应用程序的单元测试。它不仅支持Struts actions, servlets,过滤器和标签类还包括一个JDBC和一个JMS测试框架,可以用于测试基于EJB的应用程序。
http://mockrunner.sourceforge.net/index.html
DBMonster是一个用生成随机数据来测试SQL数据库的压力测试工具。
http://dbmonster.kernelpanic.pl/
MockEJB是一个不需要EJB容器就能运行EJB并进行测试的轻量级框架。
http://mockejb.sourceforge.net/
StrutsTestCase 是Junit TestCase类的扩展,提供基于Struts框架的代码测试。StrutsTestCase同时提供Mock 对象方法和Cactus方法用来实际运行Struts ActionServlet,你可以通过运行servlet引擎来测试。因为StrutsTestCase使用ActionServlet控制器来测试你的代码,因此你不仅可以测试Action对象的实现,而且可以测试mappings,from beans以及forwards声明。StrutsTestCase不启动servlet容器来测试struts应用程序(容器外测试)也属于Mock对象测试,但是与EasyMock不同的是,EasyMock是提供了创建Mock对象的API,而StrutsTest则是专门负责测试Struts应用程序的Mock对象测试框架。
http://strutstestcase.sourceforge.net/
JFCUnit使得你能够为Java偏移应用程序编写测试例子。它为从用代码打开的窗口上获得句柄提供了支持;为在一个部件层次定位部件提供支持;为在部件中发起事件(例如按一个按钮)以及以线程安全方式处理部件测试提供支持。
http://jfcunit.sourceforge.net/
JTestCase 使用XML文件来组织多测试案例数据,声明条件(操作和期望的结果),提供了一套易于使用的方法来检索XML中的测试案例,按照数据文件的定义来声明结果。
http://jtestcase.sourceforge.net/
SQLUnit是一个单元测试框架,用于对数据库存储过程进行回归测试。用 Java/JUnit/XML开发。
http://sqlunit.sourceforge.net
JTR (Java Test Runner)是一个开源的基于反转控制(IOC)的J2EE测试框架。它允许你构建复杂的J2EE测试套件(Test Suites)并连到应用服务器执行测试,可以包括多个测试实例。JTR的licensed是GPL协议。
http://jtrunner.sourceforge.net/
Marathon是一个针对使用Java/Swing开发GUI应用程序的测试框架,它由recorder, runner 和 editor组成,测试脚本是python代码。Marathon的焦点是放在最终用户的测试上。
http://marathonman.sourceforge.net
TestNG是根据JUnit 和 NUnit思想而构建的一个测试框架,但是TestNG增加了许多新的功能使得它变得更加强大与容易使用比如:
*支持JSR 175注释(JDK 1.4利用JavaDoc注释同样也支持)
*灵活的Test配置
*支持默认的runtime和logging JDK功能
*强大的执行模型(不再TestSuite)
*支持独立的测试方法。
http://testng.org/
Surrogate Test framework是一个值得称赞单元测试框架,特别适合于大型,复杂Java系统的单元测试。这个框架能与JUnit,MockEJB和各种支持模拟对象(mock object )的测试工具无缝给合。这个框架基于AspectJ技术。
http://surrogate.sourceforge.net
MockCreator可以为给定的interface或class生成模拟对象(Mock object)的源码。
http://mockcreator.sourceforge.net/
jMock利用mock objects思想来对Java code进行测试。jMock具有以下特点:容易扩展,让你快速简单地定义mock objects,因此不必打破程序间的关联,让你定义灵活的超越对象之间交互作用而带来测试局限,减少你测试地脆弱性。
http://www.jmock.org/
EasyMock为Mock Objects提供接口并在JUnit测试中利用Java的proxy设计模式生成它们的实例。EasyMock最适合于测试驱动开发。
http://www.easymock.org/
The Grinder是一个负载测试框架。在BSD开源协议下免费使用。
http://grinder.sourceforge.net/
XMLUnit不仅有Java版本的还有.Net版本的。Java开发的XMLUnit提供了两个JUnit 扩展类XMLAssert和XMLTestCase,和一组支持的类。这些类可以用来比较两张XML之间的不同之处,展示XML利用XSLT来,校验XML,求得XPath表达式在XML中的值,遍历XML中的某一节点利DOM展开。
http://xmlunit.sourceforge.net/
Jameleon一个自动化测试工具。它被用来测试各种各样的应用程序,所以它被设计成插件模式。为了使整个测试过程变得简单Jameleon提供了一个GUI,因此Jameleon实现了一个Swing 插件。
http://jameleon.sourceforge.net/index.html
J2MEUnit是应用在J2ME应用程序的一个单元测试框架。它基于JUnit。
http://j2meunit.sourceforge.net/
Jetif是一个用纯Java实现的回归测试框架。它为Java程序单元测试以及功能测试提供了一个简单而且可 伸缩的架构,可以用于个人开发或企业级开发的测试。它容易使用,功能强大,而且拥有一些企业级测试的重要功能。Jetif来源于JUnit, JTestCase以及TestNG的启发,有几个基本的概念直接来自于JUnit, 比如说断言机制,Test Listener的概念,因此从JUnit转到Jetif是非常容易的。
http://jetif.sourceforge.net/
GroboUtils使得扩展Java测试变得可能。它包括用在Java不同方面测试的多个子项目。在GroboUtils中最常被到的工具是:多线程测试(multi-threaded tests),整体单元测试(hierarchial unit tests),代码覆盖工具(code coverage tool)。
http://groboutils.sourceforge.net/
TESTARE是用来简化分布式应用程序(比如:在SERVLETS,JMS listeners, CORBA ORBs或RMI环境下)测试开发过程的一个测试框架。
https://testare.dev.java.net/
一点说明:OO的五大原则是指SRP、OCP、LSP、DIP、ISP。这五个原则是书中所提到的。除此之外,书中还提到一些高层次的原则用于组织高层的设计元素,这些放到下次再写。当然,OO设计的原则可能不止这五个,希望大家多提宝贵意见,多多交流。
在学习和使用OO设计的时候,我们应该明白:OO的出现使得软件工程师们能够用更接近真实世界的方法描述软件系统。然而,软件毕竟是建立在抽象层次上的东西,再怎么接近真实,也不能替代真实或被真实替代。
OO设计的五大原则之间并不是相互孤立的。彼此间存在着一定关联,一个可以是另一个原则的加强或是基础。违反其中的某一个,可能同时违反了其余的原则。因此应该把这些原则融会贯通,牢记在心!
1. SRP(Single Responsibility Principle 单一职责原则)
单一职责很容易理解,也很容易实现。所谓单一职责,就是一个设计元素只做一件事。什么是“只做一件事”?简单说就是少管闲事。现实中就是如此,如果要你专心做一件事情,任何人都有信心可以做得很出色。但如果,你整天被乱七八糟的事所累,还有心思和精力把每件事都作好么?
“单一职责”就是要在设计中为每种职责设计一个类,彼此保持正交,互不干涉。这个雕塑(二重奏)就是正交的一个例子,钢琴家和小提琴家各自演奏自己的乐谱,而结果就是一个和谐的交响乐。当然,真实世界中,演奏小提琴和弹钢琴的必须是两个人,但是在软件中,我们往往会把两者甚至更多搅和到一起,很多时候只是为了方便或是最初设计的时候没有想到。
这样的例子在设计中很常见,书中就给了一个很好的例子:调制解调器。这是一个调制解调器最基本的功能。但是这个类事实上完成了两个职责:连接的建立和中断、数据的发送和接收。显然,这违反了SRP。这样做会有潜在的问题:当仅需要改变数据连接方式时,必须修改Modem类,而修改Modem类的结果就是使得任何依赖Modem类的元素都需要重新编译,不管它是不是用到了数据连接功能。解决的办法,书中也已经给出:重构Modem类,从中抽出两个接口,一个专门负责连接、另一个专门负责数据发送。依赖Modem类的元素也要做相应的细化,根据职责的不同分别依赖不同的接口。最后由ModemImplementation类实现这两个接口。
从这个例子中,我们不难发现,违反SRP通常是由于过于“真实”地设计了一个类所造成的。因此,解决办法是往更高一层进行抽象化提取,将对某个具体类的依赖改变为对一组接口或抽象类的依赖。当然,这个抽象化的提取应该根据需要设计,而不是盲目提取。比如刚才这个Modem的例子中,如果有必要,还可以把DataChannel抽象为DataSender和DataReceiver两个接口。
软件设计是一种抽象活动,设计所要实现的是产出代码。就这一点来说,任何人都会设计。但是,正如我们日常生活中所耳闻目睹或亲身经历,设计有优劣之分。
从项目管理的角度去理解,设计是为了满足涉众(Stakeholders)的需求。显然,一个设计应该满足客户对系统的功能及非功能需求。但单是满足了这一点,并不能称为一个好的设计。因为开发者同样属于涉众!而开发者的需求又是怎样的呢?至少,应该有以下几条吧:
- 老板希望软件交付后,不应该有很高的维护成本。如果开发人员为了维护而经常出差或者加班且久久不能投入新项目,显然,换了谁是老板都不愿意这种事情发生。
- 开发人员呢?谁愿意放弃和家人朋友而拼死拼活在单位加班,总是有这么多麻烦事缠着你,烦不烦哪!
- ……等等
所以,设计应该尽可能多地照顾到维护和变更。
为了兼顾各户满意和维护成本,设计应该不断挑战其终极目标——松耦合。不管是XP或UP,这个目标都不会改变。OO设计中的五大原则,其根本目的就是降低组件间的耦合度,避免牵一发则动全身的现象发生。降低耦合度不仅能够提高软件内在的质量,还能大大减少不必要的编译时间、减少向版本控制系统提交源码的网络开销……
如何鉴别设计的这一指标?软件工程中有专用的度量:CBO(Coupling Between Objects),那是由公式计算出来的,也有很多工具支持,值得一试。(听过几次李维先生的讲座,他经常拿Together的度量功能炫耀^_^)
但是,作为一个开发人员,对手中的代码应该有适当的敏感性。毕竟,这些代码是你亲手创造的,谁不希望自己的作品得到众人的赞许?或许能换得一次加薪升职的机会^_^ 退一步,这可关系到宝贵的休息时间啊。所以,开发者应该对自己的产品有这样一种意识:及时修正设计中不合理的地方。
敏捷过程告诉我们:在代码“有味道”的时候进行重构。“有味道”是代码正在变质的标志,很遗憾,能够使代码保持原味的防腐剂还没发明。为了保证代码质量,及时重构是必要的。这就像在烧烤的时候为了防止烤焦,你得坐在炉子前经常翻动肉块一样。
如何闻出代码的味道?认真学习一下OO吧,别以为OO很简单,就是继承+封装+多态,谁都会。即使是书中记述的五大原则,想要运用自如,也得多感觉感觉才行。很多时候,我们不知不觉就把蛆虫放进了代码中……
好了,下一篇:OO五大原则
最近正在读这本书,喜欢影印版,是因为书中漂亮的插图。:)惭愧,如此的好书到现在才去读。
准备边读边记录些心得,今天先说些废话。:P
先粗略地概览了一遍全书。本书主要分以下几个部分:
- 敏捷软件过程。主要以XP为例。这部分的最后一章,用一个对话式的小故事讲述了一个非常小的过程。给了读者关于敏捷过程的形象化的认识。
- 敏捷设计。这部分是个很大的看点。它讲述了设计中一些常见的问题,及其应对(用几个经典的设计原则)。
- 案例实践。讲述了如何利用设计模式去实践第二部分中提到的设计原则和避免设计中的“味道”。
之所以觉得这本书好,还与一个人有关。就是交大软件学院的林德彰老师。林先生的课,风趣幽默,能够用直观形象的语言让学生对讲课内容产生深刻的印象。(我可不是托儿,网上能搜到些林先生讲课的片断,要是怀疑,可以验证一番)。记得在软件工程这门课里,林先生给我们讲了很多有关设计原则的内容,其中就有“开闭原则(OCP)”、“里氏替换原则(LSP)”等……就把这本书当作是一本补充读物吧。
言归正传。个人感觉这本书的总体风格,就和所要讲的“敏捷”一样,并不带着厚重的学院派风味,而是更注重实践。并不是没有理论,只是把理论融入到了实践中,简化了理论的复杂性。读起来感觉很带劲儿。
废话说到这里,下一步的计划就是跟着自己的进度写读书心得了。我想把对书中内容的理解和以前在林先生的课上所学的结合在一起,导出阅读此书时的大脑活动镜像。
用 Cobertura 测量测试覆盖率
找出隐藏 bug 的未测试到的代码 |
|
|
级别: 初级
Elliotte Rusty Harold, 副教授, Polytechnic University
2005 年 5 月 26 日
Cobertura 是一种开源工具,它通过检测基本的代码,并观察在测试包运行时执行了哪些代码和没有执行哪些代码,来测量测试覆盖率。除了找出未测试到的代码并发现 bug 外,Cobertura 还可以通过标记无用的、执行不到的代码来优化代码,还可以提供 API 实际操作的内部信息。Elliotte Rusty Harold 将与您分享如何利用代码覆盖率的最佳实践来使用 Cobertura。
尽管测试先行编程(test-first programming)和单元测试已不能算是新概念,但测试驱动的开发仍然是过去 10 年中最重要的编程创新。最好的一些编程人员在过去半个世纪中一直在使用这些技术,不过,只是在最近几年,这些技术才被广泛地视为在时间及成本预算内开发健壮的无缺陷软件的关键所在。但是,测试驱动的开发不能超过测试所能达到的程度。测试改进了代码质量,但这也只是针对实际测试到的那部分代码而言的。您需要有一个工具告诉您程序的哪些部分没有测试到,这样就可以针对这些部分编写测试代码并找出更多 bug。
Mark Doliner 的 Cobertura (cobertura 在西班牙语是覆盖的意思)是完成这项任务的一个免费 GPL 工具。Cobertura 通过用额外的语句记录在执行测试包时,哪些行被测试到、哪些行没有被测试到,通过这种方式来度量字节码,以便对测试进行监视。然后它生成一个 HTML 或者 XML 格式的报告,指出代码中的哪些包、哪些类、哪些方法和哪些行没有测试到。可以针对这些特定的区域编写更多的测试代码,以发现所有隐藏的 bug。
阅读 Cobertura 输出
我们首先查看生成的 Cobertura 输出。图 1 显示了对 Jaxen 测试包运行 Cobertura 生成的报告(请参阅 参考资料)。从该报告中,可以看到从很好(在 org.jaxen.expr.iter 包中几乎是 100%)到极差(在 org.jaxen.dom.html 中完全没有覆盖)的覆盖率结果。 图 1. Jaxen 的包级别覆盖率统计数据
Cobertura 通过被测试的行数和被测试的分支数来计算覆盖率。第一次测试时,两种测试方法之间的差别并不是很重要。Cobertura 还为类计算平均 McCabe 复杂度(请参阅 参考资料)。
可以深入挖掘 HTML 报告,了解特定包或者类的覆盖率。图 2 显示了 org.jaxen.function 包的覆盖率统计。在这个包中,覆盖率的范围从 SumFunction 类的 100% 到 IdFunction 类的仅为 5%。 图 2. org.jaxen.function 包中的代码覆盖率
进一步深入到单独的类中,具体查看哪一行代码没有测试到。图 3 显示了 NameFunction 类中的部分覆盖率。最左边一栏显示行号。后一栏显示了执行测试时这一行被执行的次数。可以看出,第 112 行被执行了 100 次,第 114 行被执行了 28 次。用红色突出显示的那些行则根本没有测试到。这个报告表明,虽然从总体上说该方法被测试到了,但实际上还有许多分支没有测试到。 图 3. NameFunction 类中的代码覆盖率
|
Cobertura 是 jcoverage 的分支(请参阅 参考资料)。GPL 版本的 jcoverage 已经有一年没有更新过了,并且有一些长期存在的 bug,Cobertura 修复了这些 bug。原来的那些 jcoverage 开发人员不再继续开发开放源码,他们转向开发 jcoverage 的商业版和 jcoverage+,jcoverage+ 是一个从同一代码基础中发展出来的封闭源代码产品。开放源码的奇妙之处在于:一个产品不会因为原开发人员决定让他们的工作获得相应的报酬而消亡。 | |
确认遗漏的测试
利用 Cobertura 报告,可以找出代码中未测试的部分并针对它们编写测试。例如,图 3 显示 Jaxen 需要进行一些测试,运用 name() 函数对文字节点、注释节点、处理指令节点、属性节点和名称空间节点进行测试。
如果有许多未覆盖的代码,像 Cobertura 在这里报告的那样,那么添加所有缺少的测试将会非常耗时,但也是值得的。不一定要一次完成它。您可以从被测试的最少的代码开始,比如那些所有没有覆盖的包。在测试所有的包之后,就可以对每一个显示为没有覆盖的类编写一些测试代码。对所有类进行专门测试后,还要为所有未覆盖的方法编写测试代码。在测试所有方法之后,就可以开始分析对未测试的语句进行测试的必要性。
(几乎)不留下任何未测试的代码
是否有一些可以测试但不应测试的内容?这取决于您问的是谁。在 JUnit FAQ 中,J. B. Rainsberger 写到“一般的看法是:如果 自身 不会出问题,那么它会因为太简单而不会出问题。第一个例子是 getX() 方法。假定 getX() 方法只提供某一实例变量的值。在这种情况下,除非编译器或者解释器出了问题,否则 getX() 是不会出问题的。因此,不用测试 getX() ,测试它不会带来任何好处。对于 setX() 方法来说也是如此,不过,如果 setX() 方法确实要进行任何参数验证,或者说确实有副作用,那么还是有必要对其进行测试。”
|
理论上,对未覆盖的代码编写测试代码不一定就会发现 bug。但在实践中,我从来没有碰到没有发现 bug 的情况。未测试的代码充满了 bug。所做的测试越少,在代码中隐藏的、未发现的 bug 就会越多。 | |
我不同意。我已经记不清在“简单得不会出问题”的代码中发现的 bug 的数量了。确实,一些 getter 和 setter 很简单,不可能出问题。但是我从来就没有办法区分哪些方法是真的简单得不会出错,哪些方法只是看上去如此。编写覆盖像 setter 和 getter 这样简单方法的测试代码并不难。为此所花的少量时间会因为在这些方法中发现未曾预料到的 bug 而得到补偿。
一般来说,开始测量后,达到 90% 的测试覆盖率是很容易的。将覆盖率提高到 95% 或者更高就需要动一下脑筋。例如,可能需要装载不同版本的支持库,以测试没有在所有版本的库中出现的 bug。或者需要重新构建代码,以便测试通常执行不到的部分代码。可以对类进行扩展,让它们的受保护方法变为公共方法,这样就可以对这些方法进行测试。这些技巧看起来像是多此一举,但是它们曾帮助我在一半的时间内发现更多的未发现的 bug。
并不总是可以得到完美的、100% 的代码覆盖率。有时您会发现,不管对代码如何改造,仍然有一些行、方法、甚至是整个类是测试不到的。下面是您可能会遇到的挑战的一些例子:
- 只在特定平台上执行的代码。例如,在一个设计良好的 GUI 应用程序中,添加一个 Exit 菜单项的代码可以在 Windows PC 上运行,但它不能在 Mac 机上运行。
- 捕获不会发生的异常的
catch 语句,比如在从 ByteArrayInputStream 进行读取操作时抛出的 IOException 。
- 非公共类中的一些方法,它们永远也不会被实际调用,只是为了满足某个接口契约而必须实现。
- 处理虚拟机 bug 的代码块,比如说,不能识别 UTF-8 编码。
考虑到上面这些以及类似的情况,我认为一些极限程序员自动删除所有未测试代码的做法是不切实际的,并且可能具有一定的讽刺性。不能总是获得绝对完美的测试覆盖率并不意味着就不会有更好的覆盖率。
然而,比执行不到的语句和方法更常见的是残留代码,它不再有任何作用,并且从代码基中去掉这些代码也不会产生任何影响。有时可以通过使用反射来访问私有成员这样的怪招来测试未测试的代码。还可以为未测试的、包保护(package-protected)的代码来编写测试代码,将测试类放到将要测试的类所在那个包中。但最好不要这样做。所有不能通过发布的(公共的和受保护的)接口访问的代码都应删除。执行不到的代码不应当成为代码基的一部分。代码基越小,它就越容易被理解和维护。
|
不要漏掉测量单元测试包和类本身。我不止一次注意到,某些个测试方法或者类没有被测试包真正运行。通常这表明名称规范中存在问题(比如将一个方法命名为 tesSomeReallyComplexCondition ,而不是将其命名为 testSomeReallyComplexCondition ),或者忘记将一个类添加到主 suite() 方法中。在其他情况下,未预期的条件导致跳过了测试方法中的代码。不管是什么情况,都是虽然已经编写了测试代码,但没有真正运行它。JUnit 不会告诉您它没有像您所想的那样运行所有测试,但是 Cobertura 会告诉您。找出了未运行的测试后,改正它一般很容易。 | |
运行 Cobertura
在了解了测量代码覆盖率的好处后,让我们再来讨论一下如何用 Cobertura 测量代码覆盖率的具体细节。Cobertura 被设计成为在 Ant 中运行。现在还没有这方面的 IDE 插件可用,不过一两年内也许就会有了。
首先需要在 build.xml 文件中添加一个任务定义。以下这个顶级 taskdef 元素将 cobertura.jar 文件限定在当前工作目录中:
<taskdef classpath="cobertura.jar" resource="tasks.properties" />
|
然后,需要一个 cobertura-instrument 任务,该任务将在已经编译好的类文件中添加日志代码。todir 属性指定将测量类放到什么地方。fileset 子元素指定测量哪些 .class 文件:
<target name="instrument">
<cobertura-instrument todir="target/instrumented-classes">
<fileset dir="target/classes">
<include name="**/*.class"/>
</fileset>
</cobertura-instrument>
</target>
|
用通常运行测试包的同一种类型的 Ant 任务运行测试。惟一的区别在于:被测量的类必须在原始类出现在类路径中之前出现在类路径中,而且需要将 Cobertura JAR 文件添加到类路径中:
<target name="cover-test" depends="instrument">
<mkdir dir="${testreportdir}" />
<junit dir="./" failureproperty="test.failure" printSummary="yes"
fork="true" haltonerror="true">
<!-- Normally you can create this task by copying your existing JUnit
target, changing its name, and adding these next two lines.
You may need to change the locations to point to wherever
you've put the cobertura.jar file and the instrumented classes. -->
<classpath location="cobertura.jar"/>
<classpath location="target/instrumented-classes"/>
<classpath>
<fileset dir="${libdir}">
<include name="*.jar" />
</fileset>
<pathelement path="${testclassesdir}" />
<pathelement path="${classesdir}" />
</classpath>
<batchtest todir="${testreportdir}">
<fileset dir="src/java/test">
<include name="**/*Test.java" />
<include name="org/jaxen/javabean/*Test.java" />
</fileset>
</batchtest>
</junit>
</target>>
|
Jaxen 项目使用 JUnit 作为其测试框架,但是 Cobertura 是不受框架影响的。它在 TestNG、Artima SuiteRunner、HTTPUni 或者在您自己在地下室开发的系统中一样工作得很好。
最后,cobertura-report 任务生成本文开始部分看到的那个 HTML 文件:
<target name="coverage-report" depends="cover-test">
<cobertura-report srcdir="src/java/main" destdir="cobertura"/>
</target>
|
srcdir 属性指定原始的 .java 源代码在什么地方。destdir 属性指定 Cobertura 放置输出 HTML 的那个目录的名称。
在自己的 Ant 编译文件中加入了类似的任务后,就可以通过键入以下命令来生成一个覆盖报告:
% ant instrument
% ant cover-test
% ant coverage-report
|
当然,如果您愿意的话,还可以改变目标任务的名称,或者将这三项任务合并为一个目标任务。
结束语
Cobertura 是敏捷程序员工具箱中新增的一个重要工具。通过生成代码覆盖率的具体数值,Cobertura 将单元测试从一种艺术转变为一门科学。它可以寻找测试覆盖中的空隙,直接找到 bug。测量代码覆盖率使您可以获得寻找并修复 bug 所需的信息,从而开发出对每个人来说都更健壮的软件。
参考资料
|
关于作者
|
摘要: EasyMock 2.0_ReleaseCandidate Readme
Documentation for release 2.0_ReleaseCandidate (October 15 2005)© 2001-2005 OFFIS, Tammo Freese.
翻译:GHawk, 2005-12-15
EasyMock 2 is a library that provides an ...
阅读全文
这个月刚进入公司,加入了一个10人左右的团队,用Java做一个网站后台。
客户是日本公司,他们做了项目的大部分分析(Requirements, Use cases, Domain model...)。我们负责的是详细设计和开发。我是项目开始几星期后才进的公司。Schedule也已经为我分配好了。大家都按照schedule上的安排工作着。
上星期开会的时候得知,日本这次采用的是agile过程。而我们的schedule更类似于RUP这样的过程。RUP这个学院派和Agile这个造反派狭路相逢,问题也就出现了。
大家工作都很卖力,为了能按进度提交制品,有时还通宵达旦解决问题。我们这支团队的战斗力和信心是不容怀疑的。可是大家努力的结果换来的却是用户的抱怨。大家都困惑不解。问题究竟出在哪儿?
日方在项目中强调的是Agile过程,我们采用的则是传统的过程。一开始,两个过程方法之间的差异并不大;对我们提交的制品,客户也没有什么异议。但是,直到客户提出问题之前,我们所提交的制品都是一些设计文档。而我们的制品也仅限于此——没有一个可用的EAR包、没有写过 test case。很明显,我们犯了agile的大忌。
Agile所强调的是快速的构建、轻量级的迭代、TDD等。由于之前没有写test case,详细设计也没有test case可以参照。设计本身是不是合理,是不是testable也不得而知。致使在设计test case的时候无从下手,很多类甚至都没有办法测试。整个架构的可行性很难估算。
往后考虑。一次大规模的重构可能是少不了的。虽然agile过程本身提倡以TDD为基础的重构。但是现在的重构可能造成的代价已经不是一次轻量级的增量迭代了。
说到这里,总结几点,希望能在以后的工作中引起注意:
1. Agile很难管理,项目早期应该对各种风险有尽可能全面的评估,schedule的设置中应该定义好 test case 和 build 的时间点。
2. 设计不必太详细,用频繁的测试和重构完善设计。
3. Test case 优先设计,这样在架构中就会对testability有足够多的考虑。
4. 团队内部对共同的难题应该及早进行讨论和解决,问题的解决方案应该传递到每个组员,尽可能保证团队的能力同步。
EJB 异常处理的最佳做法
学习在基于 EJB 的系统上编写可以更快解决问题的代码 |
|
|
级别: 初级
Srikanth Shenoy, J2EE 顾问
2002 年 5 月 05 日
随着 J2EE 成为企业开发平台之选,越来越多基于 J2EE 的应用程序将投入生产。J2EE 平台的重要组件之一是 Enterprise JavaBean(EJB)API。J2EE 和 EJB 技术一起提供了许多优点,但随之而来的还有一些新的挑战。特别是企业系统,其中的任何问题都必须快速得到解决。在本文中,企业 Java 编程老手 Srikanth Shenoy 展现了他在 EJB 异常处理方面的最佳做法,这些做法可以更快解决问题。
在 hello-world 情形中,异常处理非常简单。每当碰到某个方法的异常时,就捕获该异常并打印堆栈跟踪或者声明这个方法抛出异常。不幸的是,这种办法不足以处理现实中出现的各种类型的异常。在生产系统中,当有异常抛出时,很可能是最终用户无法处理他或她的请求。当发生这样的异常时,最终用户通常希望能这样:
- 有一条清楚的消息表明已经发生了一个错误
- 有一个唯一的错误号,他可以据此访问可方便获得的客户支持系统
- 问题快速得到解决,并且可以确信他的请求已经得到处理,或者将在设定的时间段内得到处理
理想情况下,企业级系统将不仅为客户提供这些基本的服务,还将准备好一些必要的后端机制。举例来说,客户服务小组应该收到即时的错误通知,以便在客户打电话求助之前服务代表就能意识到问题。此外,服务代表应该能够交叉引用用户的唯一错误号和产品日志,从而快速识别问题 ― 最好是能把问题定位到确切的行号或确切的方法。为了给最终用户和支持小组提供他们需要的工具和服务,在构建一个系统时,您就必须对系统被部署后可能出问题的所有地方心中有数。
在本文中,我们将谈谈基于 EJB 的系统中的异常处理。我们将从回顾异常处理的基础知识开始,包括日志实用程序的使用,然后,很快就转入对 EJB 技术如何定义和管理不同类型的异常进行更详细的讨论。此后,我们将通过一些代码示例来研究一些常见的异常处理解决方案的优缺点,我还将展示我自己在充分利用 EJB 异常处理方面的最佳做法。
请注意,本文假设您熟悉 J2EE 和 EJB 技术。您应理解实体 bean 和会话 bean 的差异。如果您对 bean 管理的持久性(bean-managed persistence(BMP))和容器管理的持久性(container-managed persistence(CMP))在实体 bean 上下文中是什么意思稍有了解,也是有帮助的。请参阅 参考资料部分了解关于 J2EE 和 EJB 技术的更多信息。
异常处理基础知识
解决系统错误的第一步是建立一个与生产系统具有相同构造的测试系统,然后跟踪导致抛出异常的所有代码,以及代码中的所有不同分支。在分布式应用程序中,很可能是调试器不工作了,所以,您可能将用 System.out.println() 方法跟踪异常。 System.out.println 尽管很方便,但开销巨大。在磁盘 I/O 期间, System.out.println 对 I/O 处理进行同步,这极大降低了吞吐量。在缺省情况下,堆栈跟踪被记录到控制台。但是,在生产系统中,浏览控制台以查看异常跟踪是行不通的。而且,不能保证堆栈跟踪会显示在生产系统中,因为,在 NT 上,系统管理员可以把 System.out 和 System.err 映射到 ' ' ,在 UNIX 上,可以映射到 dev/null 。此外,如果您把 J2EE 应用程序服务器作为 NT 服务运行,甚至不会有控制台。即使您把控制台日志重定向到一个输出文件,当产品 J2EE 应用程序服务器重新启动时,这个文件很可能也将被重写。
|
异常处理的原则
以下是一些普遍接受的异常处理原则:
- 如果无法处理某个异常,那就不要捕获它。
- 如果捕获了一个异常,请不要胡乱处理它。
- 尽量在靠近异常被抛出的地方捕获异常。
- 在捕获异常的地方将它记录到日志中,除非您打算将它重新抛出。
- 按照您的异常处理必须多精细来构造您的方法。
- 需要用几种类型的异常就用几种,尤其是对于应用程序异常。
第 1 点显然与第 3 点相抵触。实际的解决方案是以下两者的折衷:您在距异常被抛出多近的地方将它捕获;在完全丢失原始异常的意图或内容之前,您可以让异常落在多远的地方。
注:尽管这些原则的应用遍及所有 EJB 异常处理机制,但它们并不是特别针对 EJB 异常处理的。 | |
由于以上这些原因,把代码组装成产品并同时包含 System.out.println 并不是一种选择。在测试期间使用 System.out.println ,然后在形成产品之前除去 System.out.println 也不是上策,因为这样做意味着您的产品代码与测试代码运行得不尽相同。您需要的是一种声明控制日志机制,以使您的测试代码和产品代码相同,并且当记录日志以声明方式关闭时,给产品带来的性能开销最小。
这里的解决方案显然是使用一个日志实用程序。采用恰当的编码约定,日志实用程序将负责精确地记录下任何类型的消息,不论是系统错误还是一些警告。所以,我们将在进一步讲述之前谈谈日志实用程序。
日志领域:鸟瞰
每个大型应用程序在开发、测试及产品周期中都使用日志实用程序。在今天的日志领域中,有几个角逐者,其中有两个广为人知。一个是 Log4J,它是来自 Apache 的 Jakarta 的一个开放源代码的项目。另一个是 J2SE 1.4 捆绑提供的,它是最近刚加入到这个行列的。我们将使用 Log4J 说明本文所讨论的最佳做法;但是,这些最佳做法并不特别依赖于 Log4J。
Log4J 有三个主要组件:layout、appender 和 category。 Layou代表消息被记录到日志中的格式。 appender是消息将被记录到的物理位置的别名。而 category则是有名称的实体:您可以把它当作是日志的句柄。layout 和 appender 在 XML 配置文件中声明。每个 category 带有它自己的 layout 和 appender 定义。当您获取了一个 category 并把消息记录到它那里时,消息在与该 category 相关联的各个 appender 处结束,并且所有这些消息都将以 XML 配置文件中指定的 layout 格式表示。
Log4J 给消息指定四种优先级:它们是 ERROR、WARN、INFO 和 DEBUG。为便于本文的讨论,所有异常都以具有 ERROR 优先级记录。当记录本文中的一个异常时,我们将能够找到获取 category(使用 Category.getInstance(String name) 方法)的代码,然后调用方法 category.error() (它与具有 ERROR 优先级的消息相对应)。
尽管日志实用程序能帮助我们把消息记录到适当的持久位置,但它们并不能根除问题。它们不能从产品日志中精确找出某个客户的问题报告;这一便利技术留给您把它构建到您正在开发的系统中。
要了解关于 Log4J 日志实用程序或 J2SE 所带的日志实用程序的更多信息,请参阅 参考资料部分。
异常的类别
异常的分类有不同方式。这里,我们将讨论从 EJB 的角度如何对异常进行分类。EJB 规范将异常大致分成三类:
- JVM 异常:这种类型的异常由 JVM 抛出。
OutOfMemoryError 就是 JVM 异常的一个常见示例。对 JVM 异常您无能为力。它们表明一种致命的情况。唯一得体的退出办法是停止应用程序服务器(可能要增加硬件资源),然后重新启动系统。
- 应用程序异常:应用程序异常是一种定制异常,由应用程序或第三方的库抛出。这些本质上是受查异常(checked exception);它们预示了业务逻辑中的某个条件尚未满足。在这样的情况下,EJB 方法的调用者可以得体地处理这种局面并采用另一条备用途径。
- 系统异常:在大多数情况下,系统异常由 JVM 作为
RuntimeException 的子类抛出。例如, NullPointerException 或 ArrayOutOfBoundsException 将因代码中的错误而被抛出。另一种类型的系统异常在系统碰到配置不当的资源(例如,拼写错误的 JNDI 查找(JNDI lookup))时发生。在这种情况下,系统就将抛出一个受查异常。捕获这些受查系统异常并将它们作为非受查异常(unchecked exception)抛出颇有意义。最重要的规则是,如果您对某个异常无能为力,那么它就是一个系统异常并且应当作为非受查异常抛出。
注: 受查异常是一个作为 java.lang.Exception 的子类的 Java 类。通过从 java.lang.Exception 派生子类,就强制您在编译时捕获这个异常。相反地, 非受查异常则是一个作为 java.lang.RuntimeException 的子类的 Java 类。从 java.lang.RuntimeException 派生子类确保了编译器不会强制您捕获这个异常。
EJB 容器怎样处理异常
EJB 容器拦截 EJB 组件上的每一个方法调用。结果,方法调用中发生的每一个异常也被 EJB 容器拦截到。EJB 规范只处理两种类型的异常:应用程序异常和系统异常。
EJB 规范把 应用程序异常定义为在远程接口中的方法说明上声明的任何异常(而不是 RemoteException )。应用程序异常是业务工作流中的一种特殊情形。当这种类型的异常被抛出时,客户机会得到一个恢复选项,这个选项通常是要求以一种不同的方式处理请求。不过,这并不意味着任何在远程接口方法的 throws 子句中声明的非受查异常都会被当作应用程序异常对待。EJB 规范明确指出,应用程序异常不应继承 RuntimeException 或它的子类。
当发生应用程序异常时,除非被显式要求(通过调用关联的 EJBContext 对象的 setRollbackOnly() 方法)回滚事务,否则 EJB 容器就不会这样做。事实上,应用程序异常被保证以它原本的状态传送给客户机:EJB 容器绝不会以任何方式包装或修改异常。
系统异常被定义为受查异常或非受查异常,EJB 方法不能从这种异常恢复。当 EJB 容器拦截到非受查异常时,它会回滚事务并执行任何必要的清理工作。接着,它把该非受查异常包装到 RemoteException 中,然后抛给客户机。这样,EJB 容器就把所有非受查异常作为 RemoteException (或者作为其子类,例如 TransactionRolledbackException )提供给客户机。
对于受查异常的情况,容器并不会自动执行上面所描述的内务处理。要使用 EJB 容器的内部内务处理,您将必须把受查异常作为非受查异常抛出。每当发生受查系统异常(如 NamingException )时,您都应该通过包装原始的异常抛出 javax.ejb.EJBException 或其子类。因为 EJBException 本身是非受查异常,所以不需要在方法的 throws 子句中声明它。EJB 容器捕获 EJBException 或其子类,把它包装到 RemoteException 中,然后把 RemoteException 抛给客户机。
虽然系统异常由应用程序服务器记录(这是 EJB 规范规定的),但记录格式将因应用程序服务器的不同而异。为了访问所需的统计信息,企业常常需要对所生成的日志运行 shell/Perl 脚本。为了确保记录格式的统一,在您的代码中记录异常会更好些。
注:EJB 1.0 规范要求把受查系统异常作为 RemoteException 抛出。从 EJB 1.1 规范起规定 EJB 实现类绝不应抛出 RemoteException 。
常见的异常处理策略
如果没有异常处理策略,项目小组的不同开发者很可能会编写以不同方式处理异常的代码。由于同一个异常在系统的不同地方可能以不同的方式被描述和处理,所以,这至少会使产品支持小组感到迷惑。缺乏策略还会导致在整个系统的多个地方都有记录。日志应该集中起来或者分成几个可管理的单元。理想的情况是,应在尽可能少的地方记录异常日志,同时不损失内容。在这一部分及其后的几个部分,我将展示可以在整个企业系统中以统一的方式实现的编码策略。您可以从 参考资料部分下载本文开发的实用程序类。
清单 1 显示了来自会话 EJB 组件的一个方法。这个方法删除某个客户在特定日期前所下的全部订单。首先,它获取 OrderEJB 的 Home 接口。接着,它取回某个特定客户的所有订单。当它碰到在某个特定日期之前所下的订单时,就删除所订购的商品,然后删除订单本身。请注意,抛出了三个异常,显示了三种常见的异常处理做法。(为简单起见,假设编译器优化未被使用。) 清单 1. 三种常见的异常处理做法
100 try {
101 OrderHome homeObj = EJBHomeFactory.getInstance().getOrderHome();
102 Collection orderCollection = homeObj.findByCustomerId(id);
103 iterator orderItter = orderCollection.iterator();
104 while (orderIter.hasNext()) {
105 Order orderRemote = (OrderRemote) orderIter.getNext();
106 OrderValue orderVal = orderRemote.getValue();
107 if (orderVal.getDate() < "mm/dd/yyyy") {
108 OrderItemHome itemHome =
EJBHomeFactory.getInstance().getItemHome();
109 Collection itemCol = itemHome.findByOrderId(orderId)
110 Iterator itemIter = itemCol.iterator();
111 while (itemIter.hasNext()) {
112 OrderItem item = (OrderItem) itemIter.getNext();
113 item.remove();
114 }
115 orderRemote.remove();
116 }
117 }
118 } catch (NamingException ne) {
119 throw new EJBException("Naming Exception occurred");
120 } catch (FinderException fe) {
121 fe.printStackTrace();
122 throw new EJBException("Finder Exception occurred");
123 } catch (RemoteException re) {
124 re.printStackTrace();
125 //Some code to log the message
126 throw new EJBException(re);
127 }
|
现在,让我们用上面所示的代码来研究一下所展示的三种异常处理做法的缺点。
抛出/重抛出带有出错消息的异常
NamingException 可能发生在行 101 或行 108。当发生 NamingException 时,这个方法的调用者就得到 RemoteException 并向后跟踪该异常到行 119。调用者并不能告知 NamingException 实际是发生在行 101 还是行 108。由于异常内容要直到被记录了才能得到保护,所以,这个问题的根源很难查出。在这种情形下,我们就说异常的内容被“吞掉”了。正如这个示例所示,抛出或重抛出一个带有消息的异常并不是一种好的异常处理解决办法。
记录到控制台并抛出一个异常
FinderException 可能发生在行 102 或 109。不过,由于异常被记录到控制台,所以仅当控制台可用时调用者才能向后跟踪到行 102 或 109。这显然不可行,所以异常只能被向后跟踪到行 122。这里的推理同上。
包装原始的异常以保护其内容
RemoteException 可能发生在行 102、106、109、113 或 115。它在行 123 的 catch 块被捕获。接着,这个异常被包装到 EJBException 中,所以,不论调用者在哪里记录它,它都能保持完整。这种办法比前面两种办法更好,同时演示了没有日志策略的情况。如果 deleteOldOrders() 方法的调用者记录该异常,那么将导致重复记录。而且,尽管有了日志记录,但当客户报告某个问题时,产品日志或控制台并不能被交叉引用。
EJB 异常处理探试法
EJB 组件应抛出哪些异常?您应将它们记录到系统中的什么地方?这两个问题盘根错结、相互联系,应该一起解决。解决办法取决于以下因素:
- 您的 EJB 系统设计:在良好的 EJB 设计中,客户机绝不调用实体 EJB 组件上的方法。多数实体 EJB 方法调用发生在会话 EJB 组件中。如果您的设计遵循这些准则,则您应该用会话 EJB 组件来记录异常。如果客户机直接调用了实体 EJB 方法,则您还应该把消息记录到实体 EJB 组件中。然而,存在一个难题:相同的实体 EJB 方法可能也会被会话 EJB 组件调用。在这种情形下,如何避免重复记录呢?类似地,当一个会话 EJB 组件调用其它实体 EJB 方法时,您如何避免重复记录呢?很快我们就将探讨一种处理这两种情况的通用解决方案。(请注意,EJB 1.1 并未从体系结构上阻止客户机调用实体 EJB 组件上的方法。在 EJB 2.0 中,您可以通过为实体 EJB 组件定义本地接口规定这种限制。)
- 计划的代码重用范围:这里的问题是您是打算把日志代码添加到多个地方,还是打算重新设计、重新构造代码来减少日志代码。
- 您要为之服务的客户机的类型:考虑您是将为 J2EE Web 层、单机 Java 应用程序、PDA 还是将为其它客户机服务是很重要的。Web 层设计有各种形状和大小。如果您在使用命令(Command)模式,在这个模式中,Web 层通过每次传入一个不同的命令调用 EJB 层中的相同方法,那么,把异常记录到命令在其中执行的 EJB 组件中是很有用的。在多数其它的 Web 层设计中,把异常记录到 Web 层本身要更容易,也更好,因为您需要把异常日志代码添加到更少的地方。如果您的 Web 层和 EJB 层在同一地方并且不需要支持任何其它类型的客户机,那么就应该考虑后一种选择。
- 您将处理的异常的类型(应用程序或系统):处理应用程序异常与处理系统异常有很大不同。系统异常的发生不受 EJB 开发者意图的控制。因为系统异常的含义不清楚,所以内容应指明异常的上下文。您已经看到了,通过对原始异常进行包装使这个问题得到了最好的处理。另一方面,应用程序异常是由 EJB 开发者显式抛出的,通常包装有一条消息。因为应用程序异常的含义清楚,所以没有理由要保护它的上下文。这种类型的异常不必记录到 EJB 层或客户机层;它应该以一种有意义的方式提供给最终用户,带上指向所提供的解决方案的另一条备用途径。系统异常消息没必要对最终用户很有意义。
处理应用程序异常
在这一部分及其后的几个部分中,我们将更仔细地研究用 EJB 异常处理应用程序异常和系统异常,以及 Web 层设计。作为这个讨论的一部分,我们将探讨处理从会话和实体 EJB 组件抛出的异常的不同方式。
实体 EJB 组件中的应用程序异常 清单 2 显示了实体 EJB 的一个 ejbCreate() 方法。这个方法的调用者传入一个 OrderItemValue 并请求创建一个 OrderItem 实体。因为 OrderItemValue 没有名称,所以抛出了 CreateException 。 清单 2. 实体 EJB 组件中的样本 ejbCreate() 方法
public Integer ejbCreate(OrderItemValue value) throws CreateException {
if (value.getItemName() == null) {
throw new CreateException("Cannot create Order without a name");
}
..
..
return null;
}
|
清单 2 显示了 CreateException 的一个很典型的用法。类似地,如果方法的输入参数的值不正确,则查找程序方法将抛出 FinderException 。
然而,如果您在使用容器管理的持久性(CMP),则开发者无法控制查找程序方法,从而 FinderException 永远不会被 CMP 实现抛出。尽管如此,在 Home 接口的查找程序方法的 throws 子句中声明 FinderException 还是要更好一些。 RemoveException 是另一个应用程序异常,它在实体被删除时被抛出。
从实体 EJB 组件抛出的应用程序异常基本上限定为这三种类型( CreateException 、 FinderException 和 RemoveException )及它们的子类。多数应用程序异常都来源于会话 EJB 组件,因为那里是作出智能决策的地方。实体 EJB 组件一般是哑类,它们的唯一职责就是创建和取回数据。
会话 EJB 组件中的应用程序异常 清单 3 显示了来自会话 EJB 组件的一个方法。这个方法的调用者设法订购 n 件某特定类型的某商品。 SessionEJB() 方法计算出仓库中的数量不够,于是抛出 NotEnoughStockException 。 NotEnoughStockException 适用于特定于业务的场合;当抛出了这个异常时,调用者会得到采用另一个备用途径的建议,让他订购更少数量的商品。 清单 3. 会话 EJB 组件中的样本容器回调方法
public ItemValueObject[] placeOrder(int n, ItemType itemType) throws
NotEnoughStockException {
//Check Inventory.
Collection orders = ItemHome.findByItemType(itemType);
if (orders.size() < n) {
throw NotEnoughStockException("Insufficient stock for " + itemType);
}
}
|
处理系统异常
系统异常处理是比应用程序异常处理更为复杂的论题。由于会话 EJB 组件和实体 EJB 组件处理系统异常的方式相似,所以,对于本部分的所有示例,我们都将着重于实体 EJB 组件,不过请记住,其中的大部分示例也适用于处理会话 EJB 组件。
当引用其它 EJB 远程接口时,实体 EJB 组件会碰到 RemoteException ,而查找其它 EJB 组件时,则会碰到 NamingException ,如果使用 bean 管理的持久性(BMP),则会碰到 SQLException 。与这些类似的受查系统异常应该被捕获并作为 EJBException 或它的一个子类抛出。原始的异常应被包装起来。清单 4 显示了一种处理系统异常的办法,这种办法与处理系统异常的 EJB 容器的行为一致。通过包装原始的异常并在实体 EJB 组件中将它重新抛出,您就确保了能够在想记录它的时候访问该异常。 清单 4. 处理系统异常的一种常见方式
try {
OrderHome orderHome = EJBHomeFactory.getInstance().getOrderHome();
Order order = orderHome.findByPrimaryKey(Integer id);
} catch (NamingException ne) {
throw new EJBException(ne);
} catch (SQLException se) {
throw new EJBException(se);
} catch (RemoteException re) {
throw new EJBException(re);
}
|
避免重复记录
通常,异常记录发生在会话 EJB 组件中。但如果直接从 EJB 层外部访问实体 EJB 组件,又会怎么样呢?要是这样,您就不得不在实体 EJB 组件中记录异常并抛出它。这里的问题是,调用者没办法知道异常是否已经被记录,因而很可能再次记录它,从而导致重复记录。更重要的是,调用者没办法访问初始记录时所生成的唯一的标识。任何没有交叉引用机制的记录都是毫无用处的。
请考虑这种最糟糕的情形:单机 Java 应用程序访问了实体 EJB 组件中的一个方法 foo() 。在一个名为 bar() 的会话 EJB 方法中也访问了同一个方法。一个 Web 层客户机调用会话 EJB 组件的方法 bar() 并也记录了该异常。如果当从 Web 层调用会话 EJB 方法 bar() 时在实体 EJB 方法 foo() 中发生了一个异常,则该异常将被记录到三个地方:先是在实体 EJB 组件,然后是在会话 EJB 组件,最后是在 Web 层。而且,没有一个堆栈跟踪可以被交叉引用!
幸运的是,解决这些问题用常规办法就可以很容易地做到。您所需要的只是一种机制,使调用者能够:
您可以派生 EJBException 的子类来存储这样的信息。清单 5 显示了 LoggableEJBException 子类: 清单 5. LoggableEJBException ― EJBException 的一个子类
public class LoggableEJBException extends EJBException {
protected boolean isLogged;
protected String uniqueID;
public LoggableEJBException(Exception exc) {
super(exc);
isLogged = false;
uniqueID = ExceptionIDGenerator.getExceptionID();
}
..
..
}
|
类 LoggableEJBException 有一个指示符标志( isLogged ),用于检查异常是否已经被记录了。每当捕获一个 LoggableEJBException 时,看一下该异常是否已经被记录了( isLogged == false )。如果 isLogged 为 false,则记录该异常并把标志设置为 true 。
ExceptionIDGenerator 类用当前时间和机器的主机名为异常生成唯一的标识。如果您喜欢,也可以用有想象力的算法来生成这个唯一的标识。如果您在实体 EJB 组件中记录了异常,则这个异常将不会在别的地方被记录。如果您没有记录就在实体 EJB 组件中抛出了 LoggableEJBException ,则这个异常将被记录到会话 EJB 组件中,但不记录到 Web 层中。
单 6 显示了使用这一技术重写后的清单 4。您还可以继承 LoggableException 以适合于您的需要(通过给异常指定错误代码等)。 清单 6. 使用 LoggableEJBException 的异常处理
try {
OrderHome orderHome = EJBHomeFactory.getInstance().getOrderHome();
Order order = orderHome.findByPrimaryKey(Integer id);
} catch (NamingException ne) {
throw new LoggableEJBException(ne);
} catch (SQLException se) {
throw new LoggableEJBException(se);
} catch (RemoteException re) {
Throwable t = re.detail;
if (t != null && t instanceof Exception) {
throw new LoggableEJBException((Exception) re.detail);
} else {
throw new LoggableEJBException(re);
}
}
|
记录 RemoteException
从清单 6 中,您可以看到 naming 和 SQL 异常在被抛出前被包装到了 LoggableEJBException 中。但 RemoteException 是以一种稍有不同 ― 而且要稍微花点气力 ― 的方式处理的。
|
会话 EJB 组件中的系统异常
如果您决定记录会话 EJB 异常,请使用 清单 7所示的记录代码;否则,请抛出异常,如 清单 6所示。您应该注意到,会话 EJB 组件处理异常可有一种与实体 EJB 组件不同的方式:因为大多数 EJB 系统都只能从 Web 层访问,而且会话 EJB 可以作为 EJB 层的虚包,所以,把会话 EJB 异常的记录推迟到 Web 层实际上是有可能做到的。 | | 它之所以不同,是因为在 RemoteException 中,实际的异常将被存储到一个称为 detail (它是 Throwable 类型的)的公共属性中。在大多数情况下,这个公共属性保存有一个异常。如果您调用 RemoteException 的 printStackTrace ,则除打印 detail 的堆栈跟踪之外,它还会打印异常本身的堆栈跟踪。您不需要像这样的 RemoteException 的堆栈跟踪。
为了把您的应用程序代码从错综复杂的代码(例如 RemoteException 的代码)中分离出来,这些行被重新构造成一个称为 ExceptionLogUtil 的类。有了这个类,您所要做的只是每当需要创建 LoggableEJBException 时调用 ExceptionLogUtil.createLoggableEJBException(e) 。请注意,在清单 6 中,实体 EJB 组件并没有记录异常;不过,即便您决定在实体 EJB 组件中记录异常,这个解决方案仍然行得通。清单 7 显示了实体 EJB 组件中的异常记录: 清单 7. 实体 EJB 组件中的异常记录
try {
OrderHome orderHome = EJBHomeFactory.getInstance().getOrderHome();
Order order = orderHome.findByPrimaryKey(Integer id);
} catch (RemoteException re) {
LoggableEJBException le =
ExceptionLogUtil.createLoggableEJBException(re);
String traceStr = StackTraceUtil.getStackTrace(le);
Category.getInstance(getClass().getName()).error(le.getUniqueID() +
":" + traceStr);
le.setLogged(true);
throw le;
}
|
您在清单 7 中看到的是一个非常简单明了的异常记录机制。一旦捕获受查系统异常就创建一个新的 LoggableEJBException 。接着,使用类 StackTraceUtil 获取 LoggableEJBException 的堆栈跟踪,把它作为一个字符串。然后,使用 Log4J category 把该字符串作为一个错误加以记录。
StackTraceUtil 类的工作原理
在清单 7 中,您看到了一个新的称为 StackTraceUtil 的类。因为 Log4J 只能记录 String 消息,所以这个类负责解决把堆栈跟踪转换成 String 的问题。清单 8 说明了 StackTraceUtil 类的工作原理: 清单 8. StackTraceUtil 类
public class StackTraceUtil {
public static String getStackTrace(Exception e)
{
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
return sw.toString();
}
..
..
}
|
java.lang.Throwable 中缺省的 printStackTrace() 方法把出错消息记录到 System.err 。 Throwable 还有一个重载的 printStackTrace() 方法,它把出错消息记录到 PrintWriter 或 PrintStream 。上面的 StackTraceUtil 中的方法把 StringWriter 包装到 PrintWriter 中。当 PrintWriter 包含有堆栈跟踪时,它只是调用 StringWriter 的 toString() ,以获取该堆栈跟踪的 String 表示。
Web 层的 EJB 异常处理
在 Web 层设计中,把异常记录机制放到客户机端往往更容易也更高效。要能做到这一点,Web 层就必须是 EJB 层的唯一客户机。此外,Web 层必须建立在以下模式或框架之一的基础上:
- 模式:业务委派(Business Delegate)、FrontController 或拦截过滤器(Intercepting Filter)
- 框架:Struts 或任何包含层次结构的类似于 MVC 框架的框架
为什么异常记录应该在客户机端上发生呢?嗯,首先,控制尚未传到应用程序服务器之外。所谓的客户机层在 J2EE 应用程序服务器本身上运行,它由 JSP 页、servlet 或它们的助手类组成。其次,在设计良好的 Web 层中的类有一个层次结构(例如:在业务委派(Business Delegate)类、拦截过滤器(Intercepting Filter)类、http 请求处理程序(http request handler)类和 JSP 基类(JSP base class)中,或者在 Struts Action 类中),或者 FrontController servlet 形式的单点调用。这些层次结构的基类或者 Controller 类中的中央点可能包含有异常记录代码。对于基于会话 EJB 记录的情况,EJB 组件中的每一个方法都必须具有记录代码。随着业务逻辑的增加,会话 EJB 方法的数量也会增加,记录代码的数量也会增加。Web 层系统将需要更少的记录代码。如果您的 Web 层和 EJB 层在同一地方并且不需要支持任何其它类型的客户机,那么您应该考虑这一备用方案。不管怎样,记录机制不会改变;您可以使用与前面的部分所描述的相同技术。
真实世界的复杂性
到现在为止,您已经看到了简单情形的会话和实体 EJB 组件的异常处理技术。然而,应用程序异常的某些组合可能会更令人费解,并且有多种解释。清单 9 显示了一个示例。 OrderEJB 的 ejbCreate() 方法试图获取 CustomerEJB 的一个远程引用,这会导致 FinderException 。 OrderEJB 和 CustomerEJB 都是实体 EJB 组件。您应该如何解释 ejbCreate() 中的这个 FinderException 呢?是把它当作应用程序异常对待呢(因为 EJB 规范把它定义为标准应用程序异常),还是当作系统异常对待? 清单 9. ejbCreate() 方法中的 FinderException
public Object ejbCreate(OrderValue val) throws CreateException {
try {
if (value.getItemName() == null) {
throw new CreateException("Cannot create Order without a name");
}
String custId = val.getCustomerId();
Customer cust = customerHome.fingByPrimaryKey(custId);
this.customer = cust;
} catch (FinderException ne) {
//How do you handle this Exception ?
} catch (RemoteException re) {
//This is clearly a System Exception
throw ExceptionLogUtil.createLoggableEJBException(re);
}
return null;
}
|
虽然没有什么东西阻止您把 FinderException 当应用程序异常对待,但把它当系统异常对待会更好。原因是:EJB 客户机倾向于把 EJB 组件当黑箱对待。如果 createOrder() 方法的调用者获得了一个 FinderException ,这对调用者并没有任何意义。 OrderEJB 正试图设置客户远程引用这件事对调用者来说是透明的。从客户机的角度看,失败仅仅意味着该订单无法创建。
这类情形的另一个示例是,会话 EJB 组件试图创建另一个会话 EJB,因而导致了一个 CreateException 。一种类似的情形是,实体 EJB 方法试图创建一个会话 EJB 组件,因而导致了一个 CreateException 。这两个异常都应该当作系统异常对待。
另一个可能碰到的挑战是会话 EJB 组件在它的某个容器回调方法中获得了一个 FinderException 。您必须逐例处理这类情况。您可能要决定是把 FinderException 当应用程序异常还是系统异常对待。请考虑清单 1 的情况,其中调用者调用了会话 EJB 组件的 deleteOldOrder 方法。如果我们不是捕获 FinderException ,而是将它抛出,会怎么样呢?在这一特定情况中,把 FinderException 当系统异常对待似乎是符合逻辑的。这里的理由是,会话 EJB 组件倾向于在它们的方法中做许多工作,因为它们处理工作流情形,并且它们对调用者而言是黑箱。
另一方面,请考虑会话 EJB 正在处理下订单的情形。要下一个订单,用户必须有一个简档 ― 但这个特定用户却还没有。业务逻辑可能希望会话 EJB 显式地通知用户她的简档丢失了。丢失的简档很可能表现为会话 EJB 组件中的 javax.ejb.ObjectNotFoundException ( FinderException 的一个子类)。在这种情况下,最好的办法是在会话 EJB 组件中捕获 ObjectNotFoundException 并抛出一个应用程序异常,让用户知道她的简档丢失了。
即使是有了很好的异常处理策略,另一个问题还是经常会在测试中出现,而且在产品中也更加重要。编译器和运行时优化会改变一个类的整体结构,这会限制您使用堆栈跟踪实用程序来跟踪异常的能力。这就是您需要代码重构的帮助的地方。您应该把大的方法调用分割为更小的、更易于管理的块。而且,只要有可能,异常类型需要多少就划分为多少;每次您捕获一个异常,都应该捕获已规定好类型的异常,而不是捕获所有类型的异常。
结束语
我们已经在本文讨论了很多东西,您可能想知道我们已经讨论的主要设计是否都物有所值。我的经验是,即便是在中小型项目中,在开发周期中,您的付出就已经能看到回报,更不用说测试和产品周期了。此外,在宕机对业务具有毁灭性影响的生产系统中,良好的异常处理体系结构的重要性再怎么强调也不过分。
我希望本文所展示的最佳做法对您有益。要深入理解这里提供的某些信息,请参看 参考资料部分中的清单。
参考资料
关于作者
|
|
Srikanth Shenoy 专门从事大型 J2EE 和 EAI 项目的体系结构、设计、开发和部署工作。他在 Java 平台一出现时就迷上了它,从此便全心投入。Srikanth 已经帮他的制造业、物流业和金融业客户实现了 Java 平台“一次编写,随处运行”的梦想。您可以通过 srikanth@srikanth.org与他联系。 | |
原著:Steve Mansour
sman@scruznet.com
Revised: June 5, 1999
(copied by jm /at/ jmason.org from http://www.scruz.net/%7esman/regexp.htm, after the original disappeared! )
翻译:Neo Lee
什么是正则表达式
一个正则表达式,就是用某种模式去匹配一类字符串的一个公式。很多人因为它们看上去比较古怪而且复杂所以不敢去使用——很不幸,这篇文章也不能够改变这一点,不过,经过一点点练习之后我就开始觉得这些复杂的表达式其实写起来还是相当简单的,而且,一旦你弄懂它们,你就能把数小时辛苦而且易错的文本处理工作压缩在几分钟(甚至几秒钟)内完成。正则表达式被各种文本编辑软件、类库(例如Rogue Wave的tools.h++)、脚本工具(像awk/grep/sed)广泛的支持,而且像Microsoft的Visual C++这种交互式IDE也开始支持它了。
我们将在如下的章节中利用一些例子来解释正则表达式的用法,绝大部分的例子是基于vi中的文本替换命令和grep文件搜索命令来书写的,不过它们都是比较典型的例子,其中的概念可以在sed、awk、perl和其他支持正则表达式的编程语言中使用。你可以看看不同工具中的正则表达式这一节,其中有一些在别的工具中使用正则表达式的例子。还有一个关于vi中文本替换命令(s)的简单说明附在文后供参考。
正则表达式基础
正则表达式由一些普通字符和一些元字符(metacharacters)组成。普通字符包括大小写的字母和数字,而元字符则具有特殊的含义,我们下面会给予解释。
在最简单的情况下,一个正则表达式看上去就是一个普通的查找串。例如,正则表达式"testing"中没有包含任何元字符,,它可以匹配"testing"和"123testing"等字符串,但是不能匹配"Testing"。
要想真正的用好正则表达式,正确的理解元字符是最重要的事情。下表列出了所有的元字符和对它们的一个简短的描述。
元字符 |
|
描述 |
|
|
|
. |
|
匹配任何单个字符。例如正则表达式r.t匹配这些字符串:rat、rut、r t,但是不匹配root。 |
$ |
|
匹配行结束符。例如正则表达式weasel$ 能够匹配字符串"He's a weasel"的末尾,但是不能匹配字符串"They are a bunch of weasels."。 |
^ |
|
匹配一行的开始。例如正则表达式^When in能够匹配字符串"When in the course of human events"的开始,但是不能匹配"What and When in the"。 |
* |
|
匹配0或多个正好在它之前的那个字符。例如正则表达式.*意味着能够匹配任意数量的任何字符。 |
\ |
|
这是引用府,用来将这里列出的这些元字符当作普通的字符来进行匹配。例如正则表达式\$被用来匹配美元符号,而不是行尾,类似的,正则表达式\.用来匹配点字符,而不是任何字符的通配符。 |
[ ] [c1-c2] [^c1-c2] |
|
匹配括号中的任何一个字符。例如正则表达式r[aou]t匹配rat、rot和rut,但是不匹配ret。可以在括号中使用连字符-来指定字符的区间,例如正则表达式[0-9]可以匹配任何数字字符;还可以制定多个区间,例如正则表达式[A-Za-z]可以匹配任何大小写字母。另一个重要的用法是“排除”,要想匹配除了指定区间之外的字符——也就是所谓的补集——在左边的括号和第一个字符之间使用^字符,例如正则表达式[^269A-Z] 将匹配除了2、6、9和所有大写字母之外的任何字符。 |
\< \> |
|
匹配词(word)的开始(\<)和结束(\>)。例如正则表达式\<the能够匹配字符串"for the wise"中的"the",但是不能匹配字符串"otherwise"中的"the"。注意:这个元字符不是所有的软件都支持的。 |
\( \) |
|
将 \( 和 \) 之间的表达式定义为“组”(group),并且将匹配这个表达式的字符保存到一个临时区域(一个正则表达式中最多可以保存9个),它们可以用 \1 到\9 的符号来引用。 |
| |
|
将两个匹配条件进行逻辑“或”(Or)运算。例如正则表达式(him|her) 匹配"it belongs to him"和"it belongs to her",但是不能匹配"it belongs to them."。注意:这个元字符不是所有的软件都支持的。 |
+ |
|
匹配1或多个正好在它之前的那个字符。例如正则表达式9+匹配9、99、999等。注意:这个元字符不是所有的软件都支持的。 |
? |
|
匹配0或1个正好在它之前的那个字符。注意:这个元字符不是所有的软件都支持的。 |
\{i\} \{i,j\} |
|
匹配指定数目的字符,这些字符是在它之前的表达式定义的。例如正则表达式A[0-9]\{3\} 能够匹配字符"A"后面跟着正好3个数字字符的串,例如A123、A348等,但是不匹配A1234。而正则表达式[0-9]\{4,6\} 匹配连续的任意4个、5个或者6个数字字符。注意:这个元字符不是所有的软件都支持的。 |
最简单的元字符是点,它能够匹配任何单个字符(注意不包括新行符)。假定有个文件test.txt包含以下几行内容:
he is a rat
he is in a rut
the food is Rotten
I like root beer
我们可以使用grep命令来测试我们的正则表达式,grep命令使用正则表达式去尝试匹配指定文件的每一行,并将至少有一处匹配表达式的所有行显示出来。命令
在test.txt文件中的每一行中搜索正则表达式r.t,并打印输出匹配的行。正则表达式r.t匹配一个r接着任何一个字符再接着一个t。所以它将匹配文件中的rat和rut,而不能匹配Rotten中的Rot,因为正则表达式是大小写敏感的。要想同时匹配大写和小写字母,应该使用字符区间元字符(方括号)。正则表达式[Rr]能够同时匹配R和r。所以,要想匹配一个大写或者小写的r接着任何一个字符再接着一个t就要使用这个表达式:[Rr].t。
要想匹配行首的字符要使用抑扬字符(^)——又是也被叫做插入符。例如,想找到text.txt中行首"he"打头的行,你可能会先用简单表达式he,但是这会匹配第三行的the,所以要使用正则表达式^he,它只匹配在行首出现的h。
有时候指定“除了×××都匹配”会比较容易达到目的,当抑扬字符(^)出现在方括号中是,它表示“排除”,例如要匹配he ,但是排除前面是t or s的情性(也就是the和she),可以使用:[^st]he。
可以使用方括号来指定多个字符区间。例如正则表达式[A-Za-z]匹配任何字母,包括大写和小写的;正则表达式[A-Za-z][A-Za-z]* 匹配一个字母后面接着0或者多个字母(大写或者小写)。当然我们也可以用元字符+做到同样的事情,也就是:[A-Za-z]+ ,和[A-Za-z][A-Za-z]*完全等价。但是要注意元字符+ 并不是所有支持正则表达式的程序都支持的。关于这一点可以参考后面的正则表达式语法支持情况。
要指定特定数量的匹配,要使用大括号(注意必须使用反斜杠来转义)。想匹配所有100和1000的实例而排除10和10000,可以使用:10\{2,3\},这个正则表达式匹配数字1后面跟着2或者3个0的模式。在这个元字符的使用中一个有用的变化是忽略第二个数字,例如正则表达式0\{3,\} 将匹配至少3个连续的0。
简单的例子
这里有一些有代表性的、比较简单的例子。
vi 命令 |
作用 |
|
|
:%s/ */ /g |
把一个或者多个空格替换为一个空格。 |
:%s/ *$// |
去掉行尾的所有空格。 |
:%s/^/ / |
在每一行头上加入一个空格。 |
:%s/^[0-9][0-9]* // |
去掉行首的所有数字字符。 |
:%s/b[aeio]g/bug/g |
将所有的bag、beg、big和bog改为bug。 |
:%s/t\([aou]\)g/h\1t/g |
将所有tag、tog和tug分别改为hat、hot和hug(注意用group的用法和使用\1引用前面被匹配的字符)。 |
|
|
中级的例子(神奇的咒语)
例1
将所有方法foo(a,b,c)的实例改为foo(b,a,c)。这里a、b和c可以是任何提供给方法foo()的参数。也就是说我们要实现这样的转换:
之前 |
|
之后 |
foo(10,7,2) |
|
foo(7,10,2) |
foo(x+13,y-2,10) |
|
foo(y-2,x+13,10) |
foo( bar(8), x+y+z, 5) |
|
foo( x+y+z, bar(8), 5) |
下面这条替换命令能够实现这一魔法:
:%s/foo(\([^,]*\),\([^,]*\),\([^)]*\))/foo(\2,\1,\3)/g
现在让我们把它打散来加以分析。写出这个表达式的基本思路是找出foo()和它的括号中的三个参数的位置。第一个参数是用这个表达式来识别的::\([^,]*\),我们可以从里向外来分析它:
[^,] |
|
除了逗号之外的任何字符 |
[^,]* |
|
0或者多个非逗号字符 |
\([^,]*\) |
|
将这些非逗号字符标记为\1,这样可以在之后的替换模式表达式中引用它 |
\([^,]*\), |
|
我们必须找到0或者多个非逗号字符后面跟着一个逗号,并且非逗号字符那部分要标记出来以备后用。 |
现在正是指出一个使用正则表达式常见错误的最佳时机。为什么我们要使用[^,]*这样的一个表达式,而不是更加简单直接的写法,例如:.*,来匹配第一个参数呢?设想我们使用模式.*来匹配字符串"10,7,2",它应该匹配"10,"还是"10,7,"?为了解决这个两义性(ambiguity),正则表达式规定一律按照最长的串来,在上面的例子中就是"10,7,",显然这样就找出了两个参数而不是我们期望的一个。所以,我们要使用[^,]*来强制取出第一个逗号之前的部分。
这个表达式我们已经分析到了:foo(\([^,]*\),这一段可以简单的翻译为“当你找到foo(就把其后直到第一个逗号之前的部分标记为\1”。然后我们使用同样的办法标记第二个参数为\2。对第三个参数的标记方法也是一样,只是我们要搜索所有的字符直到右括号。我们并没有必要去搜索第三个参数,因为我们不需要调整它的位置,但是这样的模式能够保证我们只去替换那些有三个参数的foo()方法调用,在foo()是一个重载(overoading)方法时这种明确的模式往往是比较保险的。然后,在替换部分,我们找到foo()的对应实例,然后利用标记好的部分进行替换,是的第一和第二个参数交换位置。
例2
假设有一个CSV(comma separated value)文件,里面有一些我们需要的信息,但是格式却有问题,目前数据的列顺序是:姓名,公司名,州名缩写,邮政编码,现在我们希望讲这些数据重新组织,以便在我们的某个软件中使用,需要的格式为:姓名,州名缩写-邮政编码,公司名。也就是说,我们要调整列顺序,还要合并两个列来构成一个新列。另外,我们的软件不能接受逗号前后面有任何空格(包括空格和制表符)所以我们还必须要去掉逗号前后的所有空格。
这里有几行我们现在的数据:
Bill Jones, HI-TEK Corporation , CA, 95011
Sharon Lee Smith, Design Works Incorporated, CA, 95012
B. Amos , Hill Street Cafe, CA, 95013
Alexander Weatherworth, The Crafts Store, CA, 95014
...
我们希望把它变成这个样子:
Bill Jones,CA 95011,HI-TEK Corporation
Sharon Lee Smith,CA 95012,Design Works Incorporated
B. Amos,CA 95013,Hill Street Cafe
Alexander Weatherworth,CA 95014,The Crafts Store
...
我们将用两个正则表达式来解决这个问题。第一个移动列和合并列,第二个用来去掉空格。
下面就是第一个替换命令:
:%s/\([^,]*\),\([^,]*\),\([^,]*\),\(.*\)/\1,\3 \4,\2/
这里的方法跟例1基本一样,第一个列(姓名)用这个表达式来匹配:\([^,]*\),即第一个逗号之前的所有字符,而姓名内容被用\1标记下来。公司名和州名缩写字段用同样的方法标记为\2和\3,而最后一个字段用\(.*\)来匹配("匹配所有字符直到行末")。替换部分则引用上面标记的那些内容来进行构造。
下面这个替换命令则用来去除空格:
我们还是分解来看:[ \t]匹配空格/制表符,[ \t]* 匹配0或多个空格/制表符,[ \t]*,匹配0或多个空格/制表符后面再加一个逗号,最后,[ \t]*,[ \t]*匹配0或多个空格/制表符接着一个逗号再接着0或多个空格/制表符。在替换部分,我们简单的我们找到的所有东西替换成一个逗号。这里我们使用了结尾的可选的g参数,这表示在每行中对所有匹配的串执行替换(而不是缺省的只替换第一个匹配串)。
例3
假设有一个多字符的片断重复出现,例如:
Billy tried really hard
Sally tried really really hard
Timmy tried really really really hard
Johnny tried really really really really hard
而你想把"really"、"really really",以及任意数量连续出现的"really"字符串换成一个简单的"very"(simple is good!),那么以下命令:
:%s/\(really \)\(really \)*/very /
就会把上述的文本变成:
Billy tried very hard
Sally tried very hard
Timmy tried very hard
Johnny tried very hard
表达式\(really \)*匹配0或多个连续的"really "(注意结尾有个空格),而\(really \)\(really \)* 匹配1个或多个连续的"really "实例。
困难的例子(不可思议的象形文字)
Coming soon.
不同工具中的正则表达式
OK,你已经准备使用RE(regular expressions,正则表达式),但是你并准备使用vi。所以,在这里我们给出一些在其他工具中使用RE的例子。另外,我还会总结一下你在不同程序之间使用RE可能发现的区别。
当然,你也可以在Visual C++编辑器中使用RE。选择Edit->Replace,然后选择"Regular expression"选择框,Find What输入框对应上面介绍的vi命令:%s/pat1/pat2/g中的pat1部分,而Replace输入框对应pat2部分。但是,为了得到vi的执行范围和g选项,你要使用Replace All或者适当的手工Find Next and Replace(译者按:知道为啥有人骂微软弱智了吧,虽然VC中可以选中一个范围的文本,然后在其中执行替换,但是总之不够vi那么灵活和典雅)。
sed
Sed是Stream EDitor的缩写,是Unix下常用的基于文件和管道的编辑工具,可以在手册中得到关于sed的详细信息。
这里是一些有趣的sed脚本,假定我们正在处理一个叫做price.txt的文件。注意这些编辑并不会改变源文件,sed只是处理源文件的每一行并把结果显示在标准输出中(当然很容易使用重定向来定制):
sed脚本 |
|
描述 |
|
|
|
sed 's/^$/d' price.txt |
|
删除所有空行 |
sed 's/^[ \t]*$/d' price.txt |
|
删除所有只包含空格或者制表符的行 |
sed 's/"//g' price.txt |
|
删除所有引号 |
awk
awk是一种编程语言,可以用来对文本数据进行复杂的分析和处理。可以在手册中得到关于awk的详细信息。这个古怪的名字是它作者们的姓的缩写(Aho,Weinberger和Kernighan)。
在Aho,Weinberger和Kernighan的书The AWK Programming Language中有很多很好的awk的例子,请不要让下面这些微不足道的脚本例子限制你对awk强大能力的理解。我们同样假定我们针对price.txt文件进行处理,跟sed一样,awk也只是把结果显示在终端上。
awk脚本 |
|
描述 |
|
|
|
awk '$0 !~ /^$/' price.txt |
|
删除所有空行 |
awk 'NF > 0' price.txt |
|
awk中一个更好的删除所有行的办法 |
awk '$2 ~ /^[JT]/ {print $3}' price.txt |
|
打印所有第二个字段是'J'或者'T'打头的行中的第三个字段 |
awk '$2 !~ /[Mm]isc/ {print $3 + $4}' price.txt |
|
针对所有第二个字段不包含'Misc'或者'misc'的行,打印第3和第4列的和(假定为数字) |
awk '$3 !~ /^[0-9]+\.[0-9]*$/ {print $0}' price.txt |
|
打印所有第三个字段不是数字的行,这里数字是指d.d或者d这样的形式,其中d是0到9的任何数字 |
awk '$2 ~ /John|Fred/ {print $0}' price.txt |
|
如果第二个字段包含'John'或者'Fred'则打印整行 |
grep
grep是一个用来在一个或者多个文件或者输入流中使用RE进行查找的程序。它的name编程语言可以用来针对文件和管道进行处理。可以在手册中得到关于grep的完整信息。这个同样古怪的名字来源于vi的一个命令,g/re/p,意思是global regular expression print。
下面的例子中我们假定在文件phone.txt中包含以下的文本,——其格式是姓加一个逗号,然后是名,然后是一个制表符,然后是电话号码:
Francis, John 5-3871
Wong, Fred 4-4123
Jones, Thomas 1-4122
Salazar, Richard 5-2522
grep命令 |
|
描述 |
|
|
|
grep '\t5-...1' phone.txt |
|
把所有电话号码以5开头以1结束的行打印出来,注意制表符是用\t表示的 |
grep '^S[^ ]* R' phone.txt |
|
打印所有姓以S打头和名以R打头的行 |
grep '^[JW]' phone.txt |
|
打印所有姓开头是J或者W的行 |
grep ', ....\t' phone.txt |
|
打印所有姓是4个字符的行,注意制表符是用\t表示的 |
grep -v '^[JW]' phone.txt |
|
打印所有不以J或者W开头的行 |
grep '^[M-Z]' phone.txt |
|
打印所有姓的开头是M到Z之间任一字符的行 |
grep '^[M-Z].*[12]' phone.txt |
|
打印所有姓的开头是M到Z之间任一字符,并且点号号码结尾是1或者2的行 |
egrep
egrep是grep的一个扩展版本,它在它的正则表达式中支持更多的元字符。下面的例子中我们假定在文件phone.txt中包含以下的文本,——其格式是姓加一个逗号,然后是名,然后是一个制表符,然后是电话号码:
Francis, John 5-3871
Wong, Fred 4-4123
Jones, Thomas 1-4122
Salazar, Richard 5-2522
egrep command |
|
Description |
|
|
|
egrep '(John|Fred)' phone.txt |
|
打印所有包含名字John或者Fred的行 |
egrep 'John|22$|^W' phone.txt |
|
打印所有包含John 或者以22结束或者以W的行 |
egrep 'net(work)?s' report.txt |
|
从report.txt中找到所有包含networks或者nets的行 |
正则表达式语法支持情况
命令或环境 |
. |
[ ] |
^ |
$ |
\( \) |
\{ \} |
? |
+ |
| |
( ) |
vi |
X |
X |
X |
X |
X |
|
|
|
|
|
Visual C++ |
X |
X |
X |
X |
X |
|
|
|
|
|
awk |
X |
X |
X |
X |
|
|
X |
X |
X |
X |
sed |
X |
X |
X |
X |
X |
X |
|
|
|
|
Tcl |
X |
X |
X |
X |
X |
|
X |
X |
X |
X |
ex |
X |
X |
X |
X |
X |
X |
|
|
|
|
grep |
X |
X |
X |
X |
X |
X |
|
|
|
|
egrep |
X |
X |
X |
X |
X |
|
X |
X |
X |
X |
fgrep |
X |
X |
X |
X |
X |
|
|
|
|
|
perl |
X |
X |
X |
X |
X |
|
X |
X |
X |
X |
vi替换命令简介
Vi的替换命令:
其中
网上有很多vi的在线手册,你可以访问他们以获得更加完整的信息。