2005年09月09日

Borland公司是一家有着世界上最优秀的工程师与最糟糕的管理者的公司。我爱Borland,因为她优秀的令微软望尘莫及的技术;我恨Borland,因为她总是在每一个紧要的关口由于领导人的失误一次再一次的错失良机。

  百年不可一遇的天才Anders Hejlsberg走了,Java天才Blake Stone也龟缩在被遗忘的角落,一个又一个软件天才在郁闷中离开。就连华人心目中的大师李维也等待被fire的命运,我们不禁要问,曾经叱咤风云的Borland到底怎么了?

  《程序设计》作为我在大学一年级的专业基础课,令我第一次品尝Pascal语言的诗的优美与贵妇的典雅,并为之如痴如醉。

  “Pascal,好爽!”,一阵笑声把我从睡梦中惊醒,我居然在梦里对这个庄重且优雅的图腾也惊叹不已,那是发生在1991年12月的故事。

  从此,我爱上了这个无所不能的精灵。从turbo pascal 3.0到turbo pascal 7.0,从Delphi 1.0到 Delphi2005,从TSR到Midas,从Midas到DataSnap,从BDE到DBExpress,再到BDP,我时刻在激动与崇拜中伴随着Borland的每一个里程碑。我分享着Delphi3.0给Borland带来的成功,我失落着Delphi4.0的失落,我也辉煌着Delphi5.0带来的辉煌。

可以说,我最近几年的职业生涯伴随着Delphi5到Delphi2005的演变历程,正是Delphi把我推向个人的辉煌,也是Delphi让我拥有了目前拥有的一切。是Delphi给了我C++/C#所不能给我的深厚的技术架构背景与分析能力。

  我的生命中可以离开女人,却不能离开Delphi。近乎14年了,几乎每一个深夜都是在Pascal/Delphi的陪伴下快乐而精彩的度过。

  虽然Borland那糊涂的决策者们不再让我们看到希望,但是已经把Delphi融入到灵魂中的我,绝不会因此而放弃自我。由于有了自己的公司,我不可能投入所有的时间去研究技术,也不愿意再一次的聆听到来自Borland的不幸消息,但在日常繁忙的工作外,我还是会挤出一点时间来关注Delphi的进展,毕竟,我已经离不开她了。

  最近,我喜欢了Free Pascal,这个开放的语言很奇妙。也许,在Borland决定研发Kylix的时候,就应该走出这一步。看到那么多的志同道合者加入到Free Pascal中来,一个步入中年的Delphi崇拜者,颇感近几年从来没有过的快感。

  顺便说一句,每每看到那些带着C++/C#或Java光环的浅薄无知者对Delphi妄加评论的时候,我从心底喷射出对他们的不屑一顾。Java有什么?一个架构于虚拟机上的啃着“大饼”的蜗牛而已;C++有什么?一个放荡不羁、自恋十足、霸道无比且习惯于自我炒作的西部牛仔而已;C#有什么?一个惯于用$挖别人墙角的小偷把偷得的“战利品”做一个并不高明的克隆而已。它们有的, Delphi早已经有了!

事实上,做软件做到我这一步,已经不再关注那一种语言的优劣了。对技术架构的融汇贯通,对中间件技术的深刻把握并游刃有余,无论是.net,j2ee,corba还是web service,都已经成为我手中的玩物。但是我依然渴望着那一天,公司不再需要我的身体力行,我便有时间写一点东西,让这个被嫉妒与野蛮无情亵渎的美人鱼,展露更多深刻、优雅且灵动的光芒。

一、当Borland已成往事

昨天看了高论发的关于Borland最新的IDE——DeXter的一个DemoVideo,对于Borland这个公司,我们的观点是基本一致的。

  Borland已经变了。不再是以前那个意气风发的江湖侠客,而是一个脑大肠肥的地方富贾;他拥有的,不再是那些充满灵气的开发工具,而是那些沾满铜钱味的企业解决方案。

  Borland,不再是我以前认识的那个Borland了。

  不过作为一家商业公司,利润远比技术理想更重要。当开发工具带来的利润越来越少时,在股东的压力下,必须要有新的利润增长点才行。

  七八年前,Borland第一次尝试从开发工具领域向企业应用领域时,公司甚至为此改了个名字叫Inprise。结果Anders离开了,Borland差点把自己玩死。

  前一两年,Borland又开始转向企业解决方案,不过这次是针对软件开发企业,倒还没有偏离太多。但结果又是Black.Stone, Chuck.Jazdzewski等人离开了。

  与此相反的,Borland的每一次辉煌,都与开发工具紧密相连。

  83年公司成立,就是仗着Anders的成名作:Turbo Pascal 1.0。之后的整个DOS时代,开发工具几乎都是Borland的天下:Turbo Pascal,Turbo C,Turbo C++……

  Windows 3.x的时代,又是Borland的Borland C++出来拯救了广大的开发人员。再之后便是Delphi。

  但是这些都已经是往事了(详情请自行参见李维的《Borland传奇》一书)。

  问题在于现在如何了呢?

自从.net出来以后,Borland就乱了阵脚。首先是Kylix表现平平,加之Borland打算加入Linux阵营领导集团的企图也告失败,只好退出Linux平台下的开发工具领域。然后是MS放出风声说要把Win32全部转到.net下,Borland又匆匆忙忙推出for .net的C#Builder和Delphi 8,结果又是大败。想要搞一个平台无关的C++BuilderX,同样还是惨遭失败。

  当C++BuilderX出来时,我写过一篇《C++ BuilderX的问题与展望》,后来在Delphi 2005出来前,我又写了一篇《传说中的DELPHI9–DiamondBack》。但是现在还有什么好写的呢?

  这个DeXter看上去还好:仍然是那个叫Galileo的IDE,也许它会是BDS4,还是在.net平台下跑,比Delphi 2005增加了对原生C++的支持,基本上相当于把C++Builder 6集成到Delphi 2005里。仅此而已。

  当然,要说增强的方面也不是没有,至少Delphi 2005中增加的像重构,单元测试,增强的调试功能……这些都是C++ Builder 6所没有的。且不说这些方面在DeXter中能做到什么程度还是一个未知数,更何况在Delphi.net中那些重量级的增强功能像ECOII和 Together却应该是用不上的。最关键的是:

  一个做原生C++应用开发的,为什么需要依赖.net?

  C+ +BuilderX用的IDE——PrimeTime——依赖JAVA已经让人很不爽了。VS做大而全有它的平台基础,Borland没有自己的平台,在.net下根本不是VS.net的对手。还不如放弃这个Galileo的IDE,把Delphi.net、Together、ECOII以for VS.net的方式提供,集中力量把该做的事做好。至于原生应用,就继续按照原生的路子走,别老想着把原生的东西弄到某个平台上,不论是.net还是 JAVA。

  一不小心又对Borland指手划脚了,还是回头做我的ABAP吧。.net还是JAVA跟偶有什么关系呢?

  也许到很多年以后,当有人提起Borland时,我大概还是会想起曾经发生过的那些往事吧。

  BTW:据蔡蔡回复说DeXter是基于Eclipse的CDE,如果是这样的话,那还不错。不过我很怀疑Borland会真的下定决心抛弃Galileo和PrimeTime这两个IDE,而转向Eclipse。毕竟Borland不是IBM。

