J2EE应用系统JPetstore

来源:百度文库 编辑:神马文学网 时间:2024/04/27 17:25:55

 J2EE应用系统JPetstore

以IBatis.com的iBATIS-Jpetstore为例,我们使用Jdon框架对其重构成为Jdon-JPetstore,本章开发环境是Eclipse(本章案也适用其他开发工具),部署运行环境是JBoss。

Eclipse是一个非常不错的开源开发工具,使用Eclipse开发和使用JBuilder将有完全不同的开发方式。我们使用Eclipse基于Jdon框架开发一个完全Web应用,或者可以说,开发一个轻量(lightweight)的J2EE应用系统。

通过这个轻量系统开发,说明Jdon框架对完全POJO架构的支持,因为EJB分布式集群计算能力,随着访问量提升,可能需要引入EJB架构,这时只要使用EJB session Bean包装POJO服务则可以无缝升级到EJB。使用Jdon框架可实现方便简单地架构升迁。

Eclipse安装简要说明

1.下载Eclipse:在http://www.eclipse.org 的下载点中选择tds ISP 比较快。

2.安装免费插件:

编辑Jsp需要lomboz : http://www.objectlearn.com/projects/download.jsp

注意对应的Eclipse版本。

编辑XML,使用Xmlbuddy: http://xmlbuddy.com/

基本上这两个插件就够了。

 

如果希望开发 Hibernate,插件:

http://www.binamics.com/hibernatesync

 

代码折叠

http://www.coffee-bytes.com/eclipse/update-site/site.xml

 

3. 关键学习ant的编写build.xml,在build.xml将你的jsp javaclass打包成war或jar或ear就可以。都可以使用ant的jar打包,build.xml只要参考一个模板就可以:SimpleJdonFrameworkTest.rar 有一个现成的,可拷贝到其它项目后修改后就可用.

然后在这个模板上修改,参考 ant的命令参考:

http://ant.apache.org/manual/tasksoverview.html

网上有中文版的ant参考,在google搜索就能找到。

关键是学习ant的build.xml编辑,SimpleJdonFrameworkTest.rar 有一个现成的,可拷贝到其它项目后修改后就可用.

用ant编译替代Eclipse的缺省编译:选择项目属性-->Builders ---> new --> Ant Builder --->选择本项目的build.xml workspace 选择本项目

eclipse开发就这些,非常简单,不象Jbuilder那样智能化,导致项目目录很大。eclipse只负责源码开发,其它都由ant负责

架构设计要点

Jdon-JPetstore除了保留iBATIS-JPetstore 4.0.5的域模型、持久层ibatis实现以及Jsp页面外,其余部分因为使用了Jdon框架而和其有所不同。

保留域模型和Jsp页面主要是在不更改系统需求的前提下,重构其架构实现为Jdon框架,通过对比其原来的实现或Spring的JPetstore实现,可以发现Jdon框架的使用特点。

在原来jpetstore iBatis包会延伸到表现层,例如它的分页查询PaginatedList,iBatis只是持久层框架,它的作用范围应该只限定在持久层,这是它的专业范围,如果超过范围,显得 ….。所以,在Jdon-Jpetstore中将iBatis封装在持久层(砍掉PaginatedList这只太长的手),Jdon框架是一种中间层框架,联系前后台的工作应该由Jdon这样的中间层框架完成。

在iBatis 4.0.5版本中,它使用了一个利用方法映射Reflection的小框架,这样,将原来需要在Action实现方法整入了ActionForm中实现,ActionForm变成了一个复杂的对象:页面表单抽象以及与后台Service交互,在ActionForm中调用后台服务是通过单态模式实现,这是在一般J2EE开发中忌讳的一点,关于单态模式的讨论可见:http://www.jdon.com/jive/article.jsp?forum=91&thread=17578

Jdon框架使用了微容器替代单态,消除了Jpetstore的单态隐患,而且也简化了ActionForm和服务层的交互动作(通过配置实现)。

用户注册登陆模块实现

用户域建模(Model)

首先,我们需要从域建模开始,建立正确的领域模型,以用户账号为例,根据业务需求我们确立用户账号的域模型Account,该模型需要继承Jdon框架中的com.jdon.controller.model.Model。

 

public class Account extends Model {

 

  private String username;

  private String password;

  private String email;

  private String firstName;

  private String lastName;

  ……

 

}

username是主键。

域模型建立好之后,就可以花开两朵各表一支,表现层和持久层可以同时开发,先谈谈持久层关于用户模型的CRUD功能实现。

持久层Account CRUD实现

主要是用户的新增和修改,主要用于注册新用户和用户资料修改。

public interface AccountDao {

  Account getAccount(String username);  //获得一个Account

  void insertAccount(Account account);  //新增

  void updateAccount(Account account); //修改

}

持久层可以使用多种技术实现,例如Jdon框架的JdbcTemp代码实现比较方便,如果你的sql语句可能经常改动,使用iBatis的sql语句XML定义有一定好处,本例程使用Jpetstore原来的持久层实现iBatis。见源码包中的Account.xml

 

表现层Account表单创建(ModelForm)

这是在Domain Model建立后最重要的一步,是前台表现层Struts开发的起步,表单创建有以下注意点:

表单类必须继承com.jdon.model.ModelForm

表单类基本是Domain Model的影子,每一个Model对应一个ModelForm实例,所谓对应:就是字段名称一致。ModelForm实例是由Model实例复制获得的。

 

public class AccountForm extends ModelForm {

 

  private String username;

  private String password;

  private String email;

  private String firstName;

  private String lastName;

  ……

 

}

当然AccountForm可能有一些与显示有关的字段,例如注册时有英文和中文选择,以及类别的选择,那么增加两个字段在AccountForm中:

  private List languages;

  private List categories;

这两个字段需要初始化值的,因为在AccountForm对应的Jsp的页面中要显示出来,这样用户才可能进行选择。选择后的值将放置在专门的字段中。

有两种方式初始化这两个字段:

1. 在AccountForm构造方法中初始化,前提是:这些初始化值是常量,如:

public AccountForm() {

    languages = new ArrayList();

    languages.add("english");

    languages .add("japanese");

}

