lbom

小江西

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  18 随笔 :: 21 文章 :: 69 评论 :: 0 Trackbacks

2006年1月10日 #

昨日年终总结会,公司总裁给出IT产业未来发展的几个新趋势,值得我们这些从事软件工作的人员沉思,现对他的发言摘录如下:
1)管理虚拟化:有形的组织型管理和虚拟的IT流程管理相结合管理模式;
2)制造虚拟化:由生产线工人和由程序控制的机器人相结合的生产模式;
3)渠道虚拟化:由实体销售店和虚拟的网上销售相结合的渠道管理模式;
4)服务虚拟化:实体保养维修和远程诊断,软件更新相结合的服务模式;
5)组织虚拟化:垂直的组织机构和横向的项目型组织机构相结合的企业组织模式。
posted @ 2014-01-24 22:28 lbom 阅读(277) | 评论 (0)编辑 收藏

今天,花费1个小时,研究了一下在Windows下,使用Telent登录至UNIX的脚本,现将其贴下,以供下次使用(tt.bat)

 

 

@echo off
echo set sh=WScript.CreateObject("WScript.Shell") >telnet_tmp.vbs

echo WScript.Sleep 3000 >>telnet_tmp.vbs
rem ----------------UNIX IPAddress
echo sh.SendKeys "open 10.0.18.100{ENTER}" >>telnet_tmp.vbs

echo WScript.Sleep 3000 >>telnet_tmp.vbs
rem ----------------userID
echo sh.SendKeys "root{ENTER}" >>telnet_tmp.vbs

echo WScript.Sleep 3000 >>telnet_tmp.vbs
rem ----------------password
echo sh.SendKeys "root{ENTER}" >>telnet_tmp.vbs

echo WScript.Sleep 3000 >>telnet_tmp.vbs

echo sh.SendKeys "ls {ENTER}">>telnet_tmp.vbs

start telnet

cscript //nologo telnet_tmp.vbs

rem del telnet_tmp.vbs

 

posted @ 2011-10-10 16:02 lbom 阅读(3996) | 评论 (1)编辑 收藏

1)启动LiveWriter客户端

2)添加Blog帐户:

image image

3)设置日志类型:

image

4)连接测试

posted @ 2010-09-25 18:02 lbom 阅读(301) | 评论 (0)编辑 收藏

很久没有动手写WebService了,这次,借项目间隙,对系统进行一个小改造,将一部分功能使用WS进行封装,为下一步异构系统集成打下基础。

但在WS化时,由于日久生疏,一个小小的WS化变动,却花了整整好几天时间!为此,狠下以来,将其过程进行记录,以便下次参考。

 

WS整体流程:

clip_image001

以下分别介绍:

1、设计和实现WebService服务端功能组件,用于统一处理针对本应用系统所需进行WebService化的逻辑实现。并将系统逻辑处理中的对象转成序列化后的String对象,以符合WebService交互标准。

clip_image002

2、根据SBPApi.java,生成WSDL等:通过Eclipse右键菜单中的WebService-->Create Web Service项。完成后,会在web目录下建立wsdl目录和SBPApi.wsdl,在WEB-INF目录下建立(改写)server-config.wsdd等文件,并完成对web.xml的修改。其操作流程示如下:

clip_image003

3、根据SBPApi.wsdl,生成WebService客户端开发包和部署文件:

1)为不影响已有项目,可另建java Web项目;

2)将wsdl目录复制至新项目对应的web目录下;

3)通过eclipse已提供的webService插件(右键)功能,生成客户端开发包所各文件。此时,所生成的文件与服务端对象文件结构一至。

clip_image004

4)调整关联引用文件,将其调整至客户端开发包,从而避免与服务器端的引用路径重复而引发不便,并将服务器SDK中已有文件删除。

clip_image005

5)建立客户端的快速使用代理SBPClient.java,对WebService服务端交互工作的SBPApiSoapBindingStub.java进行客户端封装,并根据服务端中交互对象进行反向工程,其示例结构如下:

clip_image006

6)将clientApi下的所有文件打包后,加入测试项目进行测试。此时,因客户端所使用的服务端对象未包含在WebService客户端开发包中,因此需要将服务端对象也一同打包。

7)测试。

4、开发环境:Eclipse3.3.1.1 + JDK1.5.06 + Apache Axis version: 1.4

posted @ 2010-04-02 15:45 lbom 阅读(2154) | 评论 (4)编辑 收藏

在一次基于多线程的编码测试中,发现继承Runnable接口的线程实现类在运行时并未按预计启动多线程,经分析和比较后,找出问题所现,现将其记录下来,以供分享。

Java中,多线程编程中的线程编写,有两种方式,即扩展Thread基类或继承Runnable接口;例如:

public class T extends Thread {

public void run() {

……

}

}

public class R implements Runnable {

public void run() {

……

}

}

对于扩展Thread的实现类T,可以使用T.start()来启动此线程;如

public static void main(String[] args) {

Thread t = new T();

t.start();

}

但对于继承Runnable接口的实现类R,因接口中并没有提供直接启动线程的start()方法,只有一个线程主逻辑运行的run()方法。此时,如执行run(),会因为R.run()只是作为此线程实现类的一个方法,并未在主线程之外,启动另一个线程,从而导致R.run()阻断主线程继续向下执行;并未达到多线程运行的目的。

错误启动代码如下:

public static void main(String[] args) {

R r = new R();

r.run();

}

那么,如何使用另外线程来启动继承Runnable接口的实现类呢?以下就是它的正确的使用方式:

public static void main(String[] args) {

R r = new R();

Thread t = new Thread(r);

t.start();

}

此时,需注意,在主线程执行时,需等待子线程执行,否则,当主线程结束后,子线程也将结束。

posted @ 2010-03-10 16:48 lbom 阅读(1570) | 评论 (2)编辑 收藏

需求:

系统A与系统B分别部署在不同域的两台服务器中,但它们的身份都统一在身份认证服务器中;身份认证信息以Session方式存贮于各自系统中,并辅以cookie进行使用。

当用户在A系统登录后,访问B系统时,由于是跨域访问,导致身份信息不能正确的传递到B系统中,从而致使用户需在B系统中重新登录。

clip_image001[6]

解决方案:

处理这类跨域访问时,我们最先使用从B系统向C通过HttpRequest(类AJAX请求)的方式获取身份信息,此方式好处是同步处理,方便用使用;但其限制诸多,如需设置信任站点、用户访问确认等,甚至,在对应用服务器作了一次安全升级后,根本无法访问了。因此,需另行开辟途径,于是,在同事建议下,我们使用IFrame内嵌跨域验证网页,来解决此问题。

1、原理设计:用户在访问B系统时,先使用一内置的iframe,并将iframe的src指向身份认证服务器系统代理验证接口;如果用户已经在A系统中进行过登录,即A域了中已存在着身份认证信息后,身份认证服务器中也将具有其身份信息将其附带着身份认证信息后重定向访问B系统代理接口;B系统代理验证接口在接收到由A系统传递而来的身份认证信息后,通过身份认证服务器验证后,在B系统所在域重建身份认证信息。

2、实现逻辑贴码:

1)B系统代理验证接口:

(1)IFrame逻辑贴码:

clip_image002[6]

(2)JS检测是否通过认证逻辑贴码:

clip_image003[6]

2)身份认证服务器端接口(JSP):

clip_image004[6]

3、注意事项:

1)由于身份认证中心使用cookie作为身份标识,因此,需要用户在浏览器中允许使用cookie的设置;

2)由于在iframe中进行跨域重定向,因此需在IE安全中的跨域浏览子框架项设为启用:

clip_image005[6]

4、源码文件:

……

posted @ 2010-02-08 17:55 lbom 阅读(3472) | 评论 (3)编辑 收藏

在windows下进行j2ee项目开发和部署时,常需要对系统存在问题进行更深入的分析。由此,实时的javacore就是分析的最佳方式之一。但如何以最方便直接的方式产生javacore文件,就是这项工作必需做的准备工作了。

1、通过dos窗口,进入至jdk安装目录下的bin目录中;

2、运行jconsole.exe,并设置信息输出的目标文件,以便于分析,否则将直接输出至屏幕上;

image

3、连接正在运行的目标jvm;

image

4、连接后的jconsole如下:

image

5、通过通过Ctrl+Break组合键,产生javacore至指定文件中。

6、下一步就是对所产生的javacore文件进行具体的分析和使用了。

posted @ 2009-12-10 15:00 lbom 阅读(2338) | 评论 (2)编辑 收藏

某日,公司进行年度一次的体检!

在连续查出10个脂肪肝后,医生对第11个进来检查的人说:“等会,我们的B超机好像出问题了,等检修后再进行”

这是一个真实的事件,我们这些IT行业的从业人员,多坐少动,压力大,时间长,导致体质差的边医生都怀疑机器了!

唉!

posted @ 2009-10-29 15:32 lbom 阅读(322) | 评论 (0)编辑 收藏

一、项目建立及应用实现

1、建立J2ME项目

clip_image001

2、在完成开发后,进入Application Descriptor编辑界面

clip_image002

3、因默认情况下,Application Descriptor文件中未定义MIDlet启动对象,因此需使用EditPlus或记事本等文本编辑器,编辑Application Descriptor文件(位于项目根目录下),并添加以下项目,如:

clip_image003

4、运行Application Descriptor编辑界面中的Lanuch as enumlated Java ME JAD,进行测试

clip_image004