二、献给曾经的Borland

  不记得曾经多久没有谈论和关心Borland了。每天习惯性的输入“borland.mblogger.cn”的时候,居然也很少联想到这个曾经心目中最了不起的公司。

  今天看了一个Borland新版IDE的Flash演示。虽然现在看到新版的Borland产品,不再像以前那样心潮澎湃欣喜若狂,但还是让我不自觉的想起曾经迷恋Borland,迷恋Borland产品,迷恋Borland传奇的那些日子。

  注册了Borland板块的Blog以来,我几乎没有写过一篇跟borland有关的blog,心里不免是有些愧疚的,今天,借着这个机会,让我小小的弥补一下吧。

  曾经的Borland像一个侠客,面对微软、IBM、Sun这样的大公司,Borland一不趋炎附势,二不低头认输,虽然曾经几起几伏,但还是凭借自己在RAD工具、C++编译器方面的深厚功力顽强生存了下来,并且在软件开发的市场占有了一席之地。

  Turbo C 2.0、Turbo Pascal 7.0、Borland C++ 3.1,这些昔日的经典作品,曾经让多少人废寝忘食,从此走上编程之路。

  Delphi、JBuilder、C++ Builder,这些重量级的产品,又曾经让多少人如释重负,将枯燥乏味的Windows开发变成轻松的享受。

  就连那些失败的产品,也可圈可点。比如Kylix,险些就改变了Linux下应用程序的开发方式;比如C++Builder X,提出的很多概念让人耳目一新。

  如果不是那该死的.NET……

  微软的.NET宣传突然间铺天盖地,来势汹汹,为我们营造了一个“.NET everywhere”的世界。比起其前辈Java的“write once, run everywhere”有过之而无不及。在如此强大的宣传攻势下,有几个人能够保持清醒的头脑呢?

  Borland自然也不例外,他希望自己再次站在.NET的前沿阵地。

  于是,C# Builder来了,Delphi.NET 也来了。

但是.NET和Windows API不一样。Windows API是flat function的形式,对于OOP并不十分友好,甚至有些理念(如回调函数)还有相冲突之处,因此,VCL将其封装成OO的形式,是十分有利于快速开发的;而.NET从一诞生起就考虑到了组件化和可视化的问题,因此,使用VCL将其再次封装,不仅没有什么好处,反而让人产生了使用上的不习惯。

  再加上如今.NET并没有微软当初宣传的那样流行,Borland的迎合举动,反而让自己陷入了一个尴尬境地。

  在.NET宣传如日中天的时候,C++ Builder坚持走本地化开发的路,总算是Borland的一次明智举动,否则,如今的BCB一定会像managed C++那样消失的无影无踪(因为Borland不可能敢像微软那样大刀阔斧的改动C++本身)。但是,那个如同测试版一样的BCBX实在是伤了大家的心,BCB也因此险些从Borland的开发计划中消失。真是可悲可叹。

  其实当初BCBX的概念提出的时候我是非常看好它的,因为它的许多概念(比如多GUI框架支持,多编译器、多平台支持,C++代码重构)都是很先进的。可惜那个最终实现……

  现在,Borland新的产品又出现了。很久没有关注Borland的我,没有了以前的那份激动。我的感觉是,产品成熟了,但是没有了那份锐气。

  Borland的网站也改版了,遵循了XHTML标准,板块区分也清晰了。但是,以前长长一串的products名单,现在变成了寥寥3项:Application LifeCycle、IDE、Application Middleware。

  Borland已经变了。不再是以前那个意气风发的江湖侠客,而是一个脑大肠肥的地方富贾;他拥有的,不再是那些充满灵气的开发工具,而是那些沾满铜钱味的企业解决方案。

  Borland,不再是我以前认识的那个Borland了。

  看着现在的borland.com首页,我突然想起了一句话:

  那都是很好很好的,可是我偏不喜欢。

  就让从前的那个Borland,伴随着他的经典作品,一起封存在我的记忆中吧。

2005年08月25日

Checklist这个词是我在做一个外包项目时学到的,问题源于我们最初提交的代码质量非常的差,很多基本的内容编码人员都没有去处理,例如缩进,注释,日志处理,异常处理等,而且由于我们使用的是他们提供的框架,框架本身也有一些要求,到了后来,我们根据以前常见的问题罗列了一个list,而另外抽调了几个人专门根据这个列表进行检查,就演变为checklist,而且这个列表是不断更新的,发现新的共通的错误就加进去,如果有原来的检查项在经过一段时间后几乎就没有再出现过就从列表中删除以减少检查人员不必要的工作。最重要的是这个经验后来被推广到其它的项目组,以及项目的各个阶段,例如需求分析有需求分析的checklist,设计有设计的checklist,即使是测试组也有自己的checklist,因为有些刚刚开始做测试的人对于基本的测试原理并不熟悉,例如边界测试,极大值极小值测试,异常系测试,对于经常被新人忽略的的测试类似就会有一个这样的list,而且每个系统会有一些自己特有的需要特别关注或者以前比较容易出问题的地方,也可以列到这个list里面。这样在整个软件周期中我们可以避免很多常见的问题,大大提升软件的整体质量。

根据经验,最初做成一个checklist也许比较容易,可是以后遵守起来就比较难了,很少有人真正的按照checklist上的东西进行检查,而更多的是根据自己的经验主管判断。因此,如何控制checklist被完全遵守就是一个重要的问题了。

还有checklist往往也不能被及时更新,很多情况下是在自己发现新问题时由于自己的惰性而不去更新checklist,这也是比较难控制的问题。

  1. 共通的内容要易于使用和理解,例如定义的方法名要比较贴切。
  2. 要写比较详细的说明文档,例如给大家发邮件或者发布到内部使用的论坛系统中。
  3. 在公布之前经过充分的测试,否则使用的时候总是有各种问题会导致大家不敢再用共通的代码。
  4. 对于类似的功能有其共通的代码,例如使用struts的系统有自己的系统的顶层的BaseAction,在这个BaseAction中定义系统中的子类需要实现的业务逻辑方法入口,而BaseAction要实现structs要求的execute方法,并完成所有的共通任务,例如是否登录的检查,session是否超时等
  5. 业务功能要比较少的关心杂项共通功能,例如定义logger,获取数据库连接,关闭数据库连接,异常处理等,这些功能都可以定义在BaseAction或者是系统的顶层基类中。

而且通过使用共通功能也可以很大程度提高系统的质量,因为通过这些年的实践发现,很多新人由于开始不理解系统的要求,很多地方就是先抄袭别人甚至完全拷贝别人的代码,如果别人的代码是有问题的,那么在没有出问题之前是很难被自己发现的,而到了发现的时候已经积重难返了,而通过顶层类的封装,底层的类的空间就狭窄了,犯错误的可能性就小多了,因为很多系统的共通要求在顶层类中已经实现了。

源代码: 

  1. /**
  2.  *  Simple examples of the use of the new assertion feature in JDK1.4
  3.  *
  4.  * @author S.Ritter  16/7/2001
  5.  **/
  6. public class AssertExample {
  7.   public static void main(String[] args) {
  8.     int x = 10;
  9.     if (args.length > 0) {
  10.       try {
  11.         x = Integer.parseInt(args[0]);
  12.       } catch (NumberFormatException nfe) {
  13.         /*  Ignore  */
  14.       }
  15.     }
  16.     System.out.println("Testing assertion that x == 10");
  17.     assert x == 10:"Our assertion failed";
  18.     System.out.println("Test passed");
  19.   }
  20. }


由于引入了一个新的关键字,所以在编译的时候就需要增加额外的参数,要编译成功,必须使用JDK1.4的javac并加上参数’-source 1.4′,例如可以使用以下的命令编译上面的代码:
javac -source 1.4 AssertExample.java 

以上程序运行使用断言功能也需要使用额外的参数(并且需要一个数字的命令行参数),例如:
java -ea AssertExample 1

程序的输出为: 
Testing assertion that x == 10
Exception in thread "main" java.lang.AssertionError: Our assertion failed
        at AssertExample.main(AssertExample.java:20)
        
