可爱的javaee:非框架架构漫谈(控制器篇)

概述

你可以说可爱的php,可爱的ror,可爱的python,甚至可爱的.net,但是javaee?他太复杂了。相比其他两种技术,javaee的技术体系更全面、更规整也更复杂,他的复杂性也让很多厂商望而止步,宁可选择简单甚至简陋的php,这充分说明快速开发是这个时代最迫切的需求。

javaee的servlet、javabean、jdbc规范给了我们组件和容器的唯一标准,而更高级的支持,jsf、jdo规范却没能给予我们唯一的框架级标准,他们被认可的程度远低于相同领域的开源框架。尽管开源社区给了我们最丰富的选择,但是相比.net、php、ror的全栈式服务,javaee开发者必须DIY。DIY不但需要时间而且需要冒险,这种发烧友做的事情是企业所不愿意做的。一段时间以来,公司javaee方向的招聘几乎清一色的要求struts、spring、hibernate这几种主流框架的能力就是一种证明。

javaee的开发往往避免不了配置之旅,尽管很多框架都有自动生成工具,但是,面对一个中型项目,仍然容易造成混乱。配置使你无论在开发、测试、集成还是维护的时都要配置代码两头看。配置给了框架一个注入服务的切入点,但是对人并无优雅可言。ror给了我们启发,尽管企业开发是复杂的,但是大多数的需求都是通用的,事实证明,ror把这部分通用性用约定的方式抽象得很好。其实javaee并不缺乏约定,因为他本身就是建立于一系列规范的基础之上,而规范就是约定。所以,javaee实际上有机会成为相对简洁的开发技术,只不过由于种种原因,这种规范并未出现。

在众多的javaee开发框架中,struts+spring+hibernate有着黄金组合的美誉,用的人多,会的人多,就算是没出校门的学生,也知道学会ssh的重要性。但是学会和学懂是两码事,对于一个中型项目,ssh就成了一柄双刃剑,需要由高水平的设计师引路,才能披荆斩棘。spring+hibernate给了设计者广阔的空间,而设计需要因项目的前进而演进,如果你的项目进度紧张,人手不足,设计质量就难以保障,给系统带来隐患。

“任何优秀的语言,都可以帮助开发者写出优秀的代码,但不能阻止开发者写出糟糕的代码”。在这一点上,无论是javaee,.net,ror,php都不会例外。而开发框架就像是“一间有很多屋梁的房子”,“框架的强大之处不是他能让你做什么,而是他不能让你做什么”,其实如同语言一样,框架虽然可以给予开发一定程度的规范指导,但是这种指导仍然是有限的,这真应了那句老话:事在人为。

本文试图探讨如何简化javaee开发中不必要的复杂,并给出的是一个不使用任何框架的架构模型,让我们看看仅仅通过用编码约定,结构设计和使用方式的组合能不能满足项目开发的主要需求—短期培训,降低隐患和快速开发。

问题的源头

应用软件开发是复杂的,但其基本模型至为简单,请求-处理-响应。对应于软件的层次结构就是:请求-Cortrol(C);处理-Model(M);响应-View(V)。在早期的javaee应用中,servlet负责C,javabean和jdbc在M,jsp是V。这些就是javaee的基础设施,他们职责划分的方式被称为JSP Model2,已经可以满足web开发的基本需要,javaee的开发始终都围绕着这几项主要技术,框架也不例外。以下的内容,将从这些技术的应用与不足说起,然后介绍主流框架的解决方案,之后再介绍我们不用框架的处理方式。

(C)选择控制器

基础规范的不足

任何web应用,处理请求之后返回响应是必须的环节,如果编码规范,传统的响应就是转向到某个页面,servlet处理转向有两种方式,其中request转向隐藏着重复提交的问题,response重定向带来参数传递的编码解码的问题,同时众多的转向地址直接写在servlet中也十分不雅,另外,jsp和javabean有一种出色的关联技术,就是在jsp里可以把来自请求表单的数据自动拼装到javabean中。糟糕的是,这么有用的技术却无法在servlet中使用,所以Model2缺乏对表单数据的自动转换处理。servlet有这些不足很好理解,因为servlet毕竟是较早出现的技术,他的职责只是将(http)请求转化为面向对象的视图和输出响应而已,由于他是低阶组件,所以部分功能的缺失是正常的。不过这就让servlet成为了Model2最薄弱的一环。
开发框架的解决方案