5、在步骤4之后,会在项目根目录下的.mtj.tmp中生成LaunchFromJAD子目录,其中的worm.jad和worm.jar即是手机程序的安装文件

clip_image005

6、将worm.jad和worm.jar复制至手机中,运行worm.jad进行安装后,即可使用

二、问题分析:

1、如报【文件不完整】错误,则检查worm.jad中的项目是否完整。在Eclipse中使用Lanuch as enumlated Java ME JAD测试通过,并自动生成的此文件,一般都是完整的,不需作任何修改。

2、如报【版本错误】,则检查您在Eclipse中使用的的模拟器版本是否是您手机所支持的,出现此错误后,将模拟器版本调低试试。其位置如下

clip_image006

三、开发环境:

1、java JDK1.6.0_10;

2、EclipseV3.3.1.1;

3、sun公司J2ME-WTK开发包:sun_java_wireless_toolkit-2_5_2-ml-windows.exe

4、Eclipse移动应用开发包:eclipseme.feature_1.7.9_site.zip

posted @ 2009-10-15 17:50 lbom 阅读(1447) | 评论 (0)编辑 收藏

 

 

消息中心产品简介

产品简介

XXX产品框架中,我们根据产品发展规划和业务领域需要,使用基于JMS技术,通过应用WEBService,开发了消息中心中间件(简称MC)。通过消息中间件,我们可以实现各系统间的异步数据交换和事务处理、执行不需前台使用人员干预的如后台业务和数据同步工作,也可用来处理一些受到安全和其它一些因素制约,导致无法直接通过数据库或应用系统进行处理的受限业务。

消息中心中间件,包括消息总线和消息客户端两部分:消息客户端负责业务类消息实例的产生、发送消息实例到消息总线、接收从消息总线转发而来的消息实例、将收到的消息实例交由其载体应用系统进行与之对应的业务处理等活动;消息总线负责接收从消息客户端产生并发送而来的消息实例、消息重建、根据消息配置进行消息实例重建,将重建后的消息实例转发至对应的消息客户端等活动。

消息客户端与XXX各应用系统集成在一起,并通过应用系统开放WEBService端口进行消息的发送和接收等,从而避免单独部署和发布所带来的困难和额外资源消耗。消息总线可单独部署,也可和消息客户端一样,与XXX应用系统集成部署,在XXX产品框架下,有且只需要一套消息总线即可满足需要。消息配置中心,其作用包括配置和管理消息中心各组成部分的部署方式和访问信息,以此将消息中心各部有机的联系起来;同时,各消息业务应用,也使用配置文件进行配置化管理,并与消息中心各组成部分进行关联配置,从而形成一个统一且开放的整体;其它的如性能优化处理、日志记录等也在配置中心进行配置和管理。

应用现状

在消息中间件V1.0版本开发完成后,我们即将其投入实用。在XXX各分子系统这近一年时间的运行和使用过程中,消息中心很好的完成了预定任务,其可靠性、可扩展性和适用性得到很好的验证。以此为据,通过使用消息中心,开发出基于消息中心的客户化应用和业务活动也在持续的增加中,到现在为止,已经有包括网络检测、信息同步、配置更新、电子目录树更新、权限同步等诸多应用是基于消息中心应用开发,并很好的使用在XXX各分子系统的测试和内网正式环境中。

问题出现、描述、分析与处理记录

问题出现

在XXX系统正式接入外网后,通过对业务进行跟踪,发现外网用户(系统)所产生的消息实例无法正常的到达指定的消息总线及消息客户端。最主要的体现是权限同步消息应用无法正常完成的问题,导致外网用户权限未得到及时更新。对此过程中消息中心所涉及部分进行分析发现:所有的权限同步消息实例在产生后,不能正常的将此消息实例发送至消息总线,分析失败原因,只有一种,那就是”connect time out”。从此信息可看出,应该是外网系统所发出的消息无法通过WEBService送达指定的消息总线接收端所至。但从内网发出的同一类消息,其发送和接收却又都是正常的。

分析过程记录

1、先分析我们系统的整体部署方式,如下图所示:

根据外网用户可正常登录和访问系统,并可通过系统准确及时的发出执行指令操作,完成其所需的业务活动来看,网络方面和系统和硬件方面都不存在问题。

2、在外网环境下,直接进行各消息客户端和消息总线的服务的检测,所发请求都能够正确的到达指定目标,WEBService的响应也正常且正确,也就是说,各应用系统加载的消息服务运行也正常。

3、根据本次检测需要,另行开发消息中心专用检测工具,为本次和今后的行的消息中心检测和问题分析,作好更充分的准备。

4、通过检测工具,发现,外网环境下,消息客户端和消息总线之间不能够联通,从而找到问题所出:即不知是何原因,导致外网消息与外网的消息总线间联络不通!

5、对外网用户消息产生和发送的过程和逻辑实现进行分析:我们发现,为了满足应用系统外网访问的需要,我们对消息系统配置信息中服务地址的ServerName进行了伪处理,即在运行时,根据用户浏览器的请求头来判断用户使用的是哪一个WEB服务器地址,并将此地址动态的代替消息配置中的各ServerName信息,从而保证各使用用户只能够访问其指定的WEB服务器,从而避免因WEB服务器的不匹配而影响其访问速度、处理效率等故障的发生。此方式已在我部门多套同时服务于内外网络的系统中得到可靠的验证。

那么,会不会因为ServerName在动态解释过程中,因多并发情况下,因后访问者将前访问者的ServerName改写而导致错误的解释,即将不同网络用户的消息地址进行张冠李戴而导致消息无法正常发送呢?

分析消息中心各部分WEBService生成和使用机制:因系统的并发性要求较高,在高峰期其在线用户可达3000人,并发用户在300以上,且系统稳定性要求极高。为提高系统的性能和稳定性,在系统启动时即将消息中心各部的WEBService连接进行创建和缓存,以提升消息中心资源利用率,并提升其访问性能。

当存在多网络用户访问时,可能因消息中心存贮的WEBService连接并不是其用户所使用的那个网络的WEBService服务地址,此时,消息肯定是无法送达至此用户所需要的目标的。因此,报”connect time out”错误就成必然的了。

既然已找到问题的可能原因,我们立即进行着手分析和解决:根据部署要求,我们对对消息服务连接服务进行了升级,即将服务请求进行分类处理和实现,并在消息配置中对所使用的部署方式、代理实现后,交由测试人员进行部署和测试。

测试结果:令人失望的是,此问题依然存在!在通过外网WEB服务器访问的系统,其消息还是无法发送至消息总线。由此得知,此种分析方向是错误的!

至此,好像已经走入了死区,能想到的方式都已经想到了,但问题到底出在哪呢?

问题解决

在一次与同事聊天的过程中,忽然想到一个问题,那就是:我们的消息的产生和应用都是由应用系统和与之集成在一起的消息客户端自动产生和处理的,此过程中完全不受人工干预和影响。而应用系统是部署在应用服务器中,WEB服务器仅是用来处理用户的HTTP请求à将此请求转发至对应的应用服务器后à将应用服务器的响应返回给用户。

在此过程中WEB服务器并未对用户业的http请求进行过任何业务上的处理!那么,问题会不会出在WEB服务器端呢?检查一下消息中心的配置不管是使用ServerName还是写死IP和域名,我们的消息中心配置的地址都是指向WEB服务器。而在应用系统发现消息时,其所在位置是应用服务器。而应用服务器是无法直接访问部署于外网IP中的WEB服务器的,当然,消息无法发送至目标就成为必然了。

既然已经找到问题,那就动手,将消息中心的配置信息指向应用服务器后,重启应用系统后测试,问题果然解决!

通过应用服务器进行后台自动处理的,进行HTTPWEBService活动,其目标必需是它能够访问的有效地址!这个问题在以前也曾经碰到过,只是由于时间隔得太久,且这些场景应用出现太少,而导致再次发生。

补充与心得

1、    基于应用系统或后台自动触发的一些业务逻辑,如其中存在着系统间相互访问或远程调用等,必需以应用系统自身为根,进行连接测试;通过外层包装或其它代理,进行访问时,必需先剥离过外层包装或其它代理后,再进行连接测试,并以测试结果,作为决策的依据!此举适用于各类系统的架构设计和逻辑实现过程中。

2、    基于中间件产品应用,及时开发与之配套的检测和使用工具,是一件必不可少的工作,此举将为后期的实施和问题分析节省大量的工作量。

posted @ 2009-10-07 17:06 lbom 阅读(1466) | 评论 (0)编辑 收藏

    几天前,偶尔和邻居聊天,她说要去买顶蚊帐过夏,不由的也动了心。是啊,在小时候,那家不是用蚊帐来保证漫长夏的良好睡眠呢!现在随着科技发达,家家户户,特别是城市住户都已经将蚊帐扔掉而改用蚊香或蚊片了。
    于是等夫人下班回家,和其商量,却是死活不同意,理由如下:1)影响卧室美观;2)挤占空间;3)拆洗不便。。。。。。
    没办法,为了达到目的,我只得绞尽脑汁,想出各种理由,以期望能够说服夫人:
    1)身体牌:我对蚊香有过敏,因此,在夏天,我们是不能点蚊香的;再说,各种灭蚊产品其主要成份都有毒性,不管其含量多少,都对身体无益。
    2)经济牌:在重庆,一年有6个月的夏天,以每晚一片灭蚊片计算,一年下来,加上电热灭蚊器最少需要投入120元以上才能保证夏季无忧,三四年下来,就可以买个较好的蚊帐了;再说,灭蚊片用久后,还得防止蚊子产生的抗药性;但使用蚊帐却不需要用电,一年下来,其电费也能节约不少,还可防止可能因电热灭蚊器散热不良而导致的用电风险的发生;
    3)环保牌:在卧室支个蚊帐,即温馨又浪漫,还无灭蚊产品的各类化学合成的气味,最环保不过了;
    4)卫生牌:在床上支个蚊帐,将有效的减少灰尘降落,也免了蜘蛛等小虫在晚间不意间打扰我们的安眠,多好的一件事!
    。。。。。。
    啰啰嗦嗦,直说的口干舌燥,并许下一堆好处之后,终于换来老婆的点头。于是二话不说,拉上夫人,直奔商场,在东挑西比之后,买下一款合意的落地式蚊帐。
    至此,我的环保蚊帐计划就此实现!
 
    所以,回归和怀旧,并不都是倒退!