由于输入的参数不等于10,因此断言功能使得程序运行时抛出断言错误,注意是错误,这意味着程序发生严重错误并且将强制退出。断言使用boolean值,如果其值不为true则抛出AssertionError并终止程序的运行。
由于程序员的问题,断言的使用可能会带来副作用,例如: 
boolean isEnable=false;
//…
assert isEnable=true;

这个断言的副作用是因为它修改程序变量的值并且没有抛出错误,这样的错误如果不细心检查很难发现。但是同时我们可以根据以上的副作用得到一个有用的特性,根据它测试是否将断言打开了。 

  1. /**
  2.  *  Simple examples test enable assertion feature in JDK1.4
  3.  *
  4.  * @author Cherami  25/4/2002
  5.  **/
  6. public class AssertExample2 {
  7.   public static void main(String[] args) {
  8.     boolean assertEnable=false;
  9.     assert assertEnable=true;
  10.     
  11.     if (assertEnable==false)
  12.     {
  13.         throw new RuntimeException("Assertions should be enable");
  14.     }
  15.   }
  16. }


如果我们不使用-ea参数运行上面的程序,则控制台将输出:

Exception in thread "main" java.lang.RuntimeException: Assertions should be enab
le
        at AssertExample.main(AssertExample.java:14)

在编写应用程序的时候,需要面对的一个问题是如何来处理与locale相关的一些信息。比如,页面上的一些静态文本就希望能够以用户习惯的语言显示。最原始的做法是将这些信息硬编码到程序中(可能是一大串判断语句),但是这样就将程序代码和易变的locale信息捆绑在一起,以后如果需要修改locale信息或者添加其它的locale信息,你就不得不重新修改代码。而资源包可以帮助你解决这个问题,它通过将可变的locale信息放入资源包中来达到两者分离的目的。应用程序可以自动地通过当前的locale设置到相应的资源包中取得所要的信息。资源包的概念类似于Windows编程人员使用的资源文件(rc文件)。

一般来说,资源包需要完成两个功能:和具体的locale进行绑定以及读取locale相关信息。


ResourceBundle类


你可以把资源包看作为一个由许多成员(子类)组成的大家庭,其中每个成员关联到不同的locale对象,那它是如何完成关联功能的呢?

资源包中的每个成员共享一个被称作基名(base name)的名称,然后在此基础上根据一定的命名规范进行扩展。下面就列出了一些成员的名称:
    LabelResources
         LabelResources_de
         LabelResources_de_CH
         LabelResources_de_CH_UNIX
可见这些子类依据这样的命名规范:baseName_language_country_variant,其中language等几个变量就是你在构造Locale类时所使用的。而资源包正是通过这个符合命名规范的名称来和locale进行关联的,比如LabelResource_de_CH就对应于由德语(de)和瑞士(CH)组成的locale对象。

当你的应用程序需要查找特定locale对象关联的资源包时,它可以调用ResourceBundle的getBundle方法,并将locale对象作为参数传入。

  1. Locale currentLocale = new Locale("de""CH""UNIX");
  2. ResourceBundle myResources =
  3.       ResourceBundle.getBundle("LabelResources", currentLocale);


如果该locale对象匹配的资源包子类找不到,getBundle将试着查找最匹配的一个子类。具体的查找策略是这样的:getBundle使用基名,locale对象和缺省的locale来生成一个候选资源包名称序列。如果特定locale对象的语言代码、国家代码和可选变量都是空值,则基名是唯一的候选资源包名称。否则的话,具体locale对象(language1,country1和variant1)和缺省locale(language2,country2和variant2)将产生如下的序列:


  • baseName + "_" + language1 + "_" + country1 + "_" + variant1
  • baseName + "_" + language1 + "_" + country1 
  • baseName + "_" + language1 
  • baseName + "_" + language2 + "_" + country2 + "_" + variant2 
  • baseName + "_" + language2 + "_" + country2 
  • baseName + "_" + language2 
  • baseName 


然后,getBundle方法按照产生的序列依次查找匹配的资源包子类并对结果子类初始化。首先,它将寻找类名匹配候选资源包名称的类,如果找到将创建该类的一个实例,我们称之为结果资源包。否则,getBundle方法将寻找对应的资源文件,它通过候选资源包名称来获得资源文件的完整路径(将其中的“.”替换为“/”,并加上“.properties”后缀),如果找到匹配文件,getBundle方法将利用该资源文件来创建一个PropertyResourceBundle实例,也就是最终的结果资源包。与此同时,getBundle方法会将这些资源包实例缓存起来供以后使用。

如果没有找到结果资源包,该方法将抛出MissingResourceException异常。所以为了防止异常的抛出,一般来说都需要至少实现一个基名资源包子类。

注意:基名参数必须是一个完整的类名称(比如LabelResources,resource.LabelResources等),就相当于你引用一个类时需要指定完整的类路径。但是,为了和以前的版本保持兼容,在使用PropertyResourceBundles时也允许使用“/”来代替“.”表示路径。

比如你有以下这些资源类和资源文件:MyResources.class, MyResources_fr_CH.properties, MyResources_fr_CH.class, MyResources_fr.properties, MyResources_en.properties, MyResources_es_ES.class。你利用以下的locale设置来调用getBundle方法,你将会得到不同的结果资源包(假设缺省locale为Locale(“en”, “UK”)),请参考表13.4。
       表13.4 locale设置与结果资源包
locale设置        结果资源包
Locale("fr", "CH")    MyResources_fr_CH.class
Locale("fr", "FR")        MyResources_fr.properties
Locale("de", "DE")        MyResources_en.properties
Locale("en", "US")        MyResources_en.properties
Locale("es", "ES")        MyResources_es_ES.class

创建了具体的资源包子类实例以后,就需要获得具体的信息。信息在资源包中是以键值对的方式存储的,表13.5列出的是LabelResources.properties文件的内容。

表13.5 LabelResources.properties

  1. # This is LabelResources.properties file
  2. greetings = 您好!
  3. farewell = 再见。
  4. inquiry = 您好吗?


其中等号左边的字符串表示主键,它们是唯一的。为了获得主键对应的值,你可以调用ResourceBundle类的getString方法,并将主键作为参数。此外,文件中以“#”号开头的行表示注释行。


ListResourceBundle和PropertyResourceBundle子类


抽象类ResourceBundle具有两个子类:ListResourceBundle和PropertyResourceBundle,它们表示资源包子类两种不同的实现方式。

PropertyResourceBundle是和资源文件配对使用的,一个属性文件就是一个普通的文本文件,你只需要为不同的locale设置编写不同名称的资源文件。但是,在资源文件中只能包含字符串,如果需要存储其它类型对象,你可以使用ListResourceBundle。

ListResourceBundle是将键值对信息保存在类中的列表中,而且你必须实现ListResourceBundle的具体子类。

如果ListResourceBundle和PropertyResourceBundle不能够满足你的需要,你可以实现自己的ResourceBundle子类,你的子类必须覆盖两个方法:handleGetObject和getKeys。


使用资源文件


使用资源包最简单的方法就是利用资源文件,利用资源文件一般需要以下几个步骤:
1、创建一个缺省的资源文件
为了防止找不到资源文件,你最好实现一个缺省的资源文件,该文件的名称为资源包的基名加上.properties后缀。
2、创建所需的资源文件
为你准备支持的locale设置编写对应的资源文件。
3、设置locale
你必须在程序中的某个地方提供locale的设置或者切换功能,或者将其放入配置文件中。
4、根据locale设置创建资源包
ResourceBundle resource =
        ResourceBundle.getBundle("LabelBundle",currentLocale);
5、通过资源包获取locale相关信息
String value = resource.getString("welcome");