2.如果初始化值是必须从数据库中获取,那么采取前面章节介绍的使用ModelHandler来实现,这部分又涉及配置和代码实现,缺省时我们考虑通过jdonframework.xml配置实现。

 

Account CRUD的struts-config.xml的配置

第一步配置ActionForm:

上节编写了ModelForm代码,ModelForm也就是struts的ActionForm,在struts-config.xml中配置ActionForm如下:

 

第二步配置Action:

这需要根据你的CRUD功能实现需求配置,例如本例中用户注册和用户修改分开,这样,配置两套ModelViewAction和ModelSaveAction,具体配置见源码包中的struts-config-security.xml,这里将struts-config.xml根据模块划分成相应的模块配置,实现多模块开发,本模块是用户注册登陆系统,因此取名struts-config-security.xml。

Account CRUD的Jdonframework.xml配置

   

     

     

       

                                     

         

         

         

         

       

     

     

.其中有一个initMethod主要用于AccuntForm对象的初始化。其他都是增删改查的常规实现。

Account CRUD 的Jsp页面实现

在编辑页面EditAccountForm.jsp中加入:

在新增页面NewAccountForm.jsp加入:

所有的字段都是直接来自accountFrom。

 

整理模块配置

商品模块功能完成,struts提供了多模块开发,因此我们可以将这一模块单独保存在一个配置中:/WEB-INF/struts-config-security.xml,这样以后扩展修改起来方便。

 

商品查询模块实现

在iBATIS-JPetstore中没有单独的CategoryForm,而是将三个Model:Category、Product、,Item合并在一个CatalogBean中,这样做的缺点是拓展性不强,将来这三个Model也许需要单独的ActionForm。

由于我们使用Jdon框架的CRUD功能配置实现,因此,不怕细分这三个Model带来代码复杂和琐碎。

由于原来的Jpetstore“偷懒”,没有实现Category Product等的CRUD功能,只实现它们的查询功能,因此,我们使用Jdon框架的批量查询来实现查询。

持久层 Product批量查询实现

商品查询主要有两种批量查询,根据其类别ID:CategoryId查询所有该商品目录下所有的商品;根据关键字搜索符合条件的所有商品,下面以前一个功能为例子:

iBatis-jpetstore使用PaginatedList作为分页的主要对象,该对象需要保存到HttpSession中,然后使用PaginatedList的NextPage等直接遍历,这种方法只适合在小数据量合适,J2EE编程中不推荐向HttpSession放入大量数据,不利于cluster。

根据Jdon批量查询的持久层要求,批量查询需要两种SQL语句实现:符合条件的ID集合和符合条件的总数:以及单个Model查询。

  //获得ID集合

 List getProductIDsListByCategory(String categoryId, int pagessize);

  //获得总数

  int getProductIDsListByCategoryCount(String categoryId);

  //单个Model查询

  Product getProduct(String productId) ;

这里我们需要更改一下iBatis原来的Product.xml配置,原来,它设计返回的是符合条件的所有Product集合,而我们要求是Product ID集合。

修改Product.xml如下:

   

              

                  

        

    ProductDao是IBatis DAO实现,读取Product.xml中配置:

    public List  getProductIDsListByCategory(String categoryId, int start, int pagessize) {

        return sqlMapDaoTemplate.queryForList(

                "getProductListByCategory", categoryId, start, pagessize);

    }

 

    public int getProductIDsListByCategoryCount(String categoryId){

        Integer countI = (Integer)sqlMapDaoTemplate.queryForObject(

                "getProductListByCategoryCount", categoryId);

        return countI.intValue();

    }  