posted @ 2009-05-26 09:43 lbom 阅读(288) | 评论 (0)编辑 收藏

        在一周前,项目组碰到一个大问题:NTKO Office Activex控件在上传文件及提交页面信息时,其提交的页面元素输入中文值变成了无法识别的、也不属于已知编码中的任何一种编码格式的乱码;但在NTKO Office Activex控件包装项目组提供的的测试项目中,此问题并未出现,因此判断是项目兼容性所导致。
       在项目组功能开发员和控件包装组成员进行近一周的努力后,也未解决此问题。最后,此问题交由我来做最后分析和处理。
       经过三天时间对问题项目的分拆、组装、分析、测试后,终于找到问题所出,现将此过程进行记录,以备参考:
        1)以控件包装组测试项目为基准,建立项目级测试项目,并保证在此测试项目中不存在兼容性问题;
        2)检测web.xml:将问题项目的web.xml代替测试项目中的web.xml,未出现兼容性问题,从而排除因web.xml的差异而导致的兼容性;
        3)测试问题项目中的项目依赖:将问题项目的项目依赖关系复制至测试项目中,发现兼容性问题未出现,从而排除项目依赖导致的兼容性;
        4)检测支持包:将问题项目中的支持包(各jar)代替测试项目中的支持包,未出现兼容性问题,从而排除因支持包的差异导致的兼容性,也就排除了各servers,servlet,listener等导致的兼容性问题;
        5)检测js支撑:将问题项目中的所有相关js文件取出,代替测试项目中的相关js文件,未出现兼容性问题,从而排除因js支持文件的差异导致的兼容性;
        6)检测css支撑:将问题项目中的所有相关css文件取出,代替测试项目中的相关js文件,未出现兼容性问题,从而排除因css支持文件的差异导致的兼容性;
        7)检测tld,xml文件:将问题项目中的tld,xml文件取出,代替测试项目中的tld,xml文件,未出现兼容性问题,从而排除因tld,xml的差异导致的兼容性;
        8)至此,正常解决的兼容性措施都已用完,还是未找到问题所出!如何办?
        9)开始使用非正常手段进行排查:
            <1>对比检查.project和.classes未发现异常,从而排除基本项目配置导致的兼容性;
            <2>将问题项目的web项目设置文件(.settings)代替测试项目的web项目设置文件(.settings),问题出现了!继续排队分析,发现问题出现在文件org.eclipse.wst.common.component中,
问题项目的设置为:
                                    <?xml version="1.0" encoding="UTF-8"?>
                                    <project-modules id="moduleCoreId" project-version="1.5.0">
                                       <wb-module deploy-name="XXX_IC">
                                       <wb-resource deploy-path="/" source-path="/web"/>
                                       <wb-resource deploy-path="/WEB-INF/classes" source-path="/src"/>
                                       <wb-resource deploy-path="/WEB-INF/classes" source-path="/test"/>
                                       <property name="java-output-path" value="build/classes"/>
                                       <property name="context-root" value="XXX_IC"/>
                                   </wb-module>
                                   </project-modules>
测试项目设置为:
                                    <?xml version="1.0" encoding="UTF-8"?>
                                    <project-modules id="moduleCoreId" project-version="1.5.0">
                                       <wb-module deploy-name="test">
                                       <wb-resource deploy-path="/" source-path="/web"/>
                                       <wb-resource deploy-path="/WEB-INF/classes" source-path="/src"/>
                                       <wb-resource deploy-path="/WEB-INF/classes" source-path="/test"/>
                                       <property name="java-output-path" value="build/classes"/>
                                       <property name="context-root" value="test"/>
                                   </wb-module>
                                   </project-modules>
且无论如何修改"XXX_IC",都会导致兼容性出现,最后没办法,将下划线"_"去掉,奇迹出现了。
        原来NTKO Office Activex控件在提交数据时,是通过scoket模拟Http进行文件和页面元素的提供,如提交的页URL完整路径中包含了"_"等字符时,将导致无法识别,从而导致兼容性的产生。
posted @ 2009-05-15 22:13 lbom 阅读(1501) | 评论 (2)编辑 收藏

 

从小到大,椰子已经吃过很多次了,但在这些吃椰子有经历中,我只知道一种吃法:开孔à插吸管à喝椰汁à丢椰壳à完事!

       但在今天,事情有了些变化,于是产生的椰子的第二种吃法。

我和夫人在散步时,顺便准备到超市买点水果,看到水果区的椰子又大又好且正在打特价。心中一动,就挑了个大的,买回家准备细细品尝一番。在按通常吃法吸光椰子中的水汁后,突然想起,我们平时很喜欢吃超市中的一种叫椰角小吃的,又甜又韧很有嚼劲,但椰角是长大哪的呢?不会是椰子树上的另一种产品吧!看着椰子开口的硬壳下面的白色软组织,我们就突发奇想,这白色软组织会不会就是那椰角呢?

说动就动,先用刀将空椰子壳砍开,发现其内层确实是一层约0.8cm厚的白色果肉。小心的切下一小块尝一尝,味道淡淡的,很有韧劲,确实就是那椰角的源料。这就是我发现的椰子的第二种吃法了。

在生活中有很多事:你没经历,就不会想到;当你想不到时,美好的事情就可能错过!
    这就是生活。

                                                                                    2009/5/5

posted @ 2009-05-05 22:31 lbom 阅读(2346) | 评论 (2)编辑 收藏

 

在我们公司的软件研发体系中,存在着三种截然不同的软件开发方式。而我,作为公司最老同事之一,也是这三种开发模式的亲历者,曾不只一次的被公司同事问过我关于这三类方式之间的异同点。于是利用空闲时间,对其进行一番整理、分析和对比。

1、全能型

部门经理在接到项目之后,将此项目交给部门内的熟练程序员后,此程序员就自动被委任其为项目经理。从此开始,程序员将根据项目售前方案和销售合同内容,在项目进行过程中分别担当起项目经理、功能设计师、数据存贮设计师、程序员、测试员和项目实施人员等诸多角色。并在项目进行的过程中,带领少量其它程序员和辅助资源来完成此项目的所有工作。

此类项目其功能单一且不复杂,只是为了帮助用户提升某一项工作的工作效率或解决客户在其工作中的一些问题,如工作日志信息的采集和分析业务项目、办公用品的申请和领用等。它们因为其涉及范围小,使用人员不多,从而具有项目总费用少、开发和实施周期短、对性能要求不高的特点。

在此类开发模式中,程序员由于其工作的全面性,使他们在进入项目组后,能够得到很快且全面的提升,并会在与客户交往的过程中,建立起良好的客户关系处理经验,为其今后的成长和发展打开良好的基础。

由于项目需要,程序员需要掌握全面技能,容易造成其在项目开发过程中需要全面的接触项目管理及人际关系、需求分析、数据库及对象设计、人机交互和用户体验设计、系统设计和开发、测试和系统提升、应用实施和售后维护等诸多截然不同的领域范围;所以,作为此类程序员,其工作压力之大,事务之复杂、综合素质要求之高,是其它模式的程序员所无法对比的,这也是造成此类项目按时完成率极低、尾款回收困难、项目售后工作难做、用户满意度差、二次项目获取困难的根本原因。

同时,由于程序员被大量的非开发性事务所干扰,造成他们无法专心致力于专业技能的学习和提升,也就无法造就一支高效率、高稳定性、配合默契的开发队伍,这也是造成公司内此类人才大量流失的重要原因。

2、英雄型

       部门经理在接到此类项目后,按项目所涉及的领域范畴,将其按领域进行分工。以企业信息协同系统为例,我们将进行如下分工:门户信息的获取、聚合、交互和展现工作交给专职于门户开发的程序员;内部邮件系统的分析、设计、和实现将给邮件开发程序员;日程和事务的设计交给日程开发程序员;工作流应用工作交由工作流客户化开发程序员等等。

       在此类型的开发模式中,程序员将会是某一领域内的英雄式人物!由于他长期且相对稳定的负责着这个有限领域范围内的一切事务,可以帮助他在一定时期内进行系统而稳定的业务研究和分析工作,进而成长为此领域内的业务专家。而且,通过持续的对其工作进行迭代式开发、升级和完善,可使此产品在产品品质、适用性和用户体验等方面得到稳定的提升,进而提升了整个产品的品质。

       如果此领域内的工作产品能够得到合理的规划和实现,进而将其进行单独的封装、应用集成和推广,就有可能形成一个具有相当竞争力的产品,从而为公司获取新的销售机会和利润点。

       但是,此类开发模式中的分工也容易造成程序员涉及业务领域单一和适应性窄的缺陷:由于其长期面对和研究着单一业务领域内的业务活动,而无法更多的接收和参考来自于用户、企业和其它行业内的非它业务发展需要和趋势,从而对其在产品领域内的发展产生限制,并造成其产品方向上的不准确或错误定位;由于其长期的在单一领域内工作,并在此领域内获得了公司内的认可,这也将限制他在领域间的流动性。当公司或部门的产品方向和需要调整和改变时,此类程序员就需要被迫改变甚至放弃其在原领域内的所有积累而重新开始,从而造成巨大的浪费。