注意:在使用基名的时候,特别要注意给出完整的类名(或者路径名),比如你的应用程序所在的类包为org.javaresearch.j2seimproved.i18n,而你的资源文件在你的应用程序下的resource子目录中,那你的基名就应该是org.javaresearch.j2seimproved.i18n.resource.LabelBundleBundle而不是resource.LabelBundleBundle。



使用ListResourceBundle


使用ListResourceBundle和使用资源文件的步骤基本上一样,只不过你需要用ListResourceBundle子类来替换相应的资源文件。比如你的应用程序的基名是LabelBundle,而且准备支持Locale("en","US")和Locale("zh","CN"),那你需要提供以下几个Java文件,注意类名和locale的对应关系。
LabelBundle_en_US.java
LabelBundle_zh_CN.java
LabelBundle.java(缺省类)

代码13.3列出的是LabelBundle_zh_CN.java的源代码,相对于资源文件中“key = value”的写法,在此文件中你首先利用键值对来初始化一个二维数组,并在getContents方法中返回该数组。
      
代码13.3:LabelBundle_zh_CN.java

  1. package org.javaresearch.j2seimproved.i18n;import 
  2. java.util.ListResourceBundle;
  3. public class LabelBundle_zh_CN extends ListResourceBundle {   
  4.   public Object[][] getContents() {     
  5.     return contents;   
  6.   }   
  7.   private Object[][] contents = {      
  8.     {"title""称谓"},      
  9.     {"surname""姓"},      
  10.     {"firstname""名"},   
  11.   };
  12. }



创建完资源类以后,同样需要设置locale以及根据locale来创建资源包。在通过资源包获取具体值的时候,你不能再使用getString方法,而应该调用getObject方法,而且由于getObject方法返回一个Object对象,你还需要进行正确的类型转换。其实,为了你的程序通用性,我们建议在使用资源文件的时候你也应该调用getObject方法,而不是getString方法。

  1.     String title = (String)resource.getObject("title");


关于ListResourceBundle的详细使用,可以参考本书所附代码中国际化一节的ListResourceBundleSample.java程序。


MessageFormat类


上面我们讲到利用资源文件来分离代码和可变的信息。但是在实际过程中,有些信息并不能够完全事先定义好,其中可能会用到运行时的一些结果,最典型例子的就是错误提示代码,比如提示某个输入必须在一定范围内。利用上面所讲的资源文件并不能够很好地解决这个问题,所以Java中引入了MessageFormat类。

MessageFormat提供一种语言无关的方式来组装消息,它允许你在运行时刻用指定的参数来替换掉消息字符串中的一部分。你可以为MessageFormat定义一个模式,在其中你可以用占位符来表示变化的部分,比如你有这样一句话:

您好,peachpi!欢迎来到Java研究组织网站!当前时间是:2003-8-1 16:43:12。

其中斜体带下划线的部分为可变化的,你需要根据当前时间和不同的登录用户来决定最终的显示。我们用占位符来表示这些变化的部分,可以得到下面这个模式:

您好,{0}!欢迎来到Java研究组织网站!当前时间是:{1,date} {1,time}。

占位符的格式为{ ArgumentIndex , FormatType , FormatStyle },详细说明可以参考MessageFormat的API说明文档。这里我们定义了两个占位符,其中的数字对应于传入的参数数组中的索引,{0}占位符被第一个参数替换,{1}占位符被第二个参数替换,依此类推。
最多可以设置10个占位符,而且每个占位符可以重复出现多次,而且格式可以不同,比如{1,date}和{1,time}。而通过将这些模式定义放到不同的资源文件中,就能够根据不同的locale设置,得到不同的模式定义,并用参数动态替换占位符。

下面我们就以MessageFormatSample.java程序(源文件见本书所附代码)为例,来详细说明其中的每个步骤。
1、找出可变的部分,并据此定义模式,将模式放入不同的资源文件中。
比如针对上面的模式,定义了下面两个资源文件:
MessagesBundle_en_US.properties
Welcome = Hi, {0}! Welcome to Java Research Organization!
MessagesBundle_zh_CN.properties
Welcome = 您好,{0}!欢迎来到Java研究组织网站!

2、创建MessageFormat对象,并设置其locale属性。

  1.       MessageFormat formatter = new MessageFormat("");
  2.       formatter.setLocale(currentLocale);


3、从资源包中得到模式定义,以及设置参数。

  1. messages =     ResourceBundle.getBundle(
  2.   "org.javaresearch.j2seimproved.i18n.resource.MessagesBundle",currentLocale);
  3. Object[] testArgs = {"peachpi",new Date()};


4、利用模式定义和参数进行格式化。

  1.       System.out.println(formatter.format(messages.getString("welcome"),testArgs));




关于资源包的组织


一般来说,你是按照资源的用途来组织资源包的,比如会把所有的页面按钮的信息放入一个名为ButtonResources的资源包中。在实际的应用过程中,以下几个原则可以帮你决定如何组织资源包:
1、要易于维护。
2、最好不要将所有的信息都放入一个资源包中,因为这样资源包载入内存时将会很耗时。
3、最好将一个大的资源包分为几个小的资源包,这样可以在使用的时候才导入必须的资源,减少内存消耗。

在基于 Java 语言的编程中,我们经常碰到汉字的处理及显示的问题。一大堆看不懂的乱码肯定不是我们愿意看到的显示效果,怎样才能够让那些汉字正确显示呢?Java 语言默认的编码方式是UNICODE ,而我们中国人通常使用的文件和数据库都是基于 GB2312 或者 BIG5 等方式编码的,怎样才能够恰当地选择汉字编码方式并正确地处理汉字的编码呢?本文将从汉字编码的常识入手,结合 Java 编程实例,分析以上两个问题并提出解决它们的方案。 


现在 Java 编程语言已经广泛应用于互联网世界,早在 Sun 公司开发 Java 语言的时候,就已经考虑到对非英文字符的支持了。Sun 公司公布的 Java 运行环境(JRE)本身就分英文版和国际版,但只有国际版才支持非英文字符。不过在 Java 编程语言的应用中,对中文字符的支持并非如同 Java Soft 的标准规范中所宣称的那样完美,因为中文字符集不只一个,而且不同的操作系统对中文字符的支持也不尽相同,所以会有许多和汉字编码处理有关的问题在我们进行应用开发中困扰着我们。有很多关于这些问题的解答,但都比较琐碎,并不能够满足大家迫切解决问题的愿望,关于 Java 中文问题的系统研究并不多,本文从汉字编码常识出发,分析 Java 中文问题,希望对大家解决这个问题有所帮助。 

汉字编码的常识 

我们知道,英文字符一般是以一个字节来表示的,最常用的编码方法是 ASCII 。但一个字节最多只能区分256个字符,而汉字成千上万,所以现在都以双字节来表示汉字,为了能够与英文字符分开,每个字节的最高位一定为1,这样双字节最多可以表示64K格字符。我们经常碰到的编码方式有 GB2312、BIG5、UNICODE 等。关于具体编码方式的详细资料,有兴趣的读者可以查阅相关资料。我肤浅谈一下和我们关系密切的 GB2312 和 UNICODE。GB2312 码,中华人民共和国国家标准汉字信息交换用编码,是一个由中华人民共和国国家标准总局发布的关于简化汉字的编码,通行于中国大陆地区及新加坡,简称国标码。两个字节中,第一个字节(高字节)的值为区号值加32(20H),第二个字节(低字节)的值为位号值加32(20H),用这两个值来表示一个汉字的编码。UNICODE 码是微软提出的解决多国字符问题的多字节等长编码,它对英文字符采取前面加“0”字节的策略实现等长兼容。如 “A” 的 ASCII 码为0×41,UNICODE 就为0×00,0×41。利用特殊的工具各种编码之间可以互相转换。 

Java 中文问题的初步认识 