这样,结合配置的iBatis DAO和Jdon框架批量查询,在ProductManagerImp中创建PageIterator,当然这部分代码也可以在ProductDao实现,创建PageIterator代码如下:

      public PageIterator getProductIDsListByCategory(String categoryId, int start, int count)

       {

           PageIterator pageIterator = null;

            try {

                List list = productDao.getProductIDsListByCategory(categoryId, start, count);

                int allCount = productDao.getProductIDsListByCategoryCount(categoryId);

                int currentCount = start + list.size();

                pageIterator = new PageIterator(allCount, list.toArray(), start,

                        (currentCount < allCount)?true:false);

 

         } catch (DaoException daoe) {

             Debug.logError(" Dao error : " + daoe, module);

         }

       

       return pageIterator;

表现层Product批量查询实现

    根据批量查询的编程步骤,在表现层主要是实现ModelListAction继承、配置和Jsp编写,下面分步说:

    第一步,创建一个ModelListAction子类ProductListAction,实现两个方法:getPageIterator和findModelByKey,getPageIterator方法如下:

      public PageIterator getPageIterator(HttpServletRequest request, int start,

            int count) {

        ProductManager productManager = (ProductManager) WebAppUtil.getService(

                "productManager", request);

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

        return productManager.getProductIDsListByCategory(categoryId, start, count);

                

    }

    findModelByKey方法如下:

      public Model findModelByKey(HttpServletRequest request, Object key) {

        ProductManager productManager = (ProductManager) WebAppUtil.getService(

                "productManager", request);

        return productManager.getProduct((String)key);

    }

    由于我们实现的是查询一个商品目录下所有商品功能,因此,需要显示商品目录名称,而前面操作的都是Product模型,所以在显示页面也要加入商品目录Category模型,我们使用ModelListAction的customizeListForm方法:

    public void customizeListForm(ActionMapping actionMapping,

            ActionForm actionForm, HttpServletRequest request,

            ModelListForm modelListForm) throws Exception {

        ModelListForm listForm = (ModelListForm) actionForm;

        ProductManager productManager = (ProductManager) WebAppUtil.getService(

                "productManager", request);

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

        Category category = productManager.getCategory(categoryId);

        listForm.setOneModel(category);

    }

    第二步,配置struts-config.xml,配置ActionForm和Action:

   

    action配置如下:

   

type="com.jdon.framework.samples.jpetstore.presentation.action.ProductListAction"

      name="productListForm" scope="request"

      validate="false" >

     

   

  第三步,编写Category.jsp

  从productListForm中取出我们要显示两个模型,一个是oneModel中的Category;另外一个是Product Model集合list,Jsp语法如下:

我们可以显示商品目录名称如下:

这样我们就可以遍历productList中的Product如下:

 

 

 

 

加上分页标签库如下:

 

               paramId="categoryId" paramName="category" paramProperty="categoryId">

至此,一个商品目录下的所有商品批量查询功能完成,由于是基于框架的模板化编程,直接上线运行成功率高。

商品搜索批量查询:

参考上面步骤,商品搜索也可以顺利实现,从后台到前台按照批量查询这条线索分别涉及的类有:

持久层实现:ProductDao中的三个方法:

List searchProductIDsList(String keywords, int start, int pagessize); //ID集合

 

int searchProductIDsListCount(String keywords); //总数

 

Product getProduct(String productId) ; //单个Model

表现层:建立ProductSearchAction类,配置struts-config.xml如下:

   

type="com.jdon.framework.samples.jpetstore.presentation.action.ProductSearchAction"

      name="productListForm" scope="request"

      validate="false">

     

   

与前面使用的都是同一个ActionForm:productListForm

编写SearchProducts .jsp,与Category.jsp类似,相同的是ActionForm;不同的是action。

商品条目Item批量查询

条目Item批量实现与Product批量查询类似:

持久层:ItemDao提供三个方法:

List getItemIDsListByProduct(String productId, int start, int pagessize);//ID集合

 

  int getItemIDsListByProductCount(String productId);//总数

 

  Item getItem(String itemId); //单个Model

表现层:创建一个ItemListAction继承ModelListAction:完成getPageIterator和findModelByKey,如下:

public class ItemListAction extends ModelListAction {

 

    public PageIterator getPageIterator(HttpServletRequest request, int start,

            int count) {

        ProductManager productManager = (ProductManager) WebAppUtil.getService(

                "productManager", request);

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

        return productManager.getItemIDsListByProduct(productId, start, count);

    }

 

    public Model findModelByKey(HttpServletRequest request, Object key) {

        ProductManager productManager = (ProductManager) WebAppUtil.getService(

                "productManager", request);

        return productManager.getItem((String)key);

    }

 

    public void customizeListForm……….

}

与前面的ProductListAction相比,非常类似,不同的是Model名称不一样,一个是Product一个是Item;

struts-config.xml配置如下:

   

 

   

type="com.jdon.framework.samples.jpetstore.presentation.action.ItemListAction"

      name="itemtListForm" scope="request"

      validate="false">

     

   

比较前面product的配置,非常类似,其实itemListForm和productListForm是同一个ModelListForm类型,可以合并起来统一命名为listForm,节省ActionForm的配置。

Product.jsp页面与前面的Category.jsp SearchProdcuts.jsp类似。

分页显示:

 

              paramId="productId" paramName="product" paramProperty="productId">

    …..

商品条目Item单条查询

单个显示属于CRUD中的一个查询功能,我们需要建立Model对应的ModelForm,将Item的字段拷贝到ItemForm中。配置这个ActionForm如下:

   

type="com.jdon.framework.samples.jpetstore.presentation.form.ItemForm"/>

第二步:因为这个功能属于CRUD一种,无需编程,但是需要配置jdonframework.xml:

  

     

     

       

         

       

     

   

配置中只要一个方法getMethod就可以,因为只用到CRUD中的读取方式。

第三步:配置struts-config.xml如下:

   

                   name="itemForm" scope="request"

                   validate="false">

     

     

   

第四步编辑Item.jsp,现在开始发现一个问题,Item.jsp中不只是显示Item信息,还有Product信息,而前面我们定义的是Item信息,如果使得Item.jsp显示Product信息呢,这就从设计起源Domain Model上考虑,在Item的Model中有Product引用:

  private Product product;

  public Product getProduct() { return product; }

  public void setProduct(Product product) { this.product = product; }

Item和Product的多对一关系其实应该在域建模开始就考虑到了。

那么,我们只要在持久层查询Item时,能够将其中的Product字段查询就可以。在持久层的iBatis的Product.xml实现有下列SQL语句:

 

这段语法实际在查询Item时,已经将Product查询出来,这样Item Model中已经有Product数据,因为ActionForm是Model映射,因此,前台Jsp也可以显示Product数据。

在Item.jsp中,进行下面定义:

将itemForm中product属性定义为product即可;这样不必大幅度修改原来的Item.jsp了。

整理模块配置

商品模块功能完成,struts提供了多模块开发,因此我们可以将这一模块单独保存在一个配置中:/WEB-INF/struts-config-catalog.xml,这样以后扩展修改起来方便。

 

购物车模块实现

购物车属于一种有状态数据,也就是说,购物车的scope生命周期是用户,除非这个用户离开,否则购物车一直在内存中存在。

有态POJO服务

现在有两种解决方案:

第一,将购物车状态作为数据类,保存到ActionForm中,设置scope为session,这种形式下,对购物车的数据操作如加入条目等实现不很方便,iBatis-jpetstore 4.0.5就采取这个方案,在数据类Cart中存在大量数据操作方法,那么Cart这个类到底属于数据类Model?还是属于处理服务类呢?

在我们J2EE编程中,通常使用两种类来实现功能,一种是数据类,也就是我们设计的Model;一种是服务类,如POJO服务或EJB服务,服务属于一种处理器,处理过程。使用这两种分类比较方便我们来解析业务需求,EJB中实体Bean和Session Bean也是属于这两种类型。

iBatis-jpetstore 4.0.5则是将服务和数据类混合在一个类中,这也属于一种设计,但是我们认为它破坏了解决问题的规律性,而且造成数据和操作行为耦合性很强,在设计模式中我们还使用桥模式来分离抽象和行为,因此这种做法可以说是反模式的。那么我们采取数据类和服务分离的方式方案来试试看:

第二.购物车功能主要是对购物车这个Model的CRUD,与通常的CRUD区别是,数据是保存到HttpSession,而不是持久化到数据库中,是数据状态保存不同而已。所以如果我们实现一个CartService,它提供add或update或delete等方法,只不过操作对象不是数据库,而是其属性为购物车Cart,然后将该CarService实例保存到HttpSession,实现每个用户一个CartService实例,这个我们成为有状态的POJO服务。

这种处理方式类似EJB架构处理,如果我们业务服务层使用EJB,那么使用有态会话Bean实现这个功能。

现在问题是,Jdon框架目前好像没有提供有状态POJO服务实例的获得,那么我们自己在WebAppUtil.getService获得实例后,保存到HttpSession中,下次再到HttpSession中获得,这种有状态处理需要表现层更多代码,这就不能使用Jdon框架的CRUD配置实现了,需要我们代码实现ModelHandler子类。

考虑到可能在其他应用系统还有这种需求,那么能不能将有状态的POJO服务提炼到Jdon框架中呢?关键使用什么方式加入框架,因为这是设计目标服务实例的获得,框架主要流程代码又不能修改,怎么办?

Jdon框架的AOP功能在这里显示了强大灵活性,我们可以将有状态的POJO服务实例获得作为一个拦截器,拦截在原来POJO服务实例获得之前。在Jdon框架设计中,目标服务实例的获得一般只有一次。

创建有状态POJO服务拦截器com.jdon.aop.interceptor. StatefulInterceptor,再创建一个空接口:com.jdon.controller.service.StatefulPOJOService,需要实现有状态实例的POJO类只要继承这个接口就可以。

配置aspect.xml,加入这个拦截器:

 

pointcut="pojoServices" />      

这里需要注意的是:你不能让一个POJO服务类同时继承Poolable,然后又继承Stateful,因为这是两种不同的类型,前者适合无状态POJO;后者适合CartService这样有状态处理;这种选择和EJB的有态/无态选择是一样的。

Model和Service设计

购物车模块主要围绕域模型Cart展开,需要首先明确Cart是一个什么样的业务模型,购物车页面是类似商品条目批量查询页面,不过购物车中显示的不但是商品条目,还有数量,那么我们专门创建一个Model来指代它,取名为CartItem,CartItem是Item父集,多了一个数量。

这样购物车页面就是CartItem的批量查询页面,然后还有CartItem的CRUD操作,所以购物车功能主要是CartItem的CRUD和批量查询功能。

iBatis 4.0.5原来设计了专门Cart Model,其实这个Cart主要是一个功能类,因为它的数据项只有一个Map和List,这根本不能代表业务需求中的一个模型。虽然iBatis 4..0.5也可以自圆其说实现了购物车功能,但是这种实现是随心所欲,无规律性可遵循,因而以后维护起来也是困难,维护人员理解困难,修改起来也没有章程可循,甚至乱改一气。

CartItem可以使用iBatis原来的CartItem,这样也可保持Cart.jsp页面修改量降低。删除原来的Cart这个Model,建立对应的CartService,实现原来的Cart一些功能。

public interface CartService {

       CartItem getCartItem(String itemId);

       void addCartItem(EventModel em);

       void updateCartItem(EventModel em);

       void deleteCartItem(EventModel em);

       PageIterator getCartItems();

}

CartServiceImp是CartService子类,它是一个有状态POJO服务,代码简要如下:

public class CartServiceImp implements CartService, Stateful{

    private ProductManager productManager;

    //将原来iBatis 中Cart类中两个属性移植到CartServiceImp中

    private final Map itemMap = Collections.synchronizedMap(new HashMap());

    private  List itemList = new ArrayList();

 

    public CartServiceImp(ProductManager productManager) {

        super();

        this.productManager = productManager;

    }

    ……

}

itemMap是装载CartItem的一个Map,是类属性,由于CartServiceImp是有状态的,每个用户一个实例,那么也就是每个用户有自己的itemMap列表,也就是购物车。

CartServiceImp中的 getCartItemIDs是查询购物车当前页面的购物条目,属于批量分页查询实现,这里有一个需要考量的地方,是getCartItems方法还是getCartItemIDs方法?也就是返回CartIem的实例集合还是CartItem的ItemId集合?按照前面标准的Jdon框架批量分页查询实现,应该返回CartItem的ItemId集合,然后由Jdon框架的ModelListAction根据ItemId首先从缓存中获得CartItem实例,但是本例CartItem本身不是持久化在数据库,而也是内存HttpSession中,所以ModelListAction这种流程似乎没有必要。

如果将来业务需求变化,购物车状态不是保存在内存而是数据库,这样,用户下次登陆时,可以知道他上次购物车里的商品条目,那么采取Jdon框架标准查询方案还是有一定扩展性的。

这里,我们就事论事,采取返回CartIem的实例集合,展示一下如何灵活应用Jdon框架的批量查询功能。下面是CartServiceImp 的getCartItems方法详细代码:

 public PageIterator getCartItems(int start, int count) {

        int offset = itemList.size() - start; //获得未显示的总个数

        int pageCount = (count < offset)?count:offset;

        List pageList = new ArrayList(pageCount); //当前页面记录集合

        for(int i=start; i< pageCount + start;i++){

            pageList.add(itemList.get(i));

        }

        int allCount = itemList.size();

        int currentCount = start + pageCount;

        return new PageIterator(allCount, pageList.toArray(new CartItem[0]), start,

                (currentCount < allCount)?true:false);

}

getCartItems方法是从购物车所有条目itemList中查询获得当前页面的条目,并创建一个PageIterator。

注意,现在这个PageIterator中keys属性中装载的不是数据ID集合,而是完整的CartItem集合,因为上面代码中pageList中对象是从itemList中获得,而itemList中装载的都是CartItem。

表现层购物车显示功能

由于PageIterator中封装的是完整Model集合,而不是ID集合,所以现在表现层有两种方案,继承框架的ModelListAction;或重新自己实现一个Action,替代ModelListAction。

这里使用继承框架的ModelListAction方案,巧妙地实现我们的目的,省却编码:

public class CartListAction extends ModelListAction {

 

    public PageIterator getPageIterator(HttpServletRequest request, int arg1, int arg2) {

        CartService cartService = (CartService)WebAppUtil.getService("cartService", request);

        return cartService.getCartItems();

    }

 

    public Model findModelByKey(HttpServletRequest arg0, Object key) {

        return (Model)key; //因为key不是主键,而是完整的Model,直接返回

    }

   

    protected boolean isEnableCache(){

        return false;  //无需缓存,CartItem本身实际是在内存中。

    }

 

}

配置struts-config.xml:

    

  

   

 

   

type="com.jdon.framework.samples.jpetstore.presentation.action.CartListAction"

      name="listForm" scope="request"

      validate="false">

     

   

    ……

 

上面是购物车显示实现,只要调用/shop/viewCart.shtml就可以显示购物车了。

在Cart.jsp页面插入下面标签:

    ….

分页显示标签如下:

 

 

购物车新增删除条目功能

前面完成了购物车显示功能,下面是设计购物车的新增和删除、修改功能。

参考Jdon框架的CRUD功能实现,Model是CartItem,配置jdonframework.xml使其完成新增删除功能:

                  

                            class="com.jdon.framework.samples.jpetstore.domain.CartItem">

                           

                           

                                    

                                              

                                              

                                    

                           

                  

在这个配置中,只有新增和删除方法,修改方法没有,因为购物车修改主要是其中商品条目的数量修改,它不是逐条修改,而是一次性批量修改,这里的Model是CartItem,这是购物车里的一个条目,因此如果这里写修改,也只是CartItem一个条目的修改,不符合我们要求。下面专门章节实现这个修改。

表现层主要是配置,没有代码,代码都依靠cartService中的addCartItem和deleteCartItem实现:例如:

    public void addCartItem(EventModel em) {

        CartItem cartItem = (CartItem) em.getModel();

        String workingItemId = cartItem.getWorkingItemId();

        ……

    }

注意addCartItem中从EventModel实例中获取的Model是CartItem,这与我们在jdonframework.xml中上述定义的Model类型是统一的。

Struts-config.xml中定义是CRUD的标准定义,注意,这里只有ModelSaveAction无需ModelViewAction,因为将商品条目加入或删除购物车这个功能没有专门的显示页面:

   

      name="cartItemForm" scope="request"

      validate="false">

     

   

 

   

      name="cartItemForm" scope="request"

      validate="false">

     

   

注意,调用删除功能时,需要附加action参数:

/shop/removeItemFromCart.shtml?action=delete

而/shop/addItemToCart.shtml是新增属性,缺省后面无需跟参数。

购物车条目批量修改功能

上面基本完成了购物车主要功能;购物车功能一个复杂性在于其显示功能和修改功能合并在一起,修改功能是指修改购物车里所有商品条目的数量。

既然有修改功能,而且这个修改功能比较特殊,我们需要设计一个独立的ActionForm,用来实现商品条目数量的批量修改。

首先设计一个ActionForm(ModelForm),该ModelForm主要用来实现购物车条目数量的更改,取名为CartItemsForm,其内容如下:

public class CartItemsForm extends ModelForm {

    private String[] itemId;

    private int[]quantity;

    private BigDecimal totalCost;

    …..

}

itemId和quantity设计成数组,这样,Jsp页面可以一次性提交多个itemId和quantity数值。

现在CartItemsForm已经包含前台jsp输入的数据,我们还是将其传递递交到服务层实现处理。因此建立一个Model,内容与CartItemsForm类似,这里的Model名为CartItems,实际是一个传输对象。

public class CartItems extends Model{

    private String[] itemId;

    private int[] quantity;

    private BigDecimal totalCost;

    ……

}

表现层在jdonframework.xml定义配置就无需编码,配置如下:

                  

                            class="com.jdon.framework.samples.jpetstore.domain.CartItems">

                           

                           

                                    

                                              

                                    

                           

                  

上面配置中,Model是CartItems,ActionForm是cartItemsForm,这两个是专门为批量修改设立的。只有一个方法updateMethod。因为在这个更新功能中,没有根据主键从数据库查询Model的功能,因此,这里model的key可以为空值。

服务层CartServiceImp的updateCartItems方法实现购物车条目数量更新:

    public void updateCartItems(EventModel em) {

        CartItems cartItems = (CartItems) em.getModel();

        try {

            String[] itemIds = cartItems.getItemId();

            int[] qtys = cartItems.getQuantity();

            int length = itemIds.length;

            for (int i = 0; i < length; i++) {

                updateCartItem(itemIds[i], qtys[i]);//逐条更新购物车中的数量

            }

        } catch (Exception ex) {

            logger.error(ex);

        }

    }

 

注意updateCartItems中从EventModel取出的是CartItems,和前面addCartItem方法中取出的是CartItem Model类型不一样,这是因为这里我们在jdonframework.xml中定义与updateCartItems相对应的Model是CartItems.

最后一步工作是Cat.jsp中加入CartItemsForm,能够在购物车显示页面有一个表单提交,客户按提交按钮,能够立即实现当前页面购物车数量的批量修改。Cat.jsp加入如下代码:

……

">

 

property="quantity"/>" />

…….

注意,一定要有action赋值edit这一行,这样提交给updateCartQuantities.shtml实际是ModelSaveAction时,框架才知道操作性质。

购物车总价显示功能

最后,还有一个功能需要完成,在购物车显示时,需要显示当前购物车的总价格,注意不是显示当前页面的总价格,所以无法在Cart.jsp直接实现,必须遍历购物车里所有CartItem计算总数。

该功能是购物车显示时一起实现,购物车显示是通过CartListAction实现的,这个CartListAction实际是生成一个ModelListForm,如果ModelListForm能够增加一个getTotalPrice方法就可以,因此有两种实现方式:继承ModelListForm加入自己的getTotalPrice方法;第二种无需再实现自己的ModelListForm,ModelListForm可以携带一个Model,通过setOneModel即可,这个方法是在ModelListAction的子类CartListAction可以override覆盖实现的,在CartListAction加入下列方法:

  protected Model setOneModel(HttpServletRequest request){

        CartService cartService = (CartService)WebAppUtil.getService("cartService", request);

        CartItems cartItems = new CartItems();

        cartItems.setTotalCost(cartService.getSubTotal());       

        return cartItems;

    }

我们使用空的CartItems作为携带价格总数的Model,然后在Cart.jsp中再取出来显示:

Sub Total:

将当前页面listForm中属性oneModel定义为cartItems,它实际是我们定义的CartItems,

下一行取出总价即可。

用户喜欢商品列表功能

在显示购物车时,需要一起显示该用户喜欢的商品列表,很显然这是一个批量分页查询实现,但是它有些特殊,它首先显示的第一页不是由URL调用的,而是嵌入在购物车显示中,那么只能在购物车显示页面的ModellistForm中做文章。

在上节中,在CartListAction中setOneModel方法中,使用CartItems作为价格总数的载体,现在恐怕我们也要将之作为本功能实现载体。

还有一种实现载体,就是其他scope为session的ActionForm,AccountForm很适合做这样的载体,而且和本功能意义非常吻合,所以在AccountForm/Account中增加一个myList字段,在myList字段中,放置的是该用户喜欢的商品Product集合,注意不必放置Product的主键集合,因为我们只要显示用户喜欢商品的第一页,这一页是嵌入购物车显示页面中,所以第一页显示的个数是由程序员可事先在程序中定义。

这样在Account获得时,一起将myList集合值获得。

订单模块实现

我们还是从域模型开始,Order是订单模块的核心实体,其内容可以确定如下:

public class Order extends Model {

 

  /* Private Fields */

 

  private int orderId;

  private String username;

  private Date orderDate;

  private String shipAddress1;

  private String shipAddress2;

  …..

}

第二步,建立与Model对应的ModelForm,我们可以称之为边界模型,代码从Order拷贝过来即可。当然OrderForm还有一些特殊的字段以及初始化:

public class OrderForm extends ModelForm

private boolean shippingAddressRequired;

    private boolean confirmed;

    static {

        List cardList = new ArrayList();

        cardList.add("Visa");

        cardList.add("MasterCard");

        cardList.add("American Express");

        CARD_TYPE_LIST = Collections.unmodifiableList(cardList);

      }   

   

    public OrderForm(){

        this.shippingAddressRequired = false;

        this.confirmed = false;       

    }

   …..

}