3、专业型

       项目经理在接到项目之后,根据项目组成员的能力、特长职业规划,对他们进行适当且专业化分工:由业务规划人员负责项目的需求收集、业务规划和需求分析;由系统架构师对系统的进行技术构架和支撑性技术的规划和引进;由数据库专员负责数据库对象的设计和性能调优;由功能分析员在人机交互人员辅助下负责功能设计和人机交互模式;由业务逻辑实现专员根据功能设计进行高性能的业务逻辑处理实现和外部接口的设计和实现;由页面开发人员负责实现人机交互;测试人员负责对系统进行全过程的测试和质量监督;专业化实施人员可快速高效的进行系统实施和在线维护,售后服务工作也将由专人负责;

       通过恰当和合理的分工,将软件研发过程中的各个环节进行拆分,从而将复杂的软件工程分解成一个个相对独立且又紧密关联的工作项,从而有效的降低了软件开发过程中的困难度和风险性;项目经理把分解后的工作项交给项目组中的合适项目成员,并根据项目组成员的能力、工作难度和工作量,制定出科学的项目计划;同时,项目组成员在项目经理的协调和管理下进行密切的分工合作,此举即能调动项目组成员的其工作积极性,又能使他们将工作、兴趣和个人职业成长规划进行有效的结合,从而使其在技能、收入和社会认可度等诸多方面得到快速成长,达到人尽其材,材尽其用的目的。通过使用专业的人做专业的事,公司将在人员分工、资源使用和业务拓展等领域走向专业化、规模化,最终成为专业且强大的产业实体。

       但此开发模式也具有相当的局限性!其一,如何合理的利用项目组资源?项目组成员因其性格、能力和兴趣各有不同,如何能将他们按项目分工和角色组成需要,进行专业化训练和培养;其二,因项目组成员长期单一职能的工作,与其它环节的交叉和交流都受到限制,对其未来的全面发展和综合成长都很不利;其三、各角色之间的分工、合作与工业化生产中的生产线相似,那么,建立与之相适应的质量保证体系,保证各工序之间生产产品的质量,从而从事实上提升软件产品的整体质量?

       通过对这三类开发模式的分析,我们可以看出,它们各有合理性,也又具有的相当的局限性。

       全能型开发模式是早期的CS类项目开发的主要模式,其适用于哪些规模小,程序员少的小规模IT开发企业进行小型项目的开发中。但对于那些工期较长、业务范畴广、复杂度较大的项目,此种开发模板将采用将导致风险最大化,失败几乎是其唯一的结局。

       英雄型开发模式,因项目组成员领域化的分工和合作,使它在通用型复合类产品开发中具有优势。通过对产品的各组成部份进行持续的改进和迭代性开发,使产品在功能、性能、用户体验等方面得到持续的改善和提升,从而有利于产品拓展并在此过程中做大做强,最终取得竞争优势。但此开发模式也将导致项目组成员之间的工作协调、技术互用等方面存在诸多不便;另外,因领域的专业性和不可替换性,也就限制了公司在处理关健人员的流动性方面存在诸多困难,并在核心竞争力的保证方面存在着很大风险。

       专业型开发模式,通过对人员进行专业化分工,从而在软件开发过程中最大的利用了人力资源,提升软件的生产效率,并降低了软件的从业门槛。此方式在新形式下的项目开发和产品研发中都具有相当的竞争力,也易有利于保证公司的核心竞争力。但采用此种开发模式时,需要完善内部的人员激励机制,保证各角色的从业人员都有与之适应的职位规划和发展模式,并能根据项目组成员所处阶段的需要,提供相应的技能培训和交流机会,从而促进其成长,激励其上进。

       总之,采取何种开发模式,要根据公司的实际业务情况,发展规划和人员构成,进行科学的分析之后,再采取行动、从而得到具有延续性和竞争性,并与自身相匹配的软件开发模式。

                                                               2009/4/20

posted @ 2009-04-21 21:52 lbom 阅读(1856) | 评论 (2)编辑 收藏

         

今天,在陪夫人逛街回来的路上,看到一幕惨剧的发生:一位中年男子,从家中跳楼而下,坠落于坚硬的水泥地面上,当场身亡!

人生为何?为已?为亲?还是为他?

人生为已,就应该珍惜自己的生命,为自己这短暂的一生中,充实、幸福和快乐的活着,而不能因为暂时的挫折、失败而绝望和放弃;人生漫漫而无现存路,需要自己去探索和开拓,在此过程中,必然会经历曲折和无法避免的挫折。但是,不经历风雨,如何能见彩虹!挫折过后,往往就伴随着一段平坦的直道;人生艰难,生活、事业、家庭、社会各种矛盾在我这会聚,理不清也扯不断!既然如此,何不干脆看开,将不可调和的矛盾进行暂时地休眠,让时间这个解决矛盾的最好的润滑剂来慢慢解决它。

人生为亲,为父母:他们含辛茹苦的将你从无到有、从幼养成,而你却要在他们需要照顾和看护的年龄离他们而去!你忍心吗!为妻:相汝以沫几十年、同床共枕伴一生!锅碗瓢盆一屋住,酸甜苦辣是生活。你就忍心在她人生路中间,正需你坚强的肩膀作支撑时,却抛下她一人孤苦的走在这漫长的人生路上,你安心吗?人生为子:父亲是儿女的榜样和偶像,他们需要借助你那成熟的智慧来打开事业的大门,也需要你那丰富的阅历来开拓自己的人生路,更需要你成功的经验来保护雏鸟并解决初飞时所遇到的种种风险!在这种关键的时候,你却抛弃了他,你放心吗?

人生为他,为朋友兄弟:有多少美好时光值得回忆,有多少美妙经历值得回味,又有多少坎坷担当值得珍惜!你就此离开,从此兄弟聚首少一人,朋友举杯缺一环!为事业:你正值人生当年,恰逢事业当期,失败你经历,成功应有你,酸甜苦辣都尝遍,还有什么过不去?为社会:当今社会多少不平事,何必事事放我心!不必为人先,也不全落后;比上我不足,哪我就比下,实在比不过,阿Q一把也不错!

生命如此脆弱,稍有不慎就将坠落:走在路上被车撞死,走下楼下被东西砸死,乘车坠桥而死,去医院被错药药死,上班被累死,下班被烦死,既然如此,何必再来一个自己寻死呢!

请珍惜生命,爱护自己!

 

20090330于家中

posted @ 2009-04-13 15:51 lbom 阅读(131) | 评论 (0)编辑 收藏

        项目组使用润乾报表已一年多了,说实话,润乾报表在国内同类产品中属于非常不错的最好的报表开发和应用产品。相应的支持也比较到位,使用人员及交流社区也开展的很合适。在这先给它们作个广告!!!
        在项目中使用润乾报表,对数据进行专业的报表应用和开发,我对其作简单总结:
        1)对其服务器运行系统进行项目性客户化开发,从而利用项目中的权限管理和模块,实现对报表进行访问控制。否则,这对企业级应用将是一个非常大的考验。
        2)润乾报表自带的参数生成模块、报表运行载体的样式、风格都极其简陋,与项目的实际风格可能存在很大的差距。因此必需对其进行深入的扩展和开发。我们项目组的经验就是单独开发参数生成模块和润乾报表载体,如此才保证了报表中心与项目的用户体验和交互性的一致性。
        3)为了更好的利用项目组资源,我们将润乾报表开发人员独立出来,形成专门的报表开发团队。此团队负责根据业务的需要,利用润乾报表开发工具进行报表开发,即开发.raq报表文件。此部份人员可从项目组的普通成员和新进人员中进行培养,而无需占用大量的项目组中中高级开发人员资源,从而节约了项目组的资源。
        4)润乾报表对过JSP标签包含在jsp页面中进行加裁我运行。我们称此jsp页面为润乾报表运行载体。我们根据润乾报表的运行载体进行了科学的分类,并根据分类开发出统一的报表运行载体页面(jsp)。从而避免针对每个报表文件而开发与之对应的运行载体。此举也大为减少了项目组的JSP开发人员的工作量。
        5)建立润乾报表运行专用配置文件,将报表参数生成模块、运行载体及润乾报表三者之间的关系进行配置化管理,并以此为纽带,将润乾报表开发人员、JSP开发人员(开发报表运行载体和报表参数生成功能)联系起来。

         在开发过程中,我们碰到并解决了如下问题:
         1)填报类润乾报表在进行数据验证时,其提示信息(以js的alert("...")方式提示用户)成乱码显示:此问题是由于润乾报表在V4.1以后,统一使用UTF-8作编译编码。因此,要解决此问题,需要将项目的编码也改成UTF-8
         2)在润乾报表的参数赋值需按序依次进行赋值,而不能采用参数名进行统一赋值。因为,如果在润乾报表的SQL中使用了重复的参数进行赋值时,会报参数找不至的错误。
         3)在参数生成模块中将中文参数值传递给润乾报表时,会导致少量的参数值在传递过程中发生改变,如“机油”变成了“箕油”。此问题是由于在urlEncode和urlDecode的bug导致,请在开发时需特别注意。我们是通过自己对信息进行加码和解码来解决此bug。
         4)润乾报表的运行环境与应用服务器的编码方式有关:我们项目和报表中心的编码方式为UTF-8,但运用服务器(WAS6.1)的编码方式为GBK时,通过参数生成功能将中文参数传递给润乾报表时,会出现乱码问题。在将WAS的输出和运行编码改成UTF-8后,才解决此问题。


