JSP是J2EE中最基本也是使用最广泛的Web开发技术,以致于从某种程度来说,很多开发人员认为 J2EE 就是JSP/Servlet再加上Struts/Spring/Hibernate等一些开源框架。JSF则是J2EE官方组织JCP所力推的新一代Web开发技术, 虽然前几年一直不愠不火,但近一两年来,隐隐然有要爆发之势。那么,到底什么是JSP?什么是JSF? 这两者之间又有什么差异呢? 本文试图对这两种技术进行一些简单的分析。
随着互联网的进一步发展,当今应用系统的主流架构已经从C/S过渡到B/S,同时,这也带来了一个巨大的困难: Web原本只是一种信息的发布媒介,而现在,人们期望Web能够成为一种新的应用形式的载体。Web所依赖的基础 http 与 html 已经不足以支撑这种技术架构的迁移。http,是一种简单的无状态的协议,为了克服这个困难,人们发明了 cookie、session等技术;html,原本只是一种简单的UI描述语言,为了让它拥有更好的人机体验,人们发明了JavaScript、 CSS、 DHTML等技术。但是,如何在服务器端动态的生成 Web 页面,长久以来,人们并没有非常好的解决方案。
在J2EE世界中,人们最开始发明了 Servlet 这种动态网页技术,但这种技术只是帮助我们简化了对 http 协议的处理。简单来说就是我们不需要再关心什么是GET或POST,我们只需要在 doGet或doPost方法中,对 response 输出流进行操作即可。但很快,人们发觉通过 "out.println("<html><body></body></html>");" 这种对response输出流直接操作的方法拼凑页面内容实在是有些不堪其烦,为了减化对输出流操作,人们又发明了 JSP 这种动态网页技术,默认的内容直接输出到response流中,而需要后台执行的代码片断则以<%%>括起来。
但万变不离其宗,无论 JSP/Servlet 技术如何变化,其核心思想一直没有变化,那就是:动态的拼凑HTML页面内容。
很快,人们发觉 Servlet/JSP 存在一个很大的不足:展现层与业务层纠缠在一起,开发与维护成本太高。试想,在一个网页中,大量的 <%%> 是多么的令人烦恼!为了解决这个问题,人们试图把 MVC 的思想引入到 Web 编程,于是出现了类似 Struts 的所谓 Model2 框架。但笔者从不认同 Struts 是一种好的解决方案。甚至于,笔者颇有些刻薄的认为:Struts的成功,并不是它的技术有多先进,而是因为它在一个恰当的时机披着 MVC 的外衣满足了一些人的需要而已,再加上 Apache 的品牌,于是造就了 Struts 的空前繁荣。
事实上,Struts带给我们的更多的是一种思想上的普及,它在技术上并没有解决太多问题。举个简单的例子,html tag在面对Tree、DataGrid等复杂组件便无能为力,FormBean在 AJAX交互中也失去了作用,至于为了导航而存在的 struts-config.xml 文件嘛,笔者也一直认为那是为了方便IDE而不是为了方便程序员的。
于是,有些人士便开始思索:为什么在C/S架构下,我们没有这么多的烦恼呢?
多年C/S架构下的UI开发,人们积累了大量的开发经验与智慧结晶,其核心是:
-
基于组件完成UI的搭建
-
基于事件调用业务逻辑
但在B/S架构下,人们忽然发觉:这些开发经验与知识积累一夜之间失去了用武之地。 当Web仅仅承担一种信息的发布媒介时,我们还能够容忍自己通过诸如 jsp/servlet 等动态网页技术进行html流的拼凑,但是,当Web已经成为一种新的应用载体, 当浏览器已经成为人们的主要工作平台,尤其是当多种浏览器纷芸而来而我们又试图兼容包并之时, 人们从来没有像今天这样苦恼过Web下UI的开发。
那么,是否能够从 C/S 架构下积累的经验与智慧结晶中,获得一种针对 B/S 架构下的解决方案呢?
这就是 JSF(笔者从不排斥还有其它的解决方案,但本文只谈JSF,毕竟JSF是Java官方组织JCP的主推标准)。
2001年中期,J2EE社区中的几位知名人士,Amy Fowler、Ed Burns和Craig McClanahan(注意,此人同时也是 Struts 的创始人)聚在一起,发起了JSR 127,经过JCP的详细审查和投票,2004年3月,JavaServer Faces规范和参考实现正式公开发布。在 JSR 127中,产生了两个版本的 Specification,分别是 JSF 1.0 和 JSF 1.1。
在JSF 1.1诞生之后,JCP又发起了 JSR 252,并在2006年5月,发布了 JSR 252的 Final Release,这就是 JSF 1.2。
那么,到底什么是 JSF ?它又试图解决什么问题呢?不妨来看一下在规范中对JSF的定义:
JavaServer Faces (JSF) is a user interface (UI) framework for Java web applications. It is designed to significantly ease the burden of writing and maintaining applications that run on a Java application server and render their UIs back to a target client. JSF provides ease-ofuse in the following ways:
-
Makes it easy to construct a UI from a set of reusable UI components
-
Simplifies migration of application data to and from the UI
-
Helps manage UI state across server requests
-
Provides a simple model for wiring client-generated events to server-side application code
-
Allows custom UI components to be easily built and re-used
不妨简单翻译一下(颇有凑字赚钱之嫌):
JavaServer Faces(JSF)是一种基于Java Web应用的UI层框架。它的目标是大幅降低B/S应用的开发与维护成本(注意:这里的B,并不仅仅指代运行于 PC 的 Browser,JSF是可以跨设备的;这里的 S,自然指J2EE应用服务器)。JSF在以下方面提供了便利:
-
通过可重用的UI组件来简化UI界面的构建(点评:重点之一,组件)
-
简化应用数据的展现与收集(点评:意指在应用逻辑层与界面UI层之间有一种很好的粘接,即ManagedBean)
-
简化在每次请求之间维护UI状态(点评:真是成也萧何,败也萧何。JSF对UI状态的维护,使我们简化了应用的开发模式;但这也成为很多人对JSF的诟病之一)
-
提供一种简单的模型,能够将客户端事件包装成服务器端事件(点评:重点之二,事件)
-
允许用户能够很方便的开发并重用自定义组件(点评:重点依然是,组件)
好了,至此,我们可以看到:JSF确实是将 C/S 架构的开发模型拿到了 B/S之上(也许这句话并不确切,但有一定的道理)。
那么,这种“用C/S架构的开发模式来开发B/S应用”的解决方案,是否可行?又有怎样的利弊呢?
坦白说,笔者个人认为:这跟规范本身关系不大,而跟众厂商对规范的具体实现,甚至跟用规范的具体实现来构建应用的开发者相关。
规范是最聪明,最偷懒,也是最无辜的,规范认为:B/S之间是无状态的,但通过对UIState的维护,这道鸿沟就被抹平了; 但如果某厂商的具体实现很糟糕,为了维护UIState需要消耗大量的带宽或者计算能力,那么,最终构建出来的应用自然是不可接受的; 而更有趣的是,规范希望:对页面开发者来说,这种对UIState的维护是透明的(接自JSF 1.2:“For the page author, state management happens transparently”),但倘若页面开发者真的认为JSF抹平了B/S架构之间的鸿沟,真的把B/S应用当成C/S架构来开发,那么, 你开发的应用自然也好不到哪里去。
OK,对JSF利弊的讨论,不是本文的重点,网上有大把口水类文章,本文试图说点实在的:JSP与JSF之间的编程差异。
很多从事过多年JSP开发的同学,乍一使用JSF,浑身不对劲。他们已经习惯了在页面中大量嵌入 <% %>的形式。坦白说,这不是一种好的开发模型,但同时,这又是一种无所不能的开发模型。所以,很多同学一遇到JSF这种 <w:textField value="#{myBean.someValue}"/> tag,就有些无所适从。萦绕在他们心中的问题是:
1. 我还能够在JSF的页面中嵌入 <% %> 代码片断吗?
2. 我需要动态控制页面中的控件,该怎么办?
3. JSF与JSP能混合使用吗?
其实,只需要理解了“JSF的渲染机制”,这些疑问都将一通百通。
浏览器是需要一堆由 html/javascript/css 等组成的页面内容,才能够将其展现成一张网页。而这些页面内容,是通过 http 从服务器端获得的。
JSP是怎样生成这些网页内容的?是将JSP编译成Servlet,再将 Servlet 编译成 class,在class中通过 “out.print”等手段对页面输出流进行操作,动态拼凑成页面内容。因此,你在 <% %>中嵌入的任何代码,其实都会直接影响到对输出流的操作。
而JSF输出的页面内容,则是由组件负责渲染的。举个简单的例子,你在JSF页面中放一个<w:textField value="#{myBean.someValue}"/>,最终,会在输出的页面内容中出现类似这样一行代码:<input type="text" value="theValue"/>。而这行代码的输出,是由组件负责生成的。组件在何时进行渲染输出?是JSF引擎在接收一个request 请求,构建或重新恢复组件树后,在“Render Response”阶段中产生的。说的再白一些,对JSF来说,它的页面就是一棵组件树,其根节点就是<f:view> (试想, Swing/SWT又何尝不是这样的呢?只不过 Swing 组件树的根节点可能是JFrame罢了)。 因此,JSF就需要一种“组件树的描述技术”。在 JSF 规范制定最初,确实是希望把 JSP 的 tag-lib 作为“组件树描述技术”的,但最终发觉,这两者之间的编程思想差异太大,以致于,用JSP作为JSF的“组件树描述技术”, 会给用户造成很大的误解(这又引出了另一个问题:JSP并不试合作为JSF页面的载体,Facelets 才是JSF更好的外衣,本文暂且不谈,另有文章介绍)。
换言之:在JSP/Servlet中,你可以直接通过页面输出流进行操作;而在JSF中,是由组件通过页面输出流负责渲梁; 这两者之间是有差异的。因此,你不能在一个 JSF 页面中通过 <%%>这种方式改变渲染结果,否则,这两种不同的方式会打架的。
由此,引出了第二个问题:即然你不允许我通过<%%>来控制页面输出结果,那么,如果我需要动态控制页面中的控件,该怎么办?
答案是:你可以通过类似于 jstl 的解决方案;或者通过组件的服务器端API。
举个简单的例子:在进行网上支付时,我们可能会选择信用卡或者银联卡,无论你选择哪种类型的卡片,都会导向到同一个支付页面, 但如果选择的是信用卡,除了需要输入卡号以外,还需要多输入卡片背面末三位。也就是说:根据用户的不同选择, 页面内容要有不同的变化。