第三步,建立Order Model的业务服务接口,如下:

public interface OrderService {

       void insertOrder(Order order);

       Order getOrder(int orderId);

       List getOrdersByUsername(String username);

}

第四步,实现OrderService的POJO子类:OrderServiceImp。

第五步,表现层实现,本步骤可和第四步同时进行。

OrderService中有订单的插入创建功能,我们使用Jdon框架的CRUD中create配置实现,配置struts-config.xml和jdonframework.xml:

     

type="com.jdon.framework.samples.jpetstore.presentation.form.OrderForm"/>

 

        

                            class="com.jdon.framework.samples.jpetstore.domain.Order">

                           

                           

                                    

                                              

                                    

                           

        

第六步:根据逐个实现界面功能,订单的第一个功能创建一个新的订单,在新订单页面NewOrderForm.jsp推出之前,这个页面的ActionForm已经被初始化,是根据购物车等Cart其他Model数据初始化合成的。

新订单页面初始化

根据Jdon框架中CRUD功能实现,初始化一个ActionForm有两种方法:一继承ModelHandler实现initForm方法;第二通过jdonframework.xml的initMethod方法配置。

这两个方案选择依据是根据用来初始化的数据来源什么地方。

订单表单初始化实际是来自购物车信息或用户账号信息,这两个都事先保存在HttpSession中,购物车信息是通过有态CartService实现的,所以这些数据来源是和request相关,那么我们选择第一个方案。