posted @ 2009-03-05 23:08 lbom 阅读(7984) | 评论 (24)编辑 收藏

        前段时间,项目组安排同事进行项目移植,并考虑在其过程中进行技术预研等相关工作,以对项目进行优化;
        在此过程中,有同事误解面向对象化开发的精髓,在匆匆了了解JavaScript面向对象的方法和示例后,对项目中的公共门户头以对象的方式进行重写。结果,将原30行的单一文件代码变成了400多行,并分布于多个文件了。
        在拿到此结果之后,我是哭笑不得,于是得出了:“新技术的引入必需能够提高生产效率或降低工作难度,否则,没有引入的必要”这句话。

posted @ 2009-02-21 20:52 lbom 阅读(1313) | 评论 (3)编辑 收藏

 

总是听说Vista在软件兼容性上有诸多问题,且一直未得到很好的解决,由于一直使用XP,对此也就不太在意。但是,因工作因素,需要升级我的饭碗(购买新笔记本,操作系统为vista)后,麻烦果然来了:

     先是Oracle数据库安装不了。还好,我在开发时可以使用公司的数据库进行开发,不在本机安装数据库还可节约一笔硬盘空间和内存。故其影响并不大,只是在下班离开公司后,没时使用数据库而已。

     安装Eclipse,继续java项目开发,未发现兼容性问题;

     成功安装Tomcat(版本号为5.5.17),但在启动时,发现其只能用管理员身份进行启动,而无法向往常一样,通过开始菜单直接启动。进入Eclipse,启动项目(WebApplication),发现麻烦来了,不管我用何种方式,TomcatServer一直报服务超时,不能正常启动!

     唉,难道要我重新恢复XP吗,这可不是一张恢复盘的问题,而是我花了两天时间,进行操作系统和相关相关软件安装,我的妈也!!

     到网上查找相关资料,也未获取明确的解决之道;到MS支持网站,没找到合适的方案;问周边同事,得到N种可能的解决方式;经过一天时间,逐个试验,终获解决之道。

      可在环境操作系统变量中添加classpath项,其值如:C:/Program Files/Java/jdk1.5.0_11/lib;C:/Program Files/Java/jdk1.5.0_11/lib/tools.jar

posted @ 2007-12-29 14:52 lbom 阅读(4300) | 评论 (8)编辑 收藏

昨日,在将应用程序(JSF应用,其中包含Tiles包)发布至测试服务器(Solaris8+Tomcate5.5)时,发现其不能正常运行,其错误如下:
......
2006-11-1 17:09:54 org.apache.tiles.servlets.TilesServlet init
信息: Initializing TilesServlet
2006-11-1 17:09:54 org.apache.tiles.servlets.TilesServlet readFactoryConfig
信息: CONFIG FILES DEFINED IN WEB.XML
信息: initializing definitions factory...ets.TilesServlet initDefinitionsFactory
2006-11-1 17:09:54 org.apache.tiles.servlets.TilesServlet saveExceptionMessage
警告: Caught exception when initializing definitions factory
2006-11-1 17:09:54 org.apache.tiles.servlets.TilesServlet saveExceptionMessage
警告: I/O Error reading definitions.
2006-11-1 17:09:54 org.apache.tiles.servlets.TilesServlet saveExceptionMessage
s.
2006-11-1 17:09:54 org.apache.tiles.servlets.TilesServlet saveExceptionMessage
警告: Caught exception when initializing definitions factory
2006-11-1 17:09:54 org.apache.tiles.servlets.TilesServlet saveExceptionMessage
警告: I/O Error reading definitions.
2006-11-1 17:09:54 org.apache.tiles.servlets.TilesServlet saveExceptionMessage
警告: javax.servlet.ServletException: I/O Error reading definitions.
2006-11-1 17:09:55 org.apache.coyote.http11.Http11BaseProtocol start
信息: Starting Coyote HTTP/1.1 on http-8800
......

根据此错误分析,是由于TilesServlet未正确读取tiles.xml配置文件,但在对tiles.xml进行权限变更后,也未解决此问题!!!
但是,此应用在开发环境下是正常的,如何是好??
我和同事在对比开发环境和测试环境之后,发现二者的运行环境差别只有操作系统(UNIX<>WINDOWS XP);
搜索Google和BeiDu,找到一篇相类似的报到,据其所说,当他的系统在断开网络后会出现类似的情况,难道是TilesServlet必需联上互联网?
在分析tiles.xml后,发现,其中有如下一句:
   <!DOCTYPE tiles-definitions PUBLIC "-//Apache Software Foundation//DTD Tiles Configuration 2.0//EN" "http://struts.apache.org/dtds/tiles-config_2_0.dtd">
原来,当互联网断开之后,不能从tiles-config_2_0.dtd中获取验证,导致此文件解释失败,将此删除之后,系统就可正常部属在测试环境之中了.

posted @ 2006-11-02 14:30 lbom 阅读(865) | 评论 (0)编辑 收藏

怀疑论者的 JSF: JSF 组件开发

省时运动使得构建 JSF 组件轻而易举

developerWorks
文档选项
将此页作为电子邮件发送

将此页作为电子邮件发送

未显示需要 JavaScript 的文档选项

Discuss

Sample code


对此页的评价

帮助我们改进这些内容


级别: 中级

Rick Hightower , CTO, ArcMind

2005 年 8 月 16 日

在四部分的 怀疑论者的 JSF 系列的最后一期中,Rick Hightower 介绍了省时运动,它可以一次或永远地说服您:JSF 组件开发要比您想像的更容易。

组件模型的关键考验就是:能否从第三方供应商购买组件,并把它们插入应用程序?与可购买可视 Swing 组件一样,也可以购买 Java ServerFaces (JSF) 组件!需要一个好玩的日历?可以在开源实现和商业组件之间选择。可以选择购买一个,而不是自行开发复杂的基于 Web 的 GUI 组件。

JSF 拥有一个与 AWT 的 GUI 组件模型类似的组件模型。可以用 JSF 创建可重用组件。但不幸的是,存在一个误解:用 JSF 创建组件很困难。不要相信这些从未试过它的人们的 FUD!开发 JSF 组件并不困难。由于不用一遍又一遍重复相同的代码,可以节约时间。一旦创建了组件,就可以容易地把组件拖到任何 JSP、甚至任何 JSF 表单中,如果正在处理的站点有 250 个页面,这就很重要了。JSF 的大多数功能来自基类。因为所有的繁重工作都由 API 和基类完成,所以 JSF 把组件创建变得很容易。

贯穿这个系列,我一直在试图帮助您克服造成许多 Java 开发人员逃避使用 JSF 技术的 FUD。我讨论了对这项技术的基本误解,介绍了它的底层框架和它最有价值的开发特性。有了这些基础工作之后,我认为您已经可以采取行动,开发自己的定制 JSF 组件了。使用 JSF 的东西,我敢保证要比您想像的要更加容易,而且从节约的时间和精力上来说,回报如此之多,多得不能忽略。

这篇文章中的示例是用 JDK 1.5 和 Tomcat 开发的。请单击页面顶部的 示例代码 下载示例源代码。注意,与以前的文章不同,这篇文章没有关联的 build 文件,因为我特意把它留给您作为一个练习了。只要设置 IDE 或编译器,把 /src 中的类编译到 /webapp/WEB-INF/classes,并在 /webapp/WEB-INF/lib 中包含所有 JAR 文件(以及 servlet-api.jarjsp-api.jar,它们包含在 Tomcat 中)。

JSF 组件模型

JSF 组件模型与 AWT GUI 组件模型类似。它有事件和属性,就像 Swing 组件模型一样。它也有包含组件的容器,容器也是组件,也可以由其他容器包含。从理论上说,JSF 组件模型分离自 HTML 和 JSP。JSF 自带的标准组件集里面有 JSP 绑定,可以生成 HTML 渲染。

JSF 组件的示例包括日历输入组件和 HTML 富文本输入组件。您可能从来没时间去编写这样的组件,但是如果它们已经存在,那会如何呢?通过把常用功能变成商品,组件模型降低了向 Web 应用程序添加更多功能的门槛。

组件的功能通常围绕着两个动作:解码和编码数据。解码 是把进入的请求参数转换成组件的值的过程。编码 是把组件的当前值转换成对应的标记(也就是 HTML)的过程。

JSF 框架提供了两个选项用于编码和解码数据。使用直接实现 方式,组件自己实现解码和编码。使用委托实现 方式,组件委托渲染器进行编码和解码。如果选择委托实现,可以把组件与不同的渲染器关联,会在页面上以不同的方式渲染组件;例如多选列表框和一列复选框。

因此,JSF 组件由两部分构成:组件和渲染器。JSF 组件 类定义 UI 组件的状态和行为;渲染器 定义如何从请求读取组件、如何显示组件 —— 通常通过 HTML 渲染。渲染器把组件的值转换成适当的标记。事件排队和性能验证发生在组件内部。

