这是我年初写的一篇文章,当时用webwork+Spring+Hibernate开发一个web应用,一种很流行的J2EE开发模式,在开发了两周后,对Web开发的一个总结,我觉得对新手还是有一定帮助的。很模糊的记得在Javaeye有人写过类似的文章,在此表示感谢了。
发布到这儿,因为觉得Javaeye的blog比其它的好用,以后就在这儿安家啦。
作为一个表示层框架,无论是Struts、Webwork,还是SpringMVC、JSF,它们都是解决同一类问题。
表示层框架,从分层的角度考虑,它的核心是作为一个Front Controller,在Struts里面是ActionSerlvet+RequestProcessor+Action,前两个是框架本身的,只有 Action需要我们处理。它的职责也就是协调用户的输入,将请求dispatch给后端business,由后端business系统处理后,将相应的结果返回给Controller,再由Controller处理返回给用户。
对于开发来说,特别需要注意的是职责分配的问题,这是我们开发过程中最容易犯的毛病,也是最需要注意的地方。Front Controller只是一个Controller,也就是Call别的接口,自己本身不应该有处理业务的职责,它应该尽量的thin。如果不遵守这个原则呢?那带来的将是系统难以测试,难以重用,难以维护。
具体来说,我们的业务系统是可以重用的,可以提供Service接口给Web层调用,也可以提供给其它系统通过Web Services接口调用,还可以给分布式client调用。它们的调用方式,一般有三种:
最普通的调用。在web系统中,就是表示层Controller直接调用服务层。
依赖注入式调用(Dependency Injection)。它需要IoC容器支持,如Spring,picoContainer,它对解耦和测试非常有用
依赖查找式调用(Dependency Lookup)。也就是EJB里面的JNDI,它最大的好处是支持分布式开发和组件开发。
我们一定要牢牢记住表示层的职责。表示层框架很容易变,但系统业务还是相对固定,如果你接触过物流系统,你就会明白什么叫业务了,那时候在表示层里处理业务会非常可怕,因为我的业务要同时支持几种客户端:桌面client、Brower、移动client,难道你需要在每种client里面写业务吗?而且,表示层框架的开发人员可以不需要懂太多的业务就可以利用自己的技术优势,来调用业务相关接口。
下面我再具体谈谈表示层需要处理的几个部分:
整体说明
举个例子,我现在想对updateUser.do?userId=19做出请求,然后在一个UpdateUserAction里处理用户预更新,然后将结果导向到updateUser.jsp页面。这大概是我们所希望的最简单的处理方式吧。
上面那三个元素的相互关系在web框架的配置文件里面定义。现在的framework都倾向于config和metadata oriented,因为这样的框架会比较灵活、好维护,试试直接用JSP和Serlvet开发你就知道要做多少重复工作和hard code了。
在webwork里面,是xwork..xml,在Struts里面,是struts-config.xml,在JSF里面,是faces- config.xml。所以说,框架都是相通的。我这里需要特别指出的是,上面那些文件和文件名都是默认的,实际上,为了支持组件开发和团队协作开发,保持开发模块之间的松耦合,我们应该将配置文件分解成一个个的segment。
对于action,也就是前面提到的Front Controller。 在Struts里面,是通过继承基类的Action,在webwork,是通过实现Action接口。
在Struts里面,通过继承方式来实现Action,那么我们代码是和框架紧密耦合的。而且,只支持JSP,不支持模板语言,如Velocity、 FreeMaker,而这些可以脱离Servlet容器,最大的好处是,我们的网页设计人员可以专心做html界面。分工就意味着效率和质量。但 Struts这种框架就支持不够。
而webwork就很好地做到了这种解耦,action和前端表示完全分离,如表示页面还可以直接发布成pdf文档。
那么,继承基类和实现接口,对于我们开发人员意味着什么呢?灵活性、容易测试,这让我开发更容易一些。
Scenario 1 我们一些简单的CRUD操作,完全可以在一个action里面处理。虽然Struts也可以这样,因为它有一个DispatchAction,但你会发现,项目中,我们经常引入一个BaseAction,而这个BaseAction是继承DispatchAction还是Action,往往很难决择,因为我们这两种需要都有,虽然DispathAction也是继承Action。而webwork的处理就方便多了,你需要验证、国际化、安全等等很多操作,你都可以通过实现一个接口实现。
Scenario 2 我们测试Action的时候,总是要启动容器,启动一般要花将近半分钟,这会让我们很多时间都花在等待之中,由于Action和容器脱离,我们的测试会更迅速和直接。
我现在就按照整个页面流程来分类说明,在一个web框架下开发,或是开发web应用,或者开发Web Framework必须考虑到的各个方面:
1. 表单提取
我们开发web MIS系统,从技术角度考虑,第一步就是在页面输入数据,然后将它们保存到后端数据库。
框架应该自动提取表单数据,将这些String类型的数据转换成对应的数据类型,并封装为值对象(Value Object),提供给Controller,作为Controller的输入。
Struts是通过ActionForm,非常笨拙,而且对类型转换都支持不够。Webwork在这方面做得优雅多了,它将请求参数封装为一个Map对象。其实,请求本来就是key-value对。然后当数据传入到action后,就和Serlvet容器,web脱离了关系,这带来的解耦的好处就是还可以支持多种客户端,支持容器外测试。
具体实现上,Struts通过BeanUtil工具对ActionForm进行反射;webwork是通过一个拦截器Parameters Interceptor来获取,而action实现该接口,然后在配置文件里申明一下就ok了。
2. 表单验证
我们输入数据,一般需要符合一定的规范,如必须输入数字,不能为空,必须为邮政编码等等。
解决这个问题,如果不用framework,我们需要在jsp页面上用javascript验证,还要在servlet里面获取参数,然后验证,对于不合理的数据,需要将之保存,并附上错误说明,然后forword到原来jsp页面显示处理,非常烦琐和机械。
Struts框架原来处理还是挺机械的,主要在ActionForm的validate方法里验证,后来加入一个Validation框架,也就是一个plug-in,非常好用。Webwork也沿用了这一点,两者在验证上都不错。
但我们必须注意的一个问题,那就是验证不通过时,怎么保持原来的数据,特别是下拉框里面的数据,Struts处理很麻烦(难道我没用好?), webwork好用多了,不过有个tips,那就是将select的下拉列表保存在session里,不用每次验证不通过都重新从数据库加载。
另外还有一点,那就是和表单重复提交结合时会出现的问题,struts中,处理不当,就会出现在你验证不通过,修改后再次提交时总是被提示重复提交。 Webwork是通过一个Interceptor截获,处理很自然。不过我建议时提交通过后,用redirect,这样就不存在重复提交的缺陷。你总不希望因为网络慢,你的订单由于刷新而提交两遍,收到两份相通的货物吧。
3. 国际化和resource文件
Web界面是给用户看的。譬如,我们公司内网打算采用一套很知名的blog系统,但它是德国人开发的,但是你总不会希望看到界面是德文吧。这套blog开发商为了全球推广,也不至于为每种语言都开发一套吧,那不累死,维护简直就是恶梦。
我之所以现在要提到国际化,不光是国际化的问题,因为在表单验证时,如果field不通过,都会提示用户,但这些提示语一般都会重用,如create和 update用户时“用户名不能为空!”,我们可以在resource文件里定义一个key: error.user.username.required,然后在各处引用。
Webwork和Struts对这个都处理比较好,这也是一个表示层框架必须处理的。我建议不要在Action-validation.xml文件里hard code这些信息,而是将它们分离出来。
4. 请求路由
当我们将user成功提交后,我们让它由谁来处理呢?这就是请求路由的问题。
这种关系在配置文件里面定义,我上文都提到过。为了方便,往往将多个请求提交到一个action的不同方法,这是我们应该好好利用的。而它们在 webwork里面都可以很好配置。例如,将saveCreatedUser 路由到UserAction的saveCreatedUser方法里。
5. 请求处理
当请求发生,并且路由到正确的action后,我们的action就要处理它了,包括初始化,处理,清场。
关于Action需要注意的问题,我现在还要重申的是:一定要注意它的职责:不要处理它不应该处理的事情。
例如,注册用户时,用户姓名可以重复,但用户登录账号却不能重复。论坛发帖子就没有这样要求。再例如,会员系统中,用户积分达到100就是可以下载,但什么时候规则变了,需要积分150才能。这些规则在哪儿处理呢?最简单当然是在Action里:没有满足条件,就forward到输入页面。当规则很多时,你就很难维护了。反问一下,它是web层关心的问题吗?
在任何框架设计,包括web框架设计时,一般都有一个非常核心的模式,那就是template模式,它和一种称为callback的调用方式相关,也就是那个著名的好莱坞原则:don’t call me, I’ll call you! 具体到我们的web框架,也就是action,如webwork里面的ActionSupport,它们都是这种处理方式,我们不知道我们的Action 是被谁调用的(当然是框架了),但我们这样写,然后在xml一配置,就work well。
我们可以研究一下Struts的ActionSerlvet,特别是ReqeustProcessor,后者就是一个典型的template模式, action是作为里面一个command运行的。Command模式也是用callback调用。关于这两种模式,google一下就清楚了。
Spring就是使用Template和callback模式,对底层实现做了统一封装,其中template负责那些通用的功能,如事务、资源管理、异常处理,callback则完成变化的那部分,如处理具体的数据库statement操作。我们最常用的HibernateCallback和 HibernateDaoSupport就体现了这一点,具体可参照其源码。
另外,对于Action,一定要注意线程安全的问题。Struts的action是非线程安全的,大家可以参考一个RequestProcessor中定义的Map类型的actions字段,而Webwork是线程安全的,因为其ActionContext是 ThreadLocal的,也就是说每个线程都有自己的变量,互不干扰。具体体现是Struts的Action里面不能有字段申明,只能有局部变量。而 Webwork可以,这对于获取页面表单数据非常方便。
6. 请求应答
在Action执行完毕后,我们应该为其导航到结果页面,
在webwork里面,也就是Result Type。它定义了好几种方式。比较有意思的是它处理象freemaker(html模板),XML/XSLT,PDF这些格式输出。而Struts基本上是很单一JSP。
而且webwork还可以自定义Result类型,可以为我们提供Swing、mobile客户端显示。
7. 异常处理
任何操作都有出错的可能,怎么优雅地处理错误,这也是每个框架都应该考虑的。
有一些异常处理最佳实践,《Effective Java》就专门有一章谈到。在OnJava上也有一篇文章:http://www.onjava.com/pub/a/onjava/2003/11/19/exceptions.html,非常值得一读。
1.选择Checked还是Unchecked
2.Exception的封装
3.如无必要不要创建自己得Exception
4.不要用Exception来作流程控制
5.不要轻易的忽略捕获的Exception
6.不要简单地捕获顶层的Exception
Struts和Webwork里面都支持异常处理,webwork是通过Interceptor支持的:ExceptionMappingInterceptor。
在我的项目实践中,我喜欢将业务异常封装在业务层,申明为checked Exception,web的Action必须捕获,获取exception的ErrorCode,此ErrorCode就是resource文件里面的 key,也就是说它支持多语言,如ErrorCode.USER_EXISTED=”error.user.userExisted”。这类信息应该是业务层抛出的。如果将它由action处理,这是一种设计上的错误。
8. 灵活性、扩展性、可插入性
作为框架,就应该保证其灵活,支持不同用户的业务需求;容易扩展其功能,插入系统级的功能。
就Struts而言,在可插入性上还是不错的,这得益于它的plug-in支持,实现其PlugIn接口,然后在其struts-config.xml文件中配置一下,如它的验证框架、网页布局Tiles,内存数据库,都是其典型应用。
另外,以前的Struts还可以扩展它的RequestProcessor,实现一些特有的功能,如日志、安全,因为它预留了一个缺省方法processPreprocess()。
Webwork也是高度可扩展的,它主要是通过实现Interceptor接口。
9. 模块、组件化
在大型web项目中,模块、组件这些概念就非常适用了,因为一个大的工程,可能由不同公司开发,或是不同项目组、不同成员协作开发,最后要很容易地集成在一起。另外,还有第三方公司开发的组件插入进来,如漂亮的web界面组件,如Tree、table等。
Tapestry就是一种基于组件的开发框架,不过我没有研究,呵呵。
但webwork确实可以支持,如xwork.xml 配置的package就是支持模块申明,其附带的FreeMaker就可以将开发好的模块打包成jar文件,在Servlet容器外加载。
JSF对组件支持是相当到位的。它所设计的目标,就是在IDE里面,直接拖拽web组件,象table,tree,就可以形成漂亮的界面,真正实现所见即所得。实现自己的组件,只要实现其UIComponent接口就行了。应该说JSF是一种非常有前途的Web层框架,它会把Web开发真正达到easy。因为直到现在,还没有一种框架能够很容易做到,它的界面开发始终没法和微软相提并论,虽然它有界面布局框架Tiles和SiteMesh,以及模板语言 Velocity、FreeMaker、Servlet2.4支持的EL。
不过遗憾的是,JSF开发工具还没有达到那么完善,除了Sun自己一直、推广的NetBeans IDE。Sun的官方有其demo,用flash演示,还是蛮cool的。
关于Ajax和请求模式
Ajax其实早在五年前就有了,只是没有被人注意罢了,直到2005年google把它应用在google map和gmail上,才开始被人关注。它也是web2.0时代一种重要的技术,ajax强调用户体验(XP)。因为它的本质就是一个异步调用。而我们的一般web页面请求都是同步的,你必须等到服务器全部处理完成。同步变异步,确实是一种革命,虽然它的技术很简单,只是一个XMLHttpRequest 的javascript对象。它让我们的Web版的Rich Client成为可能。Google现在正看到了web office的应用前景,对MS构成巨大威胁,大家可以试试它的Web版的excel和、日历、邮件,不久就会推出正式的web word,很cool。
另外,Ajax更接近实际的web请求,因为一般web请求时,每一次,我们获得的是整个页面内容,无论这个页面有多少数据是冗余的。譬如,我们分页浏览时,整个页面布局是由header,left navigator、footer组成,中间是content,翻页时,应该从服务器返回一个xml的用户列表数据格式,而且不包括html table的tag,这正是ajax另外一个方面:返回的是数据,而不是整个页面。这对于窄带宽,也是非常有益的。
Webwork的2.2.2版本支持ajax,底层是dojo和DWR这两个著名的ajax框架。但是webwork的ajax Tag做得太粗糙,用了ajax后,页面布局,特别是表单位置没法控制,而且对IE浏览器支持不够。我现在几乎都放弃了。
没有好的ajax框架,使得ajax开发起来很困难,也许因为它还不到一周岁吧。
我们现在的web框架,一般都是基于请求的模式,也就是说我们请求时总是会考虑它的目的URL,将提交数据保存在该请求中。这种模式,让我们开发时总在考虑这些所谓的http协议。一种完善的框架,应该尽量去屏蔽这些与底层协议相关的东西。
JSF给我们一种全新的方式,它让我们按照操作桌面应用,如Swing程序的方式处理网络请求,因为它是基于事件的。在Sun的petstore的WAF 框架中就有这种原始的思想。基于事件,让我们开发rich client提供了一种思想,但是,感觉和MS的asp.net相比,还是差很远。
事件驱动有一种核心的模式,那就是Observer模式,所谓的Observer就是listener(只是一个用眼睛,一个用耳朵,呵呵)。 Oberver是一种推(push)模式,也就是说事件Event发生时(如model发生改变),它会通知你,而不是你去取(拉模式)。
在Servlet规范中,也引入了listener的概念,如HttpSessionListener,它对于监视用户上线下线非常有帮助,它们是生命周期lifecycle的范畴。