开发一个网上商店系统

  作者:板桥banq

<<Java实用系统开发指南>>

PetStore(http://developer.java.sun.com/developer/releases/petstore/)是著名的J2EE学习例程。但是从实用角度考虑,该系统中很多功能无法实现重用。本章讨论网上商店系统,立足实用,并基于前面章节介绍的EJB Service方法调用框架,讨论如何建立一套快速的数据操作通用框架。


数据的增、删、改、查是信息系统最常用的基本功能。在传统的语言环境中,该功能虽然能够很方便地实现,但扩展性和维护性很差。在J2EE框架下,由于引入了多层结构又显得过于复杂,每个功能的实现需要穿越多个层次才能完成,降低了这些基本功能的开发速度。
本章将针对这种情况,讨论设计出一套J2EE框架下的数据操作通用框架。虽然该框架设计实现时有一定难度,但是使用起来却非常方便,可以在大量不同项目中反复重用,这将大大简化数据的增、删、改、查功能的开发过程,极大地提高J2EE开发速度,同时,又不丧失多层结构的天然优势,继承延续J2EE特有的可伸缩性和可扩展性。

8.1  系统需求和设计

网上商店是一个在互联网上进行商品销售管理的电子商务系统,该系统一般有以下基本功能:
会员注册登录功能。该功能可以参考前面章节介绍的“用户安全管理系统”实现。
商品管理功能。该部分功能包括商品管理和商品浏览查询两大部分,商品管理是面向商店管理者,分商品类别、商品、商品品种管理等3个部分,这3个部分都包括各自数据的新增、删除、修改和查询等功能。按实际需要,应能够设置多级商品分类,每个商品类别下有不同商品,每个商品中又有不同的具体规格;图片上传功能等。面向商店顾客的主要是商品的查询和搜索功能,有条件的查询并且多页显示一个类别下所有商品,强大的全文检索功能。
购物车管理功能。商店客户在浏览商品时,可以将自己愿意购买的商品加入购物车,同时,也可以对购物车进行修改、查询和删除。当进行结账时,购物车变为订单。
订单管理功能。当商店客户完成购物后,可以根据情况选择支付方式,购物车中商品转变为一个新的订单。订单管理分两种:面向商店管理者和面向商店客户,商店管理者可以查询订单,修改订单状态,如是否已经结算,是否已经发货,是否已经结单等。商店客户可以查询订单状态。
本系统用例如图8-1所示。
1
图8-1  用例图
该网上商店将在Struts+EJB框架下实现。其中购物车由于和具体客户相关,也就是说,只要客户Session存在,购物车就可以跨越多个页面保持原来的数据,这种机制非常适合使用有状态Session Bean来实现。
商店客户购物完成,需要结算,形成订单,这由MDB实现。订单形成后,还需要商品管理者确认,或者连接远程信用卡结算中心实现结算,这些都需要花费一定的时间,如果还是采取Session Bean这样的同步机制来处理,必然响应缓慢,甚至出错导致订单数据丢失。

8.1.1  基本业务对象

本系统中,基本业务对象有8个,分别是商品类别(Category)、商品(Product)、商品规格条目(Item)、订单(Order)、订单条目(OrderItem)、订单状态(OrderStatus)、订单地址(Address)和订单客户(Customer)。
这8个对象可以分为两大部分:商品目录部分和订单部分,前者属于静态数据,是由商品管理者录入管理,后者是由商店客户动态产生。
Category和Product之间是1:N关系,每个类别下有多种商品,而Product与Item之间也是1:N关系,每个商品细分了很多规格品种,这3个基本对象组成了商品目录部分。
其余的属于订单部分。一个订单中包含有多种信息:下订单的客户,订单中购买的产品细则,订单的发货地址或收款地址以及订单状态,订单状态可以帮助商店客户和商店管理者了解彼此关于该订单的处理情况。
这些基本业务对象之间的关系如图8-2所示。
图8-2显示网上商店系统整个基本业务对象的模型,通过该模型进而可以确定系统的数据模型。
1
图8-2  基本业务对象之间的关系

8.1.2  数据表设计

商品目录Category的数据模型如下:
CREATE TABLE category (
  catId                  char(10) NOT NULL,
  category_details_Id      char(10) default '',
  PRIMARY KEY  (catId),
  KEY category_details_Id (category_details_Id),
   FOREIGN KEY (`category_details_Id`) REFERENCES `category_details` (`catId`)
) TYPE=InnoDB;

CREATE TABLE category_details (
  catId             char(10) NOT NULL,
  name            varchar(100) default '' ,    #类别名称
  description        varchar(250) default '' ,      #类别描述
  PRIMARY KEY (catId)
)TYPE=InnoDB;
使用了两个数据表来实现Category,category和category_details之间的关系是1:1关系,将catId单独设立一个表也是为提高访问性能设置。
商品目录和商品之间的1:N关系用独立数据表category_product表达如下:
CREATE TABLE category_product (
  productId       char(10) NOT NULL,      #产品ID
  catId           char(10) NOT NULL,      #类别ID
  KEY catId          (catId),
  PRIMARY KEY     (productId)
)TYPE=InnoDB;
商品数据表如下:
CREATE TABLE product(
  productId         char(10) NOT NULL,
  name            varchar(100) DEFAULT '' ,            #产品名称
  imagePath        varchar(200) DEFAULT '' ,          #图片
  description        text  DEFAULT '' ,                 #有关产品的详情描述
  PRIMARY KEY     (productId)
)TYPE=InnoDB;
每个商品有N种规格品种,product和item之间的1:N的比例也使用独立数据表product_item来表达如下:
CREATE TABLE product_item(
  itemId          char(10) not null,                       #产品条目ID
  productId        char(10) not null,                    #产品ID
  KEY productId (productId),
  PRIMARY KEY   (itemId)
)TYPE=InnoDB;
商品的品种规格数据表如下:
CREATE TABLE item(
  itemId          char(10) not null,                      #产品条目ID
  name           varchar(100) DEFAULT '',             #条目名称
  listprice         float(6,2)  default '0',                 #实际价格
  unitcost         float(6,2)  default '0',                 #单价
  imagePath       varchar(200) DEFAULT '',            #图片
  description       varchar(200) DEFAULT '' ,         #描述
  inventoryId      char(10) DEFAULT '',                #与inventory是1:1关系
  KEY inventoryId (inventoryId),
  PRIMARY KEY   (itemId)
)TYPE=InnoDB;
每个商品品种都有库存数量,而库存数量通常和仓库信息系统相连,因此单独设立表inventory,商品品种item和inventory之间是1:1关系。
CREATE TABLE inventory (
  inventoryId         char(10) not null,
  qty                float(20)  default '0',
  PRIMARY KEY     (inventoryId)
)TYPE=InnoDB;
对于品种属性,有的商品除了规定品种规格外,还有其他各种小的特性,通过数据表item_attrs实现,item表和该表是1:N关系。这里没有专门设立一个1:N关系表,而是直接在item_attrs中实现。
CREATE TABLE item_attrs (
  itemAttrsId       char(10) not null,
  itemId           char(10) default '',           #与item是n:1关系
  name            VARCHAR(100) default '' ,
  attr              VARCHAR(100) default '',
  KEY itemId      (itemId),
  PRIMARY KEY     (itemAttrsId)
)TYPE=InnoDB;
图片数据表,上传的商品图片保存在images中。
CREATE TABLE images (
  imagePath          char(200) not null,
  name              VARCHAR(100) ''default '' ,
  data               LONGBLOB,
  PRIMARY KEY     (imagePath)
)TYPE=InnoDB;
订单orders数据表实际是一个订单和其他数据表对应关系的表,订单具体细节在order_details中。
CREATE TABLE orders(
  orderId             char(10) not null,
  customerId          char(10) default '',                 #谁下的订单
  shippingId           char(10) default '' ,             #运输送达的地址
  billingId            char(10) default '' ,                 #账单送达地址
  orders_details_Id     char(10) default '',
  KEY orders_details_Id (orders_details_Id),
  KEY shippingId      (shippingId),
  KEY billingId       (billingId),
  KEY customerId     (customerId),
  PRIMARY KEY     (orderId)
)TYPE=InnoDB;
订单细节数据表。
CREATE TABLE orders_details(
  orderId            char(10) not null,
  email             varchar(30) DEFAULT '' ,                    #下订单的E-mail
  date              DATETIME        default '0000-00-00',       #订单日期
  totalprice          float(10,2)        default '0.00',            #总价
  taxmoney          float(6,2)         default '0.00',             #税额
  shipmoney         float(6,2)         default '0.00',            #运输价格
  payment_method    varchar(30)     default '',                      #支付方式:信用卡或其他
  shipping_method    varchar(30)     default '',                     #运输方式
  comment           varchar(100)    default '',                     #备注
  PRIMARY KEY       (orderId)
)TYPE=InnoDB;
一个订单中,根据商品条目不同,有不同的订单条目,订单和订单条目是1:N的关系。
CREATE TABLE order_item(
  orderItemId           char(10)        NOT NULL,
  orderId               char(10)        default '',
  linenum              tinyint(2)       default '0',            #条目顺序
  saleprice             float(6,2)       default '0',           #实际购买价格
  qty                  float(20)        default '0',          #购买数量
  itemId                char(10)        DEFAULT '',        #产品itemID
  statusId               char(10) DEFAULT '',
  KEY itemId           (itemId),
  KEY statusId          (statusId),
  KEY orderId         (orderId),
  PRIMARY KEY      (orderItemId)
)TYPE=InnoDB;
订单条目对应一个订单状态,如缺货还是已经发送等。订单条目和订单状态的关系是1:1关系。
CREATE TABLE order_status (
  statusId                  char(10) NOT NULL,
  orderItemId              char(10) default '',
  timestamp                DATETIME  default '0000-00-00',   #改变状态的时间
  status                     tinyint(2) default '0',                  #订单状态
  KEY orderItemId      (orderItemId),
  PRIMARY KEY       (statusId)
)TYPE=InnoDB;
商店客户资料数据表,该表的customerId将和客户注册登录系统的userId对应。
CREATE TABLE customer (
  customerId           char(10) NOT NULL,
  firstName            varchar(50) default '',        #姓
  lastName             varchar(50) default '',       #名
  UNIQUE KEY customerId (customerId)
) TYPE=InnoDB;
地址数据表,将本系统中付款地址、或者送货地址等专门使用一个数据表来存储。
CREATE TABLE address (
  addressId               char(20) NOT NULL ,
  fullname                varchar(50)        default '',          #full name
  address                 varchar(250)      default '',          #详细地址
  address2                varchar(250)      default '',          #详细地址2
  city                     varchar(50)        default '',          #城市
  state                   varchar(50)        default '',          #省份
  zipcode                 varchar(50)        default '',          #邮编
  phone                  varchar(50)        default '',          #固定电话
  mobilephone          varchar(50)        default '',          #移动电话
  fax                    varchar(50)        default '',          #传真
  country                varchar(50)        default '',          #国家
  comment                varchar(150)      default '',          #备注
  PRIMARY KEY      (addressId)
)TYPE=InnoDB;
数据表创建完成后,可以直接创建实体Bean,还可以采取JBuilder等工具自动生成,提高开发效率,因为篇幅有限,这里省略具体步骤。

8.2  数据操作通用框架

在本系统中,有相当一部分是数据的新增、修改、删除和查询,这些功能分别是在EJB和Web两层中实现的。
在EJB层,通过Facade Session Bean调用实体Bean,实现数据库数据的增、改、删和查,这些都是数据库的基本操作,能非常方便而且有规律地实现。
在Web层,需要通过相应界面来激活后台EJB的数据库操作功能,在使用Strut的情况下,这个过程也是有规律的。
首先看看在一般情况下,增、改、删和查功能是怎样在Strut下实现的,以本项目中的商品类别数据对象Category为例。
对于Category的新增功能,客户从浏览器调用addCategory.jsp,addCategory.jsp是一个表单,代码如下:
<html:form action="/admin/saveCategoryAction.do" method="POST">
类别名称:<html:text property="name"/>
类别描述:<html:textarea rows="4" cols="32" property="description"/>
<html:submit property="submit" value="新增"/>
<html:reset value ="复位"/>
</html:form>
</body>
在上面代码中,需要客户输入类别名称和类别描述两个数据,saveCategoryAction.do接受新增表单提交的数据并保存到数据库中。
有了新增界面,必然有编辑修改界面,用于对已经存在的数据Category实现编辑修改功能,修改界面editCategory.jsp的代码如下:
<html:form action="/admin/saveCategoryAction.do" method="POST">
<html:hidden property="catId"/> //与addCategory.jsp不同的一行
类别名称:<html:text property="name"/>
类别描述:<html:textarea rows="4" cols="32" property="description"/>
<html:submit property="submit" value="编辑"/>
<html:reset value ="复位"/>
</html:form>
</body>
对比editCategory.jsp和addCategory.jsp,这两个JSP只是相差了一个catId的赋值。因此,可以在大多数情况下,将这两个JSP页面合成一个JSP页面。这样做的好处是:如果表单数据项发生变化,只要修改一个JSP,而不是两个JSP,工作量减轻,出错率降低,也方便美工设计师设计修改页面。
addCategory.jsp和editCategory.jsp对应的都是同一个CategoryForm,两者在Category Form上的区别是,当客户调用addCategory.jsp时,因为是新增数据,因此CategoryForm为空,没有任何数值;而客户调用editCategory.jsp时,是要编辑已经存在的数据,因此,CategoryForm必定要包含从数据库中查询出的数据,这样客户可以在原有数据上进行修改操作。客户需要修改Category数据时,不能直接调用editCategory.jsp,只能首先调用一个Action(实则是Servlet),在这个Action中,先从数据库中查询出原来的数据,保存在CategoryForm,再输出至界面editCategory.jsp。
如果要将新增和编辑这两个功能JSP界面合成一个JSP页面,那么客户必须首先调用Action。由这个Action根据客户输入参数来决定输出新增性质的category.jsp,或者是编辑性质的category.jsp。这个Action可以称为ViewAction,专门用来根据客户要求输出不同的界面。category.jsp重整后的代码如下:
<html:form action="/admin/saveCategoryAction.do" method="POST">
<html:hidden property="action" />  //用于区分是新增还是编辑修改、删除
<html:hidden property="catId"/>
类别名称:<html:text property="name"/><br>
类别描述:<html:textarea rows="4" cols="32" property="description"/><br>
<logic:equal name="categoryForm" property="action" value="create">
  <html:submit property="submit" value="新增"/>
</logic:equal>
<logic:equal name="categoryForm" property="action" value="edit">
  <html:submit property="submit" value="修改"/>
</logic:equal>
<html:reset value ="复位"/>
<script>
function Delid(){
   if (confirm( '删除本类别 ! \n\n确定吗 ? '))
   {
      document.categoryForm.action.value ="delete";
      return true;
   }else{
      return false;
   }
}
</script>
<input type="submit" value="删除" onclick="return Delid()" >
</html:form>
上面category.jsp实现了新增、修改和删除混合界面的一个JSP,表单提交的Action都是/admin/saveCategoryAction.do,因此在这个Action中,要根据提交表单的Action值,判断是新增保存、还是修改保存或者是直接删除,这个Action称为SaveAction。
这样,一个数据对象的新增、修改、删除和查询可以用一个标准流程来统一。客户使用不同的输入参数调用统一的ViewAction,ViewAction根据输入参数判断是新增还是修改,如果是修改,则查询数据库获得已经存在的数据,然后统一输出一个JSP页面;当这个JSP页面中的数据表单提交后,由统一的SaveAction来处理,根据提交的性质,决定是实现数据库的插入还是更新,或者删除等其他操作,如图8-3所示。
通过图8-3所示的交互操作流程可以实现数据的增、改、删和查,前面章节“网站内容管理系统”中的表现层实现,也是按照这种设计思路实现的。
这种思路具体实现起来分为下列步骤。
(1)创建一个ViewAction,控制输出界面,ViewAction的流程图如图8-4所示。
1         1
图8-3  Strut调用                      图8-4  ViewAction流程图
在图8-4中,Action值如为空,则直接输出新增性质的JSP页面。而Action值如为edit,则视为客户进行编辑页面的调用,根据ID查询数据库获得存在的数据,然后输出编辑修改性质的JSP页面。
当然,在ViewAction中还有一些具体参数的检查,如果是编辑,则关键字ID不能为空,如果数据库未查询到,则要显示无此记录等信息。
(2)创建公用的JSP页面。
(3) 创建SaveAction,用来接受提交表单的数据。SaveAction中主要是调用EJB Service实现数据库操作,调用EJB之前,需要将表单的数据ActionForm转为一个DTO,然后作为参数传送给EJB,EJB Service处理完成后,返回DTO,SaveAction需要检查EJB是否操作成功等。
(4)编辑struts_config.xml。下面是数据对象Customer的增、删、改和查的配置。
<action attribute="customerForm"
type="com.jdon.samples.web.CustomerViewAction"
validate="false" scope="request"
path="/customerAction">
      <forward name="create" path="/customer.jsp" />
      <forward name="edit" path="/customer.jsp" />
</action>
<action name="customerForm"
type="com.jdon.samples.web.SaveCustomerAction"
input="/customer.jsp" scope="request"
path="/saveCustomerAction">
      <forward name="success" path="/customerOk.jsp" />
</action>
第一个action是为ViewAction配置,控制输出JSP界面。第二个Action是为SaveAction配置,接受表单输入,保存到数据库,成功后,导出customerOk.jsp。
共使用4步实现一个数据对象的增、删、改和查。在一个大型系统中,数据对象可能有非常多,那么上述4步流程就成为了一种机械固定的工作,大部分是琐碎的细节。随着数据对象增多,由于开发人员粗心,出错的可能性很大。因此,虽然这些步骤是标准化的,但是由于需要人员介入,在代码稳定性方面还是有很大的隐患。
那么,能否制造一个自动化的“机械”,协助简化开发人员开发这4步的工作量,让琐碎相同的细节由“自动化机械”完成,开发人员只是根据具体不同的数据对象作一些简单的参数设置?这样做的最大好处是:开发出来的代码稳定性强,可以一步到位生产出可直接使用的稳定代码。
这个“自动化机械”就是通常所说的框架。通过将这4步中固定相同的部分上升为框架,可以达到反复重用,简化编程工作量,提高开发速度和质量。因此框架提炼和设计是J2EE项目中必不可少的一项基本设计工作。

8.2.1  框架的提炼和设计

在设计一个框架之前,首先需要明确这个框架操作的对象是什么。
图8-5是从Model Object角度提炼数据对象的增、删、改和查功能抽象原理图,该图将一个数据对象在J2EE中划分为3种Object。第一种是界面显示的表单,这其实是一个ActionForm对象;中间一种是Model或者是DTO,在表现层和EJB层之间实现数据交换;第三种是EJB持久层的实体Bean Object,代表数据库中数据表数据。
1
图8-5  数据对象Object
前面章节已经分析过,通过引入了DTO模式,实现了Web层和EJB的解耦。两者之间通过信使性质的DTO(Model)实现数据交换。这样在Web层的Strut应用框架中,主要是涉及ActionForm和Model(DTO)两种数据对象的操作,再通过前面章节“HTTP-INVOKER框架系统”中介绍的EJB Service方法调用,可以组建一个完整的Web应用框架了。
具体地说,在ViewAction中,主要是从EJB Service获得数据Model,然后将这个Model中数据转换到ActionForm。这样可以实现编辑修改功能;而在SaveAction中,正好相反,将ActionForm中数据转换到Model中,然后以Model作为参数,调用EJB Service方法。
因此,在Strut应用框架中,需要设计出ActionForm和Model的抽象部分,以实现相同的功能。在大多数情况下,不一定能预先知道这些相同部分功能是什么,但是设定一个抽象类或接口的目标是明确的,哪怕它们暂时是空的。
设计ActionForm抽象类ModelForm如下:
/**
 * 所有ActionForm必须继承该Form
 * <p>Copyright: Jdon.com Copyright (c) 2003</p>
 * <p>Company: 上海汲道计算机技术有限公司</p>
 * @author banq
*/
public abstract class ModelForm extends ActionForm {

  public final static String QUERY_STR = "query";
  public final static String CREATE_STR = "create";
  public final static String EDIT_STR = "edit";
  public final static String DELETE_STR = "delete";
  public final static String ADD_STR = "add";
  public final static String REMOVE_STR = "remove";

  private String action;
  public void setAction(String action) {
    this.action = action;
  }
  public String getAction() {
    return action;
  }
}

在这个ModelForm中,抽象出了action的set或get方法。因为前面已经分析过,新增或修改的性质需要根据客户输入的参数来决定。因此使用action变量来保存客户调用的性质,具体数值也是被固化的,是那些静态变量中的任何一个,如CREATE_STR或EDIT_STR等。
ActionForm中其他不同部分就是具体数据对象的字段了,因此需要开发者根据具体对象继承这个ModelForm实现。
Model的抽象暂时无法抽象出相同的部分,可以设定为一个空接口,为以后拓展提供了可能,代码如下:
public interface Model extends Cloneable, Serializable {
}
提炼了框架的操作对象后,下面是框架功能部分的提炼,功能提炼取决于框架的设计目标。
在该框架设计目标中,除了固化数据对象的MVC操作流程,还有一个设计目标:缓存设计。一个系统成熟实用的重要标志是缓存系统的设计,只有运用适当的缓存,才能大大提高J2EE系统运行性能。这也使得本Strut的应用框架能够成为一种真正实战意义上的实用框架。
缓存设计主要是针对Model的缓存,Web层不断从EJB层获得Model,如果这些Model以前使用过,那么,直接从Web层的缓存系统获取这些Model,这样会大幅度提供性能。
针对数据对象的增、删、改和查功能,缓存设计方案如下:
新增:在调用EJB Service保存到持久层之前,保存到缓存中。
修改:根据Model的ID,更新持久层数据同时,更新缓存中的Model数据。
删除:根据ID,删除缓存中相应的Model数据。
查询:首先查询缓存系统,如果没有,则调用EJB Service从持久层中获取,获取后,保存到缓存系统,以备下次调用。
这样,一个增、删、改、查通用数据操作框架基本形成。这个增、删、改、查通用数据操作框架虽然是建立在Strut基础上,但是也可以推广到其他MVC产品里实现。
增、删、改、查功能实现主要是体现MVC流程中实现,在前面已经讨论过,在Strut框架下,增、删、改、查功能的实现需要通过4个步骤,分别是编制ViewAction、SaveAction、配置struts-config.xml以及编制JSP页面。
深入研究这4个步骤中相同和不同点后,会发现主要不同点是,操作的数据对象是有区别的,如商品目录是Category数据对象、商品是Product数据对象,但是由于已经对这些数据对象实现了抽象,通过ModelForm和Model两个抽象类或接口来代表这些千差万别的数据对象,这样,屏蔽了不同点之后,获得了新的层面上的相同点。下面将在这个新的层面具体实现框架系统。
在ViewAction中,根据Action决定推出页面,根据ID查询数据库获取存在的数据等功能都是属于相同部分,可以固化到新的层面上,这样,ViewAction、SaveAction、ModelForm和Model就组成了一个抽象的新层面。
在这个新层面中,还需要有关数据库操作,也就是后台EJB的Service调用,ViewAction和SaveAction分别都有数据库的操作,前者在推出编辑页面之前,要查询数据库获得已经存在的数据对象;后者则是要保存客户提交的数据对象,或者删除。


1
图8-6  委托模式

为了方便使用者使用本框架,可采取委托模式,将这两个Action的两个功能方法统一委托Handler类来具体实现,如图8-6所示。
这样,开发者使用本框架时,就无需分别继承ViewAction和SaveAction,只要继承实现一个Handler就可以了,相应减轻编程工作量;而且还有一个好处:Struts-config.xml配置简化了,可以直接将action的type值配置成框架中的ViewAction和SaveAction。
因此,本框架系统是由下列类组成的:
ViewAction:Action的子类,用于控制JSP界面输出,无需开发者具体实现。
SaveAction:Action的子类,用于保存结果,无需开发者具体实现。
ModelHandler:实现与数据库操作相关的功能,需要由开发者具体实现。
Model:用于在Web层和EJB之间传送数据对象,需要由开发者具体实现。
ModelForm:ActionForm的子类,代表JSP界面显示的数据对象,需要由开发者具体实现。
图8-7是客户和ViewAction调用的交互顺序图。
图8-8是客户和SaveAction调用的交互顺序图。
在本框架中,ModelHandler被委托实现两个很重要的方法:findModelByKey和serviceAction,分别是从数据库根据主键查询获得数据对象以及保存客户提交的数据结果两个功能。
1
图8-7  ViewAction顺序图
1
图8-8  SaveAction顺序图
以上是确定了框架系统的流程和功能。在这个框架中,有3个类: Modelhandler、Model和ModelForm是需要框架使用者在应用时具体实现的,那么如何将使用者的具体实现告知这个框架呢?
使用配置文件可以实现类的动态装载,以Category和Product两个数据对象为例,创建modelmapping.xml的配置文件。
<modelmappings>
    <modelmapping  formName = "categoryForm"
                   key="catId"
                   model="com.jdon.estore.model.Category"
                   handler = "com.jdon.estore.web.catalog.CategoryHandler" />
    <modelmapping  formName = "productForm"
                   key="productId"
                   model="com.jdon.estore.model.Product"
                   handler = "com.jdon.estore.web.catalog.ProductHandler" />
    …
</modelmappings >
每一行modelmapping中有4个值,formName是指在struts-config.xml中form-bean的name值;key是数据对象的关键字,数据对象编辑查询都是需要主键的,这里的key实际是数据表的主键;model是对应的数据对象Model的具体实现;而handler值则是ModelHandler的具体实现。

8.2.2  增、删、改、查框架实现

现在开始设计本框架系统的具体代码,将ViewAction具体实现为ModelViewAction,java,代码如下:
public  class ModelViewAction extends Action {
  private final static String module = ModelViewAction.class.getName();
  protected final static CacheFactory cacheFactory = CacheFactory.getInstance();
  private final static ModelPoolFactory modelPoolFactory = ModelPoolFactory.
      getInstance();
  //Action的重要方法
  public ActionForward execute(ActionMapping actionMapping,
                               ActionForm actionForm,
                               HttpServletRequest request,
                               HttpServletResponse response) throws
      Exception {
    //获得
    String formName = actionMapping.getAttribute();
    if (formName == null)
      throw new Exception("must define action attribute in struts_config.xml");

    //生成新的ActionForm,保存在atrribute中
    ModelForm form = getModelHandler(formName).initForm(request);
   //将form保存到request或session的attribute中
    FormBeanUtil.save(form, actionMapping, request);

    //Action为空,设定为新增调用
    String action = request.getParameter("action");
    if ( (action == null) || (action.length() == 0)) {
      form.setAction(form.CREATE_STR);
      return actionMapping.findForward("create");  //推出新增页面
    }

    if (!FormBeanUtil.validateAction(action))
      throw new Exception("no this action:" + action);
    //设定为编辑调用
//此处的action值是由调用者输入的action参数值决定的,如果以xxx.do?action=create调用,则需要在struts-config.xml中配置forward的name的值为create。如下:
<forward name="create" path="/admin/category.jsp" />d
    form.setAction(action);

    //获得数据对象的key的值
    String keyName = getKeyName(formName);
    String keyValue = request.getParameter(keyName);
    if (!FormBeanUtil.notNull(keyValue, "id.required").isEmpty()) {
      saveErrors(request, FormBeanUtil.notNull(keyValue, "id.required"));
      return actionMapping.findForward(action);
    }
    Model model = (Model) cacheFactory.getObect(keyValue);
    if (model == null) //缓存没有,则从数据库读取
      model = getModelHandler(formName).findModelByKey(keyValue, request);

      //如果查询失败,显示出错信息
    if (model == null) {
      ActionErrors errors = new ActionErrors();
      errors.add(ActionErrors.GLOBAL_ERROR, new ActionError("id.notfound"));
      saveErrors(request, errors);
    } else { //查询成功
      if (model.CACHEABLE) //保存到缓存中
        cacheFactory.putObect(keyValue, model);

        //如果查询成功,数据对象复制,将model中值复制到ModelForm中
      PropertyUtils.copyProperties(form, model);
    }
    return actionMapping.findForward(action);
  }
  //从配置文件modelmapping.xml中获得数据对象的关键字key
  public String getKeyName(String formName) {
    Map mps = modelPoolFactory.getModelMappings();
    ModelMapping mp = (ModelMapping) mps.get(formName);
    return mp.getKeyValue();
  }
  //通过配置文件modelmapping.xml获得具体的委托ModelHandler实例
  private ModelHandler getModelHandler(String formName) {
    return modelPoolFactory.getHandlerObject(formName);
  }
}
在ModelViewAction代码中,ModelHandler的具体子类是从配置文件中获得,数据对象的key也是这样。使用配置文件,可以将具体的子类实现赋值到这个框架系统中。在这个新的层面框架中,有3个抽象对象需要实现:Model、ModelForm和ModelHandler,它们都要根据具体数据对象,实现不同的子类。这些都是通过modelmapping.xml配置实现的。
ModelHandler作为一个关键的被委托者,实现了增、删、改、查的重要功能,代码如下:
public interface ModelHandler {
  //ModelForm的初始化,这是在新增或修改页面出现之前执行的方法
//是在ViewAction中执行的方法
  public ModelForm initForm(HttpServletRequest request);
  //根据key查询数据库获得数据Model, 在ViewAction中执行的方法
  public Model findModelByKey(String keyValue, HttpServletRequest request);
  //保存新增或修改的结构,这是在SaveAction中执行的方法
  public void serviceAction(EventModel em, HttpServletRequest request) throws Exception;
}
前面已经分析了ViewAction的代码,还有SaveAction。在SaveAction中,是将serviceAction方法委托ModelHandler实现的,将SaveAction具体实现为ModelSaveAction,代码如下:
public class ModelSaveAction extends Action {
  //缓存工厂
  protected final static CacheFactory cacheFactory = CacheFactory.getInstance();
  //获取modelmapping.xml工厂
  private final static ModelPoolFactory modelPoolFactory = ModelPoolFactory.
      getInstance();
  //Action方法
  public ActionForward execute(ActionMapping actionMapping,
                               ActionForm actionForm,
                               HttpServletRequest request,
                               HttpServletResponse response) throws
      Exception {
    FormBeanUtil.remove(actionMapping, request);
    //获得formName
    String formName = actionMapping.getName();
    if (formName == null)
      throw new Exception("must define action name in struts_config.xml");

    //检查action参数
    ModelForm form = (ModelForm) actionForm;
    String action = form.getAction();
    if ( (action == null) || (action.length() == 0))
      throw new Exception(" no action value");

    //获得key的值
    String keyName = getKeyName(formName);
    String keyValue = request.getParameter(keyName);

    Model model = null;
    if (keyValue != null){//试图重用Model对象
      model = (Model) cacheFactory.getObect(keyValue);
      Debug.logVerbose(" get keyName:" + keyName + " keyValue:" + keyValue,
                       module);
    }
    if (model == null) // 如果没有,则直接创建Model对象
        model = createModel(formName);

    //从form复制到model中
    Debug.logVerbose(" get the model by copying from form", module);
    PropertyUtils.copyProperties(model, form);

    //创建一个事件,与EJB实现通信,并从事件中获取处理出错情况
    EventModel em = new EventModel();
    em.setActionName(module);
    //将model数据赋予事件
    em.setModel(model);
    em.setActionType(FormBeanUtil.actionTransfer(action)); //设置事件操作类型
    Debug.logVerbose(" transfer actionType ... ", module);
    //委托Model Handler实现serviceAction具体内容
    serviceAction(formName, actionForm, request, em);

    if (em.getErrors() == null) { //如果Service处理结果没有出错
      Debug.logVerbose(" save successfully ... ", module);
      model = em.getModel();
      if ( (em.getActionType() == Event.DELETE) ||
          (em.getActionType() == Event.REMOVE)) {
        cacheFactory.removeObject(keyValue); //删除动作,则删除缓存
      } else if (model.CACHEABLE) {
        cacheFactory.putObect(keyValue, model); //放入缓存,备下次读取
      }
      return actionMapping.findForward("success");
    } else {
      Debug.logVerbose(" save error!! ", module);
      ActionErrors errors = new ActionErrors();
      errors.add(ActionErrors.GLOBAL_ERROR,
                 new ActionError(em.getErrors()));
      saveErrors(request, errors);
      return actionMapping.findForward("failure");
    }
  }

  //复杂一些的功能实现可以重载本方法。
  public void serviceAction(String formName, ActionForm actionForm,
                            HttpServletRequest request,
                            EventModel em) throws
      Exception {
    getModelHandler(formName).serviceAction(em, request);
  }
  //通过modelmapping.xml获得Model实例
  public Model createModel(String formName) {
    return (Model) modelPoolFactory.getModelObject(formName);
  }
  //通过modelmapping.xml获得数据对象
  public String getKeyName(String formName) {
    Map mps  = modelPoolFactory.getModelMappings();
    ModelMapping mp = (ModelMapping) mps.get(formName);
    return mp.getKeyValue();
  }
  //通过modelmapping.xml获得ModelHandler实例
  private ModelHandler getModelHandler(String formName) {
    return modelPoolFactory.getHandlerObject(formName);
  }
}
由以上讨论可知,增、删、改、查通用数据操作框架系统基本是由ModelViewAction、ModelSaveAction、ModelHandler以及Model和ModelForm等类共同组成,主要MVC流程控制功能是在ModelViewAction和ModelSaveAction中实现,而Modelhandler、Model和ModelForm则需要应用者具体继承实现,下一节将展示如何通过继承实现使用该框架系统。

 

下页