在图 1 中可以看到数据编码和解码出现在 JSF 生命周期中的什么阶段(到现在,我希望您已经熟悉 JSF 生命周期了)。


图 1. JSF 生命周期和 JSF 组件
JSF 组件和 JSF 生命周期
提示!

在许多情况下,可以在保持组件本身不变的情况下,通过改变渲染而简化开发过程。在这些情况下,可以编写定制渲染器而不是定制组件。

更多组件概念

所有 JSF 组件的基类是 UIComponent。在开发自己的组件时,需要继承 UIComponentBase,它扩展了 UIComponent 并提供了 UIComponent 中所有抽象方法的默认实现。

组件拥有双亲和标识符。每个组件都关联着一个组件类型,组件类型用于在 face 的上下文配置文件(faces-config.xml)中登记组件。可以用 JSF-EL (表达式语言)把 JSF 组件绑定到受管理的 bean 属性。可以把表达式关联到组件上的任何属性,这样就允许用 JSF-EL 设置组件的属性值。在创建使用 JSF-EL 绑定的组件属性时,需要创建值绑定表达式。在调用绑定属性的 getter 方法时,除非 setter 方法已经设置了值,否则 getter 方法必须用值绑定获得值。

组件可以作为 ValueHolderEditableValueHolderValueHolder 与一个或多个 ValidatorConverter 相关联;所以 JSF UI 组件也与 ValidatorConverter 关联(请参阅 参考资料 获得更多关于 JSF 验证和转换的内容。)

像表单字段组件这样的组件拥有一个 ValueBinding,它必须绑定到 JavaBean 的读写属性。组件可以调用 getParent 方法访问它们的双亲,也可以调用 getChildren 方法访问它们的子女。组件也可以有 facet 组件,facet 组件是当前组件的子组件,可以调用 getFacets 方法访问它,这个方法返回一个映射。Facets 是著名的子组件。

这里描述的许多组件的概念将会是接下来展示的示例的一部分,所以请记住它们!



回页首


JSF 样式的 Hello World!

我们用一个又好又容易的示例来开始 JSF 组件的开发:我将展示如何渲染 Label 标记(示例:<label>Form Test</label>)。

下面是我要采取的步骤:

  1. 扩展 UIComponent
    • 创建一个类,扩展 UIComponent
    • 保存组件状态
    • 用 faces-config.xml 登记组件
  2. 定义渲染器或者内联地实现它
    • 覆盖 encode
    • 覆盖 decode
    • 用 faces-config.xml 登记渲染器
  3. 创建定制标记,继承 UIComponentTag
    • 返回渲染器类型
    • 返回组件类型
    • 设置可能使用 JSF 表达式的属性

Label 示例将演示 JSF 组件开发的以下方面:

  • 创建组件
  • 直接实现渲染器
  • 编码输出
  • 把定制标记与组件关联

返回 图 1,可以看到在这个示例中会有两个生命周期属性在活动。它们是 Apply Request ValueRender Response

在图 2 中,可以看到在 JSP 中如何使用 Label 标记的(<label>Form Test</label>)。


图 2. 在 JSP 中使用 JSF 标记
在 JSP 中使用 JSF 标记

第 1 步:扩展 UIComponent

第一步是创建一个组件,继承 UIOutput,后者是 UIComponent 的子类。 除了继承这个类之外,我还添加了组件将会显示的 label 属性,如清单 1 所示:


清单 1. 继承 UIComponent 并添加 label

import java.io.IOException;

import javax.faces.component.UIOutput;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;

public class LabelComponent extends UIOutput{

	private String label;

	public String getLabel() {
		return label;
	}
	public void setLabel(String label) {
		this.label = label;
	}
...

接下来要做的是保存组件状态。JSF 通常通过会话、隐藏表单字段、cookies 等进行实际的存储和状态管理。(这通常是用户配置的设置)。要保存组件状态,需要覆盖组件的 saveStaterestoreState 方法,如清单 2 所示:


清单 2. 保存组件状态

    @Override
    public Object saveState(FacesContext context) {
        Object values[] = new Object[2];
        values[0] = super.saveState(context);
        values[1] = label;
        return ((Object) (values));
    }

    @Override
    public void restoreState(FacesContext context, Object state) {
        Object values[] = (Object[])state;
        super.restoreState(context, values[0]);
        label = (String)values[1];
    }
 

可以注意到,我使用的是 JDK 1.5。我对编译器进行了设置,所以我必须指定 override 注释,以便指明哪些方法要覆盖基类的方法。这样做可以更容易地标识出 JSF 的钩子在哪。

创建组件的最后一步是用 faces-config.xml 登记它,如下所示:


<faces-config>

   <component>
      <component-type>simple.Label</component-type>
      <component-class>
         arcmind.simple.LabelComponent
      </component-class>
   </component>
...

第 2 步:定义渲染器

下面要做的是内联地定义渲染器的功能。稍后我会介绍如何创建独立的渲染器。现在,先从编码 Label 组件的输出、显示 label 开始,如清单 3 所示:


清单 3. 编码组件的输出

public class LabelComponent extends UIOutput{
	...
	public void encodeBegin(FacesContext context) 
					throws IOException {

		ResponseWriter writer = 
			context.getResponseWriter();

		writer.startElement("label", this);
        	            writer.write(label);
        	            writer.endElement("label");
        	            writer.flush();
	}
	...
}

注意,响应写入器(javax.faces.context.ResponseWriter)可以容易地处理 HTML 这样的标记语言。清单 3 的代码输出 <label> 元素体内的 label 的值。

下面显示的 family 属性用来把 Label 组件与渲染器关联。虽然目前 Label 组件还不需要这个属性(因为还没有独立的渲染器),但是在这篇文章后面,在介绍如何创建独立渲染器的时候,会需要它。


public class LabelComponent extends UIOutput{
	...
	public String getFamily(){
		return "simple.Label";
	}
	...
}

插曲:研究 JSF-RI

如果正在使用来自 Sun Microsystems 的 JSF 参考实现(不是 MyFaces 实现),那么就不得不在组件创建代码中添加下面一段:


public void encodeEnd(FacesContext context) 
			throws IOException {
	return;
}

public void decode(FacesContext context) {
	return;
}

Sun 的 JSF RI 期望,在组件没有渲染器的时候,渲染器会发送一个空指针异常。MyFaces 实现不要求处理这个需求,但是在代码中包含以上方法依然是个好主意,这样组件既可以在 MyFaces 环境中工作也可以在 JSF RI 环境中工作了。

MyFaces 更好!

如果正在使用 Sun JSF RI 或其他替代品,那么请帮自己一个忙,转到 MyFaces。虽然 MyFaces 不总是 更好的实现,但是目前它是。它的错误消息要比 Sun JSF RI 的好,而这个框架相比之下更严格。

第 3 步:创建定制标记

JSF 组件不是天生绑定到 JSP 上的。要连接起 JSP 世界和 JSF 世界,需要能够返回组件类型的定制标记(然后在 faces-context 文件中登记)和渲染器,如图 3 所示。


图 3. 连接 JSF 和 JSP
连接 JSF 和 JSP

注意,由于没有独立的渲染器,所以可以给 getRendererType() 返回 null 值。还请注意,必须已经把 label 属性的值从定制标记设置到组件上,如下所示:


[LabelTag.java]

public class LabelTag extends UIComponentTag {
…
protected void setProperties(UIComponent component) {
	/* you have to call the super class */
	super.setProperties(component);
	((LabelComponent)component).setLabel(label);
}


记住,Tag 设置从 JSP 到 Label 组件的绑定,如图 4 所示。


图 4. 绑定 JSF 和 JSP
绑定 JSF 和 JSP

现在要做的全部工作就是创建一个 TLD(标记库描述符)文件,以登记定制标记,如清单 4 所示:


清单 4. 登记定制标记

[arcmind.tld]

<taglib>
   <tlib-version>0.03</tlib-version>
   <jsp-version>1.2</jsp-version>
   <short-name>arcmind</short-name>
   <uri>http://arcmind.com/jsf/component/tags</uri>
   <description>ArcMind tags</description>
   
   <tag>
      <name>slabel</name>
      <tag-class>arcmind.simple.LabelTag</tag-class>
      <attribute> 
         <name>label</name> 
         <description>The value of the label</description>
      </attribute> 
   </tag>
...

一旦定义了 TLD 文件,就可以开始在 JSP 中使用标记了,如下面示例所示:


[test.jsp]
<%@ taglib prefix="arcmind" 
         uri="http://arcmind.com/jsf/component/tags" %>
            ...
	<arcmind:slabel label="Form Test"/>

现在就可以了 —— 开发一个简单的 JSP 组件不需要更多了。但是如果想创建稍微复杂一些的组件,针对更复杂的使用场景时该怎么办?请继续往下看。



回页首


复合组件

在下一个示例中,我将介绍如何创建这样一个组件(和标记),它可以记住最后一个人离开的位置。Field 组件把多个组件的工作组合到一个组件中。复合组件是 JSF 组件开发的重点,会节约大量时间!

Field 组件把标签、文本输入和消息功能组合到一个组件。Field 的文本输入功能允许用户输入文本。如果有问题(例如输入不正确),它的标签功能会显示红色,还会显示星号(*)表示必需的字段。它的消息功能允许它在必要的时候写出出错消息。

Field 组件示例演示了以下内容:

