2009年6月16日

Java Web Framework综述

Java Web Framework综述
0.简介
本文介绍Java Web Framework的基本工作原理,和一些常用的开源Web MVC Framework(Struts, Web Work, Tapestry, Echo, JSF, Maverick, Spring MVC, Turbine, Cocoon, Barracuda)。

Web开发的最重要的基本功是HTTP;Java Web开发的最重要的基本功是Servlet Specification。HTTP和Servlet Specification对于Web Server和Web Framework的开发实现来说,是至关重要的协议规范。

应用和剖析开源Web Framework,既有助于深入掌握HTTP & Servlet Specification, 也有助于了解一些现代的B/S Web框架设计思想,如MVC,事件处理机制,页面组件,IoC,AOP等。在这个现代化的大潮中,即使Servlet规范本身也不能免俗,不断引入Filter、Listener等现代框架设计模式。同是Sun公司出品的JSF更是如此。

关于MVC模型、项目简介、配置文件、入门示例等基础知识,网上已经有大量的重复资料信息,本文不再赘述。

文中会提到一些相关的开源项目,和一些编程思想,如有需要,可以用相关的关键字在网上搜索,获取基本的背景知识。

本文力图言简意赅,突出重点。着重描述其他资料没有提到、或很少提到的较重要内容,如运行原理、主流用法,相关知识,关键特性等。

1. Java Web程序工作原理
Tomcat的Server.xml文件中定义了网络请求路径到主机本地文件路径的映射。比如,<context path="/yourapp" docBase="yourapp_dir/webapp"/>

 

我们来看一下,一个HTTP Request-Response Cycle的处理过程。

HTTP Request URL一般分为三段:host, context, path。

http://yourhost/yourapp/en/index.html这个URL,分为host=yourhost, context=yourapp, path=en/index.html三段。其中,Context部分由request.getContext()获得,path部分由request.getServletPath()获得(返回结果是“/en/index.html”)。

yourhost主机上运行的Tomcat Web Server接收到这个URL,根据Context定义,把yourapp这个网络路径映射为yourapp_dir/webapp,并在此目录下定位en/index.html这个文件,返回到客户端。

 

如果我们这个URL更换为http://yourhost/yourapp/en/index.jsp,这个时候Tomcat会试图把yourapp_dir/webapp/en/index.jsp文件编译成Servlet,并调用运行这个Servlet。

我们再把这个URL更换为http://yourhost/yourapp/en/index.do

注意,戏剧化的事情就发生在这个时候,Servlet规范中最重要的类RequestDispatcher登场了。RequestDispatcher根据WEB-INF/web.xml配置文件的定义,调用对应的Servlet来处理en/index.do这个路径。

假设web.xml里面有这样的定义。

  <servlet>

    <servlet-name>DispatchServlet</servlet-name>

    <servlet-class>yourapp.DispatchServlet</servlet-class>

  </servlet>

  <servlet-mapping>

    <servlet-name>DispatchServlet</servlet-name>

    <url-pattern>*.do</url-pattern>

  </servlet-mapping>

那么,RequestDispatcher会调用yourapp.DispatchServlet类处理这个路径。

如果web.xml没有定义对应en/index.do这个路径的Servlet,那么Tomcat返回“您请求的资源不存在”。

RequestDispatcher用于Web Server中,也可以用于应用程序中进行处理转向,资源定位。比如,我们在处理en/index.do的代码中调用,

request.getRequestDispatcher(“cn/index.jsp”).forward(request, response), 就可以转交另外的资源cn/index.jsp来处理。

 

几乎所有的Web Framework都需要定义自己的Dispatch作用的Servlet,并调用RequestDispatcher进行转向处理。

阅读Web Framework源代码,有两条主要线索,(1)根据web.xml找到对应的Servlet类;(2)搜索包含“RequestDispatcher”词的代码文件。

 

我们看到,request, response  这两个参数,被RequestDispatcher在各种Servlet之间传来传去(JSP也是Servlet)。所以,request的setAttribute()和getAttribute()方法是Servlet之间传送数据的主要方式。

在MVC结构中,一般的处理流程如下:

处理HTTP Request的基本单位一般称为Action,是一个比Servlet轻量得多的接口定义,通常只有一两个方法,如execute(perform), validate等。

我们知道,URL->Servlet映射,定义在Web.xml配置文件里,但MVC框架通常会有另外一个定义URL-> Action映射的配置文件。

入口Dispatcher Servlet根据URL -> Action的映射关系,把请求转发给Action。

Action获得输入参数,调用商业逻辑,并把结果数据和View标识给(Model & View)返回给Dispatcher Servlet。

Dispatcher Servlet根据这个View 标识,定位相应的View Template Path,把处理转交给View(JSP +TagLib, Velocity, Free Marker, XSL等)。

View一般通过request.getAttribute()获得结果数据,并显示到客户端。至于是谁把结果数据设置到request.attribute里面,有两种可能:Action或Dispatcher Servlet。

2. Struts
http://struts.apache.org/

Struts是目前用户群最大、开发厂商支持最多的开源Web Framework。

Struts劳苦功高,为普及MVC框架作出了不可磨灭的贡献。显赫的声望,趋于老化的厚重结构,令Struts成为很多现代Web Framework参照、挑战的目标。

 

Struts应用主要包括3件事情: 配置struts-config.xml文件,实现Action类,实现View;还有一些高级扩展用法。下面分别讲述。

 

1. 配置struts-config.xml文件:

Struts支持多级配置文件,具体用法和限制,详见Struts文档。这里只讨论struts-config.xml主流配置的内容。:-)

 

(1) URL Path到Action的映射。

如<action path="/LogonSubmit" type="app.LogonAction" ... />

 

Struts的入口Servlet是ActionServlet。

ActionServlet需要此信息把URL Path调用对应的Action类处理。

在Struts运行期间,一个URL Path,只存在一个对应的Struts Action实例。所有的该URL Path的请求,都经过这同一个Struts Action实例处理。所以Struts Action必须线程安全。

想想看,其实这个要求并不过分,Action只是一个处理程序,不应该保存跨HTTP请求的状态数据,按理来说,也应该做成线程安全的。

 

(2) Template Name到View Template Path的映射。

<forward name="success" path="/pages/Welcome.jsp"/>

 

Action类返回一个Template Name,ActionServlet根据这个Template Name获得对应的View Template Path,然后调用

request.getRequestDispatcher(“View Template Path”),把处理转向路径对应的Servlet。在这个例子中,是转向/pages/Welcome.jsp编译后的Servlet。

 

我们来看一个一个Velocity的例子。

<include name="success" path="/pages/Welcome.vm"/>

web.xml的定义如下

<servlet>

  <servlet-name>velocity</servlet-name>

<servlet-class>org.apache.velocity.tools.view.servlet.VelocityViewServlet</servlet-class>

</servlet>

<servlet-mapping>

  <servlet-name>velocity</servlet-name>

  <url-pattern>*.vm</url-pattern>

</servlet-mapping>

 

这时,request.getRequestDispatcher(“/pages/Welcome.vm”)会调用VelocityViewServlet,由VelocityViewServlet负责装并驱动运行/pages/Welcome.vm这个模板文件。

这里面有一个问题,如果调用的是DispatchRequester.include()方法,那么如何才能把pages/Welcome.vm传给VelocityViewServlet呢?

如前所说,RequestDispatcher传递的参数只有两个,request和response。那么只能通过request attribute。正是为了解决这个问题,Servlet2.3规范之后,加入了javax.servlet.include.servlet_path这个属性。

参见VelocityViewServlet的代码(velocity-tool开源项目)

// If we get here from RequestDispatcher.include(), getServletPath()

// will return the original (wrong) URI requested.  The following special

// attribute holds the correct path.  See section 8.3 of the Servlet

// 2.3 specification.

String path = (String)request.getAttribute("javax.servlet.include.servlet_path");

 

从这里我们可以看出,为什么通晓Servlet Specification对于通晓Web Framework至关重要。

 

(3) Form Bean的定义

如<form-bean name="logonForm" type="app.LogonForm"/>Struts Form Bean需要继承ActionForm类。Form Bean类,主要有三个作用:

[1]根据bean的定义,利用reflection机制,自动把request参数转化为需要的数据类型,填入到bean的属性当中。ActionForm类名中虽然有Form这个词,但不仅能够获取Form提交后的HTTP Post参数,也可以获取URL后缀的HTTP Get参数。

[2]输入验证。用户可以配置validation.xml,定义各属性的验证规则。

[3]当作View Object来用。用户需要熟练掌握Struts HTML TagLib的用法,才能把Form Bean的属性正确显示出来。

 

(4)其他定义。详见Struts文档。不再赘述。

 

2.实现Action。

Action类从Form Bean或直接从request中获得输入参数,调用商业逻辑,把结果数据(也许会包装成View Object),用request.setAttribute()放到request中,最后返回一个用ForwardMapping类包装的Template Name。

 

3.实现View。