继承ModelHandler之前,我们可以考虑首先继承ModelHandler的子类XmlModelHandler,只要继承initForm一个方法即可,这样节省其他方法编写实现。

public class OrderHandler extends XmlModelHandler {

   

    public ModelForm initForm(HttpServletRequest request) throws

    Exception{

        HttpSession session = request.getSession();

        AccountForm accountForm = (AccountForm) session.getAttribute("accountForm");

        OrderForm orderForm = createOrderForm(accountForm);

        CartService cartService = (CartService)WebAppUtil.getService("cartService", request);

       

        orderForm.setTotalPrice(cartService.getSubTotal());

 

        //below can read from the user's creditCard service;

        orderForm.setCreditCard("999 9999 9999 9999");

        orderForm.setExpiryDate("12/03");

        orderForm.setCardType("Visa");

        orderForm.setCourier("UPS");

        orderForm.setLocale("CA");

        orderForm.setStatus("P");

 

        Iterator i = cartService.getAllCartItems().iterator();

        while (i.hasNext()) {

          CartItem cartItem = (CartItem) i.next();

          orderForm.addLineItem(cartItem);

        }

        return orderForm;        

    }

   

    private OrderForm createOrderForm(AccountForm account){

      ……

    }

}

ModelHandler的initForm继承后,因为这使用了Jdon的CRUD功能实现,这里我们只使用到CRUD中的创建功能,因此,findModelBykey方法就无需实现,或者可以在jdonframework.xml中配置该方法实现。