  • UIInput 组件
  • 处理值绑定和组件属性
  • 解码来自请求参数的值
  • 处理出错消息

与 Label 组件不同,Field 组件使用独立渲染器。如果为一个基于 HTML 的应用程序开发组件,那么不要费力使用独立渲染器。这么做是额外的无用功。如果正在开发许多 JSF 组件,打算卖给客户,而针对的客户又不止一个,那么就需要独立的渲染器了。简而言之,渲染器适用于商业框架的开发人员,不适用于开发内部 Web 应用程序的应用程序开发人员。

了解代码

由于我已经介绍了创建组件、定义渲染器以及创建定制标记的基本步骤,所以这次我让代码自己说话,我只点出几个重要的细节。在清单 5 中,可以看到在典型的应用程序示例中如何使用 Field 标记的:


清单 5. Field 标记

<f:view>
  <h2>CD Form</h2>
      
  <h:form id="cdForm">
        
    <h:inputHidden id="rowIndex" value="#{CDManagerBean.rowIndex}" /> 
      
      	
        <arcmind:field id="title"
                           value="#{CDManagerBean.title}"  
                           label="Title:"
                           errorStyleClass="errorText"
                           required="true" /> <br />
		
        <arcmind:field id="artist"
                           value="#{CDManagerBean.artist}"  
                           label="Artist:"
                           errorStyleClass="errorText"
                           required="true" /> <br />
      	
        <arcmind:field id="price"
                           value="#{CDManagerBean.price}"  
                           label="CD Price:"
                           errorStyleClass="errorText"
                           required="true">
           <f:validateDoubleRange maximum="1000.0" minimum="1.0"/>
        </arcmind:field>

以上标记输出以下 HTML:


<label style="" class="errorText">Artist*</label>
<input type="text" id="cdForm:artist " 
       name=" cdForm:artist " />
Artist is blank, it must contain characters

图 5 显示了浏览器中这些内容可能显示的效果。


图 5. Field 组件
Field 组件

清单 6 显示了创建 Field 组件的代码。因为这个组件负责输入文本而不仅仅是输出它(像 Label 那样),所以要从继承 UIInput 开始,而不是从继承 UIOutput 开始。


清单 6. Field 继承 UIInput

package com.arcmind.jsfquickstart;

import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;


/**
 * @author Richard Hightower
 *  
 */
public class FieldComponent extends UIInput {

	private String label;

    @Override
     public Object saveState(FacesContext context) {
        Object values[] = new Object[2];
        values[0] = super.saveState(context);
        values[1] = label;
        return ((Object) (values));
    }

    @Override
    public void restoreState(FacesContext context, Object state) {
        Object values[] = (Object[])state;
        super.restoreState(context, values[0]);
        label = (String)values[1];
    }
    
	public FieldComponent (){
		this.setRendererType("arcmind.Field");
	}

	/**
	 * @return Returns the label.
	 */
	public String getLabel() {
		return label;
	}

	/**
	 * @param label
	 *  The label to set.
	 */
	public void setLabel(String label) {
		this.label = label;
	}

	
	@Override
	public String getFamily() {
		return "arcmind.Field";
	}


	public boolean isError() {
		return !this.isValid();
	}

}

可以注意到,代表片段中遗漏了编码方法。这是因为编码和解码发生在独立的渲染器中。我稍后会介绍它。

值绑定和组件属性

虽然 Label 组件只有一个属性(JSP 属性),可是 Field 组件却有多个属性,即 labelerrorStyleerrorStyleClassvaluelabelvalue 属性位于 Field 组件的核心,而 errorStyleerrorStyleClass 是特定于 HTML 的。因为这些属性是特定于 HTML 的,所以不需要让它们作为 Field 组件的属性;相反,只是把它们作为组件属性进行传递,只有渲染器知道这些属性。

像使用 Label 组件时一样,需要用定制标记把 Field 组件绑定到 JSP,如清单 7 所示:


清单 7. 为 FieldComponent 创建定制标记

/*
 * Created on Jul 19, 2004
 *
 */
package com.arcmind.jsfquickstart;

import javax.faces.application.Application;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;
import javax.faces.webapp.UIComponentTag;


/**
 * @author Richard Hightower
 *
 */
public class FieldTag extends UIComponentTag {

     private String label;
     private String errorStyleClass="";
     private String errorStyle="";
     private boolean required;
     private String value="";
     
     /**
      * @return Returns the label.
      */
     public String getLabel() {
          return label;
     }
     /**
      * @param label The label to set.
      */
     public void setLabel(String label) {
          this.label = label;
     }
     /**
      * @see javax.faces.webapp.UIComponentTag#setProperties
      * (javax.faces.component.UIComponent)
      */
     @Override
     protected void setProperties(UIComponent component) {
          /* You have to call the super class */
          super.setProperties(component);
          ((FieldComponent)component).setLabel(label);
          component.getAttributes().put("errorStyleClass",
            errorStyleClass);
          component.getAttributes().put("errorStyle",errorStyle);
          ((FieldComponent)component).setRequired(required);
     
     
         FacesContext context = FacesContext.getCurrentInstance();
         Application application = context.getApplication();
         ValueBinding binding = application.createValueBinding(value);
         component.setValueBinding("value", binding);
          
     }
     /**
      * @see javax.faces.webapp.UIComponentTag#getComponentType()
      */
     @Override
     public String getComponentType() {
          return "arcmind.Field";     
     }

     /**
      * @see javax.faces.webapp.UIComponentTag#getRendererType()
      */
     @Override
     public String getRendererType() {
          return "arcmind.Field";     
     }

     /**
      * @return Returns the errorStyleClass.
      */
     public String getErrorStyleClass() {
          return errorStyleClass;
     }
     /**
      * @param errorStyleClass The errorStyleClass to set.
      */
     public void setErrorStyleClass(String errorStyleClass) {
          this.errorStyleClass = errorStyleClass;
     }
     
     /**
      * @return Returns the errorStyle.
      */
     public String getErrorStyle() {
          return errorStyle;
     }
     /**
      * @param errorStyle The errorStyle to set.
      */
     public void setErrorStyle(String errorStyle) {
          this.errorStyle = errorStyle;
     }

     /**
      * @return Returns the required.
      */
     public boolean isRequired() {
          return required;
     }
     /**
      * @param required The required to set.
      */
     public void setRequired(boolean required) {
          this.required = required;
     }
     