为了让 JSF 做到“动态控制页面中的控件”,你有两种选择。
让我们直接看一下代码片断(注意:这里用到的IoVC、Facelets请暂且全部忽略):
paymentType.xhtml
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
xmlns:h="http://java.sun.com/jsf/html" xmlns:ajax="http://www.apusic.com/jsf/ajax"
renderKitId="AJAX">
<w:head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</w:head>
<w:page title="选择支付类型">
<div align="center" style="font-size: 11pt; padding-top: 20px;">
<w:form>
<h3>请选择支付类型</h3>
<w:radioGroup direction="h" id="type">
<f:selectItems id="types"/>
</w:radioGroup>
<w:button id="next"/>
</w:form>
</div>
</w:page>
</f:view>
payment.xhtml
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
xmlns:h="http://java.sun.com/jsf/html" xmlns:ajax="http://www.apusic.com/jsf/ajax"
renderKitId="AJAX" xmlns:c="http://java.sun.com/jstl/core">
<w:head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</w:head>
<w:page title="支付">
<div align="center" style="font-size: 11pt; padding-top: 20px;">
<w:form>
<layout:panelGrid columns="2">
<layout:cell>请输入卡号:</layout:cell>
<layout:cell>
<w:textField id="cardNum"></w:textField>
</layout:cell>
<c:if test="#{paymentBean.type.equals('creditCard')}">
<layout:cell>卡片背面末三位:</layout:cell>
<layout:cell>
<w:textField id="last3Num"></w:textField>
</layout:cell>
</c:if>
</layout:panelGrid>
<w:button id="ok"/>
</w:form>
</div>
</w:page>
</f:view>
PaymentBean.java
@ManagedBean(name = "paymentBean", scope = ManagedBeanScope.SESSION)
public class PaymentBean implements Serializable {
@Bind
@ManagedProperty
private String type = "creditCard";
@Bind
@SelectItems
private SelectItem[] types = new SelectItem[] {
new SelectItem("creditCard", "信用卡"), new SelectItem("unionPay", "银联卡")};
@Bind(id="cardNum", attribute="value")
private String cardNum;
@Bind(id="last3Num", attribute="value")
private String last3Num;
@Action(id="next")
public String next(){
return "view:redirect:payment.xhtml";
}
@Action(id="ok")
public void ok(){
System.out.println("您选择的是通过 '" + type + "' 进行支付,您的卡号是:'"
+ cardNum + "'. ");
if("creditCard".equals(type)) {
System.out.println("您的信用卡背面末三位是:'" + last3Num + "'.");
}
}
}
我们可以观察到,在 payment.xhtml 中,为了动态判断是否在页面中显示“卡片背面末三位”的组件,我们可以通过 <c:if/>进行条件判断。 除了<c:if/>外,Facelets还提供了<c:forEach/>、 <c:catch/>、<c:set/>等tag供你使用。
还需要提及的是,在Apusic OperaMasks(目前你可以把它当成JSF的一种具体实现)中,由于 ELite 的存在,你甚至可以获得类似于<%%>的Power!请参考后续文章。
除了 jstl 这种解决方案以外,你还可以通过“组件的服务器端API”达到相同的目的,请参考下述代码:
paymentType2.xhtml
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
xmlns:h="http://java.sun.com/jsf/html" xmlns:ajax="http://www.apusic.com/jsf/ajax"
renderKitId="AJAX">
<w:head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</w:head>
<w:page title="选择支付类型">
<div align="center" style="font-size: 11pt; padding-top: 20px;">
<w:form>
<h3>请选择支付类型</h3>
<w:radioGroup direction="h" id="type">
<f:selectItems id="types"/>
</w:radioGroup>
<w:button id="next"/>
</w:form>
</div>
</w:page>
</f:view>
payment2.xhtml
<f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core"
xmlns:w="http://www.apusic.com/jsf/widget" xmlns:layout="http://www.apusic.com/jsf/layout"
xmlns:h="http://java.sun.com/jsf/html" xmlns:ajax="http://www.apusic.com/jsf/ajax"
renderKitId="AJAX">
<w:head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</w:head>
<w:page title="支付">
<div align="center" style="font-size: 11pt; padding-top: 20px;">
<w:form>
<layout:panelGrid columns="2" id="panelGrid">
<layout:cell>请输入卡号:</layout:cell>
<layout:cell>
<w:textField id="cardNum"></w:textField>
</layout:cell>
</layout:panelGrid>
<w:button id="ok" />
</w:form>
</div>
</w:page>
</f:view>
Payment2Bean.java
@ManagedBean(name = "payment2Bean", scope = ManagedBeanScope.SESSION)
public class Payment2Bean implements Serializable {
@Bind
@ManagedProperty
private String type = "creditCard";
@Bind
@SelectItems
private SelectItem[] types = new SelectItem[] {
new SelectItem("creditCard", "信用卡"), new SelectItem("unionPay", "银联卡")};
@Bind(id="cardNum", attribute="value")
private String cardNum;
@Bind(id="last3Num", attribute="value")
private String last3Num;
@Bind(id="panelGrid")
private HtmlPanelGrid panelGrid;
@Action(id="next")
public String next(){
return "view:redirect:payment2.xhtml";
}
@Action(id="ok")
public void ok(){
System.out.println("您选择的是通过 '" + type + "' 进行支付,您的卡号是:'"
+ cardNum + "'. ");
if("creditCard".equals(type)) {
System.out.println("您的信用卡背面末三位是:'" + last3Num + "'.");
}
}
@BeforeRender
public void beforeRender(boolean postBack) {
if(panelGrid != null && !postBack && "creditCard".equals(type)) {
Application app = FacesContext.getCurrentInstance().getApplication();
HtmlGridCell cell = (HtmlGridCell) app.createComponent(HtmlGridCell.COMPONENT_TYPE);
panelGrid.getChildren().add(cell);
HtmlOutputText lab = (HtmlOutputText) app.createComponent(HtmlOutputText.COMPONENT_TYPE);
lab.setValue("卡片背面末三位:");
cell.getChildren().add(lab);
cell = (HtmlGridCell) app.createComponent(HtmlGridCell.COMPONENT_TYPE);
panelGrid.getChildren().add(cell);
UITextField txt = (UITextField) app.createComponent(UITextField.COMPONENT_TYPE);
cell.getChildren().add(txt);
txt.setId("last3Num");
}
}
}
![]() |
当页面要被渲染时,会调用被 @BeforeRender 所标注的方法。在此时,我们会根据选择的卡片类型,来决定是否动态生成新的组件。 |
![]() |
获得 Application,所有组件的创建都应该通过 Application 进行。 |
![]() |
创建一个 Cell,并将其加入到 PanelGrid中。 |
![]() |
创建一个 OutputText,并将其加入到Cell中。 |
![]() |
创建一个 TextField,并将其加入到 Cell中。 |
事实上,通过服务器端API来动态控制页面内容,更类似于 C/S 架构下的编程模型。
最后,再回到第三个问题:JSF与JSP能混合使用吗?
当然可以!
有一个一直让我莫名其妙的问题是:很多同学有“健忘症”,他们一旦使用 JSF 来开发Web应用,转眼间,积累了那么多年的 JSP/Servlet 知识好像全都忘记了。
经常有同学问我:“我想做一个重定向页面怎么办?”
我说:“没有JSF之前你是怎么做的?”
回答:“很简单啊,只要在JSP中调一下response.sendRedirect 就可以了啊!”。
我说:“那你就写一个专门做重定向的JSP页面不就得了!”
至此才恍然大悟。
事实上,笔者强烈建议:用 Facelets作为JSF页面的描述载体(view-handler),而把JSP页面就当成普通的JSP来用。
<c:if test="#{paymentBean.type.equals('creditCard')}">
Application app = FacesContext.getCurrentInstance().getApplication();
HtmlGridCell cell = (HtmlGridCell) app.createComponent(HtmlGridCell.COMPONENT_TYPE);
HtmlOutputText lab = (HtmlOutputText) app.createComponent(HtmlOutputText.COMPONENT_TYPE);
UITextField txt = (UITextField) app.createComponent(UITextField.COMPONENT_TYPE);