一、概述
构造出满足商务活动要求的Web网站并非易事。现有的Java技术——JSP、Servlet和JavaBean,各有自己的优点,通常,我们需要结合运用这些技术以达到最好的效果。虽然只用JSP技术我们也可以构造出一个简单的购物篮,复杂的商务应用需要所有这三种技术的相互补充。下面我们就来看看如何结合运用这些技术获得最好的效果。
与Microsoft私有的ASP技术相对,JSP(JavaServer Pages)提供了一种100%纯Java的替代方案。JSP技术从Java Servlet技术扩展得到。实际上,运行时,JSP框架将把JSP页面转换成Servlet。与CGI脚本相比,Servlet因其体系和性能上的优势受到欢迎。Servlet也能够生成动态Web页面,能够合并静态HTML内容和数据库查询以及其他业务服务提供的动态内容。JSP构造动态网页的思路恰好和Servlet相反,它是在HTML中嵌入Java代码。这种在HTML页面中嵌入Java代码的能力为构造基于Java的Web应用系统带来了更多的灵活性。
为输出HTML,Servlet必须在println()调用中提供格式化的字符串。由于在Java代码中嵌入了大量的HTML,这种处理方式使得Java代码看起来比较混乱。另外,用Servlet生成HTML时,Web页面的设计也需要程序员的参与。JSP从Java代码分离出了HTML,使专职的HTML设计更容易实现,使网站开发更容易分离成两个独立的部分——Java设计和HTML设计,从而提高了构造网站的效率。JSP技术还能够促进业务逻辑组件与表现组件的宽松结合,方便了这两种组件的重用。本文通过一个购物篮应用探讨JSP、Servlet、JavaBean在Web应用中的角色,提供了结构化设计商务应用的实践范例。
二、购物篮概况
我们设想的购物篮用于简单的在线商店。顾客选择产品加入购物篮,再通过一系列的表单购买产品。图一显示出我们的应用由JSP、Servlet和JavaBean构成。虽然只用JSP也可以构造出简单的Web应用,但业务逻辑比较复杂的应用需要这三者的协同。
图二显示了Model-View-Controller(MVC)模式。MVC模式把应用分割成三个分离的部分:数据管理部分(Model),表现部分(View),和控制部分(Controller)。MVC模式是许多现代GUI应用的基础。这种分割有利于应用各个部分独立地发展和重用。MVC模式也可以用于Web应用,包括本文的应用。JSP最适合于实现Web应用的表现部分;JavaBean封装为网站提供动态内容的服务,简化应用各个部分之间的数据传递;Servlet做Controller最合适,控制用户请求和应用消息的传递,更新应用数据,控制应用流程。
虽然象JSP这样的技术鼓励特定的设计思想,但并不强制采用。例如,所有放入Servlet和Bean的代码同样可以放入单个JSP页面,虽然这会导致JSP页面的代码非常混乱,但JSP规范允许这种设计。另一方面,任何JSP页面能够做到的事情,Servlet也能够做到,也就是说,我们可以构造出一个完全不用JSP的Web应用系统。然而,采用设计模式意味着采用了特定的设计思路和策略。设计模式是众多开发者集体智慧的结晶,是众多开发者长期探索的成果。如果我们采用MVC模式,则这个模式要求我们不应该把应用的表现部分和控制、数据部分混合起来。具体地说,我们不应该从控制组件(Servlet)输出HTML,也不应该在表现组件(JSP)中混入控制逻辑。我们应该把JSP页面中的Java功能局限于和控制、数据组件的通信。最后,如果应用的数据模型非常复杂(在任何现实的商务应用中,情况正是如此),那么,我们不应该在表现组件和控制组件中混合数据和计算逻辑;相反,我们应该把数据和计算逻辑封装到JavaBean之中。
三、控制部分
决定了购物篮应用的设计思路之后,接下来我们来看看这个应用的设计细节问题。Listing 1显示了CustomerServlet类的doPost()方法。CustomerServlet通过两方面的工作控制应用的工作流程:维护购物篮组件(由BasketBean类实现)的状态(Model),在一系列JSP页面之间传递顾客的请求。由于购物篮总是被关联到某个特定的客户会话,因此我们在HttpSession对象中保存顾客的BasketBean实例。HttpSession会话对象为我们用唯一的键值保存和提取任意类型的Java对象提供了方便的方法。
【Listing 1:控制部分以Servlet的形式实现】
// 处理客户请求
public void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// 提取出该请求的会话对象
HttpSession session = request.getSession(true);
BasketBean basket = null;
/*
或者创建一个新的购物篮,
或者更新现有的购物篮
*/
basket = (BasketBean)session.getAttribute(
BasketBean.BASKET);
if(basket == null) {
// 新的购物者,创建一个购物篮
basket = new BasketBean();
session.setAttribute(BasketBean.BASKET, basket);
}
else {
// 已有的购物者,保存购物篮的状态信息
basket.savePurchases(request);
}
// 获得工作流程的当前状态
RequestDispatcher rd = null;
String nextPage = request.getParameter(BasketBean.PAGE);
/*
紧密结合版本:
根据客户所处的状态,确定下一个JSP页面,
或者结束当前的购物会话。Servlet清楚工作流程
中每一个JSP页面的具体情况
/*
if (nextPage == null || nextPage.equals(BasketBean.UPDATE)) {
// 从库存目录选择
rd = getServletConfig().getServletContext()
.getRequestDispatcher("/jsp/Inventory.jsp");
}
else if (nextPage.equals(BasketBean.PURCHASE)) {
// 提供购买信息
rd = getServletConfig().getServletContext()
.getRequestDispatcher("/jsp/Purchase.jsp");
}
else if (nextPage.equals(BasketBean.RECEIPT)) {
// 提供购买信息
rd = getServletConfig().getServletContext()
.getRequestDispatcher("/jsp/Receipt.jsp");
}
// 把请求传递到合适的JSP页面
if (rd != null) {
rd.forward(request, response);
}
}
在CustomerServlet中,我们首先用true参数值调用request.getSession()方法,从Servlet框架获取会话对象。true参数值表示,如果会话对象还不存在,则我们要求Servlet框架创建一个。接下来,我们尝试从会话对象获取购物篮。如果不能得到购物篮,则表明我们刚刚开始一次购物会话,必须新建一个购物篮并把它保存到会话对象;如果我们获得了购物篮,则表明我们正处于一次会话的中间过程,应该保存购物篮的状态信息。
处理好购物篮的状态之后,我们把客户的请求传递给合适的JSP页面。请求本身包含了一个表示状态的参数(BasketBean.PAGE),这个参数告诉CustomerServlet应该把请求传递到哪里。控制器提取出这个参数,然后利用一个RequestDispatcher对象把请求传递到下一个JSP页面。
四、Model部分
Listing 2显示了BasketBean如何为本文的应用实现一个简单的数据管理器(Model)。BasketBean类提供了一个客户所购产品总价的方法,以及一个更新购物篮内容的方法。它在一个散列表products_中维护着一个客户所购Product的列表,散列表以SKU编号为键。一个InventoryBean对象管理着数组形式的Product实例的目录。每一个Product实例保存四个属性:产品名称,SKU编号,每单位产品的价格,购买产品的数量。只有当数量大于0时Product才会被加入。
Listing 2:以JavaBean的形式实现应用的Model
public class BasketBean {
final static public String BASKET = "Basket";
final static public String PAGE = "Page";
/*
工作流程的状态
*/
final static public String UPDATE = "Update";
final static public String PURCHASE = "Purchase";
final static public String RECEIPT = "Receipt";
/*
当前购物篮中的产品
键:SKU# 值:Product
*/
private Hashtable products_ = new Hashtable();
public BasketBean() {
}
/*
计算购物篮内产品的总价
*/
public double getTotal() {
double totalPrice = 0.0;
Enumeration e = products_.elements();
while(e.hasMoreElements()) {
Product product = (Product)e.nextElement();
totalPrice += product.getPounds() * product.getPrice();
}
return totalPrice;
}
/*
获得购物篮内特定产品的数量
*/
public double getPounds(Product p_in_inv) {
int SKU = p_in_inv.getSKU();
Product p = (Product)products_.get(Integer.toString(SKU));
if(p == null)
return 0.0;
else
return p.getPounds();
}
/*
用当前选择的内容更新购物篮状态
*/
public void savePurchases(HttpServletRequest request) {
Product[] products = InventoryBean.getCatalogue();
String[] lbValues = request.getParameterValues("pounds");
if (lbValues != null) {
products_.clear();
for (int i = 0; i < lbValues.length; i++) {
double lbs = Double.parseDouble(lbValues[i]);
if(lbs > 0) {
Product p = null;
p = (Product)products[i].clone();
p.setPounds(lbs);
products_.put(Integer.toString(p.getSKU()), p);
}
}
}
}
/*
辅助方法。根据double值生成二位小数的字符串
*/
public static String getStringifiedValue(double value) {
String subval = "0.00";
if (value > 0.0) {
subval = Double.toString(value);
int decimal_len = subval.length() - (subval.lastIndexOf('.') + 1);
if(decimal_len > 1)
subval = subval.substring(0, subval.lastIndexOf('.') + 3);
else
subval += "0";
}
return subval;
}
/* 清除购物篮内容 */
public void clear() {
products_.clear();
}
}
五、View部分
在我们设想的应用中,购买过程分四个步骤,共三个JSP页面:Inventory.jsp,Purchase.jsp,以及Receipt.jsp。请参见图三。应用把Inventory.jsp显示给新到访的客户。客户通过对Inventory.jsp页面的一次或多次更新选择产品。选择好要购买的产品之后,客户购买产品,应用显示出Purchase.jsp。最后,客户确认购买操作,应用显示出Receipt.jsp。
JSP页面由标准的HTML元素和JSP元素混合构成。JSP规范把页面中的静态HTML称为模板,实际上,静态模板将被直接写出到HTTP应答流(根据引用和转义规则进行适当的转换)。例如,SERVLET框架不经修改就把标记直接写入到应答流。除了静态模板之外,JSP页面还可以包含指令、脚本元素和动作。本文的WEB商店要用到所有这些元素。指令的作用是向JSP框架发布命令,语法如下:
<%@ 指令 %>
page指令告诉JSP框架按照指定的方式配置环境。例如,Inventory.jsp页面用到了一个page指令:
<%@ page buffer="5kb"
language="java"
import="jsp_paper.*"
errorPage="Error.jsp" %>
这个指令告诉JSP框架,在发送输出流的内容之前先缓冲5K内容。另外,这个指令还告诉JSP框架,本页面的脚本语言是Java。这个page指令还要求JSP框架从jsp_paper包导入所有的类。最后,这个page指令命令JSP框架把所有未处理的异常重定向到Error.jsp。
include指令要求JSP框架在转换时把指定的内容插入到页面的输出结果中。Inventory.jsp页面用到了如下include指令:
<%@ include file="header.html" %>
<%@ include file="footer.html" %>
上面的第一个指令插入了标准的页头,第二个指令插入了标准的页脚。我们用include指令实现各个JSP页面统一的外观和风格。
脚本元素在JSP页面中嵌入代码。本文用到的脚本元素包括:声明,Scriptlet,表达式。如下所示:
<%! 声明; %>
<% scriptlet %>
<%= 表达式 %>
Inventory.jsp页面示范了所有这三种元素的用法。下面的JSP代码片断声明局部变量,用来保存当前的购物篮(BasketBean实例)和Product目录:
<%!
BasketBean basket;
Product[] catalogue;
%>
JSP声明必须以分号结束。JSP声明的作用范围是JSP页面。
声明这些局部变量之后,Inventory.jsp页面利用一个Scriptlet从会话对象获取购物篮对象(BasketBean),从InventoryBean获取产品目录。
<%
basket =(BasketBean) session.getAttribute( BasketBean.BASKET );
catalogue = InventoryBean.getCatalogue();
%>
JSP声明和JSP Scriptlet是位于特殊JSP标记内的Java代码。当JSP框架把JSP页面转换成Servlet时,它将把这些Java代码合并到新的Servlet里面。
我们用来获取购物篮的会话对象是一个隐含的对象。JSP框架允许我们直接访问某些Java对象,无需事先声明它们,这些对象就是所谓的隐含对象。JSP规范列出了所有这些隐含的对象。在CustomerServlet中,我们调用request.getSession(true)时返回一个session对象,它和这里的会话对象是同一个对象。另一个也属于此类的对象是HttpServletRequest对象(request),在CustomerServlet中这个对象作为参数传递给doPost()方法。因此,Inventory.jsp页面可以调用request.getSession(true).getAttribute(BasketBean.BASKET)提取出购物篮。
结合运用Scriptlet和JSP表达式为我们编写动态Web页面提供了强大的工具。在下面Inventory.jsp页面的片段中,我们通过循环依次访问清单中的每一种产品,动态为每一种产品生成HTML表格行(请参见Listing 3)。我们用Scriptlet声明循环及其边界,混合运用HTML和JSP表达式生成HTML表格的行。
Listing 3:动态生成HTML表格
<%
for(int i = 0; i < catalogue.length; i++) {
Product product = catalogue[i];
%>
<TR>
<TD>
<%= product.getSKU() %>
</TD>
<TD>
<%= product.getName() %>
</TD>
<TD>
<INPUT TYPE=text SIZE= 6 MAXLENGTH= 6 NAME= "pounds"
VALUE= <%= basket.getPounds(product) %>>
</TD>
<TD ALIGN=right>
<%= product.getPrice() %>
</TD>
</TR>
<% } %>
JSP框架把Scriptlet标记内声明的Java代码直接插入到后来生成的Servlet代码。但对于JSP表达式,JSP框架以不同的方式处理。在把JSP页面转换成Servlet时,JSP框架首先把JSP表达式转换成字符串,然后把它们嵌入到out.println()调用。Scriptlet支持条件判断和迭代,而JSP表达式则支持数据提取和格式化。
除了指令和脚本元素之外,JSP动作进一步完善了JSP页面语言。Receipt.jsp页面利用JSP动作来管理客户请求发送来的参数值。动作具有如下两种基本语法形式:
<前缀:标记 属性列表 />
<前缀:标记 属性列表> body </前缀:标记>
如果动作有一个体,则必须使用后面一种形式。动作的基本功能是把“标记句柄”关联到特定的JSP标记,这些句柄是以标记为基础执行某些操作的代码。JSP框架提供了几种标准的动作,所有这些动作的名字都带有“jsp:”前缀。例如,本文的在线商店利用一个辅助Bean简化请求参数的分析,我们利用元素创建这个辅助Bean:
<jsp:useBean
id="receiptBean"
scope="request"
class="jsp_paper.ReceiptBean"
/>
上面的代码创建了一个名为receiptBean的对象变量,它是ReceiptBean类的一个实例,作用范围为当前的请求。
象receiptBean这类Bean的主要优点在于简化HTML请求参数的分析和提取工作。声明了Bean之后,我们用元素把它的属性值设置为对应的HTML请求参数的值。属性值可以单独设置,即显式地指定属性的名字和HTML参数的名字。例如,下面几行来自Receipt.jsp页面的代码设置ReceiptBean实例的name属性:
<jsp:setProperty name="receipt_bean"
property="name" param="name"
/>
因为本例中所有Bean属性的名字与对应的参数名字相同,我们还可以用单个JSP元素设置所有的属性:
<jsp:setProperty name="receipt_bean"
property="*"
/>
上面这个元素告诉JSP框架,利用Java映像机制对所有参数名字和JavaBean属性的名字进行匹配,并把JavaBean的属性值设置为对应的HTML请求参数值。
我们用元素从辅助Bean提取属性。例如,下面的代码提取出name属性:
<jsp:getProperty name="receipt_bean"
property="name"
/>
与前面相似,Java映像机制对JSP元素和JavaBean方法名字进行匹配。为了让映像操作能够正常进行,JavaBean必须遵从特定的编码规范。在ReceiptBean类中,每一个在Receipt.jsp中使用的属性都有关联的set方法和get方法(参见ReceiptBean源代码)。
例如,下面的JSP代码:
<jsp:setProperty name="receipt_bean"
property="name" param="name"
/>
有如下对应的set方法:
void setName(String phone);
又如,JSP代码:
<jsp:getProperty name="receipt_bean"
property="name"
/>
有对应的get方法:
String getName();
六、改进应用的Model
本文的应用很简单,显然只能算是一个试验品。尽管如此,实际的应用也可以象本文的简单应用那样采样MVC模式。下面我们来看看如何对本文的应用进行扩展,构造出一个更富实用性的电子商务应用。
本文的应用通过BasketBean类实现Model部分。BasketBean类有两方面呈现明显的试验性质:首先,它的数据以“硬编码”的方式提供;其次,没有能够定义一个标准化的接口。这些缺点限制了应用的可维护性、可扩展性和可伸缩性。
一个正式的应用应该为访问应用的Model部分提供一个标准的接口。定义接口有助于建立标准化的访问约定,允许不同的实现根据需要以“插入”的方式运行。这种“即插即用”式的实现就是Bridge模式的一个例子。Bridge模式的目标是分离功能的具体实现与抽象功能。例如,本文应用的库存信息最初以静态数据的形式嵌入到Java代码,为了提高灵活性,我们应该把这些数据从代码分离出来,以文件的形式保存到文件系统。随着数据规模的增长,通常还要把数据转移到关系数据库系统(RDBMS)。如果BasketBean实现了标准的接口,那么,我们只需重新实现该接口就可以使用文件系统或RDBMS,无需重新编写CustomerServlet。
现实世界中的应用还会对数据和代码的分离提出要求。数据的改动很频繁,但代码应该尽量少改动。因此,要让本文的应用适合于现实环境,至少应该把Model部分分离成数据访问和数据管理两个层次。这种两个层次的结构使得数据规模的增长不会影响到代码。图四显示了新的设计,它把数据从数据访问逻辑分离了出来,并定义了一个标准的接口。
很多时候,可伸缩性和数据处理事务化要求在数据管理体系中引入第三层。现在,通过CORBA或者EJB接口提供数据管理服务已经很普遍。如果BasketBean实现了标准的接口,那么,我们可以把它改写为一个分布式的服务。图五显示了本文应用的Model的这种三层实现。
七、组件之间的宽松结合
用MVC模式构造JSP应用的原因之一是,MVC模式方便了为Model、View和Controller定义明确分离的角色。我们应该让这些组件之间的结合尽量地宽松。然而,我们没有保持CustomerServlet的宽松结合,因为它的编码里面包含了具体的工作流程状态,而且直接指定了具体的JSP页面名称。
Controller和View之间的紧密结合意味着,如果对其中一个组件进行了修改,另一个组件也很有可能要做相应的修改。在本文的例子中,如果我们在购物工作流程中加入了额外的JSP页面,则必须在CustomerServlet的程序逻辑中加入额外的条件判断。另一方面,CustomerServlet也强制我们以特定的方式命名JSP页面。
如果我们能够降低CustomerServlet和JSP页面结合的紧密程度,应用将具有更好的可维护性和可伸缩性。要降低这种结合的紧密程度,方法之一是为每一个JSP页面创建一个辅助Bean。我们在CustomerServlet中装入这些辅助Bean,管理所有对关联的JSP页面的请求。这种把每一个请求封装到一个请求句柄对象的做法属于Command模式。正如对于Bridge模式,实现Command模式的关键在于定义一个公共接口,每一个接口句柄必须实现该接口。这种接口最简单的形式可以只包含一个方法——例如redirect(),我们把请求参数传入该方法。由于对该接口的每一个具体实现都支持该方法,因此,CustomerServlet能够在任何给定的句柄上调用该接口定义的方法,无需知道任何具体的实现细节(参见图六)。
我们根据对应的JSP页面定制各个辅助Bean类,并尽可能地把业务逻辑放入辅助Bean类。例如,辅助Bean类可以验证那些通过请求传入的参数的合法性,或者是简单地确保输入参数非空,或者是进行验证信用卡之类的复杂操作。
一个JSP页面只有一个入口,但它可以有多种输出,每一种输出可以关联到不同的JSP页面。例如,Inventory.jsp有两种输出,一种是Purchase.jsp,另一种就是Inventory.jsp自己。我们可以利用隐藏标记把一个辅助Bean关联到各个输出点。在Inventory.jsp,把下面的代码:
<TD ALIGN=left>
<INPUT TYPE=submit
NAME=<%= BasketBean.PAGE %>
VALUE=<%= BasketBean.UPDATE %>>
</TD>
替换为:
<TD ALIGN=left>
<INPUT TYPE=hidden
NAME=<%= BasketBean.UPDATE %>
VALUE="jsp_paper.UpdateHandler">
<INPUT TYPE=submit
NAME=<%= BasketBean.PAGE %>
VALUE=<%= BasketBean.UPDATE %>>
</TD>
JavaBean “jsp_paper.UpdateHandler”应该能够被CustomerServlet实例化或定位,它应该包含一个可供CustomerServlet调用的重定向方法。UpdateHandler应该知道如何验证参数的合法性,如何更新Model,如何把请求传递给合适的JSP页面。改进后的方案避免了对JSP页面调用路径的硬编码,避免了在CustomerServlet中编写条件逻辑。
JSP技术对Servlet技术的扩展富有实用意义。JSP不会取代Servlet,在Web应用开发过程中,Servlet、JSP和JavaBean扮演着互补的角色。按照MVC模式,JSP页面既可以独立地扩展,或通过扩展Servlet、JSP页面和应用的Model满足实际应用的可伸缩性要求。应用的Model可以扩展为二层或三层结构,另外,我们还可以添加辅助Bean管理JSP工作流程,实现应用各组件之间的宽松结合。