由于以上需求是共性的,所以编写一个通用框架就成为了很多人努力的事情,struts很快推出并且很快流行。我们先来看一看struts的特性:

前端控制器:struts使用一个servlet作为前端控制器,所有请求先经过这里,再分派给配置指定的action(这里是指行为,而不是具体的Action),意图是以一个中间层将视图层和控制层解耦,这种思路带来了三种可能的好处:1 视图和控制分离,所以可以选择不同的视图技术,比如视图模板既可以用jsp,也可以用Volecity、FreeMarker;2 可以对所有请求预处理,和后处理(webwork);3 可以将响应的转向地址管理起来。前端控制器也存在一种直接的不足:配置繁琐。

ActionForm:struts主要是一个控制层框架,所以他并不意图深入到模型层,ActionForm是一种无奈的选择,尽管提供了表单数据到javabean的转换,但是遗憾的是这个javabean并不能直接使用,还要手工的转换为模型javabean,使得ActionForm的位置有些尴尬。

国际化支持、标签库和全局异常:国际化和标签库都是struts的亮点,不过全局异常作用有限。

我们的选择

javaee的控制器必然是一个servlet,我们也不能例外,因为我们必须要运行在servlet容器之中。不过,我们选择的是servlet的演进版本-jsp。别误会,我们并不是要退回到JSP Model1。一个典型的示例是,如果我有一个员工信息录入的功能点,那么为了实现这个功能,我可以建立下面两个文件:
worker_input.jsp
worker_inputOper.jsp
worker_input.jsp里不写控制代码,worker_inuptOper.jsp里也不写视图代码,这种用法实际是JSP Model1和JSP Model2的综合体。这样做最大的好处就是,免去配置的烦恼,但是等等..前端控制器呢?我们的中间层呢?

考虑一下,你有一个企业信息的表单,表单中有一个企业名称域,对这个域的要求是不能在已有企业中重名,域旁边有一个按钮,业务员可以通过点击这个按钮获得录入企业名称是否重复的提示。如果是传统方式,点击按钮将导致一个页面提交,如果用struts,将要配置这个action处理之后转向的URL地址,这就是传统web应用的响应方式-基于URL地址的页面导航。

web2.0来了,ajax来了,异步请求的出现彻底颠覆了传统的web交互模型。对于ajax应用而言,服务器端返回响应只需要out.print,请求从哪来,回哪去,转向(如果需要)和更新视图的任务都交给了客户端脚本,也就是说,基于异步交互模式的web应用,根本就没有需要配置的result URL路径。这样,页面导航的问题就自动解决了。而对于预处理,我们可以用filter替代。所以,我们完全可以和前端控制器说:再见。

由于客户端技术的趋势,在webappdemo中我们将全面使用ajax。也许你会说,如果客户端浏览器禁用脚本呢?这个担心在如今已经没有必要,你可以访问开心或者当当,看看禁用脚本他们能不能工作。时代在进步,富客户RIA是必然的选择。

使用jsp作为控制器,还使我们得到了另一个关键的特性,那就是从form表单数据到javabean的自动创建和输入,使javabean本身既是模型也是DTO,再也不必象ActionForm那样还要手工转换。这里还有一个隐含的好处,就是强制统一了表单域名和模型属性名,不然,有可能出现这样的情况:表单域:child_center;模型属性:branch。以下是worker_inputOper.jsp的写法:


// 在此输入java代码
<jsp:useBean id=
"worker" class="webappdemo.worker.entity.Worker" scope="page"/>
<jsp:setProperty name=
"worker" property="*"/>
<%
response.setContentType(
"text/x-json;charset=UTF-8");
response.setHeader(
"Cache-Control", "no-cache");

String method = request.getParameter(
"method");

if(
"save".equals(method)){

EntityService es = new EntityService();
Message m = es.add(worker);
out.print(new JSONObject().put(m.isSucceed()?
"succeed":"error", m.getMessage()));

return;
}
%>

