JSF是一种新的用于构架j2ee应用用户界面的技术,它尤其适合于基于MVC架构的应用中。虽已有很多文章介绍过了JSF,然而它们大多从理论高度来介绍JSF而不是面向于实际应用。目前对于实际应用,JSF仍有很多问题没有解决,例如:如何使JSF适应于MVC整体构架中?如何将JSF与其他Java 框架整合起来?是否应该将业务逻辑放置在JSF的backing beans中?如何处理JSF中的安全机制?更为重要的是如何利用JSF构架现实世界的Web应用?
本文将涉及到上面的这些问题,它将演示如何将JSF、Spring和Hibernate整合在一起,构架出一个名为JCatalog的在线产品价目系统
。利用该Demo,本文涵盖了Web应用开发的每一个阶段,包括需求收集、分析,技术选择,系统架构和实现。本文讨论了在JCatalog中涉及到的各种技术的优点和缺点并展示了一些关键部分的设计方法。
本文的对象是从事基于J2ee的Web应用架构人员和开发人员,它并不是对JSF、SpringFramework和Hibernate的简单介绍,如果对这些领域不甚了解,请参看相关资源。
该范例的功能需求
JCatalog是一个现实世界的Web应用,我首先描述JCatalog的需求,在通篇的技术决策和架构设计时都将涉及到本部分。
在设计Web应用的第一阶段是收集系统的功能需求,范例应用是一个典型的电子商务应用系统,用户可以浏览产品的catalog并查看产品的详细情况,而管理员可以管理产品的catalog。通过增加一些其他功能,如inventory管理和订单处理等,该应用可成为一个成熟的电子商务系统。
Use cases
Use-case分析被用来展示范例应用的功能需求,图1就是该应用的use-case图。
use-case图用于表示系统中的actors以及可能进行的operations,在该应用中将有七个use-case,用户能够浏览产品 catalog和查看产品的详细情况,一旦用户登录到系统中,她将成为管理员,从而可以创建新的产品,编辑已存在的产品或者删除老的产品等。
Business rules
JCatalog必须符合以下business rules:
-
每个产品必须具有唯一的ID
-
每个产品必须属于至少一个category
-
产品ID一旦创立不得修改
Assumptions
我们在系统的设计和实现中做以下假定:
-
英语讲是缺省语言,且不需事先国际化
-
在Catalog不讲不会超过500个产品
-
catalog将不会被频繁的修改
Page flow
图2显示了所有的JCatalog的pages以及它们之间的transitions关系:
该应用中存在两组pages:公开的internet和用于管理的intranet,其中intranet只能被那些成功登录到系统的用户访问。 ProductSummary不作为一个单独的page展示给用户,它显示在Catalog page中的frame中。ProductList只对管理员可视,它包含用于创建、编辑和删除产品的链接。
图3是一个Catalog页面的示意图,理想状况下,在需求文档中应该包含每一页的详细示意图。
构架设计
Web应用开发的下一个阶段是构架设计,它包括将应用划分为多个功能组件并将这些组件分割组合成层,高层的构架设计应该中立于所选用的特定技术。
多层架构
多层架构是将整个系统清晰的分为多个功能单元:client、presentation、business-logic、integration和 EIS,这将确保职责得到清晰的划分,使得系统更易于维护和扩展。具有三层或等多层的系统被证明比C/S模型具有更好的伸缩性和灵活性。
client层是使用和表示数据模型的地方,对于一个Web应用,client层通常是浏览器,基于浏览器的瘦客户端不包含任何表示逻辑,它依赖于
presentation
层。
presentation层将business-logic层的服务展示给用户,它应知道如何处理用户的请求,如何同business-logic层交互,并且知道如何选择下一个视图显示给用户。
business-logic层包含应用的business objects和business services。它接受来在于presentation层的请求、基于请求处理业务逻辑。业务逻辑层组件将受益于系统级的服务,如安全管理、事务管理和资源管理等。
integration层是介于
business-logic
层和EIS层之间的桥梁,它封装了与EIS层交互的逻辑。有时,将integration层和business-logic层合称为中间层。
应用的数据被保存在EIS层中,它包括关系数据库、面向对象数据库和以及遗留系统等。
JCatalog的构架设计
图4显示了JCatalog的构架设计以及如何应用于多层构架系统中。
该应用采用了多层非分布式的构架,图4展示了系统的分层以及每一层中选择的技术,它同时又是该范例的部署图,它的presentation、 business-logic和integration层将存在于同一个web容器中。定义良好的接口将孤立每一层的职责,这一架构使得应用更为简单和更好的伸缩性。
对于presentation层,经验表明,最好的方法是选择已存在的并已得到证明了的Web应用框架,而不是自己去设计和开发新的框架。我们拥有多个可选择的框架,如Struts,WebWork和JSF等,在JCatalog中,我们选择采用JSF。
EJB和POJO都可以用来创建业务逻辑层,如果应用是分布式的,采用具有remote接口的EJB是一个好的选择;由于JCatalog是一个典型的不需要远程访问的Web应用,因此选用POJO,并充分利用Spring Framework的帮助,将是实现业务逻辑层的更好选择。
integration层利用关系型数据库事先数据的持续化,存在多种方法可用来实现:
-
JDBC:这是最为灵活的方法,然而,低级的JDBC难以使用,而且质量差的JDBC代码很难运转良好
-
Entity beans:CMP的Entity bean是一种分离数据访问代码和处理ORM的昂贵的方法,它是以应用服务器为中心的方法,即entity bean不是将应用与某种数据库类型而是EJB容器约束在一起。
-
O/R mapping framework:一个ORM框架采用以对象为中心的方法实现数据持续化,一个以对象为中心的应用易于开发并具有高度的可移植性。在该领域中存在几个框架可用—JDO、Hibernate、TopLink以及CocoBase等。在我们的范例中将选用Hibernate。
现在,我们将讨论每一层中的设计问题,由于JSF是一个相对较新的技术,因此将着重于它的使用:
presentation
层和JSF
表示层的功能是收集用户的输入、展示数据、控制页面导航并将用户的输入传递给业务逻辑层,表示层同时需要验证用户的输入以及维护应用的session状态。在下面几部分中,我将讨论表示层设计时的考虑和模式,并说明选择JSF作为JCatalog表示层的原因。
MVC
MVC是Java-Blueprints推荐的架构设计模式,MVC将几个方面分离开来,从而减少代码的重复,它以控制为中心并使得应用更具扩展性。MVC同时可帮助具有不同技能的用户更关注于自己的技能,通过定义良好的接口进行相互合作。MVC是表示层的架构设计模式。
JSF
JSF是Web应用的服务器端用户组件框架,它包含以下API:表示UI组件、管理它们的状态、处理事件、服务器端验证、数据转换、定义页面导航、支持国际化,并为这些特性提供扩展能力。它同时包括两个JSP的tag库以在JSP页面中表示UI组件,以及将组件wire为服务器端对象。
JSF和MVC
JSF非常适合于基于MVC的表示层架构,它在行为和表示之间提供了清晰的分离,它使得你可以采用熟悉的UI组件和web层概念而无需受限于某种特殊的脚本技术或标记语言。
JSF backing beans是JSF的Model层,此外,它同样包含actions,action是controller层的扩展,用于将用户的请求委派给业务逻辑层。这里请注意,从整体的应用构架看,业务逻辑层也被称为model层。包含JSF标签的JSP页面是表示层,Faces Servlet提供了controller的功能。
为什么选用JSF?
JSF不仅仅是另外一个Web框架,下面这些特性是JSF区别于其他Web框架之所在:
-
类Swing的面向对象的Web应用开发:服务器端有状态的UI组件模型,配合event listeners和handlers,促进了面向对象的Web应用开发。
-
backing-bean管理: backing bean是与页面中使用的UI组件相关联的javabean组件,backing-bean管理将UI组件对象的定义同执行应用相关处理和拥有数据的对象分离开来。JSF在合适的范围内保存和管理这些backing-bean实例。
-
可扩展的UI模型:JSF的UI模型是可配置的、可重用的,用以构建JSF应用的用户界面。你可以通过扩展标准的UI组件来开发出更为复杂的组件,例如菜单条、树组件等。
-
灵活的rendering模型:renderer分离了UI组件的功能和显示,多个renderers可创建和用来为同一客户端或不同的客户端定义不同的显示。
-
可扩展的转换和验证模型:基于标准的converter和validator,你可以开发出自己的可提供更好的模型保护的converter和validator。
尽管如此,JSF目前尚未成熟,随同JSF发布的 components、converters和validators都是最基础的,而且per-component验证模型不能处理components 和validators间的many-to-many验证。此外,JSF标签不能与JSTL间无缝的整合在一起。
在下面的章节中,我将讨论几个在JCatalog实现中的关键部分和设计决策。我首先解释managed bean的定义和使用以及JSF中的backing bean,然后,我将说明如何处理安全、分页、caching、file upload、验证以及错误信息定制。
Managed bean,backing bean,view object 和domain object model
JSF中引入了两个新的名词:managed bean和backing bean。JSF提供了一个强大的managed-bean工具,由JSF来管理的JavaBean对象称为managed-bean,一个 managed bean表述了一个bean如何被创建和管理,它不包含该bean的任何功能性描述。
backing bean定义了与页面中使用的UI组件相关联的属性和处理逻辑。每一个backing-bean属性邦定于一个组件实例或某实例的value。一个 backing-bean同时定义了一组执行组件功能的方法,例如验证组件的数据、处理组件触发的事件、实施与组件相关的导航等。
一个典型的JSF应用将其中的每个页面和一个backing-bean结合起来,然而在现实应用中,强制的执行这种one-on-one的关系不是一种理想的解决方案,它可能会导致代码重复等问题。在现实的应用中,多个页面可以共享一个backing-bean,例如在JCatalog中, CreateProduct和EditProduct将共享同一个ProductBean定义。
model对象特定于表示层中的一个view对象,它包含必须显示在view层的数据以及验证用户输入、处理事件和与业务逻辑层交互的处理逻辑等。在基于 JSF的应用中backing bean就是view对象,在本文中backing bean和view对象是可互换的名词。
对比于struts中的ActionForm和Action,利用JSF中的backing-bean进行开发将能更好的遵循面向对象方法,一个 backing-bean不仅包含view数据,而且还包含与这些数据相关的行为,而在struts中,Action和ActionForm分别包含数据和逻辑。
我们都应该听说过domain object model,那么,domain object model和view对象之间有什么区别呢?在一个简单的Web应用中,一个domain object model能够横穿所有层中,而在复杂的应用中,需要用到一个单独的view对象模型。domain object model应该属于业务逻辑层,它包含业务数据和与特定业务对象相关的业务逻辑;一个view对象包含presentation-specific的数据和逻辑。将view对象从domain object model中分离出来的缺点是在这两个对象模型之间必将出现数据映射。在JCatalog中,ProductBeanBuilder和 UserBeanBuilder利用reflection-based Commons BeanUtils来实现数据映射。
安全
目前,JSF没有内建的安全特性,而对于范例应用来说安全需求是非常基础的:用户登录到administration intranet中仅需用户名和密码认证,而无需考虑授权。
针对于JSF的认证,已有几种方法提出:
-
利用一个backing bean:这一个方法非常简单,然而它却将backing bean与特殊的继承关系结合起来了
-
利用JSF的ViewHandler decorator:这一方法中,安全逻辑紧密地与一特定Web层技术联系在了一起
-
利用servlet filter:一个JSF应用与其他的Web应用没有什么两样,filter仍是处理认证检查的最好地方,这种方法中,认证逻辑与Web应用分离开来
在我们的范例程序中,SecurityFilter类被用来处理用户的认证,目前,受保护的资源只包含三个页面,出于简单的考虑,将它们的位置被硬编码到Filter类中。
分页
该应用中的Catalog页面需要分页,表示层可用来处理分页,即它取出所有的数据并保存在这一层;分页同样可在business-logic层、 integration层、甚至EIS层中实现。由于在JCatalog中假设不超过500个产品,因此所有的产品信息能存放在一个user session中,我们将分页逻辑放在了ProductListBean中,与分页相关的参数将通过JSF managed-bean工具配置。
Caching
Caching是提高Web应用性能的最重要技术之一,在应用构建中的很多层中都可以实现caching。JSF managed-bean工具可以使在表示层实现caching非常容易。通过改变一个managed bean的范围,这个managed bean中包含的数据可以在不同的范围内缓存。
范例应用中采用了两级caching,第一级caching存在于业务逻辑层,CachedCatalogServiceImpl类维护了一个所有产品和目录的读写cache,Spring将该类作为一个singleton service bean来管理,所以,一级cache是一个应用范围的读写cache。
为了简化分页逻辑并进而提高应用的速度,产品同样在session范围内缓存到表示层,每一个用户维护着他自己的ProductListBean,这一方法的缺点是内存的消耗和数据的失效问题,在一个用户session中,如果管理员更改了catalog,用户可到的将是失效的数据,然而,由于我们假设应用的数据不会经常的改变,所以这些缺点将能够忍受。
File upload
目前的JSF Sun参考实现中不支持file upload。Struts虽已具有非常不错的file upload能力,然而要想使用这一特性需要Struts-Faces整合库。在JCatalog中,一个图像与一个产品相关联,在一个用户创建了新的产品后,她必须将相应的图片上传,图片将保存在应用服务器的文件系统里,产品的ID就是图像名称。
范例应用中采用、Servlet和Jakarta Common的file-upload API来实现简单的文件上传功能,该方法包含两个参数:图像路径和图像上传结果页面。它们将通过ApplicationBean来配置,详细内容请参看 FileUploadServlet类。
Validation
JSF中发布的标准validator是非常基础的,无法满足现实的需要,但很容易开发出自己的JSF validator,在范例中,我开发了SelectedItemsRange validator,它用来验证UISelectMany组件中选择的数量:
详细情况请参看范例。
定制错误信息
在JSF中,你可以为converters和validators创建resource bundle和定制错误信息,一个resource bundle可在faces-config.xml中创建:
catalog.view.bundle.Messages
并将错误信息的key-value对加到Message.properties文件中:
javax.faces.component.UIInput.CONVERSION=Input data is not in the correct type.
javax.faces.component.UIInput.REQUIRED=Required value is missing.
业务逻辑层和Spring Framework
业务对象和业务服务存在于业务逻辑层中,一个业务对象不仅包含数据,而且包含相应的逻辑,在范例应用中包含三个业务对象:Product、Category和User。
业务服务与业务对象交互并提供更高级的业务逻辑,需要首先定义一个正式的业务接口,它是直接与终端用户交互的服务接口。在JCatalog中,通过在 Spring Framework帮助下的POJO实现业务逻辑层,其中共有两个业务服务:CatalogService包含Catalog管理相关的业务逻辑, UserService中包含User管理逻辑。
Spring是基于IoC概念的框架,在范例应用中用到的Spring特性包括:
-
Bean management with application contexts:Spring可以有效地组织我们的中间层对象,它能够消除singleton的proliferation,并易于实现良好的面向对象编程方法,即“编程到接口”。
-
Declarative Transaction management: Spring利用AOP实现事务管理,而无需借助于EJB容器,利用这种方法,事务管理可以用于任何POJO中。Spring的事务管理不局限于JTA,而是可以采用不同的事务策略,在范例应用中,我们将使用declarative transaction management with Hibernate transaction。
-
Data-access exception hierarchy:Spring提供了非常好的异常来代替SQLException,为利用Spring的异常,必须在Spring的配置文件中定义以下异常转换:
在范例应用中,如果一个具有重复ID的新产品被插入,将会抛出DataIntegrityViolationException,这一异常将被
catch并rethrown一个DuplicateProductIdException。这样,该异常就可以与其它的异常区别处理。
-
Hibernate integration:Spring与Hibernate这样的ORM框架整合的非常好,Spring提供了对Hibernate session的高效和安全的处理,它可通过application context配置Hibernate的SessionFactories和JDBC数据源,并使得应用易于测试。
Integration层和Hibernate
Hibernate是一个开源的ORM框架,它可以支持所有主流SQL数据库系统,Hibernate的查询语言为对象和关系架起了非常好的桥梁。Hibernate提供了强大的功能以实现:数据读取和更新、事务管理、数据连接池、查询和实体关系管理等。
Data Access Ojbect(DAO)
JCatalog中采用了Dao模式,该模式抽象和封装了所有对数据源的访问,该应用中包括两个DAO接口:CatalogDao和UserDao,它们相应的实现HibernateCatalogDaoImpl和HibernateUserDAoImpl包含了Hibernate特定的逻辑来实现数据的管理和持久化。
实现
现在我们来看看如何将上面讨论的这些东西包装在一起以实现JCatalog,你可以从这个地址下载源码:source code
数据库设计
我们为该范例应用创建了包含4个表的数据库,如图5所示:
类设计
图6显示了JCatalog的类图
“编程到接口”的思想贯穿了整个设计实现中,在表示层,共用到四个backing bean:ProductBean、ProductListBean、UserBean和MessageBean;业务逻辑层包含两个业务服务 (CatalogService和UserService)和三个业务对象(Product、Category和User);Integration层有两个Dao接口和它们相应的Hibernate实现,Spring的application context用来管理绝大多数的业务逻辑层和integration层的对象;ServiceLocator将JSF和业务逻辑层整合在了一起。
Wire everything up
由于篇幅所限,我们仅举例说明,范例中use case CreateProduct展示了如何装配和构建应用,在详细讲述细节前,我们利用sequence图(图7)来说明所有层的end-tp-end整合。
表示层:
表示层实现包括创建JSP页面、定义页导航、创建和配置backing bean以及将JSF与业务逻辑层整合。
-
JSP page:createProduct.jsp是用来创建新产品的页面,它包含UI组件并将组件打包成ProductBean,ValidateItemsRange标签用来验证用户选择的种类数量,对每一个产品至少要有一个种类被选中。
-
页面导航:应用中的导航被定义在应用的配置文件faces-navigation.xml中,CreateProduct的导航准则如下:
*
createProduct
/createProduct.jsp
/createProduct.jsp
success
/uploadImage.jsp
retry
/createProduct.jsp
cancel
/productList.jsp
-
Backing bean:ProductBean不仅包含有将数据映射到页面上的UI组件的属性,还包括三个action:createAction、editAction和deleteAction,下面是createAction方法的代码:
public String createAction() {
try {
Product product = ProductBeanBuilder.createProduct(this);
//Save the product.
this.serviceLocator.getCatalogService().saveProduct(product);
//Store the current product id inside the session bean.
//For the use of image uploader.
FacesUtils.getSessionBean().setCurrentProductId(this.id);
//Remove the productList inside the cache.
this.logger.debug("remove ProductListBean from cache");
FacesUtils.resetManagedBean(BeanNames.PRODUCT_LIST_BEAN);
} catch (DuplicateProductIdException de) {
String msg = "Product id already exists";
this.logger.info(msg);
FacesUtils.addErrorMessage(msg);
return NavigationResults.RETRY;
} catch (Exception e) {
String msg = "Could not save product";
this.logger.error(msg, e);
FacesUtils.addErrorMessage(msg + ": Internal Error");
return NavigationResults.FAILURE;
}
String msg = "Product with id of " + this.id + " was created successfully.";
this.logger.debug(msg);
FacesUtils.addInfoMessage(msg);
return NavigationResults.SUCCESS;
}
-
Managed-bean声明:ProductBean必须在JSF配置文件faces-managed-bean.xml中配置:
Backing bean that contains product information.
productBean
catalog.view.bean.ProductBean
request
id
#{param.productId}
serviceLocator
#{serviceLocatorBean}
-
表示层和业务逻辑层之间的整合: ServiceLocator抽象了查找服务的逻辑,在范例应用中,ServiceLocator被定义为一个接口,该接口实现为一个JSF的 managed bean,即ServiceLocatorBean,它将在Spring的application context中寻找服务:
ServletContext context = FacesUtils.getServletContext();
this.appContext = WebApplicationContextUtils.getRequiredWebApplicationContext(context);
this.catalogService = (CatalogService)this.lookupService(CATALOG_SERVICE_BEAN_NAME);
this.userService = (UserService)this.lookupService(USER_SERVICE_BEAN_NAME);
业务逻辑层
-
业务对象:由于采用Hibernate提供持久化,因此Product和Category两个业务对象需要为它们的所有field提供getter和setter。
-
业务服务:CatalogService接口中定义了所有的与Catalog management相关的服务:
public interface CatalogService {
public Product saveProduct(Product product) throws CatalogException;
public void updateProduct(Product product) throws CatalogException;
public void deleteProduct(Product product) throws CatalogException;
public Product getProduct(String productId) throws CatalogException;
public Category getCategory(String categoryId) throws CatalogException;
public List getAllProducts() throws CatalogException;
public List getAllCategories() throws CatalogException;
}
-
Spring Configuration:这里是CatalogService的Spring配置:
PROPAGATION_REQUIRED,readOnly
PROPAGATION_REQUIRED
PROPAGATION_REQUIRED
PROPAGATION_REQUIRED
-
Spring和Hibernate的整合:下面是HibernateSessionFactory的配置:
<!-- Hibernate SessionFactory Definition -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
<property name="mappingResources">
<list>
<value>catalog/model/businessobject/Product.hbm.xml</value>
<value>catalog/model/businessobject/Category.hbm.xml</value>
<value>catalog/model/businessobject/User.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">net.sf.hibernate.dialect.MySQLDialect</prop>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.cglib.use_reflection_optimizer">true</prop>
<prop key="hibernate.cache.provider_class">net.sf.hibernate.cache.HashtableCacheProvider</prop>
</props>
</property>
<property name="dataSource">
<ref bean="dataSource"/>
</property>
</bean>
CatalogDao
uses HibernateTemplate
to integrate between Hibernate and Spring. Here's the configuration for HibernateTemplate
:
<!-- Hibernate Template Defintion -->
<bean id="hibernateTemplate" class="org.springframework.orm.hibernate.HibernateTemplate">
<property name="sessionFactory"><ref bean="sessionFactory"/></property>
<property name="jdbcExceptionTranslator"><ref bean="jdbcExceptionTranslator"/></property>
</bean>
Integration层
Hibernate通过xml配置文件来映射业务对象和关系数据库,在JCatalog中,Product.hbm.xml表示了Product对象的映射,Category.hbm.xml则用来表示Category的映射,Product.hbm.xml如下:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 2.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
<hibernate-mapping package="catalog.model.businessobject">
<class name="Product" table="product">
<id name="id" column="ID" unsaved-value="null">
<generator class="assigned"/>
</id>
<property name="name" column="NAME" unique="true" not-null="true"/>
<property name="price" column="PRICE"/>
<property name="width" column="WIDTH"/>
<property name="height" column="height"/>
<property name="description" column="description"/>
<set name="categoryIds" table="product_category" cascade="all">
<key column="PRODUCT_ID"/>
<element column="CATEGORY_ID" type="string"/>
</set>
</class>
</hibernate-mapping>
CatalogDao
is wired with HibernateTemplate
by Spring:
<!-- Catalog DAO Definition: Hibernate implementation -->
<bean id="catalogDao" class="catalog.model.dao.hibernate.CatalogDaoHibernateImpl">
<property name="hibernateTemplate"><ref bean="hibernateTemplate"/></property>
</bean>
结论
本文主要讲述了如何将JSF与Spring、Hibernate整合在一起来构建实际的Web应用,这三种技术的组合提供了一个强大的Web应用开发框架。在Web应用的高层设计中应该采用多层构架体系,JSF非常适合MVC设计模式以实现表示层,Spring可用在业务逻辑层中管理业务对象,并提供事物管理和资源管理等,Spring与Hibernate结合的非常出色,Hibernate是强大的O/R映射框架,它可以在integration层中提供最好的服务。
通过将整个Web应用分割成多层,并借助于“编程到接口”,应用程序的每一层所采用的技术都是可替换的,例如Struts可以用来替换JSF,JDO可替换Hibernate。各层之间的整合不是不值得研究,采用IoC和ServiceLocator设计模式可使得整合非常容易。JSF提供了其它Web框架欠缺的功能,然而,这并不意味着你马上抛弃Struts而开始使用JSF,是否采用JSF取决于项目目前的状况和功能需求,以及开发团队的意见等。