spring jpetstore (2.56)项目分析
spring jpetstore (2.56)项目分析
近期对spring 的官方示例程序做相应的分析,在其官方网站下载spring-framework-2.5.6-with-dependencies,其中的samples当中有对应的几个示例项目,近期开始研究第一个项目jpetstore。
Petstore是一个简单的电子宠物商店,包含的功能如下:
登陆/退出
浏览宠物
宠物展示
购物车添加/减少宠物
提交订单
针对这几个功能,系统将对象划分为:
对应将各个对象的db操作封装:
对应的sqlmap为:
有了以上对应的对象、db操作并不能完成电子商店的功能。下面将详细讲解相关业务实现,并通过业务实现讲解如何使用spring的功能。
业务层分析:
主要的类和接口位于org.springframework.samples.jpetstore.domain.logic包中。业务层其实很简单,主要是一个PetStoreFacade接口,该接口在JPetStore中只有一个唯一的实现类PetStoreImpl,它提供了很多供Web层调用的方法,而绝大多数方法都只是简单的调用了数据访问层的Dao类所提供的方法。
domain.logic包中还有两个供Web层访问的validator,AccountValidator和OrderValidator,它们实现自spring的Validator接口,由于使用了spring提供的helper class ValidationUtils,因此看起来也十分简洁。通过在servlet配置文件中为AccountFormController和AccountFormController指定validator属性,从而为Account和Order提供了验证功能。不过在这里,这些Validator无疑是依赖于spring框架的。
<bean name=\”/shop/newAccount.do\”
class=\”org.springframework.samples.jpetstore.web.spring.AccountFormController\”>
<property name=\”petStore\”><ref bean=\”petStore\”/></property>
<property name=\”validator\”><ref bean=\”accountValidator\”/></property>
<property name=\”successView\”><value>index</value></property>
</bean>
<bean id=\”secure_editAccount\”
class=\”org.springframework.samples.jpetstore.web.spring.AccountFormController\”>
<property name=\”petStore\”><ref bean=\”petStore\”/></property>
<property name=\”validator\”><ref bean=\”accountValidator\”/></property>
<property name=\”successView\”><value>index</value></property>
</bean>
另外,还有一个AOP advice,SendOrderConfirmationEmailAdvice,用于在完成一条order的数据库插入之后,向用户发送一封确认邮件,相应的配置位于applicationContext.xml中。
<bean id=\”mailSender\”
class=\”org.springframework.mail.javamail.JavaMailSenderImpl\”>
<property name=\”host\”><value>${mail.host}</value></property>
</bean>
<bean id=\”emailAdvice\”
class=\”org.springframework.samples.jpetstore.domain.logic.SendOrderConfirmationEmailAdvice\”>
<property name=\”mailSender\”><ref local=\”mailSender\”/></property>
</bean>
<bean id=\”emailAdvisor\”
class=\”org.springframework.aop.support.RegexpMethodPointcutAdvisor\”>
<constructor-arg><ref local=\”emailAdvice\”/></constructor-arg>
<property name=\”pattern\”><value>.*insertOrder.*</value></property>
</bean>
在另一个包org.springframework.samples.jpetstore.domain中,包含了在整个JPetStore各个层中都会使用到的domain object。这些基本的model类,除了Cart,在数据访问层均有对应的Dao类,而由于Cart仅作为在Web层内部传递的model,因此不需要持久化。
数据访问层
在这一层里,所有的Dao类由于使用了ibatis,所以代码显得格外干净。实际上,绝大部分逻辑都被搬到了外部的sql map文件里,这些文件位于org.springframework.samples.jpetstore.dao.ibatis.maps包下。值得一提的是,JPetStore对于Sequence的处理。JPetStore专门定义了一个Sequence的sql map,里面分别针对普通情况和Oracle数据库做了单独配置。
通常情况下是在数据库中单独维护一张代表sequence的表,使用时首先利用“getSequence”获取对应name的nextid,然后代码实现累加1,完成后再利用“updateSequence”更新sequence表。以下是Sequence.xml中的相关定义:
<select id=\”getSequence\” resultMap=\”result\”>
select name, nextid from sequence where name = #name#
</select>
<update id=\”updateSequence\”>
update sequence set nextid = #nextId# where name = #name#
</update>
在SqlMapSequenceDao中的getNextId方法实现了上述的代码处理逻辑:
public int getNextId(String name) throws DataAccessException {
Sequence sequence = new Sequence(name, -1);
sequence =
(Sequence) getSqlMapClientTemplate().queryForObject(\”getSequence\”, sequence);
// …
Object parameterObject = new Sequence(name, sequence.getNextId() + 1);
getSqlMapClientTemplate().update(\”updateSequence\”, parameterObject, 1);
return sequence.getNextId();
}
而对于Oracle,则直接利用其sequence功能,相应的sql map定义如下:
<select id=\”oracleSequence\” resultMap=\”result\”>
select \’$name$\’ as name, $name$.nextval as nextid from dual
</select>
另有一个OracleSequenceDao实现,其getNextId方法如下:
public int getNextId(String name) throws DataAccessException {
Sequence sequence = new Sequence();
sequence.setName(name);
sequence = (Sequence) getSqlMapClientTemplate().queryForObject(\”oracleSequence\”, sequence);
return sequence.getNextId();
}
包括SqlMapSequenceDao在内的所有Dao类都使用了SqlMapClientDaoSupport作为基类,同时,多数Dao类实现相应的Dao接口。Dao实现类中会调用SqlMapClientDaoSupport提供了getSqlMapClientTemplate方法。这样就可以很方便的从sql map文件中读取sql配置,而无需将sql语句hard code到代码中了。spring为ibatis提供了一层简单的封装,然后将自底层抛出的异常转义为spring框架所定义的runtime异常DataAccessException,可以在上层视情况决定是否catch该异常。
所有的Dao接口都位于org.springframework.samples.jpetstore.dao包中,而所有的ibatis实现类都位于org.springframework.samples.jpetstore.dao.ibatis包
Web层
Web层是Spring JPetStore相对最为复杂的地方。Spring JPetStore同时支持了两种Web Framework:Struts和Spring Web MVC,分别位于org.springframework.samples.jpetstore.web.struts和org.springframework.samples.jpetstore.web.spring这两个包内。两者的切换只需要修改一下web.xml文件的相应配置即可。如果选择Spring Web MVC,则:
<servlet>
<servlet-name>petstore</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
DispatcherServlet是Spring Web MVC中负责请求调度的核心引擎,通过内嵌的<init-param>节点可以为其配置名为“contextConfigLocation”的
参数,该参数指定了Spring专属的Application Context配置文件的位置。如果忽略此设定,则默认为“/WEB-INF/<servlet name>-servlet.xml”,其中<servlet name>以此处的Servlet名替换(比如:petstore-servlet.xml)
在org.springframework.samples.jpetstore.web.spring包中,绝大部分类属于Controller,它们均维护了一个PetStoreFacade的实例变量,通过PetStoreFacade来访问业务层的功能。利用Spring的配置文件petstore-servlets.xml,可以将PetStoreFacade的实现类(PetStoreImpl)通过reference bean的方式“注入”到各个Controller中。
这些Controller几乎都实现自org.springframework.web.servlet.mvc.Controller接口,该接口只有一个抽象方法:
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response);
handleRequest的处理逻辑大致是:
- 从request中获取信息(并没有什么特别的,一切数据都是通过request以及session在众多jsp页面和controller间传递的)
- 调用相应的petStore业务方法(处理Cart相关的Controller例外)
- 装配并返回ModelAndView实例
- 由于可以在handleRequest中直接操纵response,因此另一种返回形式是调用reponse.sendRedirect
从中可以看出,这是与Servlet API紧密耦合的。
Web层(续)
在众多Controller当中,有两个Controller比较特殊,它们涉及表单处理,分别是OrderFormController和AccountFormController,前者派生自AbstractWizardFormController,后者派生自SimpleFormController,而它们的父类则同为AbstractFormController。Spring Web MVC对一般的Form处理,从流程(workflow)的角度做了分类(form workflow),并以抽象类的形式封装与框架代码中。像OrderFormController和AccountFormController即是从这些抽象类中扩展派生而来,它们对部分callback方法做了覆盖。这是一个典型的Template Method Pattern的运用。当然,这也使得Web层的应用代码严重依赖于Spring Web MVC框架本身。
以SimpleFormController为例,从其核心方法之一processFormSubmission的实现代码中可以看出大致的处理流程:
protected ModelAndView processFormSubmission(
HttpServletRequest request, HttpServletResponse response,
Object command, BindException errors)
throws Exception {
if (errors.hasErrors() || isFormChangeRequest(request)) {
return showForm(request, response, errors);
}
else {
return onSubmit(request, response, command, errors);
}
}
在表单验证之后,如果用户输入有误,或者表单需要再次刷新,则会重新导向当前表单页面,否则说明一切正常,调用onSubmit方法。onSubmit最终会调用doSubmitAction方法,然后调用getSuccessView获得后继视图的标识,配合errors.getModel(),组装成ModelAndView返回。而这里的doSubmitAction则是一个需要派生类覆盖的protected方法。
与上述Controller配套的还有两个Form Object:OrderForm,AccountForm,其所包含的实例变量对应于表单字段。
另一个值得一提的是拦截器SignOnInterceptor,它对部分用户操作实施了认证保护。代码实现逻辑大致如下:
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
UserSession userSession =
(UserSession) WebUtils.getSessionAttribute(request, \”userSession\”);
if (userSession == null) {
ModelAndView modelAndView = new ModelAndView(\”SignonForm\”);
throw new ModelAndViewDefiningException(modelAndView);
}
else {
return true;
}
}
这里的UserSession是一个封装了包括帐号在内的用户信息的helper class。它被当作session属性在Web层的不同对象之间传递。在petstore-servlets.xml文件中有如下一段配置信息:
<bean id=\”secureHandlerMapping\”
class=\”org.springframework.web.servlet.handler.SimpleUrlHandlerMapping\”>
<property name=\”interceptors\”>
<list>
<ref bean=\”signonInterceptor\”/>
</list>
</property>
<property name=\”urlMap\”>
<map>
<entry key=\”/shop/editAccount.do\”><ref local=\”secure_editAccount\”/></entry>
…
</map>
</property>
</bean>
<bean id=\”signonInterceptor\”
class=\”org.springframework.samples.jpetstore.web.spring.SignonInterceptor\”/>
<bean id=\”secure_editAccount\”
class=\”org.springframework.samples.jpetstore.web.spring.AccountFormController\”>
…
</bean>
由此,我们可以看到,SignonInterceptor的preHandle方法,将会在AccountFormController执行之前被调用。而当userSession为空时,则会被当作非法操作抛出ModelAndViewDefiningException异常,否则AccountFormController将会顺利执行。至于如何处理ModelAndViewDefiningException,则要“上溯”到Spring Web MVC的核心请求处理类:org.springframework.web.servlet.DispatcherServlet。在该Servlet的doDispatch方法中有如下一段代码:
try {
…
}
catch (ModelAndViewDefiningException ex) {
mv = ex.getModelAndView();
}
if (mv != null && !mv.isEmpty()) {
render(mv, processedRequest, response);
}
由于ModelAndViewDefiningException中包含了出错以后的后继视图标识,因此最终将会导向该视图(在本例中,它导向用户登录页面)。
Web层(续2)——分页机制
在Spring JPetStore中,为列表提供了简单的分页处理,这里使用了Spring的helper class:PagedListHolder。它虽是用于Web UI,不过实际上是针对bean list的维护,因此位于org.springframework.beans.support包中。在Web层中,PagedListHolder的实例会被当作session属性传递,然后在jsp页面中以model获取。
PagedListHolder itemList = new PagedListHolder(this.petStore.getItemListByProduct(productId));
itemList.setPageSize(4);
Product product = this.petStore.getProduct(productId);
request.getSession().setAttribute(\”ViewProductAction_itemList\”, itemList);
request.getSession().setAttribute(\”ViewProductAction_product\”, product);
model.put(\”itemList\”, itemList);
model.put(\”product\”, product);
在PageListHolder中提供了很多分页时经常用到的方法:isFirstPage、isLastPage、previousPage、nextPage、getPageList等。通过设置其成员变量sort,PageListHolder也支持排序,该成员变量是一个SortDefinition接口的实现类,缺省使用的是MutableSortDefinition。实际上MutableSortDefinition只不过维护了有关排序的一些状态信息,比如:以那个属性排序、降序还是升序、是否忽略大小写等。而真正的排序则是由PropertyComparator完成的,该类实现了java.util.Comparator接口,用于根据指定的bean property来比较两个bean的先后顺序。在PageListHolder的resort方法中调用了PropertyComparator的静态方法sort。
PagedHolderList提供了对不可更新的bean list的分页支持,如果需要处理可更新的bean list,可以使用RefreshablePagedListHolder。RefreshablePagedListHolder是PagedListHolder的子类,具备reloading功能。调用其refresh方法,能够根据Locale和filter的更新情况自动实现数据的reloading。为了让RefreshablePagedListHolder能够成功reload数据,我们还需要编写一个PagedListSourceProvider接口的实现类,因为RefreshablePagedListHolder会调用该接口的loadList方法。
Spring的org.springframework.beans.support包为Web分页机制提供了统一的解决方案,使用起来十分方便。不过,需要指出的是,使用PagedHolderList/RefreshablePagedHolderList的隐含前提是,你需要将后台数据表中的所有数据悉数全部取出来,然后再交由它们进行分页处理。
配置
以下是Spring JPetStore中使用的配置文件:
- web.xml
主要包含了如下内容:
定义log4j配置文件所在路径:
<context-param>
<param-name>log4jConfigLocation</param-name>
<param-value>/WEB-INF/log4j.properties</param-value>
</context-param>
定义application context配置文件的所在路径:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/dataAccessContext-local.xml /WEB-INF/applicationContext.xml
</param-value>
<!–
<param-value>
/WEB-INF/dataAccessContext-jta.xml /WEB-INF/applicationContext.xml
</param-value>
–>
</context-param>
几个重要的servlet,以及servlet-mapping定义:
<servlet>
<servlet-name>context</servlet-name>
<servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>petstore</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet>
<servlet-name>action</servlet-name>
<servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
<load-on-startup>3</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>petstore</servlet-name>
<!–
<servlet-name>action</servlet-name>
–>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
- applicationContext.xml
与应用相关的context配置信息,包含bean定义,以及对email和remoting的配置,主要涉及中间层,也是其他servlet-specific context的root。在代码中可以通过WebApplicationContextUtils.getWebApplicationContext()访问到:
在该文件以及后面的dataAccessContext-local中都引入了形如“${}”的属性,这些属性是单独定义在外部属性文件中的,需要利用PropertyPlaceholderConfigurer来读入,该bean也在applicationContext.xml中定义。
<bean id=\”propertyConfigurer\” class=\”org.springframework.beans.factory.config.PropertyPlaceholderConfigurer\”>
<property name=\”locations\”>
<list>
<value>WEB-INF/mail.properties</value>
<value>WEB-INF/jdbc.properties</value>
</list>
</property>
</bean>
在声明事务时,applicationContext.xml文件中首先定义了一个名为baseTransactionProxy的bean,其中包含了有关事务的基本配置(对所有的insert方法和update方法使用PROPAGATION_REQUIRED,对其他方法使用PROPAGATION_REQUIRED,readOnly)。其abstract属性为true,作为parent bean被petStore bean所继承,并得到扩展。
<bean id=\”baseTransactionProxy\” class=\”org.springframework.transaction.interceptor.TransactionProxyFactoryBean\”
abstract=\”true\”>
<property name=\”transactionManager\”><ref bean=\”transactionManager\”/></property>
<property name=\”transactionAttributes\”>
<props>
<prop key=\”insert*\”>PROPAGATION_REQUIRED</prop>
<prop key=\”update*\”>PROPAGATION_REQUIRED</prop>
<prop key=\”*\”>PROPAGATION_REQUIRED,readOnly</prop>
</props>
</property>
</bean>
<bean id=\”petStore\” parent=\”baseTransactionProxy\”>
<property name=\”target\”>
<bean class=\”org.springframework.samples.jpetstore.domain.logic.PetStoreImpl\”>
<property name=\”accountDao\”><ref bean=\”accountDao\”/></property>
<property name=\”categoryDao\”><ref bean=\”categoryDao\”/></property>
<property name=\”productDao\”><ref bean=\”productDao\”/></property>
<property name=\”itemDao\”><ref bean=\”itemDao\”/></property>
<property name=\”orderDao\”><ref bean=\”orderDao\”/></property>
</bean>
</property>
</bean>
petSotre bean即对应于中间层的PetStoreImpl类,在其bean reference中所出现的Dao bean定义于dataAccessContext-local.xml中。
- dataAccessContext-local.xml/dataAccessContext-jta.xml
Spring JPetStore将对Dao bean的定义单独放到了一个文件中,这种按层分文件来描述context的方式值得借鉴。local后缀的文件用于单一数据库场景,jta后缀的文件用于多数据库场景。以dataAccessContext-local为例,除了Dao bean定义,该文件中还包括了dataSource的定义,所用的是apache的DBCP:
<bean id=\”dataSource\” class=\”org.apache.commons.dbcp.BasicDataSource\” destroy-method=\”close\”>
<property name=\”driverClassName\”><value>${jdbc.driverClassName}</value></property>
<property name=\”url\”><value>${jdbc.url}</value></property>
<property name=\”username\”><value>${jdbc.username}</value></property>
<property name=\”password\”><value>${jdbc.password}</value></property>
</bean>
另外,还有关于ibatis sql map的配置信息:
<bean id=\”sqlMapClient\” class=\”org.springframework.orm.ibatis.SqlMapClientFactoryBean\”>
<property name=\”configLocation\”><value>WEB-INF/sql-map-config.xml</value></property>
<property name=\”dataSource\”><ref local=\”dataSource\”/></property>
</bean>
这里的sql-map-config.xml文件中包含了所有ibatis sql map文件所在的物理位置。
- petstore-servlet.xml
Spring web MVC的Web层context配置文件,依然是有关bean的定义,主要是Controller,定义了url与class的映射,以及一些属性,包括:对petStore bean的引用,successView,validator,viewName等。另外,viewResolver bean定义了view所在的位置和扩展名,以及对应的viewClass:
<bean id=\”viewResolver\” class=\”org.springframework.web.servlet.view.InternalResourceViewResolver\”>
<property name=\”viewClass\”><value>org.springframework.web.servlet.view.JstlView</value></property>
<property name=\”prefix\”><value>/WEB-INF/jsp/spring/</value></property>
<property name=\”suffix\”><value>.jsp</value></property>
</bean>