考虑到在initForm执行后,需要推出一个NewOrderForm.jsp页面,这是一个新增性质的页面。所以在struts-config.xml

                 

      name="orderForm" scope="request"      validate="false">

       

          

订单确认流程

新的订单页面推出后,用户需要经过两个流程才能确认保存,这两个流程是填写送货地址以及再次完整确认。这两个流程实现的动作非常简单,就是将OrderForm中的shippingAddressRequired字段和confirm字段赋值,相当于简单的开关,这是一个很简单的动作,可以有两种方案:直接在jsp表单中将这两个值赋值;直接使用struts的Action实现。后者需要编码,而且不是非有这个必要,只有第一个方案行不通时才被迫实现。

第一步:填写送货地址

使用Jdon框架的推出纯Jsp功能的Action配置struts-config.xml:

   

      name="orderForm" scope="session"      validate="false">

               

   

这是实现送货地址页面的填写,使用的还是OrderForm。更改前面流程NewOrderForm.jsp中的表单提交action值为本action path: shippingForm.shtml:

  ……

在ShippingForm.jsp中增加将shippingAddressRequired赋值的字段:

第二步:确认订单

类似上述步骤,配置struts-config.xml:

  

      name="orderForm" scope="session"  validate="false">

                

           

将上一步ShippingForm.jsp的表单action改为本action的path: confirmOrderForm.shtml:

修改ConfirmOrder.jsp中提交的表单为最后一步,保存订单newOrder.shtml:

第三步:下面是创建数据保存功能实现:

   

      name="orderForm" scope="session"

      validate="true" input="/order/NewOrderForm.jsp">

     

   

ModelSaveAction是委托ModelHandler实现的,这里有两种方式:配置方式:在jdonframework.xml中配置了方法插入;第二种是实现代码,这里原本可以使用配置方式实现,但是因为在功能上有要求:在订单保存后,需要清除购物车数据,因此只能使用代码实现方式,在ModelHandler中实现子类方法serviceAction:

public void serviceAction(EventModel em, HttpServletRequest request) throws java.lang.Exception {

   try {

      CartService cartService = (CartService) WebAppUtil.getService("cartService", request);

cartService.clear(); //清楚购物车数据

 

      OrderService orderService = (OrderService) WebAppUtil.getEJBService("orderService", request);

      switch (em.getActionType()) {

            case Event.CREATE:

                Order order = (Order) em.getModel();

                orderService.insertOrder(order);

                cartService.clear();

                break;

        case Event.EDIT:

                break;

        case Event.DELETE:

                break;

       }

   } catch (Exception ex) {

            throw new Exception(" serviceAction Error:" + ex);

   }

}

用户订单列表

用户查询自己的订单列表功能很明显可以使用Jdon框架的批量查询事先。

在struts-config.xml中配置ModelListForm如下:

     

建立继承ModelListAction子类OrderListAction:

public class OrderListAction extends ModelListAction {

 

    public PageIterator getPageIterator(HttpServletRequest request, int start, int count) {

        OrderService orderService = (OrderService) WebAppUtil.getService("orderService", request);

        HttpSession session = request.getSession();

        AccountForm accountForm = (AccountForm) session.getAttribute("accountForm");

        if (accountForm == null) return new PageIterator();

        return orderService.getOrdersByUsername(accountForm.getUsername(), start, count);

    }

 

    public Model findModelByKey(HttpServletRequest request, Object key) {

        OrderService orderService = (OrderService) WebAppUtil.getService("orderService", request);

        return orderService.getOrder((Integer)key);

    }

 

}

修改OrderService, 将获得Order集合方法改为:

public class OrderService{

 

PageIterator getOrdersByUsername(String username, int start, int count)

….

}

根据Jdon批量查询要求,使用iBatis实现返回ID集合以及符合条件的总数。

最后编写ListOrders.jsp,两个语法:logic:iterator 和MultiPages

配置jdon框架启动

目前我们有四个struts-config.xml,前面每个模块一个配置:

/WEB-INF/struts-config.xml 主配置

/WEB-INF/struts-config-catalog.xml  商品相关配置

/WEB-INF/struts-config-security.xml 用户相关配置

/WEB-INF/struts-config-cart.xml 购物车相关配置

/WEB-INF/struts-config-order.xml 订单相关配置

本项目只有一个jdonframework.xml,当然我们也可以创建多个jdonframework.xml,然后在其struts-config.xml中配置。

 

   

 

修改iBatis的DAO配置

iBatis 4.0.5中原来的配置过于扩张(从持久层扩张到业务层),AccountDao每个实例获得都需要通过daoManager.getDao这样形式,而使用Jdon框架后,AccountDao等DAO实例获得无需特别语句,我们只要在AccountService直接引用AccountDao接口,至于AccountDao的具体实例,通过Ioc注射进入即可。

因此,在jdonframework.xml中有如下配置:

        

                   class="com.jdon.framework.samples.jpetstore.persistence.dao.sqlmapdao.AccountSqlMapDao"/>

        

                            class="com.jdon.framework.samples.jpetstore.service.bo.AccountServiceImp"/>

        

                            class="com.jdon.framework.samples.jpetstore.service.bo.ProductManagerImp"/>

而AccountServiceImp代码如下:

public class AccountServiceImp implements AccountService, Poolable {

    private AccountDao accountDao;

    private ProductManager productManager;

   

    public AccountServiceImp(AccountDao accountDao,

                             ProductManager productManager){

        this.accountDao = accountDao;

        this.productManager = productManager;

    }

 

AccountServiceImp需要两个构造方法实例,这两个中有一个是AccountDao。

按照iBatis原来的AccountDao子类AccountSqlMapDao有一个构造方法参数是DaoManager,但是我们无法生成自己的DaoManager实例,因为DaoManager是由dao.xml配置文件读取后生成的,这是一个动态实例;那只有更改AccountSqlMapDao构造方法了。

根源在于BaseSqlMapDao类,BaseSqlMapDao是一个类似JDBC模板类,每个Dao都继承它,现在我们修改BaseSqlMapDao如下:

public class BaseSqlMapDao extends DaoTemplate implements SqlMapExecutor{

    …..

}

BaseSqlMapDao是XML配置和JDBC模板的结合体,在这个类中,这两者搭配在一起,在其中实现SqlMapExecutor各个子方法。

我们再创建一个DaoManagerFactory,专门根据配置文件创建DaoManager实例:

主要方法如下:

Reader reader = Resources.getResourceAsReader(daoResource);

daoManager = DaoManagerBuilder.buildDaoManager(reader);

其中daoResource是dao.xml配置文件,这个配置是在jdonframework.xml中配置:

                            class="com.jdon.framework.samples.jpetstore.persistence.dao.DaoManagerFactory">

                           

value="com/jdon/framework/samples/jpetstore/persistence/dao/sqlmapdao/sql/dao.xml"/>

        

这样,我们可以通过改变jdonframework.xml配置改变dao.xml配置。

Dao.xml配置如下:

 

   

     

value="com/jdon/framework/samples/jpetstore/persistence/dao/sqlmapdao/sql/sql-map-config.xml"/>

   

 

   

implementation="com.jdon.framework.samples.jpetstore.persistence.dao.sqlmapdao.BaseSqlMapDao"/>

 

 

在dao.xml中,我们只配置一个JDBC模板,而不是将所有的如AccountDao配置其中,因为我们需要iBatis只是它的JDBC模板,实现持久层方便的持久化,仅此而已!

DaoManagerFactory为我们生产了DaoManager实例,那么如何赋值到BaseSqlMapDao中,我们设计一个创建BaseSqlMapDao工厂如下:

 

public class SqlMapDaoTemplateFactory {

 