Struts View的标准实现方法是JSP + Struts TagLib,其中最重要的就是Struts HTML TagLib。

html:form tag则是整个HTML Tag的核心,其它的如html:input, html:select等tag,都包含在html:form tag里面。

html:form tag用来映射Form Bean(也可以通过适当定义,映射其他的bean,但使用上会有很多麻烦)。html:form tag包含的其他Struts html tag用来映射Form Bean的属性。

 

Struts Bean TagLib的用法比较臃肿,一般情况下可以用JSTL代替。当然,如果需要用到bean:message tag实现国际化,那又另当别论。

Struts Tile TagLib用于页面布局。开源Portal项目Liferay使用了Struts Tile TagLib做为布局控制。

 

4.高级扩展用法

用户可以重载Struts的一些控制类,引入自己的一些定制类。详见Struts文档。

本文不是Struts专题,只讲述最重要的主流用法,其它边边角角的,不再赘述。

3. WebWork
http://www.opensymphony.com/webwork/

WebWork由于灵活的可插拔特性,受到很多资深程序员的欢迎。似乎很有可能大肆流行起来。

WebWork项目建立在XWork项目上。入口Servlet是WebWork项目中定义的ServletDispatcher,而Action在XWork项目中定义。

XWork Action接口的execute()方法没有参数,不像Struts Action那样接受request, response参数,所以XWork Action能够脱离Web环境被直接调用,便于单元测试。

这里引入了一个问题。没有了request参数,那么XWork Action如何获得request parameters作为输入数据?又通过什么桥梁(Struts用request.setAttribute)把结果数据传送到View层?

在Web Work中,只能通过Action本身的getter, setter属性来传送输入参数和输出结果。

比如,我们有这样一个实现了XWork Action接口的类,

YourAction implements Action{

  int productId = null;

  String productName = null;

 

  public void setProductId(int productId){this.productId = productId;}

  public String getProductName(){return productName;}

 

  public String execute(){

      productName = findNameById(productId);

      return “success”;

  }

}

这个类里面的productId将接受request输入参数,productName是输出到页面显示的结果。

比如,这样的请求,http://yourhost/yourapp/MyAction.action?productId=1

Web Work会把1填到YourAction的productId里面,然后执行execute()方法,JSP里的语句<ww:property value=“productName”>会把YourAction的productName显示在页面上。

 

如果一个Web Framework采用了这种屏蔽Action的request, response参数的设计方式,一般也同时会采用这种Action和输入输出数据结合成一体的解决方式。类似的情形也存在于Tapestry和Maverick中,后面会讲到。

当WebWork ServletDispatcher接收到HTTP Request的时候,首先把所有相关的信息(包括request, response, session, servlet config, servelt context, 所有request参数)等存放到AcationContext中,然后根据Interceptor配置信息,生成一个YourAction的动态代理类对象。实际上运行的正是这个代理对象,如同Servlet Filter的工作机制一般,所有注入的Interceptor方法会先于Actio方法运行。

我们来看一下Action和Interceptor的地位:Action没有参数,无法获得ActionContext;而Interceptor接受的ActionInvoication参数拥有包括ActionContext在内的所有重要信息。

这种权力分配的不平等,注定了Action的作用非常有限,只限于调用商业逻辑,然后返回一个成功与否标志。所有与外部Web世界打交道、协调内部工作流程的重担,都责无旁贷地落在Interceptor的肩上。

我们可以设想一个极端的例子。我们声明一批不做任何事情的空Action,我们只是需要它们的空壳类名;我们制作一批对应的Interceptor,所有的转发控制、商业逻辑都在Interceptor上实现,然后把Interceptor都注入到对应的空Action。这在理论上是完全可行的。

在Web海洋的包围中,Action可少,Interceptor不可少。Action是一个孤岛,如果没有外来盟友Interceptor的协助,只能在自己的小范围内独立作战(比如Unit Test),而对整体大局的作战目标无法产生影响。

下面我们来看一下Action是如何在Interceptor的全程监管下工作的。

 

在WebWork中,我们需要如下配置XWork.xml。

<xwork>

<!-- Include webwork defaults (from WebWork-2.1 JAR). -->

<include file="webwork-default.xml" />

 

<!-- Configuration for the default package. -->

<package name="default" extends="webwork-default">

    <!-- Default interceptor stack. -->

    <default-interceptor-ref name=" defaultStack" />

 

    <!-- Action: YourAction. -->

    <action name="youraction" class="yourapp.YourAction">

        <result name="success" type="dispatcher">

YourAction.jsp

</result>

</action>

</package>

</xwork>

 

webwork-default.xml里面的相关定义如下:

<interceptors>

<interceptor name="validation" class="com.opensymphony.xwork.validator.ValidationInterceptor"/>

 

<interceptor name="static-params" class="com.opensymphony.xwork.interceptor.


StaticParametersInterceptor"/>

<interceptor name="params" class="com.opensymphony.xwork.interceptor.ParametersInterceptor

"/>

<interceptor name="conversionError" class="com.opensymphony.webwork.interceptor.


WebWorkConversionErrorInterceptor"/>

<interceptor-stack name="defaultStack">

    <interceptor-ref name="static-params"/>

    <interceptor-ref name="params"/>

    <interceptor-ref name="conversionError"/>

</interceptor-stack>

</interceptors>

 

从上述的配置信息中可以看出,YourAction执行execute()方法的前后,会被

defaultStack所定义的三个Intercepter截获。这些Interceptor的任务之一就是把输入参数设置到Action的对应属性当中。

如果我们需要加入对YourAction的属性的验证功能,只要把上述定义中的validation Interceptor加入到defaultStack中就可以了。当然,实际工作还没有这么简单,一般来说,还要为每个进行属性验证的Action的都配置一份validation.xml。

XWork Interceptor能够在Package和Action级别上,进行截获处理。

Servlet Filter能够在URL Patten级别上,进行截获处理。虽然实际上,Servlet Filter截获的是Servlet,但某些情况下,可以达到和截获一批Action的同样效果。

比如,在Web Work中,我们可以为所有admin package的Action,加入一个Interceptor,当检查到当前Session的用户没有admin权限时,统一返回一个警告页面:您没有足够的权限执行这个操作。

