某公司是一家处于快速发展中的中小型销售公司,他们打算架构一个网站。计划分两步走:首先做一个宣传型的网站,然后在公司规模达到一定程度时扩展为网上商店。www.yestar2000chinai tp ow er.comVoFZbSR
在第一步计划中,某公司的主要想法是通过网站能及时介绍公司的产品、服务以及其他公司相关情况;由于不断有新品上架,他们希望能通过公司市场销售人员直接来修改页面,将新的信息直接发布在网站上;同时他们也希望页面美工风格能定期地改进和更换。www.yestar2000chinai tp ow er.comVoFZbSR
某公司这样的需求其实代表了大多数公司进军电子商务的意图,因此这个案例有一定的普遍性。www.yestar2000chinai tp ow er.comVoFZbSR
分析这样的需求,他们需要的实际是一套能通过Web修改页面的网站内容管理系统。这套系统将来能挂接网上商店系统,因此在选择技术架构和系统设计上必须充分注意可拓展性。该系统的模式如图4-1所示。www.yestar2000chinai tp ow er.comVoFZbSR
www.yestar2000chinai tp ow er.comVoFZbSR
图4-1 用例www.yestar2000chinai tp ow er.comVoFZbSR
本项目主要是网站管理者要求能够创建页面、修改页面和删除页面。仔细分析需求,网页页面其实分两个元素:网页内容和网页外观,后者也可以称为网页模板。网页模板并不是每次修改页面时都需要变更。www.yestar2000chinai tp ow er.comVoFZbSR
因此,需要把内容和显示模板完全分离,本网站内容管理系统实际分两大部分:数据内容和显示模板。在用户获得页面时,系统自动将这两部分融合在一起输出到浏览器。www.yestar2000chinai tp ow er.comVoFZbSR
一个页面可以划分为几个固定区域,如图4-2所示。www.yestar2000chinai tp ow er.comVoFZbSR
www.yestar2000chinai tp ow er.comVoFZbSR
图4-2 页面划分区域www.yestar2000chinai tp ow er.comVoFZbSR
1. 标题栏(Header)用来放置页面头部的信息。如标题、公司名称,以及公司的LOGO或商标图片等。www.yestar2000chinai tp ow er.comVoFZbSR
2. 页尾栏(Footer)放置公司的详细地址、版权声明或其他相关信息。www.yestar2000chinai tp ow er.comVoFZbSR
3. 菜单栏(Navlink)是放置菜单的区域。菜单是页面的重要组成部分,菜单的集合定义为导航条(Navlink)。在图4-2中,菜单栏是位于页面的左边,也有可能位于页面的右边,当然,还有可能与标题栏和页尾栏在一起。这就要求在架构设计时考虑到这种灵活性,使得系统具备可扩展性。www.yestar2000chinai tp ow er.comVoFZbSR
4. 内容栏(Body)是整个网站的内容部分,这个内容有可能是以下几种类型:www.yestar2000chinai tp ow er.comVoFZbSR
a) 纯文字型: 这最容易处理,使用HTML就可以排列出一定的格式。www.yestar2000chinai tp ow er.comVoFZbSR
b) 文字和图片型:需要考虑文字和图片的排列方式,图片位于文字上方还是文字下方等。www.yestar2000chinai tp ow er.comVoFZbSR
c) 功能型:本内容有可能是系列新闻、论坛或电子购物产品列表等,这样的页面都不是静态HTML能直接处理的,需要和数据库连接,属于动态页面,功能型内容可以由专门软件去完成。www.yestar2000chinai tp ow er.comVoFZbSR
在本项目中,可以只完成文字相关的内容编辑和修改功能。在扩展性方面,需要考虑允许功能型内容的动态插入。www.yestar2000chinai tp ow er.comVoFZbSR
在进行了以上用例需求分析后,就可以依此进行架构设计和系统详细设计。
根据前面的需求分析,以一个中型系统的架构来设计本项目。在设计本例架构时,应充分考虑其扩展性和通用性。
在框架技术选择上,预备完全在J2EE的Web框架里实现。这样可以充分了解熟悉J2EE的Web技术,同时又因为Web技术相对后端EJB层来说是比较成熟的,发展变化不是非常大。
因此,本例的架构可以说是一个J2EE的Web实现的标准架构,可以将它应用到更多的中小型项目中。
Web技术的通用框架图已经在前面的用户注册系统章节描述过。实际上,很多重要逻辑功能和核心是采用Javabeans来实现的。那么在这些Javabeans中,也不是混乱纠缠一团的,也需要有清晰的层次和功能划分。
在 这样一个系统中,整个操作流程其实涉及了很多环节。如用户通过页面输入数据,页面的美化和布局等;系统接受数据后,要结合原有的数据进行一定的逻辑运算, 这部分根据系统要求的复杂性不同;数据处理后要保存以备下次再用,关于数据如何在持久化介质上存储和管理也存在相当的工作。
由此可见,如果将上述这些功能都混合在一起,必然导致以后修改维护上的困难。因此将这些功能进行归类设计,划分在不同的层次来实现,在系统伸缩性、耦合性以及重用性方面有诸多好处。
分层后的设计图如图4-3所示。
图4-3 系统架构层次图
表现层实际是JSP部分,包括一些为JSP服务的Javabeans,因为在本例中,要完全做到JSP无Java代码,必然要引入一些Javabeans为之服务。
要实现这样的目的,有很多现成的框架技术可以选择,比如JSTL(JSP Standard Tag Libraries),JSP标签库是在JSP中使用XML格式的一些特定标签来实现动态功能的。
在本例中,将采取一种Web层框架软件Strutss,这是Apache上的一个著名的开放源代码项目,在Java世界中有很多优秀的开源项目有助于启迪设计思想、提高开发速度。
用户的数据经过表现层的简单封装处理后,将被传送到第2层的逻辑处理层,由逻辑处理层根据业务逻辑来决定如何进一步操作,在这个操作中,有可能要先读取一下其他数据。那么可以通过Cache层从内存先读取,如果Cache中没有,则直接从数据库读取。
其实,Cache层不仅可以对数据库的数据实现缓冲,在性能要求比较高时,对逻辑处理中的Javabeans也可实现缓冲。
数据层主要是实现数据持久化。自从XML技术出现以后,数据持久化多了一个非常好的选择,那就是XML格式的文件。同数据库相比,XML文件对周围环境的要求相对比较低,不需要专门的数据库服务器,非常适合小型项目的成本要求。
在本例中,也使用XML文件来实现数据的持久化。
多层架构的优点主要体现为:
· 良好的解耦性: 各个功能层只负责自己相应的事务,不再相互混淆在一起。每个功能层如果在将来有所变化时,不会涉及到其他功能层,因为每个功能层是相对独立的。
· 高度的重用性: 各个层的技术都可以移植到其他应用系统。比如表现层的框架一旦确定,可以在第二个项目中同样使用这样的技术,同时还可以提高开发速度。
· 灵活的扩展性: 由于表现层和核心功能分开,可以将系统从PC应用拓展到无线等应用中,所作的修改只是表现层的更改,系统核心功能无需变化。
表现层涉及很多用户界面的元素,因此比较难以实现重用。但是,有一个宗旨是:不能将功能性的代码与显示性的代码混合在一起,否则,当需要更改页面或者扩展新功能时会带来很大的修改量,甚至破坏原有系统的稳定性。
因此,需要对表现层进行细化,可以将表现层分3个部分:
· 视图(View)负责显示功能。
· 控制器(Controller)根据Model处理结果,调节控制视图View的输出。
· 业务对象模型(Business Object Model)是对真实世界实体的抽象,可以是一些数据,也可以是一些处理对象或事件对象。在本项目中,业务对象就是那些包含状态和行为的Javabeans,如图4-4所示。
图4-4 MVC模式的流程图
图4-4是一个MVC模式的流程图。从图中可以发现一个表现层清晰的解决方案。
在MVC中,JSP实际只负责显示功能,显示功能的实现要依据客户端来确定。如果客户端是IE浏览器,那么JSP里封装的就是HTML语言;如果客户端是手机,那么JSP里封装的就是WAP语言;如果客户端是其他支持XML的客户端,那么JSP里封装的就是XML。这些客户端不同,变化的只是重新设计一套JSP,而系统的核心功能则无需任何变化,这种灵活强大的拓展性体现了MVC的魅力所在。
Servlet是控制JSP页面输出的。Servlet就像汽车调度员,可以在JSP页面非常多的情况下,实现有效清晰的流程调度管理。
使用MVC模式主要有如下好处:
· 良好的重用性:MVC可以使用多种类型的视图界面,而核心功能无需变化。比如视图界面可以从Web浏览器(HTTP)拓展到无线浏览器(WAP)。
· 极低的成本:MVC模式可以让一般水平的程序员来开发和维护用户的界面功能,降低了人员成本。
· 快速开发:由于将Java代码从JSP中去除,Java程序员和HTML 或者JSP程序员可以同时工作。而如果不采用MVC分离的模式,那么只有等Java程序员完成核心功能后,再交由美工和JSP程序员进一步加工。
· 可靠性:由于显示功能和处理功能分离,这就允许在不重新编译核心功能的情况下修改系统的视图界面和外观。
有一种观点认为只有大项目才需要采取MVC。 实际上,很多时候无法确定项目的规模。因为客户的需求始终不断在变化,如果原有的基础架构不具有很强的拓展性,那么项目进行到中途时可能要再进行重新设 计,很容易陷入左右为难的地步。所以,无论任何项目,用标准的架构去设计它,就如按标准的方法去做事一样,相当于成功了一半。
具体实现MVC模式的软件框架有很多,其中应用最广泛的有下列3种:
· Apache Struts(http://jakarta.apache.org/Strutss/) Struts是基于JSP的框架软件,有大量文章和参考资料面世,其中《Strutss in Action》一书非常值得一读。
· Apache Cocoon (http://xml.apache.org/) Cocoon是基于XML和XSLT技术的MVC模式实现框架,在Cocoon中很巧妙地利用XML技术实现了内容和模板分离的功能。
· Petstore WAF(Web Application Framework) Petstore是SUN公司推出的一个J2EE实例样本,WAF是其Web实现框架,其原理类似Struts。
由于Struts有大量实践应用,已经成为目前事实上的Web实现标准,而Cocoon是未来的一个发展趋势。本例中,使用Struts框架来构架本项目的Web层是一种实用而且理想的选择,当然也会引入更多XML技术,以能够向未来过渡。
本例另外一个关键问题是如何解决模板和内容分离。而这在Struts中,正好有Tile组件可以解决这个问题。Tile的总体思路是将页面划分成几块“碎片”,然后分别实现之。下面逐个介绍。
Struts框架是结合JSP、JSP 标签库以及Servlets的MVC模式实现。这里将简单介绍Struts的使用,进一步学习请参考相应书籍。图4-5是Struts实现MVC的流程图。
MVC模式的实现核心是控制器(Controller)部分,ActionServlet是Struts的控制器的核心,它将根据来自用户的请求,调用在Strutss-config.xml中配置好的ActionMapping,从其中寻找到相应Action具体实现类。具体实现类所要做的就是要继承实现Action类中的Execute方法(已经不推荐使用perform()方法)。
图4-5 Struts的流程图
在Action类的Execute方法中,要实现两个功能:
· 将用户输入的数据传递到后台处理,Struts已经把用户输入的数据封装在ActionForm类中,只要从其中读取数据,形成新的数据对象,递交给逻辑处理层来处理。
· 在后台处理完成后,需要根据使用Struts提供的ActionMapping来指定输出的视图(View)。
Struts的原理比较复杂,初学者若要迅速上手,则应该首先学会如何使用Struts,这样通过不断深入使用,会逐渐了解其原理和运行机制。
Tile是有关视图界面的组件。Tile主要是将一个页面划分成几个部分,分别对应不同的JSP,大大提高了视图界面元素的可重用性。
使用Tile有很多优点。例如在很多项目中,页面的头部和尾部都是固定的。一般会采取下面的做法:
<%-- include header --%>
这里是Body 内容部分:
<%-- include footer --%>
使用include的主要问题是:
· 如果要修改的不只是头部和尾部,而是整个页面的布局,那么就必须逐个进行页面的修改。
· 大量include嵌套使用,这类似面向过程语言中的GoTO语句,会使维护扩展变得非常困难,但是因为编写简单直接,不少程序员还是可能喜欢这样做。在一个项目中开这种先河,将出现大量层层重叠的include语句,严重破坏了页面的可扩展性和可维护性,使得页面修改扩展成为整个系统的恶梦。
· 调试不方便。include分动态静态两种用法,静态用法调试很不方便,因为即使被include的JSP页面在一些容器中,主页面如果不修改,容器将不会重新载入新的使用“include”语句调用的子页面,除非调用者页面和被调用者页面均被修改。
如果使用Tiles来改造上面的问题,将会有一个很巧妙干净的解决方式:
<%@ taglib uri="/WEB-INF/tiles.tld" prefix="tiles" %>
<%-- include header --%>
这里是Body 内容部分:
<%-- include footer --%>
使用tiles:insert代替了include。更深入一点,可以修改成这样:
在这段代码中,Tile将一个页面划分为4个区域,分别由对应的JSP来实现。页面布局由layout.jsp实现,这样当需要修改布局时,只要修改layout.jsp一个文件就可以了。
Tile提供的关于页面布局的解决方案非常适合应用到本项目中。
在Java中,对象序列化成二进制数据是比较方便的。但是将一个对象序列化成XML文本时,也许就没那么简单了。对象和XML之间序列化和反序列化依赖很多方面,比如文本的编码、映射设置等。
将包含数据的对象序列化成XML文本后,就可以很方便地实现数据的持久化保存。
数据持久化表示数据将脱离应用程序的生存周期,也就是说,当这个应用程序退出或计算机关机,这些数据还将继续存在。因为这些数据已经被保存到永久存储介质上,如硬盘文件系统或关系数据库系统。
关系数据库是最经常使用的存储介质,在以后章节中将讨论结合EJB来使用关系数据库。将XML文件存储到硬盘文件系统中也是一种可选的持久化方案,这种方案的优点就是开发或维护的成本比较低,本项目中将使用硬盘文件系统作为存储介质。
对象的序列化需要很丰富的XML API,最经常使用的就是SAX和DOM,但是在做一些简单的XML操作时,如获取XML中一个数据,首先遍历整个文档的树形结构,在父子或兄弟关系的节点上导航一番,这些都会需要编写很多代码,可见是非常琐碎和麻烦。
JDOM(http://www.jdom.org/)在这方面做得比较好,它用来分析XML文本是非常方便的,在本项目中,可以使用JDOM来读取系统的XML配置文件。
但是,在使用JDOM实现将对象序列化到XML文本时,代码还会涉及到该XML文本的结构。也就是说,在代码中硬编码XML文本的节点名称,这是非常不具备可重用性和拓展性的。
要达到良好的重用性和灵活的扩展性,就必须将一些操作细节封装起来,因此,需要将XML和数据交互操作的细节封装起来,这就会使用到DBO模式。
在讨论DBO模式(Data Bind Object Pattern)之前,首先必须了解一个很重要的概念,就是MVC模式中的Model。它泛指的是一种业务对象模型(Business Object Model),数据对象模型(Data Object Model)是业务对象模型的一种,它包含状态以及操作状态的行为。数据对象也可以被认为等同于经常提到的另外一个名词:值对象(Value Object)。
数据对象是对真实世界中实体软件的抽象。它可以是人、事或地方以及一些商业概念,比如订单、客户这些概念也属于业务对象。
例如“人”可以形成一个数据对象,如下:
public class Person implements java.io.Serializable {
//人的姓名
private String name = null;
// 人的年龄
private Integer age = null;
//没有名字的情况下创建一个人
public Person() {
super();
}
//以一个给定的名字创建一个人
public Person(String name) {
this.name = name;
}
// @return 返回这个人的年龄
public Integer getAge() {
return age;
}
// @return 返回这个人的名字
public String getName() {
return name;
}
// 设置这个人的年龄
public void setAge(Integer age) {
this.age = age;
}
// 设置这个人的名字
public void setName(String name) {
this.name = name;
}
}
在这个数据对象中,定义了人的一些属性。比如他的名字、年龄,而且有一些setXXX getXXX操作这些属性。这个类Person是对现实中“人”对象化的概括。
将这个类序列化成XML文本,将会涉及很多操作细节。使用DBO模式可以封装这些细节,从而达到代码的可重用性。
DBO模式有3个参与角色:
· 数据对象:将要被序列化到XML或从XML反序列化的对象。
· 数据绑定对象:这是一个工具型的基本对象,这个基本对象抽象了XML的序列化和XML API的具体使用细节。
· XML 序列化 API: 真正实现XML序列化的具体行为,具体实现可以采取JDOM、Castor、JAXB或其他类似工具。
与JDOM相比,Castor XML更好地封装XML序列化过程,使用Castor XML需要亲自编写的代码将更少。也可以说Castor XML其实是DBO模式的具体实现。
Castor(http://castor.exolab.org/)是一种将Java对象和XML自动绑定的开源软件。它可以在Java对象、XML文本、SQL数据表以及LDAP目录之间绑定。
下面CastorHandler类是使用Castor XML将Person对象序列化到XML文本,并直接持久化保存到文件系统:
/**
* 使用Castor作为XML文件和对象的绑定工具
* Copyright: Jdon.com Copyright (c) 2003
* Company:
* @author banq
* @version 1.0
*/
public class CastorHandler {
public final static String module = CastorHandler.class.getName();
private static PropsUtil propsUtil = PropsUtil.getInstance();
/**
* 获得对象和XML之间映射的关系
*/
public static Mapping getMapping(String MappingFile) {
Mapping mapping = (Mapping) SimpleCachePool.get(MappingFile);
if (mapping == null) {
String pathMappingFile = propsUtil.getConfFile(MappingFile);
try {
mapping = new Mapping();
mapping.loadMapping(pathMappingFile);
SimpleCachePool.put(MappingFile, mapping);
} catch (Exception e) {
Debug.logError("get mapping error " + e, module);
}
}
return mapping;
}
/**
* 获得反序列化的对象
*/
private static Unmarshaller getUnmarshaller(String MappingFile,
String className)
throws Exception {
Unmarshaller un = (Unmarshaller) SimpleCachePool.get(className);
if (un == null) {
try {
Class c = Class.forName(className);
un = new Unmarshaller(c);
un.setMapping(getMapping(MappingFile));
SimpleCachePool.put(className, un);
} catch (Exception e) {
Debug.logError(" getUnmarshaller error: " , module);
throw new Exception(e);
}
}
return un;
}
/**
* 从XML文件中读取对象
*/
public static Object read(String MappingFile, String className,
String xmlfile) throws Exception {
Object object = null;
try {
Unmarshaller un = getUnmarshaller(MappingFile, className);
FileReader in = new FileReader(xmlfile);
object = un.unmarshal(in);
in.close();
} catch (Exception e) {
Debug.logError(" read " + className + " form file:" + xmlfile + e, module);
throw new Exception(e);
}
return object;
}
/**
* 将对象序列化到XML文件中
*/
public static void write(String MappingFile, Object object, String outfile) throws
Exception {
try {
FileWriter out = new FileWriter(outfile);
Marshaller ms = new Marshaller(out);
ms.setMapping(getMapping(MappingFile));
ms.setEncoding(propsUtil.ENCODING);
ms.marshal(object);
out.close();
} catch (Exception e) {
Debug.logError("write object to file :" + e, module);
throw new Exception(e);
}
}
}
调用CastorHandler:
String mappingFile = "mapping.xml";
String outfile = "d:/data/person.xml";
// 创建一个新的对象
Person person = new Person("板桥里人");
person.setAge(new Integer(33));
CastorHandler.write(mappingFile, person, outfile)
其中mapping.xml是对象和XML之间结构的映射表:
这样就把person对象写入到d:/data/person.xml文件中。该CastorHandler是DBO模式中的数据绑定对象;person是数据对象;在CastorHandlet中封装的是关于XML序列化的API。例如: getMapping方法是获取org.exolab.castor.mapping.Mapping的一个实例,而getUnmarshaller是获得一个反序列化的对象等。
遵循DBO模式的CastorHandler可以被用来进行任何对象到XML的序列化或反序列化,与具体的XML文本结构就无任何关系,所以,良好的重用性是使用DBO模式的一个显著优点。当然,因为与XML文本结构的关系定义在mapping.xml配置文件中,使用CastorHandler时就必须配置这样一个mapping.xml,对于一些只有XML的读取操作来说,过于复杂了点。
但是,在本项目中,CastorHandler可以方便地实现内容数据的持久化,其操作的简易性和方便性已经远远超过它的缺点。
是否有缓冲机制(Cache)是衡量一个J2EE产品是否成熟的重要标志。因为缓冲对于J2EE系统的运行性能有至关重要的作用,特别是在大量用户并行访问时,没有缓冲机制几乎是不可想象的事情。
J2EE作为一个多层结构,每发生一次请求,将可能经过系统的许多层次。这些层次中有的可能位于另外一台服务器上,那么网络连接开销将延迟请求信号的处理时间。使用缓冲可以节约请求信号的处理时间,大大提高系统的整体响应能力。
在J2EE系统运行中,有大量经常使用的对象,如session对象和用户资料对象等,创建这些对象可能需要首先访问数据库或文件系统;而销毁这些对象则需要释放该对象占用的内存,因此,创建和销毁对象的过程可能是复杂的,造成的性能损耗也是非常大的。
为了避免创建和销毁的开销,将那些频繁使用的对象保存在内存缓冲中,这样就能大幅度提高应用系统的性能。
在单个JVM中实现缓冲是比较容易的,应用系统通过一个Singleton接口从内存缓冲中读取数据,如果缓冲中没有所要读取的数据,那么就从存储介质或网络连接中读取,读取后再将此数据保存到缓冲中,由于缓冲内存容量有限,缓冲系统会将访问不频繁的数据逐步从缓冲内存中删除,这就是 LRU (Last Recently Used)算法。
另外,缓冲还有校正的问题,缓冲只是将数据源的数据保存在内存中。在一个分布式环境中,如果数据源被其他用户从其他服务器进行更改,那么必须通知所有有关这个数据源的缓冲进行及时更新。
因此,在一个分布式的集群Cluster(负载平衡和错误恢复)环境中,Cache的设计就比较复杂。综合各种因素,使用JMS(Java Message System)进行这种通知信息提示是一种比较切实可行的方案,带来的性能损失也不是很大,支持分布式环境的不少缓冲产品已经面世。因此,一个系统从单机环境升级到分布式环境,缓冲机制可能要作一定的修改。
在本项目中,所有的数据对象都是以XML存储在文件系统中。文件的频繁读写是非常耗时的,因此需要对数据对象实现缓冲。
确定了本项目的整体架构以后,可以针对本项目所要求的具体功能,进行具体详细的设计和实现。
J2EE项目开发的第一个突破口一般是在业务对象建模,有了系统的基本对象后才可以进行数据库设计,进而再实现系统的逻辑处理,最后实现表现层。这就是所谓“Domain First;Persistence Second;Services Third;Clients Last”。
从系统需求来看,一个页面划分为页面布局、标题栏、页尾栏、菜单栏和内容栏部分,其中页面布局以及标题栏和页尾栏属于页面模板部分,可以由JSP借助Tiel来实现,每个页面变动的就是菜单栏和内容栏。因此,菜单栏和内容栏是本项目的主要处理对象,可以建立相关基本对象,如图4-6所示。
图4-6 系统的基本对象
Body对象是代表页面的内容部分,而Menu是代表菜单。由于一个页面可能有很多菜单,同时可能还有指向上一级页面的菜单,许多菜单组合在一起,可以使用Navlink对象来代表。
对象的创建顺序是:先创建菜单对象Menu,然后是Body对象,最后是模板合成生成页面,其中Menu和Body对象的创建都涉及到数据层的操作。对象创建流程如图4-7所示。
图4-7 创建流程
模型(Model)的实现类似原来传统的数据库数据表的设计。在面向对象系统分析设计中,常用域模型设计替代以往的数据表结构设计。在本项目中,由于采取数据对象持久化到XML的设计方案,因此,只要设计实现数据对象就可以,如图4-8所示。
图4-8 数据模型的实现
在MenuModel中有5个属性,分别是:
· Id:菜单的Id。
· Name:菜单的名称,比如“关于我们”或“产品服务”。
· Icon:菜单的图标,有时菜单可能不直接是菜单名的文本,而是特定图标。
· Link:菜单指向的页面,是一个JSP页面。
· Datalink:菜单指向的内容。
下一步根据Castor XML要求,需要设计数据对象和XML文本结构的映射,以Menu和Navlink为例:
type="com.jdon.cms.model.MenuModel" collection="collection" >
通过调用CastorHandler可以实现MenuModel对象到XML文件的序列化,其他数据对象也如此。
由此,数据对象的实现基本完成。当然,数据对象建模不是一次性就能成功,可能要经过反复斟酌修改。数据对象建模可以反映设计者对应用系统的真正认知能力,因此也是非常重要的一个步骤。
下一步将进入逻辑处理层的具体实现。逻辑处理层是本项目的核心功能层,设计方式将采取模式驱动设计(Pattern Driven Design)的方式。
在本项目中,使用了XML文件系统作为数据存储介质,但是如果将来系统发展到一定规模,使用关系数据库作为存储介质,那么就需要替换存储介质。这种替换工作力求修改最小,不影响原有系统的稳定性,最好能无缝过渡到数据库系统,也就是说为了让系统具备灵活的扩展性和伸缩性。
为了达到这个目的,需要将有关XML数据操作的细节封装起来,使得外界根本不知道系统内部是采取XML文件还是使用数据库来实现数据持久化的。
还有一个目的,目前本项目没有加入用户权限控制,只有一个角色“管理员”可以操作数据。如果将来需要增加新的角色,这个角色只有修改页面内容的权限,没有创建新页面或删除页面的权限。这就需要在访问数据层之间加一个网关Proxy,用来进行权限访问控制。
抽象工厂模式可以满足这些要求,抽象工厂主要是提供创建系列对象的接口,使得外界无需知道创建这系列对象的具体细节。
实现抽象工厂,需要有下面4个参与者:
· 抽象工厂:主要是提供创建对象的接口,外界直接和这个抽象工厂交互。
· 抽象工厂的具体实现者: 主要是实现创建对象的具体细节,将有关XML文件具体实现封装在这个类中,将来可以再做一个使用关系数据的具体实现者。
· 抽象产品:产品的抽象接口。
· 抽象产品的具体实现者:详细定义上述抽象产品的细节。
下面将分别编写上述几个参与者的接口或类。首先,定义一个抽象工厂类PageFactory:
/**
* Page工厂
* Copyright: Jdon.com Copyright (c) 2003
* Company:
* @author banq
* @version 1.0
*/
public abstract class PageFactory {
private static Object initLock = new Object();
//定义抽象工厂的具体实现类的名称
private static String className =
"com.jdon.cms.xml.XmlPageFactory";
private static PageFactory factory = null;
//用Singleton模式建立统一调用接口
public static PageFactory getInstance() {
if (factory == null) {
synchronized (initLock) {
if (factory == null) {
try {
//使用动态类装载机制将抽象工厂具体实现者激活
Class c = Class.forName(className);
//类似 PageFactory pageFactory = new XmlPageFactory
factory = (PageFactory) c.newInstance();
}
catch (Exception e) {
Debug.logError(" get factory instance error:" + e, module);
return null;
}
}
}
}
//其实是返回 XmlPageFactory的实例
return factory;
}
//获得导航条对象
public abstract Navlink getNavlink();
//创建新的菜单
public abstract Menu createMenu(Integer Id);
//创建新的内容
public abstract Body createBody(Integer Id);
…
}
上面PageFactory类中动态指定了XmlPageFactory作为PageFactory的具体实现者,XmlPageFactory中封装的是数据持久化保存到XML文件系统中的操作过程。如果需要使用数据库系统,那么只要制作一个DatabasePageFactory,将上面程序中的className的值改为 DatabasePageFactory,那么整个系统的存储介质就转换到数据库系统上了。
另外,如果需要访问权限控制,可以创建一个抽象工厂的实现者如PageFactoryProxy,在PageFactoryProxy中对每个数据操作方法都进行一次权限是否允许的检验。
抽象工厂模式实际上是实现了数据层和逻辑层的分离,使得架构设计中的多层概念能够得以真正实现。架构设计中多层分离不能只是一种设计预想,还必须依靠一定的手段去真正落实,而设计模式恰好是有力的实现手段,这也就是所谓Pattern Driven Architecture。
使用模式的目的是增强系统的可重用性,降低耦合性,提高灵活性,掌握这个原则才真正学会使用模式。下面的分析将自然导出委托模式的使用。
继续分析XmlPageFactory类,在这个类中需要实现有关Menu对象和Body对象的XML操作。这就会出现一个问题,在这个类中将可能混合了很多功能,随着系统的扩展,功能的增多,会使得这个类变得庞乱复杂,增加了维护的困难性。
细化是面向对象设计自始至终的目标。具体原则就是:封装和分派。以被操作的对象为归类,将有关Menu对象和Body对象的功能分别委托另外的类来实现,这样XmlPageFactory类就会干净多了,而且如果有关Menu对象再进行修改,将不会直接修改XmlPageFactory这个关键的类,只会去修改委托类。比如是NavlinkManager,降低了修改工作带来的对系统稳定性的冲击。
同样有关Body对象的功能操作,则可以委托给BodyManager来实现,如图4-9所示。
图4-9 抽象工厂的流程图
XmlPageFactory的代码如下:
public class XmlPageFactory extends PageFactory {
public final static String module = XmlPageFactory.class.getName();
private final static XmlUtil xmlUtil = XmlUtil.getInstance();
//初始化Cache
public static UtilCache contentCache = new UtilCache(5, 0);
public static UtilCache navlinkCache = new UtilCache();
public static UtilCache bodyCache = new UtilCache();
//初始化被委托者
private NavlinkManager navlinkManager = null;
private BodyManager bodyManager = null;
private ContentManager contentManager = null;
private ContentFilter contentFilter = null;
public XmlPageFactory() {
navlinkManager = new NavlinkManager();
bodyManager = new BodyManager();
contentManager = new ContentManager();
contentFilter = new HTMLFilter();
}
public Navlink getNavlink() {
NavlinkModel navlink = (NavlinkModel) navlinkCache.get(Navlink.NAME);
if ( (navlink == null) || (navlink.isModified())) {
navlink = navlinkManager.getNavlinkFromFile();
navlinkCache.put(Navlink.NAME, navlink);
}
return navlink;
}
public Integer getId() {
Integer newId = null;
Navlink navlink = getNavlink();
if (navlink.getCount() == null)
newId = new Integer(1);
else{
newId = new Integer(navlink.getCount().intValue() + 1);
}
navlink.setCount(newId);
return newId;
}
public Menu createMenu(Integer Id) {
return navlinkManager.createMenu(Id);
}
public Menu getMenu(Integer Id, Navlink navlink) {
Menu menu = null;
try {
menu = navlinkManager.findByPrimaryKey(Id, navlink);
} catch (Exception ex) {
Debug.logError(" getMenu error:" + ex, module);
}
return menu;
}
public Menu updateMenu(Menu menu, Page page) {
return navlinkManager.updateMenu(menu, page);
}
public void deleteMenu(Menu menu, Navlink navlink) {
navlinkManager.deleteMenu(menu, navlink);
}
…
}
至此,在逻辑处理层中使用了抽象工厂模式。这样既能实现系统架构的层次分离,又能实现系统的动态扩展性,为以后扩展到数据库系统做好了准备,将相关代码改动所带来的损失降低到了最低程度,保证了系统的稳定性。
本项目关键功能是将页面内容和模板有机地混合在一起,这个混合过程实际是一种组装创建的过程,依据不同的模板技术会有不同的生成组装过程。
本项目设计中采取的是Tile模板技术,一个页面由几个区域的JSP文件组成,其中关键部分有两点:
· 将菜单(Menu)数据和菜单显示JSP 结合在一起。
· 将内容(Body)数据和内容显示JSP结合在一起。
这种具体实现细节有很多种办法,每个办法都有一定的优点和缺点,其中有一个最容易实现的方案:
· 在菜单JSP中动态访问Navlink中的Menu,将Menu对象遍历后输出。
· 在内容JSP中读取Body的Xml数据,由于内容部分可能包含大量HTML,而且数据可能庞大,再采取一个简单变通的办法,直接将这些包含HTML的内容存储为内容JSP文件,那么原来Body对象实际变成了有关内容属性的对象。
上述方案只是众多方案中的一种,假设该方案叫Standard方案,如果以后有更好的算法方案,应该能方便地替换Standard方案,这时就需要有一个可替换的机制。将Standard方案封装起来就可以实现替换,生成器Builder模式能够实现这种组装过程的封装。
Builder模式是一种将复杂对象的组装创建过程封装起来的模式,将过程中具体实现细节隐含在抽象接口后面。想到Builder模式应该就会联想到汽车的组装:汽车分很多部件,部件组装起来就成为一辆汽车,Builder模式是将汽车组装过程进行了封装。
“页”是这个项目中的一个复杂对象,需要通过复杂的混合组装过程,将内容在一个“页”中以一定模板显示出来。分别依据Builder模式的参与者设计如下:
“页(Page)” 是Builder模式中要构造的复杂对象。“页(Page)”是由菜单(Menu)、内容(body)以及模板JSP等部件组成。
Builder模式中生成器是部件创建的抽象接口,在这个接口中,定义部件创建的一些方法:
/**
* Builder模式接口
* Copyright: Jdon.com Copyright (c) 2003
* Company:
* @author banq
* @version 1.0
*/
public interface PageBuilder {
//创建Menu部件的方法
public Menu buildMenu(Integer Id);
//创建Content部件的方法
public String buildContent(Integer Id);
//创建Body部件的方法
public Body buildBody(Integer Id);
//创建模板JSP部件的方法
public boolean buildTemplate(Integer Id);
//获得组装构建后的产品:“页(Page)”
public Page getPage();
}
在对“页(Page)”有了准确的部件分解后,才会有PageBuilder中抽象的部件构建方法。不管以后如何变化,Page组成的部件是不会变化的。正是有这个不变的前提,才能封装部件的创建以及部件之间组合的过程。
Builder模式中的Director 是指导部件的组装,实现与外界的接口:
/**
* Builder模式Director
* Copyright: Jdon.com Copyright (c) 2003
* Company:
* @author banq
* @version 1.0
*/
public class PageRender {
private PageBuilder pageBuilder = null;
//构造PageRender类时需要指定一个生成器
public PageRender(PageBuilder pageBuilder){
this.pageBuilder = pageBuilder;
}
//创建组构造页面,该方法是被外界访问和调用的
public boolean construct(Integer Id) {
//执行菜单Menu组装
Menu menu = pageBuilder.buildMenu(Id);
//执行Content组装
String output = pageBuilder.buildContent(Id);
//执行Body组装
Body body = pageBuilder.buildBody(Id);
//执行模板组装
if (pageBuilder.buildTemplate(Id)){
Debug.logVerbose(" -->>>finish constructing a new page", module);
return true;
}else{
return false;
}
}
//解构页面方法,如果页面需要被删除
//需要逐步删除或撤除这些部件
public void unConstruct(Integer Id) {
//首先删除模板 这与创建时顺序相反
pageBuilder.buildTemplate(Id);
//删除Content
pageBuilder.buildContent(Id);
//删除Body
pageBuilder.buildBody(Id);
//最后删除菜单
pageBuilder.buildMenu(Id);
}
}
上例中,已经看到Director如何指挥一个Page产品的组装和拆除过程。组装的过程是有先后次序的:菜单总是需要第一个被创建,有了菜单,才有菜单指向的内容,内容有了,才会有页面的模板显示。而删除页面时,整个次序则倒过来,从外向里逐个剥离开。
设计好3个主要的参与者,Builder模式基本确立,还有一个是参与者是生成器(Builder)的具体实现者。
StandardPageBuilder是PageBuilder的具体实现者。在这个类中,封装的是Standard方案。如果将来有其他更好的实现细节,可以再创建一个类。比如BestPageBuilder,使用BestPageBuilder代替StandardPageBuilder就可以使系统有一个更好的算法,而这种切换带来的修改是非常小的。从外界调用的变化中可以看出:
采取Standard方案时外界的调用如下:
PageBuilder pageBuilder = new StandardPageBuilder(pageEvent);
PageRender render = new PageRender(pageBuilder);
render.construct(Id);
采取Best方案后的调用如下:
PageBuilder pageBuilder = new BestPageuilder (pageEvent);
PageRender render = new PageRender(pageBuilder);
render.construct(Id);
可见变化的只是一个实例名的更改。
StandardPageBuilder代码的主要部分如下:
public class StandardPageBuilder implements PageBuilder {
private final static String module = StandardPageBuilder.class.getName();
private final static XmlUtil xmlUtil = XmlUtil.getInstance();
//初始化PageFactory
private final static PageFactory pageFactory = PageFactory.getInstance();
private Page page = null;
private String action = null;
public StandardPageBuilder(PageEvent pageEvent) {
this.page = pageEvent.getPage();
this.action = pageEvent.getAction();
}
/**
* 生成Menu
*/
public Menu buildMenu(Integer Id) {
if (action.equals(PageEvent.CREATE))
return createMenu(Id);
else if (action.equals(PageEvent.QUERY))
return queryMenu(Id);
else if (action.equals(PageEvent.EDIT))
return updateMenu(Id);
else if (action.equals(PageEvent.DELETE))
deleteMenu(Id);
return null;
}
/**
* 1. 创建新的Menu实例
* 2. 保存到持久层
*/
private Menu createMenu(Integer Id) {
Debug.logVerbose(" -->> enter build menu:" + page.getName(), module);
Menu menu = pageFactory.createMenu(Id);
pageFactory.updateMenu(menu, page);
Debug.logVerbose("menu object created, ID is " + menu.getId(), module);
Navlink navlink = page.getNavlink();
navlink.setId(Id);
//add the menu to the navlink
navlink.addMenu(menu);
try {
//委托pageFactory实现存储操作
pageFactory.saveNavlink(navlink);
Debug.logVerbose(" --> >build Menu ok", module);
page.setNavlink(navlink);
} catch (Exception ex) {
Debug.logError(" build Menu error:" + ex, module);
menu = null;
}
return menu;
}
…
//生成Body
public Body buildBody(Integer Id) {
if (action.equals(PageEvent.CREATE))
return createBody(Id);
else if (action.equals(PageEvent.QUERY))
return queryBody(Id);
else if (action.equals(PageEvent.EDIT))
return updateBody(Id);
else if (action.equals(PageEvent.DELETE))
deleteBody(Id);
return null;
}
//委托PageFactory实现Body数据的创建
private Body createBody(Integer Id) {
Debug.logVerbose(" -->> enter build Body " + page.getTitle(), module);
String author = "";
String title = page.getTitle();
//body's content is the file name that contains the content of the page
String content = page.getHtmlText();
Integer hits = new Integer(0);
Integer status = new Integer(0);
Body body = null;
try {
body = pageFactory.createBody(Id);
body.setAuthor(author);
body.setTitle(title);
body.setContent(content);
body.setHits(hits);
body.setStatus(status);
pageFactory.updateBody(body);
} catch (Exception e) {
Debug.logError(" build body error in :" + e);
}
return body;
}
…
//生成模板
public boolean buildTemplate(Integer Id) {
if (action.equals(PageEvent.CREATE))
return createTemplate(Id);
else if (action.equals(PageEvent.EDIT))
return createTemplate(Id);
else if (action.equals(PageEvent.DELETE))
deleteTemplate(Id);
return true;
}
//创建模板文件xxx.jsp
public boolean createTemplate(Integer Id) {
boolean success = false;
Menu menu = null;
try {
Debug.logVerbose("--> find the menu by the Id: " + Id, module);
//从PageFactory获取Menu对象
menu = pageFactory.getMenu(Id, page.getNavlink());
if (menu == null)
return success;
String titleValue = menu.getName();
String bodyValue = menu.getDataLink();
Debug.logVerbose("--> jspFileName =" + menu.getLink() + " bodyValue=" +
bodyValue, module);
String jspPathName = XmlUtil.getJspDirStr(menu.getId().toString());
//jsp文件操作实际委托JspMaker操作
success = JspMaker.generate(jspPathName, titleValue, bodyValue);
} catch (Exception ex) {
Debug.logError(" nof get the menu id =" + Id, module);
return success;
}
return success;
}
…
}
在图4-10中,PageHandler作为Builder模式的客户端,通过使用PageRender的construct访问来构建页面产品。而StandardPageBuilder作为Builder接口的一个实现,它又将有关Menu Body等对象的操作委托给PageFactory来完成。
结合图4-10和图4-9,本项目的整个流程控制图基本已经明晰,用户向表现层发出请求,在表现层封装成基本对象后,交由图4-10中的PageHandler处理,而PageHandler将复杂的组装过程委托给Builder模式来实现。在采取Standard方案组装“页(page)”的StandardPageBuilder中,凡是涉及到数据层操作,例如数据持久化保存等,都再次委托给PageFactory去处理。
图4-10 Builder模式的流程图
通过图4-9,PageFactory封装了数据层的操作细节,其中一个是使用XML文件作为数据保存的方案XmlPageFactory。XmlPageFactory又将各个部件对象的数据操作委托NavlinkManager或BodyManager这样的类来实现。在NavlinkManager中调用DBO模式的实现者CastorHandler,而在CastorHandler中封装的是数据对象序列化或反序列化的细节和相关XML API操作细节。
本 项目的整体设计主要体现了模式驱动的架构设计,在整个流程图中,每个模式很好地实现和封装了项目需求的各个功能,而且每个模式又层层相扣且紧密衔接在一 起,就如同模板有机地拼装在一起。更重要的是,这样一个系统是一个具有很强伸缩性的动态可扩展的、稳健的系统,而且便于他人阅读理解。
假 设不采取这样的模式驱动思路来设计,这个系统将会怎样?首先,多层架构分离就无法很好地实现,这样,数据持久化操作也许可能就在表现层中完成;其次,菜单 或内容对象的操作以及组装细节可能混合充斥在一两个类中实现,按照这样的过程化设计思路编制后的系统将是怎样?也许可以运行,也许调试时就会出现错误无法 定位的问题,难于调试,更加难于维护和拓展。
更有甚者,如果有一天需要使用数据库来实现菜单或内容对象的数据持久化,整个系统就会重建。重建的代价实际是破坏了系统的稳定性,客户的抱怨会严重影响公司的声誉。
所有这些代价的付出都是由于设计的简单性,其中痛苦和无奈是很多有经验的程序员经常碰到的。
表现层主要使用Struts框架完成,Struts分两个部分,一部分是视图View的实现,基本是JSP页面。另外一部分是控制器部分,具体是Action类的继承者,通过Action类的实现将前台界面用户输入的表单数据打包成基本对象,递送到后台处理。
在图4-11中,pageAction负责两种页面的输出。一个是创建页面;另一个是编辑页面。由于这两个页面数据结构类似,因此采取统一的Action控制输出。当用户填写好创建页面或编辑页面的表单数据后,单击确认按钮后提交,系统将由SavePageAction负责表单数据的处理和保存。
在SavePageAction中,主要是从用户输入的表单数据PageForm中获取数据,检查合法性,然后将这些数据封装成基本业务对象(Menu或Bodu等),委托给PageHandler实现真正的逻辑处理。而从图4-10中可以看到PageHandler以后的处理过程。
图4-11 表现层的流程图
关于Struts的开发工具,Jbuilder 8以后版本支持Struts开发,也可以使用其他工具甚至文本编辑器进行相关JSP 和Javabean的编辑。Struts的使用涉及面比较多,而且有不少类似“暗沟”的机制,只要耐心地去理解,相信会熟练掌握。
1.创建一个自己的项目目录。
要在自己硬盘上建立如下图结构的目录系统:
CMS
|
|--- WEB-INF
| |
| |--- classes
| |
| |--- lib
|
|--- navlink (菜单JSP所在目录)
|--- template (布局模板目录)
|--- admin (管理目录)
|--- body (页面内容所在目录)
2.加入所需要的Strutss1.1文件。
将Struts中的*.tld文件加入WEB-INF目录下:
Strutss-bean.tld
Strutss-html.tld
Strutss-logic.tld
Strutss-nested.tld
Strutss-template.tld
Strutss-tiles.tld
Struts的*.jar的库文件加入lib目录下。
3.建立web.xml文件如下。
关于编码问题。由于Java是跨平台的,Java在进行需要读取文件或显示等操作时,往往是按照运行平台的默认编码方式。在中文Winodws平台是GBK, 在Linux平台是ISO8859-1。由于在项目中用到很多组件框架,比如Strutss等,这些软件的默认编码方式都可能是ISO8859-1。英语环境下的人们还没习惯用UTF-8,那么就很容易造成在整个项目系统中编码不统一,从而导致乱码。
统一编码就成了编写支持中文系统的重要而且棘手的任务,本项目中将使用UTF-8为统一编码。这包括几个方面:JSP输入输出、Javabeans的编译、数据库或文件系统的访问、配置文件的设置。
Struts中需要一个ActionForm,它实际是一个Javabean,字段都是JSP中form的一些字段,如果有过JSP/Javabeans开发经验的人会使用下列代码:
这里UserInfoBean的字段对应HTML中Form表单的字段,从而在Form提交时,表单字段自动映射到UserInfoBean的字段。如果不清楚,可以参阅有关JSP/Javabeans的章节。
将要建立的PageForm其实就是类似UserInfoBean的Javabean:
//pageForm是继承Struts中的ActionForm的
public class PageForm extends ActionForm {
// Page的Id
private Integer id;
//Page的名字,就是菜单的名字
private String name;
//Page的Html内容
private String htmlText;
//Page的标题
private String title;
public void setId(Integer id) { this.id = id; }
public Integer getId() { return id; }
public void setName(String name) { this.name = name; }
public String getName() { return name; }
public void setHtmlText(String htmlText) { this.htmlText = htmlText; }
public String getHtmlText() { return htmlText; }
public void setTitle(String title) { this.title = title; }
public String getTitle() { return title; }
/**
* 下面是设置校验,进行表单的错误检查
* 如果本方法返回是null,Struts将不会进行错误输出
* 如果不为空,将会输出错误,错误内容是error.name.required的值
*/
public ActionErrors validate(ActionMapping actionMapping,
HttpServletRequest httpServletRequest) {
ActionErrors errors = new ActionErrors();
//如果没有输入页面的名字,需要报错
if ( (name == null) || (name.length() < 1)){
Debug.logVerbose(" error: name is required ", module);
errors.add("name", new ActionError("error.name.required"));
}
//如果没有输入页面的标题,需要报错
if ( (title == null) || (title.length() < 1)){
errors.add("title", new ActionError("error.title.required"));
}
return errors;
}
//复位,将数据清空
public void reset(ActionMapping actionMapping,
HttpServletRequest httpServletRequest) {
this.id = null;
this.name = null;
this.title = null;
this.htmlText = null;
}
}
在PageForm中validate方法要启用,需要几个步骤:
首先在validate方法中加入字段检验,如果返回不为空,Struts将启动错误输出。
然后,为Strutss错误输出做准备工作,比如上面的name为空,则Struts会到一个ApplicationResources.properties 文件中查询error.name.required对应的值。
在ApplicationResources.properties输入error.name.required的值:
error.name.required=
error.title.required=
error.id.required=
error.action.required=
# 下面是错误输出时的标题和尾部,可以自己定制
errors.footer=
errors.header=Validation Error
You must correct the following error
这样,Struts会取得error.name.required对应的值
至于具体位置加在JSP页面的哪个部位,则取决于页面设计了。
注意,最后还有最重要的一步,必须告诉Struts这个ApplicationResources.properties在什么位置。一般ApplicationResources.properties是放在项目的classes目录下和具体的class绑定在一起。
在WEB-INF下建立Strutss-config.xml文件,这是Struts最重要的核心配置文件,需要在其中加入:
在Struts-config.xml中,定义了一个FormBean是PageForm。它实际是PageForm类。同时,还需要告诉Struts,ApplicationResources.properties是在类classess目录的com/jdon/cms/events目录下,和com.jdon.cms.events的其他类在一起。
Action是和控制器Servlet在一起工作的,Action是整个系统中关键的、起调度控制作用的类。
在Action类中,将要处理前面页面输入本类的参数,进行相关业务逻辑运算。然后根据结果,从Strutss-config.xml中根据用户定制的Forward页面,将Forward页面推到用户的浏览器中去。
PageAction是控制页面输出的,使用pageAction.do?action=create 或pageAction.do? action=edit控制create或edit页面输出,前者会输出createPage.jsp,后者则会输出editPage.jsp。
PageAction中主要实现两种功能:
· 如果要求输出的是编辑页面,那么就需要将编辑的数据首先显示出来。
· 查询Strutss-config.xml,决定输出新增页面还是编辑页面。
PageAction主要代码如下:
public class PageAction extends Action {
public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response) {
String action = request.getParameter("action");
//清除之前的Attribute中的数据
FormBeanUtil.remove(mapping, request);
//获得pageForm对象并在attribute中保存起来,实现第一个功能
extractPageForm(action, mapping, form, request);
//根据action的值,从mapping中查询出决定跳转的页面,然后直接跳转
if (action.equals(PageEvent.DELETE))
return (mapping.findForward("deletePage"));
else if (action.equals(PageEvent.EDIT))
return (mapping.findForward("editPage"));
else
return (mapping.findForward("createPage"));
}
…
}
其中extractPageForm方法主要是实现PageAction的第一个功能,主要内容如下:
private void extractPageForm(action, ActionMapping mapping, ActionForm form,
HttpServletRequest request){
//获得id
Integer id = new Integer(request.getParameter("id"));
//通过PageHandler来获得该id的数据
PageHandler pageHandler = new PageHandler();
Page page = pageHandler.getPage(id);
//使用Javabean复制功能,将page对象中和pageForm一样的字段
//复制到pageForm中,这是一个自动方便的转换工具
PropertyUtils.copyProperties(pageForm, page);
}
PageAction中跳转页面的功能是通过mapping.findForward实现的,这个方法是从ActionMapping中获取跳转页面,而ActionMapping的数据是在Strutss-config.xml中设置的:
type="com.jdon.cms.events.PageAction"
validate="false"
scope="request"
path="/pageAction">
在action-mappings中指定了“/admin/createPage.jsp”等几个跳转的JSP文件。
PageAction的第一个功能实现是事先创建好一个ActionForm实例,根据id查询数据源,获得相应的数据并将其赋值到这个实例中,名称为pageForm。
这样,就得到了一个装有数据的pageForm实例,PageAction在自己结束后,如何让editPage.jsp自动获取这个有数据的pageForm,这里有一个Hook钩子,Struts是通过使用Servlet的request或session的attribute来作为中转储存空间的。
如果当前的Scope是request,那么使用
request.setAttribute(actionMapping.getAttribute(), pageForm);
如果当前Scope是session,那么使用
session.setAttribute (actionMapping.getAttribute(), pageForm);
那么,actionMapping.getAttribute()的值是什么?这是在Strutss-config.xml里的action-mapping中设置的,设置attribute="pageForm"。
注意这里没有设置通常的name="pageForm",这两者是有区别的。
由于没有设置name="pageForm",那么需要同时设定validate="false"。因为这个Action没有与ActionForm相联系,所以不要实现校验功能。
关于使用Action的注意点: Action其实是Servlet的一部分,而Servlet本质就是线程,在默认情况下,Action只有一个实例对象,以多线程方式为所有请求服务,因此编写Action类必须考虑线程安全性:
· 使用局部变量,不要用类变量。
· 要考虑资源争夺的问题。
· 对于Exception的捕获,捕获后输出日志系统,而不要使用throw抛出。
在JSP中消除Java代码分两个步骤:
· 能够在Action中实现的功能移动到Action中实现。
· 使用Struts标签语句替代Java代码实现的功能。在JSP中使用Struts特殊标签语句,参考http://jakarta.apache.org/Strutss/userGuide/dev_html.html。
page.jsp是将createPage.jsp和editPage.jsp的功能合并在一起,因为这两个页面的数据布局和结构非常相似。page.jsp代码如下:
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="/WEB-INF/Strutss-logic.tld" prefix="logic" %>
<%@ taglib uri="/WEB-INF/Strutss-template.tld" prefix="template" %>
<%@ taglib uri="/WEB-INF/Strutss-bean.tld" prefix="bean" %>
<%@ taglib uri="/WEB-INF/Strutss-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/app.tld" prefix="app" %>
scope="request" value="create">
scope="request" value="edit">
scope="request" value="delete">
scope="request" value="create">
新增页面
scope="request" value="edit">
编辑页面/
删除本页
scope="request" value="create">
页面名称: 页面标题: 页面内容:
从上面JSP页面看出来,这个页面中已经全部没有了Java代码,代替以特有的标签语句,其中典型html:text语法如下:
相当于以前的代码:
<% if (pageForm.getName() ! = null) %>
”>
<% else %>
由此可见,标签库使用是简单方便的。一开始有很多人并不喜欢标签库,从而在JSP中嵌入Java,这种倾向导致的危险是非常大的。可以毫不夸张地说,这样做最终会将Java体系的优越性丧失殆尽。
在page.jsp中,还自定义了一个自己特定的标签:
删除本页
这相当于:
/pageAction.do?action=delete&id=<%id%>" >
删除本页
可见,为了完全取消JSP中的Java代码,必要时,要亲自动手定制标签库。下面看看如何实现。
在page.jsp的顶部引入了app.tld:
<%@ taglib uri="/WEB-INF/app.tld" prefix="app" %>
这需要在WEB-INF目录下建立app.tld:
在这个app.tld中定义了linkHtml标签,这样在page.jsp中就可以使用
其中page和name是两个属性参数,需要输入到类com.jdon.cms.tags.LinkHtmlTag进行处理的。实际上app.tld是在JSP页面和Javabean之间作了一个连接。编写LinkHtmlTag的代码:
public class LinkHtmlTag extends TagSupport {
protected String name = null;
protected String page = null;
public String getName() { return (this.name); }
public void setName(String name) { this.name = name; }
public String getPage() { return (this.page); }
public void setPage(String page) { this.page = page; }
/**
* 生成标签开始 也就是在JSP中写
*
*/
public int doStartTag() throws JspException {
//相当于在JSP中写入<%request.getContextPath()%>
HttpServletRequest request =
(HttpServletRequest) pageContext.getRequest();
StringBuffer url = new StringBuffer(request.getContextPath());
url.append(page);
//根据name属性值,获取pageForm对象
PageForm pageForm = null;
try {
pageForm = (PageForm) pageContext.findAttribute(name);
} catch (ClassCastException e) {
pageForm = null;
}
if (page.indexOf("?") < 0)
url.append("?");
else
url.append("&");
url.append("id=");
url.append(pageForm.getId());
// 产生连接内容,相当于在JSP中写入:
HttpServletResponse response =
(HttpServletResponse) pageContext.getResponse();
StringBuffer results = new StringBuffer("
results.append(response.encodeURL(url.toString()));
results.append("/">");
// 将results字符串输出到JSP页面 JspWriter writer = pageContext.getOut();
try {
writer.print(results.toString());
} catch (IOException e) {
throw new JspException("LinkHtmlTag error");
}
return (EVAL_BODY_INCLUDE);
}
/**
* 生成替代JSP页面中代码
* 这里是以替代
*/
public int doEndTag() throws JspException {
JspWriter writer = pageContext.getOut();
try {
writer.print("");
} catch (IOException e) {
throw new JspException ("LinkHtmlTag error");
}
return (EVAL_PAGE);
}
public void release() {
super.release();
this.page = null;
}
}
标签库表面上好像比较繁琐,没有在JSP直接写Java代码来得直接方便。但是,J2EE的整个体系本身的特点就是细化、细分,这也符合面向对象分派和封装的原则,因此在J2EE中到处可以看到一点一点的“碎片”,XML配置文件和Java代码在J2EE中往往是自然地组合在一起。当然,这样带来的缺点是增加了复杂性。
上面的page.jsp是向savePageAction.do提交表单Form的,SavePageAction也是一个Action,主要负责将表单提交的数据实现保存。
SavePageAction和PageAction的用途不一样。后者是主要控制创建或编辑功能页面的输出。这两种Action基本包括了Action的用途,在其他项目中可以参照这两个Action做法,其实这也算一个模式了吧?这个模式主要是对数据创建或编辑功能适用。
为了激活savePageAction.do,需要在Strutss-config.xml中的action-mappings加入:
type="com.jdon.cms.events.SavePageAction"
validate="true"
input="/admin/pageError.jsp"
scope="request"
path="/savePageAction" />
应该注意到这里使用了name="pageForm",不同于PageAction中使用的attribute="pageForm",两者目的各有不同,因为PageForm和SavePageAction联系了在一起,当page.jsp向SavePageAction提交表单数据时,Strutss将数据保存在PageForm对应的字段中,如果validate又设置为true,那么就会执行PageForm中的validate方法。SavePageAction可以从PageForm中获取page.jsp中的表单数据。
这里还有一层含义,因为page.jsp中提交Action是savePageAction.do,而上面关于savePageAction.do的设置是使用com.jdon.cms.events.SavePageAction类,同时name是pageForm,那么就是说pageForm指向的实例是page.jsp 的FormAction了,或者说是Form Bean,这也说明了为什么在page.jsp显示之前,PageAction将包含数据的PageForm实例保存在request或session的attribute中,而page.jsp在显示时,会自动读取PageForm实例中的数据。
在上面的action-mappings配置中,激活了校验功能。如果Struts校验发现了错误,Struts将会输出错误,那么错误输出哪个JSP页面?就是input中输入的值/admin/pageError.jsp,这里专门做了一个出错页面pageError.jsp。当然不能忘记在pageError.jsp中加入
SavePageAction主要是委托业务逻辑层的PageHandler再通过数据层实现数据保存和持久化:
public class SavePageAction extends Action {
public ActionForward execute(ActionMapping actionMapping,
ActionForm actionForm,
HttpServletRequest request,
HttpServletResponse response) {
…
//生成PageEvent
PageEvent pageEvent = new PageEvent();
Page page = new Page();
try {
//将pageForm中的数据复制到Page对象中
PropertyUtils.copyProperties(page, pageForm);
//使PageEvent装载Page数据对象
pageEvent.setPage(page);
//设置操作类型是创建还是编辑
pageEvent.setAction(pageForm.getAction());
}
catch (Exception e) {
Debug.logError("copyProperties Error:" + e, module);
}
//委托pageHandler实现具体数据操作
PageHandler pageHandler = new PageHandler();
if (pageHandler.perform(pageEvent)){
Debug.logVerbose("update page success " , module);
return actionMapping.findForward("pageOk");
}else
return actionMapping.findForward("pageError");
}
}
当然页面提交后台实现操作后,如果成功,SavePageAction就向前台用户界面输出pageOk.jsp页面,表示操作成功。
至此,本项目中关于Page的内容操作流程基本结束,下面将讨论模板的实现。
在前面设计中,一个页面被分为几个区域:标题栏、页尾栏、菜单栏以及内容栏部分。如何使用Tile将这几个部分组合在一起?
Tile的页面组装配置可以在JSP,也可以在tiles-defas.xml中。
关于在JSP中定义已经在前面介绍中看到,只要输入下列语句:
tiles:insert就类似JSP的include,在这个JSP运行时,Strutss-Tiles将分别将header.jsp body.jsp和footer.jsp一起输出。
但是,如果每次将其他不变部分都写入JSP,也将是比较琐碎的事情,在配置文件tiles-defs.xml中定义将更加灵活,还可以有继承的概念。
在WEB-INF下建立tiles-defs.xml文件,当然建立好tiles-defs.xml后,还要记得告诉Strutss-Tiles,那么就要在Strutss-config.xml中加入下列语句:
这表示将要使用插件TilesPlugin,插件的配置文件是/WEB-INF/tiles-defs.xml。
下面建立/WEB-INF/tiles-defs.xml:
在tiles-defs.xml中,定义了一个叫site.mainLayout的页面,实际页面是来自相对目录/template/no1/layout/下的classicLayout.jsp。
那么应该在classicLayout.jsp中定义整个页面的大体布局,如下:
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="/WEB-INF/Strutss-tiles.tld" prefix="tiles" %>
在上面布局中,分别使用tiles:insert 来代替这里的区域,这个页面布局模板是可以自己调换,例如可以将菜单栏搬迁到右边或上下边,这些都可以根据美工师设计要求进行设计,一旦确定4个部分后,只要使用tiles:insert来放置在这些HTML语法中,将来JSP显示时,Strutss-Tiles会用实际的页面自动替代这些tiles:insert。例如:
表示分别用相对目录/tempalte/no1/下header.jsp和footer.jsp替代
那么,菜单部分使用什么JSP来替代?
菜单不像header.jsp那样,可能不是动态变化的。由于管理员随时可能会增加或删除页面,那么肯定引起指向页面的菜单按钮的变化,所以菜单的显示是需要和本项目中的逻辑处理层联系在一起的。
在上面的tiles-defs.xml中有一句定义菜单的配置:
这表示菜单是从目录navlink下的cmsMenu.jsp中读取的,因此需要创建cmsMenu.jsp:
<%
//获得PageFactory指向
PageFactory pageFactory = PageFactory.getInstance();
//从pageFactory中获得Navlink实例
Navlink navlink = pageFactory.getNavlink();
request.setAttribute("navlink", navlink);
%>
<%-- 遍历 Navlink对象中menus集合,取出com.jdon.cms.Menu 对象 --%>
property="menus" type="com.jdon.cms.Menu" >
<%
String link = menu.getLink();
%>
cmsMenu.jsp是将页面Page的Navlink中menus集合逐个读出来,然后从上向下显示。当然具体显示方式(如并排显示)可以自己定制。这个JSP含有Java代码,因此需要重整一下。类似前面章节Page操作一样,使用ActionForm或标识库实现优化。
为了验证上述配置和设计,可以在根目录下创建index.jsp作为模拟显示,测试观察效果。在index.jsp中写入:
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="/WEB-INF/Strutss-tiles.tld" prefix="tiles" %>
这表示,在这里插入tiles-defs中的site.index.page页面,而site.index.page是从tiles-defa.xml中得知,正好继承的是site.mainLayout,而site.mainLayout指向classicLayout.jsp,得到index.jsp的效果图如图4-12所示。
图4-12 index.jsp的效果图
可见,使用Tiles实现本项目中的模板功能是成功的。当管理员创建一个新的页面时,本系统将依次创建菜单、HTML内容以及模板JSP,在模板JSP中只要写入index.jsp中类似内容。例如,管理员创建了一个叫“产品和服务”的新页面,得到的页面id是2,那么在2.jsp中程序会自动写入:
这样,图4-12屏幕中的“This is body”部分将被管理员输入的有关“产品和服务”的实际内容所代替。
在J2EE应用中经常需要知道某个文件的绝对路径,如何能从Web容器中自动获得某文件的绝对路径?
例如上节中,1.jsp中body指向的是“/data/1.jsp”,在相对目录data下的1.jsp是系统程序自动生成的。也就是说使用了File有关API操作,而File操作必须提供绝对路径。例如,在项目系统建立c:/cms下,目录结构见前面章节,那么“/data/1.jsp”的绝对路径应该是c:/cms/data/1.jsp。注意,如果有本地文件操作,那么系统就不能打包成*.war,必须提供完整的目录路径。
那么如何获取根目录c:/cms?通过配置文件设定也可以,但是不灵活,每次移动项目都要修改配置文件。
Servlet中有servlet.getServletContext().getRealPath(pathname);,可以得到pathname的绝对路径,因为每个Web应用中,肯定有WEB-INF/web.xml文件,而通过上句就可以获得web.xml的绝对路径。经过简单处理就可以获得系统的绝对路径。
但是在哪里执行语句:servlet.getServletContext().getRealPath(“web.xml”)?
专门写个初始化的Servlet不是很合适;如果在JSP中写入显然不很规范。 Strutss 1.1提供了Plug-in功能,一般是在系统启动先执行Plug-in,前面定义的tiles-defa.xml就是通过Plug-in启动使用的。
只要创建一个类FileEnvPlugIn来继承org.apache.Strutss.action.PlugIn,在init方法中将系统的绝对根目录获得就可以了。有兴趣者可以进一步参考PlugIn的API。
5 项目重整Refactoring本项目主要开发过程和相关设计代码基本结束,如果需要完整的源代码,可以见本书的光盘,或者到作者网站http://www.jdon.com下载。 整个项目的开发首先是从业务逻辑开始的,也可以称之为Domain logic。其中基本业务对象的建模是一个重要的开始,一旦从一个新的项目中准确本质地提炼出业务对象,那么可以说是成功了一半。但是往往实际情况则是要经过几次反复,从反复设计和实现中会真正发现事物的本质,这也是程序员的快乐源泉之一。 当然,划分成多层后会增加系统的复杂性,Struts的MVC模式分层也增加了使用的难度。当然,随着Struts的图形化开发工具日益成熟,相信很快就可以按动鼠标拖放实现表现层的设计。 本项目很多部分是经过重整(Refactoring)而来,而且还有很多需要重整的地方,重整就是在不改变代码功能,提高代码质量而实行的修改行为,提高代码质量的目的无非是为了降低耦合性、提高重用性和增强灵活和伸缩性。 重整是J2EE项目中不可或缺的一部分,原有的代码经过好的重整,比如重整到一个设计模式等,那么新的功能增加往往只是举手之劳,相反,如果不实行重整,整个代码只能越加越乱,在平常生活中可以找到类似点,假如有一个储物室,一开始只放一点点东西,只要随手一放就可以,下次再进来就很快发现,但是当东西很多时,就不得不整理,以使得他们放置有规律有规则,很显然重整也是这样,将原有代码层次分清楚,归好类,是为了更方便的加入新的代码。 |
本项目由于只使用了J2EE中的Web框架,因此可以使用专门的Web容器服务器。Tomcat (http://jakarta.apache.org/tomcat/)是免费开源的Web服务器,是Jakarta项目中的最著名的子项目,它还是Sun公司推荐的servlet和jsp容器(http://java.sun. com/products/jsp/tomcat/)。
很多人一直对免费或开放源代码的产品实现商业应用存有疑惑,但是Tomcat作为类似Apache一样的Java服务器,经过几年来大量使用证明,其稳定性和成熟性是有目共睹的。经过最新的研究表明:系统在并发用户100以下时,Tomcat的响应时间和商业服务器相差不多,可见Tomcat是能够大量被应用到中小型企业系统中。
有关Tomcat的详细配置可以参见之前章节。下面是将本项目发布到Tomcat下调试运行:
编辑Tomcat目录下conf目录的Server.xml中找到下列行数:
其后加入本项目的配置:
reloadable="true" crossContext="true" />
其中/jdoncms是路径,也就是可以通过http://主机名/jdoncms来访问本项目,而docBase则是本项目的绝对路径。
启动Tomcat,如果想检查启动过程中的错误,可以到Tomcat/logs目录观看相应的日志。在浏览器窗口输入http://localhost:8080/jdoncms。
项目第一次调试总会存在诸多问题,因此需要打印出系统中每个步骤的运行结果,这样可以跟踪调试。以前是使用System.out.println来设置输出调试信息。但是这有一个缺点,就是当项目切入正式运行后,不希望这些日志输出到容器的控制台上。因为这样还会降低系统性能,但是如果手工逐个到程序中删除,又将为以后扩展维护带来不便。
以后章节会谈到如何使用Log4J来跟踪调试,但是在Tomcat中设置Log4J不是一件方便的事情。需要做一个专门的Proxy类,通过这个Proxy类,可以动态地设置是使用System.out.println还是Log4J,或者关闭一些调试信息。
定义Log的级别如下:
ALWAYS = 0;
VERBOSE = 1;
TIMING = 2;
INFO = 3;
IMPORTANT = 4;
WARNING = 5;
ERROR = 6;
FATAL = 7;
错误程度分别是从上到下逐步严重,一般实行调试信息输出可以使用VERBOSE,但是在一个大型系统中,如果都实行VERBOSE级别,那么,势必在调试一些重要组件时,其他信息会发生严重干扰。这时可以在这些重要组件中实行其他级别输出,如IMPORTANT,在配置文件中只要指定输出级别就可以。
在本项目配置文件中定义:
这说明将输出VERBOSE以上信息输出开启,同时表示将不使用Log4J。
在本项目中,Debug.java实现了这样日志Proxy功能。在Debug中,先读取配置文件,以确定当前开启级别是在哪个级别,当外界以如下语句调用时:
Debug.logVerbose(" this result is " + result , module);
Debug会和配置文件的设置进行比较。如果大于设置的级别,将向控制台输出。至于是使用System.out还是Log4J,将取决于配置文件的第二项配置。
当运行本项目时,Tomcat控制台会输出如下信息:
[Debug:com.jdon.cms.events.PageAction:Verbose] ----> extractPageForm Action
[Debug:com.jdon.cms.events.FormBeanUtil:Verbose] enter to save the arribute
[Debug:com.jdon.cms.xml.NavlinkManager:Verbose] ... try to get the exsited file
D:/javasource/JdonCMS/web/WEB-INF/conf/navlink.xml
这样,如果发生错误,就能跟踪到错误发生位置,从而纠正问题。
网站内容管理系统从表面上看似乎是非常简单的一个网站系统。实际上,要开发出一个真正可动态伸缩、适应不同规模网站内容管理系统是有一定的难度的。
本章主要是通过一个简单内容系统的建立,介绍了J2EE的Web层技术,与前面两章同样是介绍Web技术所不同的是:本章的主要特点是从模式和框架的角度去讨论建设一个面向对象的可重用的Web系统。
前两章虽然也是讨论在Web层技术的实现,但是主要侧重在Web层中实现数据库操作管理,这种界面显示和数据操作混合式的设计架构其实是延伸传统的设计概念,最大的缺点也是传统系统的最大问题,即可拓展性和可维护性差。由于设计开发比较简单,在一些网站应用系统或专用系统中应用广泛。
但是,一旦系统的并发访问量提高,对系统性能要求随之提高,那么就必须在这种混合式系统中加入一些提高性能的技术支持层,如对象池、缓存等,开源代码工作流OFBiz和Jive论坛是这方面的典型代表。
在真正的J2EE多层结构中,Web层只被设计成负责用户界面的显示,从Web容器的线程池实现原理也可以了解这点,线程只适合那些处理过程简单短小的功能实现。复杂的业务功能计算将通过EJB等框架实现。
通常在B/S结构下,用户界面的可重用性较低,以对象化概念来设计开发有一定的困难,但是MVC模式的提出和Struts的实现,使得开发一个可重用的面向对象的Web系统成为可能。
当然,Struts的使用对于习惯在JSP中直接写入Java代码的程序员来说是复杂了一点,一般经过多个步骤才能完成一个界面功能的开发,例如要实现ActionForm、Action和配置Strutss-config.xml、在JSP中使用taglib等。
复杂了就容易产生问题,经常可能由于某个环节不小心导致调试不通。特别是数据的增、删、改操作中,如果数据对象有几百个,可能要创建双倍的Action子类,然后再配置Strutss-config.xml,其实每次数据的增、删、改操作过程都是差不多的。
那么,能不能将这种复杂的过程简单化,同时又不丧失使用Strutss的优点呢?针对数据的增、删、改、查通用操作过程,在Struts框架基础上再架构设计一层应用框架,目的是使开发简单、快速而稳定,关于这个Struts应用框架可以见后面章节“网上商店系统”的介绍。
总之,Java是复杂的,但是,Java可以由复杂变成简单。关键在于,需要针对程序员自己的开发领域设计新的应用框架,把那些复杂的东西隐藏到框架后面,这也是“磨刀不误砍柴工”的道理吧。
单从网站内容管理系统上看,使用Java或J2EE实现这个系统并不比其他语言PHP或ASP有更多的优势和好处,但是,在目前Java世界中,网站内容管理系统已经被整合进入门户Portal系统中,内容管理成为Protal系统中一个主要的Portlet。
一个灵活的、可伸缩的门户Portal系统是复杂的,Java平台所具有的可重用性、可拓展性以及高度安全性等特点才能够得到真正的体现。