     /**
      * @return Returns the value.
      */
     public String getValue() {
          return value;
     }
     /**
      * @param value The value to set.
      */
     public void setValue(String value) {
          this.value = value;
     }
}

从概念上说,在上面的代码和 Label 组件之间找不出太大区别。但是,在这个示例中,setProperties 方法有些不同:


protected void setProperties(UIComponent component) {
    /* You have to call the super class */
    super.setProperties(component);
    ((FieldComponent)component).setLabel(label);
    component.getAttributes().put("errorStyleClass", 
      errorStyleClass);
    component.getAttributes().put("errorStyle",errorStyle);

    ((FieldComponent)component).setRequired(required);

虽然 label 属性传递时的方式与前面的示例相同,但是 errorStyleClasserrorStyle 属性不是这样传递的。相反,它们被添加到 JSF 组件的属性映射 中。Renderer 类会使用属性映射去渲染类和样式属性。这个设置允许特定于 HTML 的代码从组件脱离。

这个修订后的 setProperties 方法实际的值绑定代码也有些不同,如下所示。


protected void setProperties(UIComponent component) {
      ...	
	
     FacesContext context = FacesContext.getCurrentInstance();
     Application application = context.getApplication();
     ValueBinding binding = application.createValueBinding(value);
     component.setValueBinding("value", binding);

这个代码允许 Field 组件的 value 属性绑定到后台 bean。出于示例的原因,我把 CDManagerBean 的 title 属性绑定到 Field 组件,像下面这样:value="#{CDManagerBean.title}。值绑定是用 Application 对象创建的。Application 对象是创建值绑定的工厂。这个组件拥有保存值绑定的特殊方法,即 setValueBinding;可以有不止一个值绑定。

独立渲染器

最后介绍渲染器,但并不是说它不重要。独立渲染器必须考虑的主要问题是解码(输入) 和编码(输出)。Field 组件做的编码比解码多得多,所以它的渲染器有许多编码方法,而只有一个解码方法。在清单 8 中,可以看到 Field 组件的渲染器:


清单 8. FieldRenderer 扩展自 Renderer

package com.arcmind.jsfquickstart;

import java.io.IOException;
import java.util.Iterator;
import java.util.Map;

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;
import javax.faces.el.ValueBinding;
import javax.faces.render.Renderer;

/**
 * @author Richard Hightower
 *
 */
public class FieldRenderer extends Renderer {


  @Override 
   public Object getConvertedValue(FacesContext facesContext, UIComponent component, 
     Object submittedValue) throws ConverterException {
        

    //Try to find out by value binding
    ValueBinding valueBinding = component.getValueBinding("value");
    if (valueBinding == null) return null;

    Class valueType = valueBinding.getType(facesContext);
    if (valueType == null) return null;

    if (String.class.equals(valueType)) return submittedValue;    
    if (Object.class.equals(valueType)) return submittedValue;    

    Converter converter = ((UIInput) component).getConverter();
    converter =  facesContext.getApplication().createConverter(valueType);
    if (converter != null ) {
        return converter.getAsObject(facesContext, component, (String) submittedValue);
    }else {
        return submittedValue; 
    }
		
    }

   @Override
    public void decode(FacesContext context, UIComponent component) {
        /* Grab the request map from the external context */
       Map requestMap = context.getExternalContext().getRequestParameterMap();
        /* Get client ID, use client ID to grab value from parameters */
       String clientId = component.getClientId(context);
       String value = (String) requestMap.get(clientId);
		
        FieldComponent fieldComponent = (FieldComponent)component;
          /* Set the submitted value */
        ((UIInput)component).setSubmittedValue(value);
    }
	
   @Override
    public void encodeBegin(FacesContext context, UIComponent component)
        throws IOException {
        FieldComponent fieldComponent = (FieldComponent) component;
        ResponseWriter writer = context.getResponseWriter();
        encodeLabel(writer,fieldComponent);
        encodeInput(writer,fieldComponent);
        encodeMessage(context, writer, fieldComponent);
        writer.flush();
    }

	
	
    private void encodeMessage(FacesContext context, ResponseWriter writer, 
      FieldComponent fieldComponent) throws IOException {
        Iterator iter = context.getMessages(fieldComponent.getClientId(context));
        while (iter.hasNext()){
           FacesMessage message = (FacesMessage) iter.next();
           writer.write(message.getDetail());
        }
    }

    private void encodeLabel(ResponseWriter writer, FieldComponent 
      fieldComponent) throws IOException{
        writer.startElement("label", fieldComponent);
        if (fieldComponent.isError()) {
            String errorStyleClass = (String) fieldComponent.getAttributes().get("errorStyleClass");
            String errorStyle = (String) fieldComponent.getAttributes().get("errorStyle");

            writer.writeAttribute("style", errorStyle, "style");
            writer.writeAttribute("class", errorStyleClass, "class");
        }
        writer.write("" + fieldComponent.getLabel());
        if (fieldComponent.isRequired()) {
            writer.write("*");
        }
       writer.endElement("label");
    }
	
    private void encodeInput(ResponseWriter writer, FieldComponent 
      fieldComponent) throws IOException{
        FacesContext currentInstance = FacesContext.getCurrentInstance();
        writer.startElement("input", fieldComponent);
        writer.writeAttribute("type", "text", "type");
        writer.writeAttribute("id", fieldComponent.getClientId(currentInstance), "id");
		writer.writeAttribute("name", fieldComponent.getClientId(currentInstance), "name");
        if(fieldComponent.getValue()!=null)
            writer.writeAttribute("value", fieldComponent.getValue().toString(), "value");
        writer.endElement("input");
    }

}

编码和解码

正如前面提到的,渲染器做的主要工作就是解码输入和编码输出。我先从解码开始,因为它是最容易的。 FieldRenderer 的 decode 方法如下所示:


@Override
public void decode(FacesContext context, UIComponent component) {
       /* Grab the request map from the external context */
     Map requestMap = context.getExternalContext().getRequestParameterMap();
       /* Get client ID, use client ID to grab value from parameters */
     String clientId = component.getClientId(context);
     String value = (String) requestMap.get(clientId);
		
     FieldComponent fieldComponent = (FieldComponent)component;
       /* Set the submitted value */
     ((UIInput)component).setSubmittedValue(value);
}

Label 组件不需要进行解码,因为它是一个 UIOutput 组件。Field 组件是一个 UIInput 组件,这意味着它接受输入,所以 必须 进行解码。decode 方法可以从会话、cookie、头、请求等处读取值。在大多数请问下,decode 方法只是像上面那样从请求参数读取值。Field 渲染器的 decode 方法从组件得到 clientId,以标识要查找的请求参数。给定组件容器的路径,clientId 被计算成为组件的全限定名称。而且,因为示例组件在表单中(是个容器),所以它的 clientid 应当是 nameOfForm:nameOfComponent 这样的,或者是示例中的 cdForm:artist、cdForm:price、cdForm:title。decode 方法的最后一步是把提交的值保存到组件(稍后会转换并验证它,请参阅 参考资料 获取更多关于验证和转换的内容)。

编码方法没什么惊讶的。它们与 Label 组件中看到的类似。第一个方法 encodeBegin,委托给三个帮助器方法 encodeLabelencodeInputencodeMessage,如下所示:


@Override
public void encodeBegin(FacesContext context, UIComponent component)
       throws IOException {
     FieldComponent fieldComponent = (FieldComponent) component;
     ResponseWriter writer = context.getResponseWriter();
     encodeLabel(writer,fieldComponent);
     encodeInput(writer,fieldComponent);
     encodeMessage(context, writer, fieldComponent);
     writer.flush();
}

encodeLabel 方法负责在出错的时候,把标签的颜色改成红色(或者在样式表中指定的其他什么颜色),并用星号 (*) 标出必需的字段,如下所示:


private void encodeLabel(ResponseWriter writer, FieldComponent fieldComponent) throws IOException{
     writer.startElement("label", fieldComponent);
     if (fieldComponent.isError()) {
          String errorStyleClass = (String) fieldComponent.getAttributes().get("errorStyleClass");
          String errorStyle = (String) fieldComponent.getAttributes().get("errorStyle");

          writer.writeAttribute("style", errorStyle, "style");
          writer.writeAttribute("class", errorStyleClass, "class");
     }
     writer.write("" + fieldComponent.getLabel());
     if (fieldComponent.isRequired()) {
          writer.write("*");
     }
     writer.endElement("label");
}

首先,encodeLabel 方法检查是否有错误,如果有就输出 errorStyleerrorStyleClass(更好的版本是只有在它们不为空的时候才输出 —— 但是我把它留给您做练习!)。然后帮助器方法会检查组件是不是必需的字段,如果是,就输出星号。encodeMessagesencodeInput 方法做的就是这件事,即输出出错消息并为 Field 组件生成 HTML 输入的文本字段。

注意,神秘方法!

您可能已经注意到,有一个方法我还没有介绍。这个方法就是这个类中的“黑马”方法。如果您阅读 Renderer(所有渲染器都要扩展的抽象类)的 javadoc,您可能会感觉到这样的方法是不需要的,现有的就足够了:这就是我最开始时想的。但是,您和我一样,都错了!

实际上,基类 Renderer 并不 自动调用 Renderer 子类的相关转换器 —— 即使 Renderer 的 javadoc 和 JSF 规范建议它这样做,它也没做。MyFaces 和 JSF RI 拥有为它们的渲染器执行这个魔术的类(特定于它们的实现),但是在核心 JSF API 中并没有涉及这项功能。

相反,需要使用方法 getConvertedValues 锁定相关的转换器并调用它。清单 9 显示的方法根据值绑定的类型找到正确的转换器:


清单 9. getConvertedValues 方法

@Override
 public Object getConvertedValue(FacesContext facesContext, 
   UIComponent component, Object submittedValue) throws ConverterException {
        
     //Try to find out by value binding
     ValueBinding valueBinding = component.getValueBinding("value");
     if (valueBinding == null) return null;

     Class valueType = valueBinding.getType(facesContext);
     if (valueType == null) return null;

     if (String.class.equals(valueType)) return submittedValue;    
     if (Object.class.equals(valueType)) return submittedValue;    

     Converter converter = ((UIInput) component).getConverter();
     converter =  facesContext.getApplication().createConverter(valueType);
     if (converter != null ) {
          return converter.getAsObject(facesContext, component, (String) submittedValue);
     }else {
          return submittedValue; 
     }
		
}

清单 9 的代码添加了 Render javadoc 和 JSF 规范都让您相信应当是自动执行的功能,而实际上并不是。另一方面,请注意如果没有 独立的 Renderer,就不需要 以上(getConvertedValues)方法。UIComponentBase 类(Field 组件的超类)在直接渲染器的情况下提供了这个功能。请接受我的建议,只在特别想尝试或者在编写商业框架的时候,才考虑采用渲染器。在其他情况下,它们不值得额外的付出。

如果想知道如何把组件和渲染器关联,那么只要看看图 6 即可。


图 6. 把渲染器映射到组件
把渲染器映射到组件

定制标记有两个方法,分别返回组件类型和渲染器类型。这些方法用于查找配置在 faces-config.xml 中的正确的渲染器和组件。请注意(虽然图中没有)组件必须返回正确的 family 类型。



回页首


结束语

通过这些内容,您已经切实地了解了 JSF 组件开发的核心。当然,在这个领域还有许多其他主题需要涉及 —— 包括发出组件事件、国际化组件、创建 UICommand 样式的组件,以及更多。请参阅 参考资料 获得 JSF 的阅读列表!

在编写这篇文章的过程中,我遇到了 Renderer 的一个技术障碍,它使我发现了 getConvertedValues 方法的工作方式。尽管我以前遇到过 Converter 问题并处理过它,但是那时我是在一个紧张的(生产)日程中做这件事的。在生产工作中进行的研究,不必像在 how-to 文章中做得那么详细;所以这一次,我必须不仅学习如何修补问题,还要学习弄清如何 做对。通过这整个过程,我最终在非常深的层次上学会并体验了 JSF 组件处理工作的方式。所以,有时绕点弯路会看到优美的风景。

我希望在这个由四部分组成的系列中,您已经学到了关于使用 JSF 的优势的充足知识,以及它如何工作的基础知识,还希望您会喜欢进一步深入这项技术。而且当您有时可能迷失方向的时候,请不要陷入 FUD。相反,请记住我说过的:弯路会看到优美的风景,请继续前行。

posted @ 2006-01-10 11:26 lbom 阅读(573) | 评论 (0)编辑 收藏