我们看到也可以为所有URL Pattern为“admin/*.action”的URL定义一个Servlet Filter,当检查到当前Session的用户没有admin权限时,统一返回一个警告页面:您没有足够的权限执行这个操作。

 

WebWork的Interceptor配置是相当灵活的,相当于对Action实现了AOP。Interceptor相当于Aspect,基类AroundInterceptor的before(), after()方法相当于Advice。

另外,XWork也提供了从XML配置文件装配Component的机制,相当于实现了对于Component的IoC。

提到AOP和IoC,顺便多讲两句。Spring AOP能够截获所有Interface,不限于某个特定接口;Spring框架支持所有类型的IoC,不限于某种特定类型。

 

要知道,AOP, IoC可是现在最时髦的东西,一定不要错过啊。:D

相关概念导读(如果需要,请用如下关键字搜索网络):

AOP -- Aspect Oriented Programming -- 面向方面编程。

IoC – Inversion of Control --控制反转

Dynamic Proxy -- 动态代理,JDK1.4引入的特性。还可以进一步参考CGLib, ASM等开源项目。

 

WebWork直接支持所有主流View -- XSL,Velocity, FreeMarker,JSP。WebWork还提供了自己的TagLib。“直接支持”的意思是说,不用像Struts那样,使用Velocity的时候,还需要引入辅助桥梁Velocity-tool。

WebWork中用到一种功能和XPath类似的对象寻径语言ONGL,是一个开源项目。ONGL同样用在下面要介绍的Tapestry项目中。

Opensymphony下还有一个SiteMesh项目,通过Servlet Filter机制控制布局。可以和WebWork组合使用。

 

4. Tapestry
http://jakarta.apache.org/tapestry/

Tapestry近来突然火了起来,令我感到吃惊。也许是JSF带来的Page Component风潮令人们开始关注和追逐Tapestry。

Tapestry的重要思想之一就是Page Component。

前面讲到,XWork能够自动把request参数映射到Action的属性当中。Tapestry走得更远,甚至能够根据request参数,映射到Action(Tapestry里面称为Page)的方法,并把request参数映射为Page方法需要的参数,进行正确的调用。就这样,Tapestry不仅把输入输出数据,而且把事件方法也绑定到了Page上面。

在Tapestry框架中,Action的概念已经非常模糊,而换成了Page的概念。而Tapestry Page是拥有属性和事件的页面组件,其中的事件处理部相当于Action的职责,而属性部分起着Model的作用。

除了使用Page和其它的Tapestry页面组件,用户也可以自定义页面组件。

 

这种页面组件/属性事件的编程模型,受到一些程序员的欢迎。当然,这种编程模型并不是没有代价的,每个Tapestry模板文件都需要一个对应的.page文件。这些.page文件定义了页面组件的属性、事件、Validator等信息。

 

我们来看一下B/S结构中,组件的属性、事件和HTTP Request绑定的基本原理。一个能够发出请求的页面组件(比如Link和Button),在输出自己的HTML的时候,需要输出一些特殊的信息来标志本组件的属性/事件,这样下次HTTP Request来的时候,会把这些信息带回来,以便Web Framework加以辨认识别,发给正确的Page Component处理。

这些特殊信息通常包含在URL参数或Hidden Input里面,必要的时候,还需要生成一些Java Script。Tapestry,Echo,JSF都是这种原理。

Tapestry的例子如下:

<a href="#" jwcid="@DirectLink" parameters="ognl:currentItem.itemId" listener="ognl:listeners.showItem">

JSF用TagLib实现页面组件,也提供了类似的CommandLink和CommandButton Tag。其中对应Tapestry listener的Tag属性是action。后面会讲解。

 

Tapestry的模板标签是HTML标签的扩展,具有良好的“所见即所得”特性,能够直接在浏览器中正确显示,这也是Tapestry的一个亮点。

5. Echo
http://sourceforge.net/projects/echo

Echo提供了一套类似Swing的页面组件,直接生成HTML。

从程序员的角度看来,用Echo编写Web程序,和用Swing编写Applet一样,属于纯面向组件事件编程,编程模型也以Event/Listener结构为主体。

Echo没有Dispatcher Servlet,也没有定义URL->Action映射的配置文件。

Echo的Action就是实现了ActionListener接口(参数为ActionEvent)的Servlet(继承EchoServer类)。

所以,Echo直接由Web Server根据web.xml配置的URL -> Servlet的映射,进行转发控制。

 

Echo也没有明显的View层,Echo在页面组件方面走得更远,所有的HTML和JavaScript都由框架生成。你不必(也没有办法)写HTML,只需要(也只能)在Java代码中按照类似Swing编程方式,生成或操作用户界面。用户也可以定制自己的Echo组件。

Echo的UI Component的实现,采用了两个重要的模式。一个是Peer(Component -> ComponentPeer)模式,一个是UI Component -> Renderer模式。

虽然Echo的API更类似于Swing,但实现上却采用更接近于AWT的Peer模式。每个Component类(代表抽象的组件,比如Button),都有一个对应的ComponentPeer类(代表实际的组件,比如windows桌面的Button,Linux桌面的Button,HTML Button等)。

先别急,这个事情还没有完。虽然ComponentPeer落实到了具体的界面控件,但是它还是舍不得显示自己,进一步把显示工作交给一个Renderer来执行。

比如,在Echo里面,Button类对应一个ButtonUI(继承了ComponentPeer)类,而这个ButtonUI类会把最终显示交给ButtonRender来处理。

据说多了这么一步,能够让显示控制更加灵活丰富。比如,同一个Renderer可以处理不同的UI Component,同一个UI Component也可以交给不同的Renderer处理。

JSF的页面组件也采用了UI Component -> Renderer模式,后面会讲到。

6. JSF
http://java.sun.com/j2ee/javaserverfaces/index.jsp

http://wwws.sun.com/software/communitysource/jsf/download.html download source

 

JSF的中心思想也是页面组件/属性事件。一般来说,JSF的页面组件是一个三件套{ UI Component, Tag, Renderer}。

UI Component有可能对应Model,Event,Listener。Tag包含componentType和rendererType两个属性,用来选择对应的的UI Component和Renderer。

JSF的应用核心无疑是JSF TagLib。JSF TagLib包含了对应所有重要HTML元素的Tag,而且Input Tag可以直接包含Validator Tag或者Validator属性,来定义验证手段。

 

我们通过JSF携带的cardemo例子,来看JSF的处理流程。

(1) carDetail.jsp有如下内容:

<h:commandButton action="#{carstore.buyCurrentCar}" value="#{bundle.buy}" />

可以看到,这个button的submit action和carstore.buyCurrentCar方法绑定在一起。我们在Tapestry里面曾经看到过类似的情景。

 

(2) carstore在faces-config.cml中定义:

  <managed-bean>

    <managed-bean-name> carstore </managed-bean-name>

    <managed-bean-class> carstore.CarStore </managed-bean-class>

    <managed-bean-scope> session </managed-bean-scope>

  </managed-bean>

 

(3) carstore.CarStore类中的buyCurrentCar方法如下:

    public String buyCurrentCar() {

        getCurrentModel().getCurrentPrice();

        return "confirmChoices";

    }

 

(4) confirmChoices转向在faces-config.cml中定义:

  <navigation-rule>

    <from-view-id>/carDetail.jsp</from-view-id>

    <navigation-case>

      <description>

        Any action that returns "confirmChoices" on carDetail.jsp should

        cause navigation to confirmChoices.jsp

      </description>

      <from-outcome>confirmChoices</from-outcome>

      <to-view-id>/confirmChoices.jsp</to-view-id>

    </navigation-case>

  </navigation-rule>

 

(5)于是转到页面confirmChoices.jsp。

 

除了Interceptor之外,JSF几乎包含了现代Web Framework应该具备的所有特性:页面组件,属性事件,IoC (ManagedBean),Component -> Renderer,类似于Swing Component的Model-Event-Listener。

也许设计者认为,众多庞杂的模式能够保证JSF成为一个成功的框架。Portal开源项目eXo就是建立在JSF框架上。

 

可以看出这样一个趋势,现代Web Framework认为B/S结构的无状态特性和HTML界面是对编程来说是需要极力掩盖的一个缺陷,所以尽量模拟C/S结构的组件和事件机制,以吸引更多的程序员。

7. Maverick
http://mav.sourceforge.net/

Maverick是一个轻量而完备的MVC Model 2框架。Maverick的Action不叫Action,直截了当的称作Controller。

Controller只接受一个ControllerContext参数。request,response, servlet config, servelt context等输入信息都包装在ControllerContext里面,而且Model也通过ControllerContext的model属性返回。整个编程结构清晰而明快,令人赞赏。

但这个世界上难有十全十美的事情,由于ControllerContext只有一个model属性可以传递数据,程序员必须把所有需要的数据都打包在一个对象里面设置到model属性里。这种麻烦自然而然会导致这样的可能用法,直接把Controller本身设置为model,这又回到了Controller(Action)和Model一体的老路。

 

前面讲到,WebWork也把所有的输入信息都包装在ActionContext里面,但Action并没有权力获取。而在Maverick中,Controller对于ControllerContext拥有全权的控制,两者地位不可同日而语。当然,由于参数ControllerContext包含request,reponse之类信息,这也意味着,Maverick Controller不能像WebWork Action那样脱离Web环境独立运行。

当然,这也并不意味着任何结构性缺陷。程序的结构由你自己控制,你完全可以把需要Unit Test的那部分从Web环境脱离开来,放到Business层。

如同WebWork,Maverick直接支持所有的主流View。Maverick的配置文件采Struts, Cocoon两家之长,URL -> Action -> View映射的主体结构类似于Struts,而View定义部分对Transform的支持则类似于Cocoon。如:

<command name="friends">

<controller class="org.infohazard.friendbook.ctl.Friends"/>

<view name="success" path="friends.jsp">

        <transform path="trimInside.jsp"/>

</view>

</command>

8. Spring MVC
http://www.springframework.com/

Spring MVC是我见过的结构最清晰的MVC Model 2实现。

Action不叫Action,准确地称做Controller;Controller接收request, response参数,干脆利落地返回ModelAndView(其中的Model不是Object类型,而是Map类型)。

其它的Web Framework中, Action返回值一般都只是一个View Name;Model则需要通过其它的途径(如request.attribute,Context参数,或Action本身的属性数据)传递上去。

 

Spring以一招IoC名满天下,其AOP也方兴未艾。“Spring出品,必属精品”的观念已经深入人心。我这里多说也无益,强烈建议读者去阅读Spring Doc & Sample & Code本身。

9. Turbine
http://jakarta.apache.org/turbine/

Turbine是一个提供了完善权限控制的坚实框架(Fulcrum子项目是其基石)。Turbine的个人用户不多,但不少公司用户选择Turbine作为框架,开发一些严肃的应用(我并没有说,用其它框架开发的应用就不严肃^_^)。Portal开源项目JetSpeed建立在Turbine上。

Turbine用RunData来传递输入输出数据。如同Maverick的ControllerContext,RunData是整个Turbine框架的数据交换中心。除了request, response等基本信息,RunData直接包括了User/ACL等权限控制相关的属性和方法,另外还包括Action Name和Target Template Name等定位属性。

Module是Turbine里面除了RunData之外的又一个核心类,是Turbine框架的基本构件,Action是Module,Screen也是Module。Turbine提供了LoginUser和LogoutUser两个Action作为整个系统的出入口。而其余流量的权限控制则由类似于Servlet Filter机制的Pipeline控制。

Turbine Pipeline的编程模型和Servlet Filter一模一样:Turbine Pipeline的Valve就相当于Servlet Filter,而ValveContext则相当于Filter Chain。还有更相近的例子,Tomcat源代码里面也有Valve和ValueContext两个类,不仅编程模型一样,而且名字也一样。

 

权限控制贯穿于Turbine框架的始终。要用好Turbine,首先要通晓子项目Fulcrum 的Security部分的权限实现模型。

Fulcrum Security的权限实体包括四个-- User, Group, Role, Permission。

实体之间包含{Role,Permission}和{ Group, User, Role}两组关系。

{Role,Permission}是多对多的关系,一个Role可以具有各种Permission;{ Group, User, Role}之间是多对多的关系,一个Group可包含多个User,并可以给User分配不同的Role。

权限模型的实现同样采用Peer模式,Entity -> EntityPeer, Entity -> ManagerPeer。

Entity和EntityManger代表抽象的模型概念,而EntityPeer和ManagerPeer代表具体的实现。

用户可以根据模型,提供不同的实现,比如,用内存结构中实现,用数据表结构实现,与Windows NT权限验证机制结合,与OSWorkflow的权限控制模型结合,等等。其中,用数据表结构实现,又可以选择用Torque实现,或者用Hibernate实现。(Torque是Turbine的O/R Mapping子项目)

 

例如,Falcrum.property配置文件包含如下Security相关选项:

# -------------------------------------------------------------------

#  S E C U R I T Y  S E R V I C E

# -------------------------------------------------------------------

services.SecurityService.user.class=org.apache.fulcrum.security.impl.db.entity.TurbineUser

services.SecurityService.user.manager=org.apache.fulcrum.security.impl.db.DBUserManager

services.SecurityService.secure.passwords.algorithm=SHA

# -------------------------------------------------------------------

#  D A T A B A S E  S E R V I C E

# -------------------------------------------------------------------

services.DatabaseService.database.newapp.driver=org.gjt.mm.mysql.Driver

services.DatabaseService.database.newapp.url=jdbc:mysql://127.0.0.1/newapp

services.DatabaseService.database.newapp.username=turbine

services.DatabaseService.database.newapp.password=turbine

 

这说明,权限控制实现由数据库提供,需要根据权限模型创建如下数据表:

TURBINE_USER,TURBINE_ROLE,TURBINE_GROUP,

TURBINE_PERMISSION,TURBINE_ROLE_PERMISSION,

TURBINE_USER_GROUP_ROLE。

 

10. Cocoon
http://cocoon.apache.org/

Cocoon项目是一个叫好不叫做的框架。采用XML + XSLT Pipeline机制,Java程序只需要输出XML数据,Cocoon框架调用XSL文件把XML数据转换成HTML、WML等文件。

Cocoon强大灵活的XSL Pipeline配置功能,XSLT的内容/显示分离的承诺,一直吸引了不少程序员fans。怎奈天不从人愿,由于复杂度、速度瓶颈、XSL学习难度等问题的限制,Cocoon一直主要限于网站发布出版领域,向CMS和Portal方向不断发展。另外,Cocoon开发了XSP脚本和Cocoon Form技术。

Cocoon的sitemap.xmap配置文件比较复杂,与其它的Web Framework差别很大。

主体Pipelines配置部分采用Pattern Match的方式,很像XSL语法,也可以类比于Web.xml里面Servlet Mapping的定义。比如,一个典型的URL->Action的映射定义看起来是这个样子:

<map:pipelines>

<map:pipeline>

<map:match pattern="*-dept.html">

  <map:act set="process">

    <map:parameter name="descriptor"

                   value="context://docs/department-form.xml"/>

    <map:parameter name="form-descriptor"

                   value="context://docs/department-form.xml"/>

    <map:generate type="serverpages" src="docs/confirm-dept.xsp"/>

    <map:transform src="stylesheets/apache.xsl"/>

    <map:serialize/>

  </map:act>

  <map:generate type="serverpages" src="docs/{1}-dept.xsp"/>

  <map:transform src="stylesheets/apache.xsl"/>

  <map:serialize/>

</map:match>

</map:pipeline>

</map:pipelines>

11. Barracuda
http://barracudamvc.org/Barracuda/index.html

Barracuda是一个HTML DOM Component + Event/Listener结构的框架。

根据模板文件或配置文件生成静态Java类,并在代码中使用这些生成类,是Barracuda的一大特色。

Barracuda需要用XMLC项目把所有的HTML或WML模板文件,静态编译成DOM结构的Java类,作为页面组件。XMLC会根据HTML元素的id定义,生成相应DOM结点的简便操作方法。

 

Barracuda的事件类也需要用Barracuda Event Builder工具把event.xml编译成Java类,引入到工程中。Barracuda直接用Java类的继承关系映射事件之间的父子层次关系。比如,ChildEvent是ParentEvent的子类。

Barracuda的事件分为两类:Request Events(Control Events)和Response Events(View Events)。

 

Barracuda事件处理过程很像Windows系统消息队列的处理机制。

(1) Barracuda根据HTTP Request生成Request Event,放入到事件队列中。

(2) EventDispatcher检查事件队列是否为空,如果为空,结束。如果非空,按照先进先出的方式,从事件队列中取出一个事件,根据这个事件的类型,选择并调用最合适的EventListener,参数Event Context包含事件队列。

 “根据事件类型,选择最合适的EventListener对象”的过程是这样的:比如,

EventDispatcher从时间队列里取出来一个事件,类型是ChildEvent;Barracuda首先寻找注册了监听ChildEvent的EventListener,如果找不到,再上溯到ChildEvent的父类ParentEvent,看哪些EventListener对ParentEvent感兴趣。

详细过程参见Barracuda的DefaultEventDispatcher类。

(3) EventListener根据Event Context包含的request信息,调用商业逻辑,获得结果数据,然后根据不同情况,把新的事件加入到Event Context的事件队列中。

(4) 控制交还给EventDispatcher,回到第(2)步。

 

The End.

Enjoy.

 

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/buaawhl/archive/2004/12/21/224069.aspx

posted @ 2009-06-16 16:02 gephen 阅读(264) 评论(0) 编辑

Java Web开发构想

 
1.背景、形势
能够进行Web开发的编程语言和技术很多
(1) 动态解释语言
PHP; Perl; Python (Zope, Plone); Ruby (Ruby on Rails);
(2) 编译语言
Java; .net

Java Web开发远非一枝独秀:
除了受到来自.net 这个重量级对手的最大挑战之外,更受到Zope, Ruby on Rail 等新式轻骑兵的冲击(当然,也继续受到老式轻步兵PHP, Perl的冲击)。

官方Java走的是复杂路线,Servlet -> JSP -> Taglib。.net走的也是复杂路线,依靠成熟友好的集成化开发环境取胜。Java阵营好容易应对过来,从纷纭复杂的各种开发框架基础上,发展出了重量级Web开发框架JSF,以及相应的集成化开发环境;渴望以此应对.net的攻势。胜负未分,前途未卜。这时,另一个方向又杀来了新式轻骑Zope, Ruby on Rail。
Python, Ruby等动态解释语言,面向对象特性更好,先天支持 动态绑定、AOP、函数式编程、“编程即配置”等时髦概念。开发速度更快,代码量更小,达到killer级别。

传统的HTML Web开发领域里面,Java已经是腹背受敌。领域外也展开了征战,Rich Client Architecture的兴起:AJAX(XMLHttp), Flash RIA, XUL, XAML, Smart Client(以及从前的ActiveX, Applet, Web Start)。

Web的发展趋势是 语义Web,最终目的是让整个Web成为一个巨大的数据库。
这意味着,未来的Web应用将更加的面向文本内容数据,更加搜索引擎友好 – Search Engine Friendly.
二进制的客户端插件,如Flash RIA,  ActiveX, Applet, Web Start等,虽然交互性能最好,但不是以文本内容数据为中心,搜索引擎不友好。所以,我只是保持适当关注。我更关注基于文本的UI表现,如HTML, XUL, XAML等。XUL, XAML还没有广泛流行,只是保持一种有兴趣的关注。
当下关注的重点,还是 XHTML + CSS +  Javascript少量的 AJAX(XMLHttp)增加更好的交互性。

我一直认为:轻量、简洁、高效 才是硬道理。后面阐述我对Java Web开发的理解和构想。
2. Web开发框架层次概述
从上到下,Web开发框架的层次如下:
(1) HTML, JavaScript, CSS等页面资源。
(2) 页面模板层。
如JSP, Freemarker, Velocity, XSL,fastm等。用来生成HTML, JavaScript, CSS等页面资源。
(3) Web框架。把HTTP Request调度分派到对应的Service Entry。
(4) Business Logic.
(5) O/R Mapping.
(6) JDBC
(7) DB

根据我的经验,一个典型的Web应用中的代码比例如下:
页面逻辑约占 50%,商业逻辑约占30%,  O/R 约占20%。

但事实上,页面却是最不受重视的部分,从来都被认为是脏活,累活,杂活。典型的开发过程通常是这样:
页面设计人员迅速的用Dreamweaver等生成一堆文本杂乱无章的页面,然后交给JSP程序员加入更加杂乱无章的Java代码和Taglib。
当页面布局风格需要改变的时候,页面设计人员用Dreamweaver等生成一堆新的页面。JSP程序员再重新加入更加杂乱无章的Java代码Taglib。
至于页面中的脚本逻辑调试,更是一门精深的工夫了。

根据社会规则,通常来说,工作内容越轻松,收入越高;工作内容越脏月累,收入越低;Web开发也是如此:做着最脏最累的活的页面程序员,工资一般比不上后台业务逻辑程序员。

开发框架通常会带来这样的结果:让简单的东西,变得更简单;让复杂的东西,变得更复杂。
这其中的原因在于:
一般来说,一个应用中简单重复的东西占80%,复杂特殊的东西占20%。
简单重复的东西很容易摸清规律,进行包装,通用化。但是,在包装的同时,经常就阻挡住了底层的一些灵活强大的控制能力。在复杂特殊的需求中,确实又需要这些底层控制能力,那么为了绕开框架的限制,付出的努力要比不用框架 大得多。
打个比方,一个比较极端的例子。编译语言比汇编语言的开发效率高很多,但是却无法直接操作寄存器。当需要在编译语言中操作寄存器的时候,就非常的痛苦。比如Java,也许需要JNI,写C代码,还要在C代码里面嵌入汇编。编译、连接都很麻烦。
所以,一个框架的开发效率,就在于这个80%简单 与 20%复杂之间的平衡。
假如,不用框架来开发,简单的80%要消耗 80个资源数,复杂的20%要消耗20个资源数,总资源数是100;使用了某个框架,简单的80%只要消耗10个资源数,复杂的20%要消耗40个资源数,总资源数是50。那么,我们说,这个开发框架是有效率的。

我的思路是,同时应对复杂和简单。当然,为了应对复杂,简单的东西可能就应对得不那么好。比如,做这样一个开发框架,简单的80%要消耗20个资源数,复杂的20%要消耗10个资源数,总资源数是30。
这种开发框架是有可能实现的。而且是很有意义的。尤其是在复杂部分的比例提高的时候。越复杂的系统,这种开发框架就越有意义。

后面的关于Web各层开发的论述,主要就按照这个“应对复杂、让复杂更简单”的思路展开。
3.页面资源
也许有人会说,页面资源,不就是HTML吗?太简单,太低极了,没劲。Dreamweaver、Frontpage多简单阿。随便找个人来用就可以了。文本内容乱糟糟不要紧,浏览器里面显示出来的效果好看就行。要增加炫的、酷的动画效果,那就写JavaScript呗。写在HTML里面,看看在IE里面能不能运行就可以了呗。
这也正是大多数公司开发页面资源的方式。因为页面的需求变化是最多、最快的,而页面的制作成本很低,人们不愿意在上面投入更多的资源。

我的看法是,万丈高楼平地起。应用程序的每一个部分都应该完善管理,结构优美。越是需求变化多的地方,越是脏乱差的地方,越应该加大力度处理好。

页面结构方面,Javaeye论坛的Dlee做了很多工作。

(1) 在 2005 年我们如何写 JavaScript
http://forum.javaeye.com/viewtopic.php?t=12973

Dlee 写道

Ten good practices for writing JavaScript in 2005
http://www.bobbyvandersluis.com/articles/goodpractices.php
在这篇文档中提到的“unobtrusive techniques”是需要高度关注的一种技术。
http://www.kryogenix.org/code/browser/aqlists/

还有这篇:Unobtrusive Javascript
http://www.onlinetools.org/articles/unobtrusivejavascript/
是主要介绍如何以更有效的方式来写 JavaScript 的。


(2)使用 Unordered Lists 制作的下拉菜单和树
http://forum.javaeye.com/viewtopic.php?t=12995

Dlee 写道

Unobtrusive DHTML, and the power of unordered lists
http://www.kryogenix.org/code/browser/aqlists/

我再说一下把页面的 structure、presentation 和 behavior 分离开的意义。一般的人很容易理解把 structure 和 presentation 分离的意义,但是对于为什么需要把 presentation 和 behavior 分离开不是很清楚。
这三部分分离开,页面开发才有可能实现真正的重用,从而最终降低开发和维护的工作量。JS 代码是可以做自动测试的,使用 JsUnit 来做。Web 表示层代码测试困难是公认的,直到今天所有介绍 TDD 的经典教材也没有提出一个好方法。所以我问过一些朋友,都是倾向于不对表示层的代码做自动测试。为什么不做自动测试?没有重用价值的代码值得做自动测试吗?而且我们以前有个误区,认为如果做自动测试,表示层的所有东西都需要测试,其实这个想法是错误的。我们需要做自动测试的仅仅是 behavior 这一部分。以前的测试为什么很困难?就是因为 presentation 和 behavior 没有分离开,我们在 JS 代码中直接操作页面的样式(直接设置元素的 style)。我们不应该再这样做下去,我们应该把 presentation 的工作完全交给 CSS 来做。实现 presentation 和 behavior 的分离有两种方法,通过改变元素的 id 或者使用更通用的方法,通过改变元素的 className。而关于这个 id 或者这个 className 具体的样式在外部的 CSS 文件中设置。JS 文件可以生成新的 structure(createElement,etc.),但是不应该直接改变元素的 style。改变了 style,一切效果你都需要用眼睛看到了才算测试成功,这哪里可以做自动测试?而且假如用户对这个 style 不满意,你还需要去修改 JS 代码。你如果只改变元素的 id 或者 className,做自动测试就要容易得多,你只需要测试最终这个元素的 id 或者 className 是否变成了期望的值。而最终的样式是不是也是你期望的,那是 CSS 文件保证的事情,这只需要在第一次开发 CSS 的时候做一下人工测试就足够了。而这样以来,CSS 文件可以由美工来维护,他完全不需要知道 JS 是什么东西。界面程序员可以去做一些更加重要的事情。

所以在这里我们看到,把 presentation 和 behavior 彻底分离开是做 Web 表示层代码自动测试的关键。把这两部分分离开以后,自动测试的难题就迎刃而解了。再强调一下,只有 behavior 有可能做自动测试,presentation 是不需要也不大可能做自动测试的。

相关资料:
http://www.onlinetools.org/articles/unobtrusivejavascript/cssjsseparation.html


从上面的Dlee的论述和给出的资料。可以看出,页面资源分为三部分:
(1) XHTML。结构,Structure。
XHTML里面的Tag部分只应该包括 &lt;ul&gt; &lt;table&gt; &lt;p&gt; &lt;div&gt;&lt;span&gt;等结构布局Tag,或者&lt;strong&gt;&lt;emphasis&gt;表示语义的Tag。
XHTML里面不应该包括风格信息,比如字体、颜色、大小、粗细等,也不应该包括&lt;font&gt; &lt;b&gt; &lt;i&gt; &lt;h&gt; 等字体信息。
XHTML里面不应该包括Javascript的定义和调用。

(2) JavaScript。行为,behavior。
JavaScritp应该存在于一个独立于XHTML文件的独立文件中。这样可以做自动化单元测试。JavaScript应该只改变HTML DOM的结构和内容,而不应该改变它的风格。

(3) CSS。Style,风格。或者说,Presentation,表现。
前面说了,XHTML里面不应该包括JavaScript的调用。那么,XHTML的元素是如何JavaScript事件绑定起来?就是在CSS里面指定的。
当然,众所周知,CSS的本职工作是处理页面风格。

页面资源方面,我完全认同Dlee的观点。从技术和资源积累的长远目标看来,这方面的初期投入的回报将是非常丰厚的。
即使将来HTML消亡了,进入了XAML, XUL, RSS时代,这些结构清晰的各部分,重用的可能性都非常巨大。JavaScript + CSS + XML UI的这种经典设计思路,将留存很久。混杂成一团的HTML的命运只能是全盘被抛弃。
4.页面模板层
页面模板层是指Server端运行的用来生成HTML(或JavaScript,CSS)的Server Side Template Engine。
这一层也是著名的脏乱差楼层。著名的HTML的Java代码污染事件,就发生在这个楼层。不仅JSP有这个问题,其他的template, 如freemarker, velocity, tapestry等含有逻辑的脚本,都不同程度上有HTML的Script Logic污染问题。

Dlee的做法很美。直接就不要页面模板层,不用Server Side Template Engine。直接用JavaScript更改HTML DOM的结构、内容、数据。同时,会用到少量的浏览器端XSL。
这样带来的结果,Template就是很干净纯粹的HTML,不含有任何Server Side Script。这个效果,和Servier Side Template 的 Jivan,XMLC达到的一样。只是一个是在浏览器端执行,一个是在Server端执行。

我研究比较了几乎所有的Server Side Template Engine,力图采众家之长,避众家之短,写了一个Server Side Template Engine -- fastm, 能够最优雅方便的实现页面模板层。关于fastm,我的Blog上有不少文章论述。
我的Blog,里面专门有个fastm 分类。
http://blog.csdn.net/buaawhl 
http://buaawhl.blogdriver.com

Fastm发布在java.net上。
https://fastm.dev.java.net


我仍然对Server Side Template Engine持肯定态度。基于如下原因:
(1) JavaScript代码量大、文件多的时候,不容易管理,不容易进行语法检查,不容易跟踪调试。
这里有人会争辩,Server Side Template Engine也用到了很多脚本阿,比如Freemarker, Velocity, 而且嵌在HTML中,怎么管理,怎么调试?即使是JSP,也是Java Code嵌在HTML里面,怎么管理,怎么调试?
这里我要说,Jivan, XMLC, fastm,Wicket等Template Engine的逻辑都是在Java Code里面。

(2) 用JavaScript生成文本内容,搜索引擎不友好。
一般的网络蜘蛛程序,只根据URL获取HTML文本,搜索里面的文本内容,而不会执行里面的JavaScript脚本。

(3) JavaScript代码重用还是有些局限
比如,有两个HTML文件,一个是Table布局,一个是List布局。
我有同样的一批数据,要在这两种布局中显示。
这时候,就要给这两个HTML分别写两套JavaScript。这里面的DOM层次,元素,属性都不同,再怎么定义ID,Class,也无法用完全相同的一套JavaScript处理。
这里有人会争辩,Server Side Template Engine也无法做到。别说JSP, Velocity, Freemarker等要在两套HTML里面嵌入相同的代码,就是Jivan, XMLC, Wicket也要分别写不同的两套Java Code,因为它们的XML DOM Node / Model View (Table, List) 都是不同的。
这里我要说。fastm可以做到只用一套代码逻辑。而且只有fastm可以。fastm的代码重用率是最高的。

关于Ajax(XMLHttp),我的意见是必要时才用,而且最好采用粗粒度的用法 -- JavaScript发出一个URL请求,返回一整段HTML,直接替换到页面的某一块,而不是用JavaScript来做这样的把数据填充到HTML DOM中。如果你直接在浏览器里面输入那个URL,也可以获取那整段的HTML内容。
典型的应用场合是Portal。Portal页面的每个Portlet都含有这样的 Ajax(XMLHttp) javascript代码 -- 发出一个Portlet URL请求,返回一整段Portlet的内容,直接替换当前的Portlet块。
这样做的好处是:
(1) 减少JavaScript代码的量和复杂度。
(2) 搜索引擎友好。网络蜘蛛程序可以辨别JavaScript中的URL,并根据这个URL,获取整段处理好的HTML文本,进行内容搜索。
有人可能会争辩:如果URL请求返回的是XML数据,不是整段处理好的HTML,搜索引擎也可以进行内容搜索。
这点我同意。前提是XML数据的内容是足够连贯的,而不是散落的。比如,你返回的XML数据是“中国”。这个“中国”要放在HTML中的一个{country}位置,{country}足球。这个时候,结果HTML的内容含有“中国足球”。而XML数据中只含有“中国”。如果用户用“中国足球”作为关键字来搜索,就找不到这个URL。

从前面给出的fastm资料的连接中,可以得知。如同Jivan, XMLC, Wicket一样,fastm的template里面不含有逻辑,所有的逻辑都写在Java里面。
有人会争辩说:页面逻辑写在Java里面,我改变了页面逻辑,还需要重新编译。这也太不方便了。Velocity, Freemarker, JSP就不用重新编译。
这里我的看法是:业务逻辑代码改变了,不也需要重新编译吗?页面逻辑就不是逻辑了吗?HTML里面的脚本怎么语法检查、跟踪调试?业务逻辑需要语法检查、跟踪调试,页面逻辑就不需要语法检查、跟踪调试了吗?
对方可能会说:在我的应用中,页面逻辑的改动需求非常频繁,而且这些页面逻辑非常简单,不需要语法检查、跟踪调试。
这里我的意见是:
(1) 那就使用JSP, Velocity, Freemarker等脚本。
(2) fastm, Jivan, XMLC, Wicket的Java代码部分也可以写在脚本里面,比如,Server Side JavaScript, Jython(Python),  Groovy, Bean Shell 等脚本语言都可以很方便的和Java相互调用。

fastm的生命周期将很长。
HTML, XUL, XAML都是,或将是可以在浏览器或可视化编辑工具里面显示的XML UI定义语言。Microsoft Office的Word, Excel, Powerpoint等格式都提供了相应的XML格式。这些XML文件都可以在Office里面显示,并编辑。
Adobe公司也提供了PDF的XML格式 -- XDP。可以在Adobe Designer里面显示并编辑。
由于fastm是Designer Friendly的XML UI所见即所得的模板技术。这方面具有很大的潜力。
根本不需要第三方花大力气专门做个IDE,来显示自定义的Tag。目标文件格式提供商自己的阅读编辑工具就可以直接用了,而且效果就是运行后产生的结果文件的效果。

即使没有可视化要求的场合。比如,Web Service需要的XML数据。fastm同样有用武之地。比如,
&lt;!-- BEGIN DYNAMIC: users --&gt;
&lt;user&gt;
  &lt;name&gt;{name}&lt;/name&gt;
  &lt;address&gt;{name}&lt;/address&gt;
&lt;/user&gt;
&lt;!-- END DYNAMIC: users --&gt;

可以很容易的把一个Java Object List转化为XML数据。

另外,我不得不承认。浏览器端的JavaScript的页面逻辑,可移植性要高于Server Side Template Engine。因为Server Side Template Engine通常是特定语言相关的。
目前fastm是用Java实现的。由于实现很简单,移植到其它的语言,也很简单。如果是移植到Python, Ruby等动态解释语言,那就更简单了。我是有这个考虑,因为Zope, Ruby on Rails 的模板还是Logic 和 HTML混杂的,fastm这个思路有很大的用武之地。

前面讲了这么多。清理了两层有名的脏乱差的老大难的楼层 -- 页面资源层和页面模板层。让这两层变得和下面的楼层同样的优雅、清洁。

下面该讲到Web框架层了。在向下讲之前,由于前面提到了脚本,我想先插入一段关于“可配置”、“可编程”、“可热部署”、“脚本逻辑 vs XML Tag逻辑”的话题。把这个人们比较关心、讨论比较多的话题,先讲清楚。
5.可配置、可编程、可热部署、脚本逻辑 vs XML Tag逻辑
由于Java是编译语言,人们通常把变化的参数部分抽取出来,放到配置文件中。
这些配置文件通常是XML文件。这很好,没什么问题。XML很适合用来表达数据结构。
但是,对于某一种技术的狂热,通常引起对这种技术的过度使用,或者误用。
人们开始觉得,XML能够表达一切东西,包括for, if, else等逻辑。这方面的典型例子有 Workflow XML Definition,Logic TagLib, XSL Logic Tag等。
这点我不敢苟同。我的看法是,XML不适合表达逻辑,XML表达逻辑非常蹩脚。XML表达逻辑相当于自定义一门XML格式的脚本语言。

比如,Logic Tablib,很难自然的支持 if else, switch。只能蹩脚地支持一堆 &lt;logic:if&gt; &lt;logic:ifNot&gt; &lt;logic:exists&gt; &lt;logic:notExists&gt; &lt;logic:ifNull&gt; &lt;logic:notNull&gt;。
(注,好久没有接触过Taglib了。这些Tag Name都是凭以前的使用印象写的,也许名字不对,但表达这些意思的TagLib都还是有的)
如果要表达if () else if() else 就更蹩脚了。要进行非常麻烦的嵌套。

再比如,XSL 支持if, else 也非常蹩脚。非要多出来一个层次才行。
&lt;xsl:choose&gt;
&lt;xsl:when test="…"&gt;
   …. If ….
&lt;/xsl:when&gt;
&lt;xsl:otherwise&gt;
    … else …
&lt;/xsl:otherwise&gt;
&lt;/xsl:choose&gt;

同样,如果要表达if () else if() else 就更蹩脚了。
&lt;xsl:choose&gt;
&lt;xsl:when test="…"&gt;
   …. If ….
&lt;/xsl:when&gt;
&lt;xsl:otherwise&gt;
&lt;xsl:choose&gt;
&lt;xsl:when test="…"&gt;
   …. If ….
&lt;/xsl:when&gt;
&lt;xsl:otherwise&gt;
    … else …
&lt;/xsl:otherwise&gt;
&lt;/xsl:choose&gt;
&lt;/xsl:otherwise&gt;
&lt;/xsl:choose&gt;

可以看到,XML Tag 表达逻辑,非常麻烦,可读性很差,完全是一种误用,没有半点优势。当然,逻辑简单的情况下,还是可以接受的。
有人会说:XML表达逻辑,可以免编译阿。
那么我说:语法检查呢,跟踪调试呢?
对方说:只是一些简单的逻辑,不需要语法检查、跟踪调试。
我说:如果只是为了免编译,前面列出的那么多的解释执行的脚本语言更适合。XML表达的逻辑,比Java等编译语言还要麻烦很多,而脚本语言比Java等编译语言简洁多了,可读性非常好,而且脚本语言和Java语言有很好的交互性,可以相互调用。重用、结构方面都具有优势。

有人会举出Spring IoC为例子,说:你看,Spring IoC的配置文件不都是XML格式吗?
我说:
(1) Spring IoC的配置文件基本都是属性设置,Bean ID声明。没有逻辑。
(2) 我也不是很赞同Spring IoC在XML配置文件里面引用Java类的做法。这方面,其它的容器如 Pico, Nano都支持多种配置方式,其中包括了不少脚本方式。我觉得,在脚本里面定义生成Java Object,比在XML中要好。当然,Web.xml里面也引用了Java Class名字。但那是非常简单的情况。没有嵌套引用、属性赋值、构造参数等复杂的定义方式。XML适合描述一些通用的资源、数据、结构。比如,HTML, XUL, XAML,RSS就是XML用的恰当的例子。

所以,我的基本观点是这样。
(1) 纯数据,不用说,应该定义在XML中。
(2) 如果是系统中一些Java Object要用到的基本属性。比如,连接池大小等。定义在properties, XML, Script中都可以。如果定义中没有出现具体的Java Class名,倾向于定义在properties, XML文件中。如果出现了具体的Java Class名,倾向于定义在Script中。这个界限不那么明显,两者皆可。
(3) 复杂结构的Java Bean的构造生成,那是肯定会出现具体的Java Class名,应该定义在Script中。

关于“可配置 vs 可编程”,有一点要明确:只要是可编程的,一定是可配置的。但如果是可配置的,却不一定是可编程的。
这里的可编程,是指框架给程序员提供了API;可配置,是指框架给程序员提供了配置文件的格式写法。
“可编程”一定是“可配置”的。
(1) 用户至少可以自己定义配置文件,读取参数,调用API。
(2) 有那么多的解释脚本可以直接和Java互操作,完全可以直接用来当作配置文件,定义参数。
“可配置” 却不一定“可编程”的。
如果框架只给你提供了配置方式,而没有API,那意味着,你只能进行参数的静态配置。很难在动态期间改变这些参数了。你总不能尝试着用代码去改变配置文件的内容吧?即使你改动了,如果框架不进行文件的时间戳检查,就是一开始装载进来,就不再检查更改了,你不就一点办法都没有了吗?
比如,Struts Tiles的XML定义,你只能静态配置,你想在运行期间改变布局,没有办法。Site Mesh也是如此。而我们可以在运行期间任意操作XML DOM Node,别说布局了,任何东西都可以改变。
所以,一个框架首要注重的是提供API,而不是提供配置方式。这是一个重要的原则。

讨论完了“可编程”、“可配置”问题,我们来看“热部署”问题。
XML配置文件、脚本文件支持“热部署”当然要比编译语言程序的热部署容易得多。只要解释执行前,检查一下时间戳就可以了。要注意的问题,只是做好测试,因为没有编译期的语法检查。
不过,Java程序也是可以“热部署”的。只是稍微麻烦一点。典型的例子是JSP, EJB Jar等。JSP修改之后,会自动编译执行;EJB Jar丢到EJB Container里面,会被检测到并装载到JNDI命名空间。
编译语言Java程序的热部署的一个可能的技术难点是,Class或者Jar已经存在,如何监测到Class或者Jar的更改,并装载这个新版本,替换旧版本。
这个问题我具体没有研究过。从道理上讲,应该在Class Loader上下功夫。如果需要,可以参阅开源EJB Container的相关实现部分。Java还有一种“Hot Swap”技术,专门解决这个问题,可以搜索查阅一下。

这段小插曲,就到这里。下面讨论Web框架。
6.Web框架
Web框架层是一个清洁的楼层。很多优秀的程序员在这一层大展身手,做出了很多好作品。我感觉不错的有Spring MVC, Web Work。
对于Web应用来说,Web框架层是最重要的一层。SOA、Semantic Web等效果都要在这一层实现。
首先,我们来讨论,框架的编程结构。
我的Blog中有一篇《Java Web框架综述》的文章。讲解了一些流行的Web框架的编程结构,很多重复的内容不再赘述。
http://blog.csdn.net/buaawhl

Java Web框架综述
http://blog.csdn.net/buaawhl/archive/2004/12/21/224069.aspx

Spring MVC的编程接口是最清晰的。大多数简单情况下,Web Work的用法是最简单有效的,编程结构比较特殊,可以说具有一定的变革意义。
Spring MVC的Controller接口相当于Struts Action,也具有Request, Response两个参数,虽然编程接口非常清晰优雅,但是本质上没有什么变化。
WebWork的Action则失去了Controller的身份,只相当于FormBean的身份,或者说相当于ActionBean的身份。WebWork Action不具有Request, Response两个参数,它只具有属性,并通过属性Setter获取HTTP Request的参数,通过属性getter把结果数据输出到HTTP Response。
可以说,WebWork的这个把握是相当到位的。95%以上的情况下,程序员是不需要Request, Response参数的。当需要这些参数的时候,WebWork并没有挡住路,可以通过实现RequestAware,ResponseAware等接口来获取,或者通过一个Thread Local获取。这种情况下,编程结构的约定,就不那么清晰了。

我从Canonical的帖子和Blog受到了很多启发。
http://canonical.blogdriver.com

jsplet:对Model 2模式的批判
http://canonical.blogdriver.com/canonical/591479.html

jsplet与webwork的概念对比
http://canonical.blogdriver.com/canonical/594671.html

从级列理论看MVC架构
http://canonical.blogdriver.com/canonical/579747.html

从Canonical的文章可以看出。JSPLet用JSP文件作为Dispatcher,然后在JSP里面注册并调用对应的Object。这个寻访Object的过程,完全是根据丰富的URL定义来做的。URL里面包括Object Scope, Object Name, Method Name, Method Parameters,天生就对事件机制有良好的支持。

Zope的一些做法也有异曲同工之妙。
Zope Object Publishing
http://www.zope.org/Documentation/Books/ZDG/current/ObjectPublishing.stx
http://www.plope.com/Books/2_7Edition/ZopeArchitecture.stx#2-3

这种通过URL获取Published Object的服务的思路,是一种实现SOA效果的有效思路。

我们首先来看Web Service的现状。目前Web Service主要分为两大阵营。SOAP和REST。关于REST,请参阅
http://www.xfront.com/REST-Web-Services.html
关于SOAP和REST的比较、互操作,网上有很多文章。如果需要请搜索查阅。

我个人比较倾向于REST风格的Web Service。
因为SOAP是一门固定的协议,如果用SOAP来编写Web Service程序,需要一个SOAP协议的解析库 ,也许还需要一些专门的“SOAP 数据 -- 编程语言”映射库,如同CORBA IDL的多语言映射一样。如果你要让自己的Web应用支持SOAP,你需要把发布的服务对象、方法都包装为SOAP协议,这需要一些编程语言相关的数据结构的映射工作。
REST则只是一种风格,而不是一个协议。中心思想是简单的通过丰富的URI定义 (如XLink + XPointer等) 获取资源。如果你要让自己的Web应用支持REST,那么很简单,只要在URI上下功夫就可以了,比如,多增加一个参数format=REST,在程序中多增加一种XML输出格式就可以了。(从道理上来说,SOAP也可以这么实现,但SOAP的输入和输出都要遵守SOAP协议,SOAP的输入参数一般都包装在SOAP信封里面)

关于HTTP Get和Post,我表述一下自己的看法。
我认为,Web的精髓在于Get,而不是Post,在于获取服务器的输出,而不是输入到服务器。即,Web的精髓在于以小搏大,四两拨千斤。最经典的用法就是用一个URL,获取一个长篇的文本内容,这个内容里面充满了其他更多的资源连接。这也是超文本连接HTML发明的初衷。
至于HTTP Post,则是这上面的一个扩展。B/S结构如此流行,很多应用都要转移到Web上面,怎么办,应用总是交互的,总要让用户输入数据吧,就增加了HTTP Post协议。
HTTP Get经典、简单、有效。可以用丰富的URI定义把这个优势发挥到极致。这个实现也比较简单、优雅。就不多说了。主要的难点在于HTTP Post。下面的讨论主要应对“HTTP Post”这个复杂现象。
HTTP Post从来就不让人们满意。当输入逻辑复杂到一定程度,表单数据的繁杂、凌乱、散落,到了服务器端很难组织起来。输入方面B/S结构确实和C/S结构难以匹敌。于是,出现了XMLHttp,能够把参数在浏览器里面组织成为一个统一的XML数据结构(或其他格式),发送到服务器端,一次解析出来。SOAP做这个方面,更是拿手好戏。所以,很多XMLHttp程序直接采用SOAP作为通信协议。而REST风格的HTTP Post则和HTML Form Post没有太大的本质区别。
REST在HTTP Get方面更胜一筹,SOAP在HTTP Post方面更胜一筹。可以根据Web应用的特点,根据HTTP Get / HTTP Post 页面的比例,选择适合的技术。
我们再进一步分析HTTP Post的数据内容。HTTP Post的数据,可能包含三种类型:
(1) 需要存档在服务器的数据
比如,用户注册时候,输入的基本信息,用户名、密码、电子邮件等。这些信息要存放到服务器的数据库。
对于这种基本信息,HTTP Post,XMLHttp,SOAP处理起来,难度都不大,没有很大区别。
B2B的数据交换,也属于这个类别。用何种技术区别不大。一般采用SOAP,因为SOAP是一种流行的标准协议。
(2) 服务调用参数
比如,用户进行复合条件查询的时候,输入的查询条件。这个时候,HTTP Post处理起来就非常蹩脚。而XMLHttp,SOAP则具有很大的优势。可以把复杂的查询条件很好组织成XML数据,发送到服务器端统一处理。SOAP里面甚至可以定义对象名、方法名等详细的调用信息。
(3) 指令
这种情况比较少见。上面的参数类别中提到的“对象名、方法名等详细的调用信息”,和这个指令类别有些交叉。
假如一个SOAP调用方法里面的参数也是一个自定义的对象,这个自定义对象的属性数据在SOAP信息中进行了定义。到了服务器端之后,服务端程序首先调用这个自定义参数的构造函数,生成这个参数对象,然后调用对应的服务对象,把这个参数传给服务。这个过程可以看作是一个顺序指令:[1]构造参数[2]调用服务。
这只是最简单的情况。而目前的Web Service一般也就支持到这个程度。
我的看法是,一不做,而不休。既然都把调用信息定义到这个程度了,不如做的更彻底一些,全面完善的支持指令。这个指令则意味着逻辑。前面讲过了,我不赞成用XML Tag表示逻辑,而赞成脚本。这里比较适合的脚本是JavaScript,因为JavaScript比较通用,客户端、服务器端都可以解释执行。注意,这里和一般的做法正好相反:一般的Web应用总是把JavaScript从服务器传到浏览器里面执行,而这里是把JavaScript在浏览器里组织好,发给服务器端处理;这个JavaScript将会在服务器端执行,调用服务器端的对象。举个SOAP含有JavaScript指令的例子 (只是示意,非标准格式) :
&lt;soap envelope&gt;
  &lt;XML Data&gt;
          &lt;a&gt;
               &lt;b&gt;12&lt;/b&gt;
          &lt;/a&gt;
          &lt;c&gt;
               &lt;d&gt;21&lt;/d&gt;
          &lt;/c&gt;
          &lt;e&gt;
               &lt;e&gt;16&lt;/e&gt;
          &lt;/e&gt;
  &lt;/XML Data&gt;

  &lt;script&gt;
        final_result = default;
        result1 = service1.service(a.b);
        if(result1.ok){
             result2 = service2.service(c.d);
             if(result2.ok)
                  final_result = service3.service(e.f);
        }
  &lt;/script&gt;
&lt; /soap envelope &gt;

这个好处是:
[1] 发布了更多的基本Service。给客户提供了更大的灵活度。
比如,这里就发布了3个Service。由用户自己组织逻辑。
按照传统的做法,上述流程将整个包装在服务器端执行。发布给用户的Service只有最外面的一个Service,而且高度耦合(if, else, if, else流程hard code在服务器端),不灵活,不通用。
这里的方法,就可以让客户端随意组织service1, service2, service3的调用顺序和方式。
[2] 减少了通信次数。
假如这段Script在客户端执行,那么和服务器要进行3次通信。

传统Web的权限控制一般在URL级别,这种script -&gt; server方式的权限控制则要在对象级别、方法级别、Code片断级别了,复杂很多,也许要大量应用Java的Code权限认证机制。

以上展开讨论了 Web Service,  HTTP Get/Post。下面我们回到Web框架层。
前面说了,JSPLet给了我很大的启发。很多思路可以借鉴。
当然,我并不赞成用JSP作Dispatcher, Controller。(1) 因为JSP要编译成Servlet,而Servlet是Web Server管理的比较昂贵的资源。一个Web系统中JSP达到几千个,就会遇到性能瓶颈。(2) JSP中的代码重用很成问题。一般只能通过include file的方式。
可以借鉴的思路。(1) JSPLet 的入口是JSP文件,这一步的URL到处理程序的映射是Servlet/JSP Container自然支持的。这是免配置的。(2) 丰富的URL参数定义,良好的对象方法寻址能力。

我开发的开源Web框架lightweb,将具备如下特性:
(1) 支持两个层次的编程接口。
interface Action {  void service(request, response, servletContext);  }
这个Action比Struts Action, Spring MVC Controller高一个级别。相当于Dispatcher, 相当于JSPLet的JSP控制文件。这个用来做最外层的入口控制。
同时,也支持简单的JavaBean.method的直接调用。相当于WebWork Action,JSPLet Registered Object。这个用来做具体的事情。

(2) 支持丰富的对象寻址URI,比如http://my.com/myProject/myModule/myEntry.action?object=calculator&method=add&p1=1&p2=3
这表示要通过 myEntry.acion这个入口,调用caculator.add(1, 2)方法。
如果用URL Rewriter可以美化为
http://my.com/myProject/myModule/myEntry/calculator/add/1/3
看起来就很象XLink + XPointer了。

(3) 免配置。或者说极少的配置。
框架根据一定的匹配准则,把myModule/myEntry.action映射到
com.mycompany.mymodule.MyEntryAction 这个类的service方法。
这个service方法负责根据object, method的名字,寻找到对应的bean,并根据参数进行属性设置验证,并执行对应的bean.method。然后,把这个bean作为Model和template结合,输出结果。
同样,template的获取也是根据一定的匹配准则,根据myModule/myEntry找到
Mymodule/myentry.html 或者Mymodule/myentry/calculator.html。

这样的lightweb就能够同时对应简单和复杂。复杂控制的需求交给Action接口来做,简单的一般具体任务交给普通Java Bean去做。
Web框架层可以做的非常复杂,可以做的非常简单。Lightweb的目标,就是分成多个简单的部分;各部分合起来就能够完成从非常简单到非常复杂的需求。
接下来,我们来看O/R。
7.O/R
Hibernate, EJB Entity Bean产品,JDO产品,iBatis是比较流行的几种O/R Mapping Framework。
我做的一些工作中,经常涉及到复杂的优化过的native SQL,并且涉及到大量的批量复杂逻辑处理,现有的O/R框架都不能满足功能和性能要求。

我做出这样一个lightor框架,思路借鉴了Martin Fowler的《企业架构模式》里面讲述的一些O/R的Row Mapper,  Column Mapper等概念。

最经典的用法是:
ResultSet rs = ps.executeQuery( a long complex native sql);
//will return a lot of records
A a = new A();
B b = new B();
IMapper aMapper = MapperService.getMapper(A.class);
IMapper bMapper = MapperService.getMapper(B.class);

While(rs.next()){
   aMapper.populate(a, rs);
bMapper.populate(b, rs);

  businessLogic(a, b);
}

可以看到,Lightor不需要一下子把所有纪录都放到一个Object List里面。完全可以随取随用。整个过程中,a, b只有一份,极大的节省了空间、时间,也极大的提高了开发效率,减少了重复代码。
没有任何一个其它O/R能够支持这种用法。这里面,lightor的mapper的populate方法需要ResultSet参数。一般的O/R不屑于这么做的,别说ResultSet,连Connection都想包装起来不给你看。

Lightor的设计思路也是同时应对简单和复杂。Lightor的Mapper实体部分是自动生成代码。类似于JDO的静态Enhance。不同的是,JDO静态Enhance直接修改bean class。而Lightor则不动原有的bean,只是多生成了对应的Mapper Source/Class。这种方式是最利于跟踪调试的。至于发布部署,和JDO的情况差不多,不如Hibernate的动态代码增强。
这里我很羡慕Python, Ruby等动态解释语言的

这一层我主要关注的是性能,缓存策略等等,而不是简便。我觉得,一个应用系统的瓶颈主要存在于O/R, DB层。不应该单纯为了追求OO结构的优雅,或者编程的方便,而牺牲了一些可能优化的地方。

关于Lightor的缓存策略, 我的Blog上有几篇文章。
http://blog.csdn.net/buaawhl

数据库对象的缓存策略
http://blog.csdn.net/buaawhl/archive/2004/12/21/224184.aspx

分页 & QueryKey & 定长预取
http://blog.csdn.net/buaawhl/archive/2005/01/08/245005.aspx
8.总结
我理想中的Web开发架构是这样的:
开发速度快,运行速度快,结构清晰优雅。
具体到每一层。
Web框架层主要追求 开发速度快。
O/R层主要追求 运行速度快。
页面资源层和页面模板层主要追求 结构清晰优雅。

posted @ 2009-06-16 15:44 gephen 阅读(367) 评论(0) 编辑