可以看出,只需将实体类名引入标签,我们就可以获得自动拼装的Worker对象。对于复杂对象或复合对象,由于request里同样有我们需要的所有请求参数,所以你可以在自动创建的javabean基础上修改部分属性,以符合业务需要。

代码还展示了基于“method”的用法,这只是一个字符串,用来告诉oper jsp要用哪个方法来处理请求,这类似于ror控制器内部定义的方法以及struts的DispatchAction但比他更灵活,变通的解决了jsp的请求不能直接面向方法的不足。

在调用服务处理请求之后,worker_inputOper.jsp将处理结果out.print回客户端,这句代码的意思是新建一个JSON对象,将处理结果添加进去,然后输出这个对象,方便客户端js脚本解析。JSON对象可以增加多个处理结果,只要他们的key不同就可以。在实际应用中,往往返回处理消息,或者html视图的字符串。最后别忘了return;否则程序仍然会向下进行。

如果你的项目需要国际化,我们可以使用fmt标签,而对于反馈消息的国际化,我们也许就需要建立一个全局MessageSource对象了,这个问题在webappdemo中没有涉及,因为笔者认为这不是普遍需求。
对于异常处理,其实jsp已经提供了简单的机制,我们可以在web.xml中配置:

// 在此输入java代码
<error-page>
<error-code>404</error-code>
<location>/404.jsp</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/500.jsp</location>
</error-page>

这种简单的处理其实正是我们需要的全部,因为笔者认为,web应用的系统错误和java的异常没有区别,即检测错误和运行时错误。在web2.0时代,所有的错误都应该被捕获,并且把内容经处理后在用户输入位置反馈给用户,而不应该重新定向。运行时错误属于系统bug,是需要修复的代码级错误,这种错误是真正的“意外”,所以我们用定制的错误页面反馈给用户就可以了。

综上所述,我们用ajax+jsp+控制jsp的方式代替了servlet或者action,摆脱了前端控制器,用模型javabean代替了过程javabean ActionForm,这些使用方式使我们不需要配置即可以开发应用程序,除了ajax是相对新概念外不需要额外学习框架技术也是他的优点。
[该贴被javaonejcy于2009-10-28 17:44修改过]

呵呵~提供了一种新思路

使用jsp作为控制器是可以的,只要你不在这个JSP中加入显示的代码,完全是java代码就可以。

如果加入显示代码,那么这个JSP职责就不单一,控制和显示都有,不好。
如果不加入显示代码,都是控制性质的代码,和Servlet就没有区别了,如果觉得Servlet配置麻烦则是另外一个原因。

不过你的嫌弃MVC中控制器思维和我以前也很类似的,所以,在Jdon框架中就是使用几个配置来替代控制器,无需控制器编码。

再进一步,结合DDD + REST,我们可以直接将MVC中的模型作为资源,直接暴露给客户端,客户端通过GET/PUT/POST/DELETE统一方式对模型操作,这个新架构下,也没有控制器,这个我最看好。

感谢banq的回复,直接使用jsp确实是一种无奈的选择,有时间研读一下jdon框架。对客户端暴露服务应该是一种方向,WCF似乎也是这样。

>对客户端暴露服务
这个SOA已经做到,我想说的是,对客户端直接暴露领域模型,而不是服务,将模型作为REST架构的资源暴露,这样DDD和REST完美结合。没有控制器,有的只是事件模式。

从模型角度看,MVC中控制器属于GoF的Mediator模式,Mediator模式和观察者模式区别是,前者封装了通讯,后者暴露通讯,在解耦要求进一步的提高下,暴露通讯更好,更加异步,更加可伸缩。

所以,去除控制器,就是抛弃Mediator模式,使用观察者模式等事件模式特征明显的模式。

通过AJAX在客户端发出事件,服务器端领域模型直接响应事件,然后驱动其他技术架构,包括领域服务,持久化等完成任务,这应该是未来比较好的一个架构。

多谢指教,REST架构只是听说但还没有深入了解过,好,开始补课。

REST应该是传统MVC和SOA的颠覆者。