我们基于 Java 编程语言进行应用开发时,不可避免地要处理中文。Java 编程语言默认的编码方式是 UNICODE,而我们通常使用的数据库及文件都是基于 GB2312 编码的,我们经常碰到这样的情况:浏览基于 JSP 技术的网站看到的是乱码,文件打开后看到的也是乱码,被 Java 修改过的数据库的内容在别的场合应用时无法继续正确地提供信息。 

String sEnglish = “apple”; 

String sChinese = “苹果”; 

String s = “苹果 apple ”; 

sEnglish 的长度是5,sChinese的长度是4,而 s 默认的长度是14。对于 sEnglish来说, Java 中的各个类都支持得非常好,肯定能够正确显示。但对于 sChinese 和 s 来说,虽然 Java Soft 声明 Java 的基本类已经考虑到对多国字符的支持(默认 UNICODE 编码),但是如果操作系统的默认编码不是 UNICODE ,而是国标码等。从 Java 源代码到得到正确的结果,要经过 “Java 源代码-> Java 字节码-> ;虚拟机->操作系统->显示设备”的过程。在上述过程中的每一步骤,我们都必须正确地处理汉字的编码,才能够使最终的显示结果正确。 

“ Java 源代码-> Java 字节码”,标准的 Java 编译器 javac 使用的字符集是系统默认的字符集,比如在中文 Windows 操作系统上就是 GBK ,而在 Linux 操作系统上就是ISO-8859-1,所以大家会发现在 Linux 操作系统上编译的类中源文件中的中文字符都出了问题,解决的办法就是在编译的时候添加 encoding 参数,这样才能够与平台无关。用法是 

javac ?encoding GBK。 

“ Java 字节码->虚拟机->操作系统”, Java 运行环境 (JRE) 分英文版和国际版,但只有国际版才支持非英文字符。 Java 开发工具包 (JDK) 肯定支持多国字符,但并非所有的计算机用户都安装了 JDK 。很多操作系统及应用软件为了能够更好的支持 Java ,都内嵌了 JRE 的国际版本,为自己支持多国字符提供了方便。 

“操作系统->显示设备”,对于汉字来说,操作系统必须支持并能够显示它。英文操作系统如果不搭配特殊的应用软件的话,是肯定不能够显示中文的。 

还有一个问题,就是在 Java 编程过程中,对中文字符进行正确的编码转换。例如,向网页输出中文字符串的时候,不论你是用 

out.println(string); // string 是含中文的字符串 

还是用 

,都必须作 UNICODE 到 GBK 的转换,或者手动,或者自动。在 JSP 1.0中,可以定义输出字符集,从而实现内码的自动转换。用法是 



但是在一些 JSP 版本中并没有提供对输出字符集的支持,(例如 JSP 0.92),这就需要手动编码输出了,方法非常多。最常用的方法是 

String s1 = request.getParameter(“keyword”); 

String s2 = new String(s1.getBytes(“ISO-8859-1”),”GBK”); 

getBytes 方法用于将中文字符以“ISO-8859-1”编码方式转化成字节数组,而“GBK” 是目标编码方式。我们从以ISO-8859-1方式编码的数据库中读出中文字符串 s1 ,经过上述转换过程,在支持 GBK 字符集的操作系统和应用软件中就能够正确显示中文字符串 s2 。 

Java 中文问题的表层分析及处理 

背景 

开发环境 
JDK1.15 
Vcafe2.0 
JPadPro 

服务器端 
NT IIS 
Sybase System 
Jconnect(JDBC) 

客户端 
IE5.0 
Pwin98 



.CLASS 文件存放在服务器端,由客户端的浏览器运行 APPLET , APPLET 只起调入 FRAME 类等主程序的作用。界面包括 Textfield ,TextArea,List,Choice 等。 

I. 取中文 

用 JDBC 执行 SELECT 语句从服务器端读取数据(中文)后,将数据用 APPEND 方法加到 TextArea(TA) ,不能正确显示。但加到 List 中时,大部分汉字却可正确显示。 

将数据按“ISO-8859-1” 编码方式转化为字节数组,再按系统缺省编码方式 (Default Character Encoding) 转化为 STRING ,即可在 TA 和 List 中正确显示。 

程序段如下: 

dbstr2 = results.getString(1); 

//After reading the result from DB server,converting it to string. 

dbbyte1 = dbstr2.getBytes(“iso-8859-1”); 

dbstr1 = new String(dbbyte1); 

在转换字符串时不采用系统默认编码方式,而直接采用“ GBK” 或者 “GB2312” ,在 A 和 B 两种情况下,从数据库取数据都没有问题。 

II. 写中文到数据库 

处理方式与“取中文”相逆,先将 SQL 语句按系统缺省编码方式转化为字节数组,再按“ISO-8859-1”编码方式转化为 STRING ,最后送去执行,则中文信息可正确写入数据库。 

程序段如下: 

sqlstmt = tf_input.getText(); 

//Before sending statement to DB server,converting it to sql statement. 

dbbyte1 = sqlstmt.getBytes(); 

sqlstmt = newString(dbbyte1,”iso-8859-1”); 

_stmt = _con.createStatement(); 

_stmt.executeUpdate(sqlstmt); 

…… 

问题:如果客户机上存在 CLASSPATH 指向 JDK 的 CLASSES.ZIP 时(称为 A 情况),上述程序代码可正确执行。但是如果客户机只有浏览器,而没有 JDK 和 CLASSPATH 时(称为 B 情况),则汉字无法正确转换。 

我们的分析: 

1.经过测试,在 A 情况下,程序运行时系统的缺省编码方式为 GBK 或者 GB2312 。在 B 情况下,程序启动时浏览器的 JAVA 控制台中出现如下错误信息: 

Can’t find resource for sun.awt.windows.awtLocalization_zh_CN 

然后系统的缺省编码方式为“8859-1”。 

2.如果在转换字符串时不采用系统缺省编码方式,而是直接采用 “GBK” 或“GB2312”,则在 A 情况下程序仍然可正常运行,在 B 情况下,系统出现错误: 

UnsupportedEncodingException。 

3.在客户机上,把 JDK 的 CLASSES.ZIP 解压后,放在另一个目录中, CLASSPATH 只包含该目录。然后一边逐步删除该目录中的 .CLASS 文件,另一边运行测试程序,最后发现在一千多个 CLASS 文件中,只有一个是必不可少的,该文件是: 

sun.io.CharToByteDoubleByte.class。 

将该文件拷到服务器端和其它的类放在一起,并在程序的开头 IMPORT 它,在 B 情况下程序仍然无法正常运行。 

4.在 A 情况下,如果在 CLASSPTH 中去掉 sun.io.CharToByteDoubleByte.class ,则程序运行时测得默认编码方式为“8859-1”,否则为 “GBK” 或 “GB2312” 。 

如果 JDK 的版本为1.2以上的话,在 B 情况下遇到的问题得到了很好的解决,测试的步骤同上,有兴趣的读者可以尝试一下。 

Java 中文问题的根源分析及解决 

在简体中文 MS Windows 98 + JDK 1.3 下,可以用 System.getProperties() 得到 Java 运行环境的一些基本属性,类 PoorChinese 可以帮助我们得到这些属性。 

类 PoorChinese 的源代码: 