    private DaoManagerFactory daoManagerFactory;

 

    public SqlMapDaoTemplateFactory(DaoManagerFactory daoManagerFactory) {

        this.daoManagerFactory = daoManagerFactory;

    }

   

    public SqlMapExecutor getSqlMapDaoTemp(){

        DaoManager daoManager = daoManagerFactory.getDaomanager();

        return (SqlMapExecutor)daoManager.getDao(SqlMapExecutor.class);

    }

 

}

通过getSqlMapDaoTemp方法,由DaoManager.getDao方法获得BaseSqlMapDao实例,BaseSqlMapDao的接口是SqlMapExecutor,这样我们通过DaoManager获得一个JDBC模板SqlMapExecutor的实例。

这样,在AccountDao各个子类AccountSqlMapDao中,我们只要通过SqlMapDaoTemplateFactory获得SqlMapExecutor实例,委托SqlMapExecutor实现JDBC操作,如下:

public class AccountSqlMapDao implements AccountDao {

  //iBatis数据库操作模板

  private SqlMapExecutor sqlMapDaoTemplate;

    //构造方法

  public AccountSqlMapDao(SqlMapDaoTemplateFactory sqlMapDaoTemplateFactory) {

      sqlMapDaoTemplate = sqlMapDaoTemplateFactory.getSqlMapDaoTemp();

  }

 //查询数据库

  public Account getAccount(String username) throws SQLException{

    return (Account)sqlMapDaoTemplate.queryForObject("getAccountByUsername", username);

  }

 

 

部署调试

当在JBoss或Tomcat控制台 或者日志文件中出现下面字样标识Jdon框架安装启动成功:

<========  Jdon Framework started successfully! =========>

Jdon框架启动成功后,以后出现的错误基本是粗心大意的问题,仔细分析会很快找到原因,相反,如果编程时仔细慢一点,则后面错误出现概率很小。

使用Jdon框架开发Jpetstore, 一次性调试通过率高,一般问题都是存在数据库访问是否正常,一旦正常,主要页面就出来了,其中常见问题是jsp页面和ActionForm的字段不对应,如jsp页面显示如下错误:

No getter method available for property creditCardTypes for bean under name orderForm

表示在OrderForm中没有字段creditCardTypes,或者有此字段,但是大小写错误等粗心问题。

如果jsp页面或后台log记录显示:

System error! please call system Admin.java.lang.Exception

一般这是由于前面出错导致,根据记录向前搜索,搜索到第一个出错记录:

2005-07-0711:55:16,671 [http-8080-Processor25] DEBUGcom.jdon.container.pico.PicoContainerWrapper - getComponentClass:name=orderService

2005-07-0711:55:16,671 [http-8080-Processor25] ERRORcom.jdon.aop.reflection.MethodConstructor -  no this methodname:insertOrder

第一个出错是在MethodConstructor报错,没有insertOrder方法,根据上面一行是orderService,那么检查orderService代码看看有无insertOrder:

public interface OrderService {

       void insertOrder(Order order);

       Order getOrder(int orderId);

       List getOrdersByUsername(String username);

}

OrderService接口中是有insertOrder方法,那么为什么报错没有呢?仔细检查一下,是不是insertOrder的方法参数有问题,哦, 因为orderService的调用是通过jdonframework.xml下面配置进行的:

        

                            class="com.jdon.framework.samples.jpetstore.domain.Order">

                           

                           

                                    

                                              

                                    

                           

                  

而根据Jdon框架要求,使用配置实现ModelHandler,则要求OrderService的insertOrder方法参数必须是EventModel,更改OrderService的insertOrder方法如下:

public interface OrderService {

    void insertOrder(EventModel em);

}

同时,修改OrderService的子类代码OrderServiceImp:

  public void insertOrder(EventModel em) {

        Order order = (Order)em.getModel();

        try{

            orderDao.insertOrder(order);

        }catch(Exception daoe){

                Debug.logError(" Dao error : " + daoe, module);

                em.setErrors("db.error");

        }

    }

 

注意em.setErrors方法,该方法可向struts页面显示出错信息,db.error是在本项目的application.properties中配置的。

关于本次页面出错问题,还有更深缘由,因为我们在jdonframework.xml中中定义了createMethod,而根据前面已经有ModelHandler子类代码OrderHandler实现,所以,这里不用配置实现,jdonframework.xml的配置应该如下:

                  

                            class="com.jdon.framework.samples.jpetstore.domain.Order">

                           

                           

                  

直接定义了hanlder的class是OrderHandler,在OrderHandler中的serviceAction我们使用代码调用了OrderService的insertOrder方法,如果使用这样代码调用,无需要求OrderService的insertOrder的参数是EventModel了。

 

 

总结

Jpetstore整个开发大部分基于Jdon框架开发,特别是表现层,很少直接接触使用struts原来功能,Jdon框架的表现层架构基本让程序员远离了struts的烦琐开发过程,又保证了struts的MVC实现。

Jpetstore中只有SignonAction这个类是直接继承struts的DispatchAction,这个功能如果使用基于J2EE容器的安全认证实现(见JdonNews),那么Jpetstore全部没有用到struts的Action,无需编写Action代码;ActionForm又都是Model的拷贝,Action和ActionForm是struts编码的两个主要部分,这两个部分被Jdon框架节省后,整个J2EE的Web层开发方便快速,而且容易得多。

这说明Jdon框架确实是一款快速开发J2EE工具,而且是非常轻量的。

纵观Jpetstore系统,主要有三个层的配置文件组成,持久层由iBatis的Product.xml等配置文件组成;服务层由jdon框架的jdonframework.xml组成;表现层由struts的struts-config.xml和jdonframework.xml组成;剩余代码基本是Model之类实现。