public class PoorChinese { 

public static void main(String[] args) { 

System.getProperties().list(System.out); 





执行 java PoorChinese 后,我们会得到: 

系统变量 file.encoding 的值为 GBK ,user.language 的值为 zh , user.region 的值为 CN ,这些系统变量的值决定了系统默认的编码方式是 GBK 。 

在上述系统中,下面的代码将 GB2312 文件转换成 Big5 文件,它们能够帮助我们理解 Java 中汉字编码的转化: 



import java.io.*; 

import java.util.*; 



public class gb2big5 { 



static int iCharNum=0; 



public static void main(String[] args) { 

System.out.println("Input GB2312 file, output Big5 file."); 

if (args.length!=2) { 

System.err.println("Usage: jview gb2big5 gbfile big5file"); 

System.exit(1); 



String inputString = readInput(args[0]); 

writeOutput(inputString,args[1]); 

System.out.println("Number of Characters in file: "+iCharNum+"."); 


static void writeOutput(String str, String strOutFile) { 

try { 

FileOutputStream fos = new FileOutputStream(strOutFile); 

Writer out = new OutputStreamWriter(fos, "Big5"); 

out.write(str); 

out.close(); 



catch (IOException e) { 

e.printStackTrace(); 

e.printStackTrace(); 







static String readInput(String strInFile) { 

StringBuffer buffer = new StringBuffer(); 

try { 

FileInputStream fis = new FileInputStream(strInFile); 

InputStreamReader isr = new InputStreamReader(fis, "GB2312"); 

Reader in = new BufferedReader(isr); 

int ch; 

while ((ch = in.read()) > -1) { 

iCharNum += 1; 

buffer.append((char)ch); 



in.close(); 

return buffer.toString(); 



catch (IOException e) { 

e.printStackTrace(); 

return null; 







编码转化的过程如下: 

ByteToCharGB2312 CharToByteBig5 

GB2312——————>Unicode————->Big5 

执行 java gb2big5 gb.txt big5.txt ,如果 gb.txt 的内容是“今天星期三”,则得到的文件 big5.txt 中的字符能够正确显示;而如果 gb.txt 的内容是“情人节快乐”,则得到的文件 big5.txt 中对应于“节”和“乐”的字符都是符号“?”(0×3F),可见 sun.io.ByteToCharGB2312 和 sun.io.CharToByteBig5 这两个基本类并没有编好。 

正如上例一样, Java 的基本类也可能存在问题。由于国际化的工作并不是在国内完成的,所以在这些基本类发布之前,没有经过严格的测试,所以对中文字符的支持并不像 Java Soft 所声称的那样完美。前不久,我的一位技术上的朋友发信给我说,他终于找到了 Java Servlet 中文问题的根源。两周以来,他一直为 Java Servlet 的中文问题所困扰,因为每面对一个含有中文字符的字符串都必须进行强制转换才能够得到正确的结果(这好象是大家公认的唯一的解决办法)。后来,他确实不想如此继续安分下去了,因为这样的事情确实不应该是高级程序员所要做的工作,他就找出 Servlet 解码的源代码进行分析,因为他怀疑问题就出在解码这部分。经过四个小时的奋斗,他终于找到了问题的根源所在。原来他的怀疑是正确的, Servlet 的解码部分完全没有考虑双字节,直接把 %XX 当作一个字符。(原来 Java Soft 也会犯这幺低级的错误!) 

如果你对这个问题有兴趣或者遇到了同样的烦恼的话,你可以按照他的步骤对 Servlet.jar 进行修改: 

找到源代码 HttpUtils 中的 static private String parseName ,在返回前将 sb(StringBuffer) 复制成 byte bs[] ,然后 return new String(bs,”GB2312”)。作上述修改后就需要自己解码了: 

HashTable form=HttpUtils .parseQueryString(request.getQueryString())或者 

form=HttpUtils.parsePostData(……) 

千万别忘了编译后放到 Servlet.jar 里面。 

五、 关于 Java 中文问题的总结 

Java 编程语言成长于网络世界,这就要求 Java 对多国字符有很好的支持。 Java 编程语言适应了计算的网络化的需求,为它能够在网络世界迅速成长奠定了坚实的基础。 Java 的缔造者 (Java Soft) 已经考虑到 Java 编程语言对多国字符的支持,只是现在的解决方案有很多缺陷在里面,需要我们付诸一些补偿性的措施。而世界标准化组织也在努力把人类所有的文字统一在一种编码之中,其中一种方案是 ISO10646 ,它用四个字节来表示一个字符。当然,在这种方案未被采用之前,还是希望 Java Soft 能够严格地测试它的产品,为用户带来更多的方便。 

附一个用于从数据库和网络中取出中文乱码的处理函数,入参是有问题的字符串,出参是问题已经解决了的字符串。 

String parseChinese(String in) 



String s = null; 

byte temp []; 

if (in == null) 



System.out.println("Warn:Chinese null founded!"); 

return new String(""); 



try 



temp=in.getBytes("iso-8859-1"); 

temp=in.getBytes("iso-8859-1"); 

s = new String(temp); 





System.out.println("Warn:Chinese null founded!"); 

return new String(""); 



try 



temp=in.getBytes("iso-8859-1"); 

s = new String(temp); 



catch(UnsupportedEncodingException e) 



System.out.println (e.toString()); 



return s; 


我们期待自己成为一个优秀的软件模型设计者,但是,要怎样做,又从哪里开始呢? 

将下列原则应用到你的软件工程中,你会获得立杆见影的成果。 

1. 人远比技术重要 

你开发软件是为了供别人使用,没有人使用的软件只是没有意义的数据的集合而已。许多在软件方面很有成就的行家在他们事业的初期却表现平平,因为他们那时侯将主要精力都集中在技术上。显然,构件(components),EJB(Enterprise Java Beans)和代理(agent)是很有趣的东西。但是对于用户来说,如果你设计的软件很难使用或者不能满足他们的需求,后台用再好的技术也于事无补。多花点时间到软件需求和设计一个使用户能很容易理解的界面上。 

2. 理解你要实现的东西 

好的软件设计人员把大多数时间花费在建立系统模型上,偶尔写一些源代码,但那只不过是为了验证设计过程中所遇到的问题。这将使他们的设计方案更加可行。 

3. 谦虚是必须的品格 

你不可能知道一切,你甚至要很努力才能获得足够用的知识。软件开发是一项复杂而艰巨的工作,因为软件开发所用到的工具和技术是在不断更新的。而且,一个人也不可能了解软件开发的所有过程。在日常生活中你每天接触到的新鲜事物可能不会太多。但是对于从事软件开发的人来说,每天可以学习很多新东西(如果愿意的话)。 

4. 需求就是需求 

如果你没有任何需求,你就不要动手开发任何软件。成功的软件取决于时间(在用户要求的时间内完成)、预算和是否满足用户的需求。如果你不能确切知道用户需要的是什么,或者软件的需求定义,那么你的工程注定会失败。 

5. 需求其实很少改变,改变的是你对需求的理解 

Object ToolSmiths公司(objecttoolsmiths.com)的Doug Smith常喜欢说:“分析是一门科学,设计是一门艺术”。他的意思是说在众多的“正确”分析模型中只存在一个最“正确”分析模型可以完全满足解决某个具体问题的需要(我理解的意思是需求分析需要一丝不苟、精确的完成,而设计的时候反而可以发挥创造力和想象力 - 译者注)。 

如果需求经常改动,很可能是你没有作好需求分析,并不是需求真的改变了。 

你可以抱怨用户不能告诉你他们想得到什么,但是不要忘记,收集需求信息是你工作。 

你可以说是新来的开发人员把事情搞得一团糟,但是,你应该确定在工程的第一天就告诉他们应该做什么和怎样去做。 

如果你觉得公司不让你与用户充分接触,那只能说明公司的管理层并不是真正支持你的项目。 

你可以抱怨公司有关软件工程的管理制度不合理,但你必须了解大多同行公司是怎么做的。 

你可以借口说你们的竞争对手的成功是因为他们有了一个新的理念,但是为什么你没先想到呢? 

需求真正改变的情况很少,但是没有做好需求分析工作的理由却很多。 

6. 经常阅读 

在这个每日都在发生变化的产业中,你不可能在已取得的成就上陶醉太久。 

每个月至少读2、3本专业杂志或者1本专业书籍。保持不落伍需要付出很多的时间和金钱,但会使你成为一个很有实力的竞争者。 

7. 降低软件模块间的耦合度 

高耦合度的系统是很难维护的。一处的修改引起另一处甚至更多处的变动。 

你可以通过以下方法降低程序的耦合度:隐藏实现细节,强制构件接口定义,不使用公用数据结构,不让应用程序直接操作数据库(我的经验法则是:当应用程序员在写SQL代码的时候,你的程序的耦合度就已经很高了)。 

耦合度低的软件可以很容易被重用、维护和扩充。 

8. 提高软件的内聚性 

如果一个软件的模块只实现一个功能,那么该模块具有高内聚性。高内聚性的软件更容易维护和改进。 

判断一个模块是否有高的内聚性,看一看你是否能够用一个简单的句子描述它的功能就行了。如果你用了一段话或者你需要使用类似“和”、“或”等连词,则说明你需要将该模块细化。 

只有高内聚性的模块才可能被重用。 

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年代以来就没有改变过。你必须还定义需求,建模,编码,测试,配置,面对风险,发布产品,管理工作人员等等。 

软件建模技术是需要多年的实际工作才能完全掌握的。好在你可以从我的建议开始,完善你们自己的软件开发经验。 

以鸡汤开始,加入自己的蔬菜。然后,开始享受你自己的丰盛晚餐吧。 

你觉得自己是一个Java专家吗?是否肯定自己已经全面掌握了Java的异常处理机制?在下面这段代码中,你能够迅速找出异常处理的六个问题吗? 

1    OutputStreamWriter out = … 
2    java.sql.Connection conn = … 
3    try { // ⑸ 
4        Statement stat = conn.createStatement(); 
5        ResultSet rs = stat.executeQuery( 
6        "select uid, name from user"); 
7        while (rs.next()) 
8        { 
9            out.println("ID:" + rs.getString("uid") // ⑹ 
10           ",姓名:" + rs.getString("name")); 
11       } 
12       conn.close(); // ⑶ 
13       out.close(); 
14   } 
15   catch(Exception ex) // ⑵ 
16   { 
17       ex.printStackTrace(); //⑴,⑷ 
18   } 



  作为一个Java程序员,你至少应该能够找出两个问题。但是,如果你不能找出全部六个问题,请继续阅读本文。 

  本文讨论的不是Java异常处理的一般性原则,因为这些原则已经被大多数人熟知。我们要做的是分析各种可称为“反例”(anti-pattern)的违背优秀编码规范的常见坏习惯,帮助读者熟悉这些典型的反面例子,从而能够在实际工作中敏锐地察觉和避免这些问题。 

  反例之一:丢弃异常 

  代码:15行-18行。 

  这段代码捕获了异常却不作任何处理,可以算得上Java编程中的杀手。从问题出现的频繁程度和祸害程度来看,它也许可以和C/C++程序的一个恶名远播的问题相提并论??不检查缓冲区是否已满。如果你看到了这种丢弃(而不是抛出)异常的情况,可以百分之九十九地肯定代码存在问题(在极少数情况下,这段代码有存在的理由,但最好加上完整的注释,以免引起别人误解)。 

  这段代码的错误在于,异常(几乎)总是意味着某些事情不对劲了,或者说至少发生了某些不寻常的事情,我们不应该对程序发出的求救信号保持沉默和无动于衷。调用一下printStackTrace算不上“处理异常”。不错,调用printStackTrace对调试程序有帮助,但程序调试阶段结束之后,printStackTrace就不应再在异常处理模块中担负主要责任了。 

  丢弃异常的情形非常普遍。打开JDK的ThreadDeath类的文档,可以看到下面这段说明:“特别地,虽然出现ThreadDeath是一种‘正常的情形’,但ThreadDeath类是Error而不是Exception的子类,因为许多应用会捕获所有的Exception然后丢弃它不再理睬。”这段话的意思是,虽然ThreadDeath代表的是一种普通的问题,但鉴于许多应用会试图捕获所有异常然后不予以适当的处理,所以JDK把ThreadDeath定义成了Error的子类,因为Error类代表的是一般的应用不应该去捕获的严重问题。可见,丢弃异常这一坏习惯是如此常见,它甚至已经影响到了Java本身的设计。 

  那么,应该怎样改正呢?主要有四个选择: 

  1、处理异常。针对该异常采取一些行动,例如修正问题、提醒某个人或进行其他一些处理,要根据具体的情形确定应该采取的动作。再次说明,调用printStackTrace算不上已经“处理好了异常”。 

  2、重新抛出异常。处理异常的代码在分析异常之后,认为自己不能处理它,重新抛出异常也不失为一种选择。 

  3、把该异常转换成另一种异常。大多数情况下,这是指把一个低级的异常转换成应用级的异常(其含义更容易被用户了解的异常)。 

  4、不要捕获异常。 

  结论一:既然捕获了异常,就要对它进行适当的处理。不要捕获异常之后又把它丢弃,不予理睬。 

  反例之二:不指定具体的异常 

  代码:15行。 

  许多时候人们会被这样一种“美妙的”想法吸引:用一个catch语句捕获所有的异常。最常见的情形就是使用catch(Exception ex)语句。但实际上,在绝大多数情况下,这种做法不值得提倡。为什么呢? 

  要理解其原因,我们必须回顾一下catch语句的用途。catch语句表示我们预期会出现某种异常,而且希望能够处理该异常。异常类的作用就是告诉Java编译器我们想要处理的是哪一种异常。由于绝大多数异常都直接或间接从java.lang.Exception派生,catch(Exception ex)就相当于说我们想要处理几乎所有的异常。 

  再来看看前面的代码例子。我们真正想要捕获的异常是什么呢?最明显的一个是SQLException,这是JDBC操作中常见的异常。另一个可能的异常是IOException,因为它要操作OutputStreamWriter。显然,在同一个catch块中处理这两种截然不同的异常是不合适的。如果用两个catch块分别捕获SQLException和IOException就要好多了。这就是说,catch语句应当尽量指定具体的异常类型,而不应该指定涵盖范围太广的Exception类。 

  另一方面,除了这两个特定的异常,还有其他许多异常也可能出现。例如,如果由于某种原因,executeQuery返回了null,该怎么办?答案是让它们继续抛出,即不必捕获也不必处理。实际上,我们不能也不应该去捕获可能出现的所有异常,程序的其他地方还有捕获异常的机会??直至最后由JVM处理。 

  结论二:在catch语句中尽可能指定具体的异常类型,必要时使用多个catch。不要试图处理所有可能出现的异常。 

  反例之三:占用资源不释放 

  代码:3行-14行。 

  异常改变了程序正常的执行流程。这个道理虽然简单,却常常被人们忽视。如果程序用到了文件、Socket、JDBC连接之类的资源,即使遇到了异常,也要正确释放占用的资源。为此,Java提供了一个简化这类操作的关键词finally。 

  finally是样好东西:不管是否出现了异常,Finally保证在try/catch/finally块结束之前,执行清理任务的代码总是有机会执行。遗憾的是有些人却不习惯使用finally。 

  当然,编写finally块应当多加小心,特别是要注意在finally块之内抛出的异常??这是执行清理任务的最后机会,尽量不要再有难以处理的错误。 

  结论三:保证所有资源都被正确释放。充分运用finally关键词。 

  反例之四:不说明异常的详细信息 

  代码:3行-18行。 

  仔细观察这段代码:如果循环内部出现了异常,会发生什么事情?我们可以得到足够的信息判断循环内部出错的原因吗?不能。我们只能知道当前正在处理的类发生了某种错误,但却不能获得任何信息判断导致当前错误的原因。 

  printStackTrace的堆栈跟踪功能显示出程序运行到当前类的执行流程,但只提供了一些最基本的信息,未能说明实际导致错误的原因,同时也不易解读。 

  因此,在出现异常时,最好能够提供一些文字信息,例如当前正在执行的类、方法和其他状态信息,包括以一种更适合阅读的方式整理和组织printStackTrace提供的信息。 

  结论四:在异常处理模块中提供适量的错误原因信息,组织错误信息使其易于理解和阅读。 

  反例之五:过于庞大的try块 

  代码:3行-14行。 

  经常可以看到有人把大量的代码放入单个try块,实际上这不是好习惯。这种现象之所以常见,原因就在于有些人图省事,不愿花时间分析一大块代码中哪几行代码会抛出异常、异常的具体类型是什么。把大量的语句装入单个巨大的try块就象是出门旅游时把所有日常用品塞入一个大箱子,虽然东西是带上了,但要找出来可不容易。 

  一些新手常常把大量的代码放入单个try块,然后再在catch语句中声明Exception,而不是分离各个可能出现异常的段落并分别捕获其异常。这种做法为分析程序抛出异常的原因带来了困难,因为一大段代码中有太多的地方可能抛出Exception。 

  结论五:尽量减小try块的体积。 

  反例之六:输出数据不完整 

  代码:7行-11行。 

  不完整的数据是Java程序的隐形杀手。仔细观察这段代码,考虑一下如果循环的中间抛出了异常,会发生什么事情。循环的执行当然是要被打断的,其次,catch块会执行??就这些,再也没有其他动作了。已经输出的数据怎么办?使用这些数据的人或设备将收到一份不完整的(因而也是错误的)数据,却得不到任何有关这份数据是否完整的提示。对于有些系统来说,数据不完整可能比系统停止运行带来更大的损失。 

  较为理想的处置办法是向输出设备写一些信息,声明数据的不完整性;另一种可能有效的办法是,先缓冲要输出的数据,准备好全部数据之后再一次性输出。 

  结论六:全面考虑可能出现的异常以及这些异常对执行流程的影响。 

  改写后的代码 

  根据上面的讨论,下面给出改写后的代码。也许有人会说它稍微有点?嗦,但是它有了比较完备的异常处理机制。 

OutputStreamWriter out = … 
java.sql.Connection conn = … 
try { 
   Statement stat = conn.createStatement(); 
   ResultSet rs = stat.executeQuery( 
   "select uid, name from user"); 
   while (rs.next()) 
   { 
       out.println("ID:" + rs.getString("uid") + 
       ",姓名: " + rs.getString("name")); 
   } 

catch(SQLException sqlex) 

   out.println("警告:数据不完整"); 
   throw new ApplicationException( 
   "读取数据时出现SQL错误", sqlex); 

catch(IOException ioex) 

   throw new ApplicationException( 
   "写入数据时出现IO错误", ioex); 

finally 

   if (conn != null) { 
       try { 
           conn.close(); 
       } 
       catch(SQLException sqlex2) 
       { 
           System.err(this.getClass().getName() + 
           ".mymethod - 不能关闭数据库连接: " + 
           sqlex2.toString()); 
       } 
   } 

   if (out != null) { 
       try { 
           out.close(); 
       } 
       catch(IOException ioex2) 
       { 
           System.err(this.getClass().getName() + 
           ".mymethod - 不能关闭输出文件" + 
           ioex2.toString()); 
       } 
   } 




  本文的结论不是放之四海皆准的教条,有时常识和经验才是最好的老师。如果你对自己的做法没有百分之百的信心,务必加上详细、全面的注释。 

  另一方面,不要笑话这些错误,不妨问问你自己是否真地彻底摆脱了这些坏习惯。即使最有经验的程序员偶尔也会误入歧途,原因很简单,因为它们确确实实带来了“方便”。所有这些反例都可以看作Java编程世界的恶魔,它们美丽动人,无孔不入,时刻诱惑着你。也许有人会认为这些都属于鸡皮蒜毛的小事,不足挂齿,但请记住:勿以恶小而为之,勿以善小而不为。

Java字节码能够很容易被反编译,今天下午我为了得到一个心仪已久的JBuilder Opentools,于是我不惜放下其他工作,研究了一把该软件加密方法的破解和反破解,结合以前的一些经验,作文一篇与大家共飨。 
破解之道: 
如今市面上的java obfuscator很多(可以从google分类中列出)比较著名的有4thpass的产品,不要钱的可以用JODE(JODE即是Obfuscator也是Decompiler,还提供源程序,推荐初用者使用),一般来说代码扰乱器工作原理有三种,最初级的有比如Jbuilder自带的(缺省情况下该项功能关闭),能把私有变量和方法的名称用乱码代替;稍微高级一点的能把公开变量和方法也能用乱码代替,通常是输入你要扰乱的jar和一个脚本(用来控制保留部分,否则你的主程序也不能执行了),有些不用乱码代替变量名,而是直接用Java的关键字,读者可能会感到疑惑,其实这种工具是绕过了Java编译器的限制,输入class或jar(不是源程序),扰乱后输出class或Jar,当然JVM还是能够执行的!如果用一般的方法这些类文件是没法经过反编译后修改再用javac或jikes编译的,破解之道是先用反编译软件发编译出来(所有非法变量名加前缀),然后依样画葫芦用同样的扰乱器生成修改好的类文件以达到目的;再高级一点就不是用常规方法了,一些是针对市面上出现反编译软件做一些陷阱,使得这些反编译软件不能工作;还有一种是自己做一个Java编译器(JDK里也有一个java实现的编译器),在符合JVM规范的前提下乱编译,一般用在applet上,使得一般的反编译软件根本就解释不了。目前我只能搞搞中等扰乱的程序。最著名的反编译器有JAD1.58e是用C++写的,前台还有一个Delphi的界面。用它可以很快的编译,我顺便写了一个perl小程序,可以批量编译上千个class。 
对一些提供license.key(包含授权信息的加密文件)的软件,一般这种文件会采用DES,RAS和CRC校验而且一般是二进制的(即使有时输出成BASE64编码),直接修改文件是浪费时间的,你可以先反编译通过阅读源程序来探究解密过程,如果过程是可逆的,那么你自己实现一个加密过程,可以很容易的生成你自己想要的license key;如果过程不可逆也不是就搞不定了,有些强度不大的加密算法还是可以用暴力破解法来搞定,还有一种情况是对数字加密(一般指过期时间)如果你能修改这个过期时间那么你就可以多用一会儿了,用数学方法描述一下:假设集合X是明文包含的元素集合,Y是X经过算法后的映射,包含密文元素,如果有存在两个算法A和B,能使得{Y-A->X}={Y-B->X},A算法可逆,但B算法是不可逆的,生产方用A的逆算法加密授权信息(X:String)到(Y:byte[]),并在软件中用B算法解密,这样你就搞不定了,但如果集合X的元素是有限的,假设只有0-9(new Date().getTime()格式),那么算法B就称为不可逆但不可靠的,因为你通过一个样本(一般都会给你评价版的license啦!),是可以得到某些Y集合中元素在X集合中的逆映射的,这样你可以直接用这张映射表来修改license了。 
反破解之道: 
如果是做产品或提供演示程序,加密还是有好处的,加密的软件可以用上面提到的JODE,一般都是对编译好的class文件进行扰乱,因为并不是所有的符号都需要扰乱,如果你开发的是一个类库,或者某些类需要动态装载,那些公共API就必须保留符号不变,这样别人才能使用你的类库。先编写脚本对那些需要保留的符号名称进行配置,某些扰乱器能够调整字节码的顺序,使反编译更加困难。如果你用的代码扰乱器能保证别人不能通过反编译来修改或代替你的class,那么你还得注意不要用不可靠的加密算法。