闲来无事,看看Spring这个生态系统中的各个模块挺不错,简化了很多事情,本文参考Spring Web Flow项目中的booking-mvc这个sample进行了仔细学习,很有收获,该sample主要讲解了Spirng Web Flow的一些核心概念,以及与spring mvc,spring security的组合使用,功力大大增强。
还是典型的3层模型,dao,service,controller,涉及到Java EE 6的规范主要是JPA2.0,JSR303和JSR250。废话不多说先上例子吧。
由于dao和service都比较简单,重点讲web层,稍后你会看到dao层我们只声明了接口,并没有写实现代码,这是由于我们借助了spring-data-jpa这个子项目的功能,由spring动态代理实现。大大简化了dao层。spring-data旨在提供一个统一的数据访问层接口。不管你是关系型数据库,还是no database,还是key-value,访问数据接口统一,是门面模式的体现。关于spring-data项目的详细信息,请查看官方信息:http://www.springsource.org/spring-data
首先,我们来谈谈spring web flow 的一些核心概念:什么是flow呢,引用官方文档的一句话:A flow encapsulates a reusable sequence of steps that can execute in different contexts.大致意思就是说,流就是封装了一个可复用的序列步骤,这些序列步骤可以在不同的上下文环境中执行。哎呀,翻译的好别扭啊。那什么是”state“呢,在spring web flow中,把组成流的一系列的步骤称之为state(姑且叫状态吧),通常,进入一个状态就意味着一个页面视图将要呈现给用户,在这个页面视图中用户可以输入一些数据,而且还可以触发一些事件,比如点击了某个button之类的,而这些事件通常又会把当前的state转移到另外一个state,这就是state的transition(姑且叫转移,过渡也行)。在这些state中还可以执行一些动作,比如将收集到的用户数据,持久化到db中等等。
介绍完一些基本概念后,我们来具体说一下操作。首先先讲spring web flow 和 spring mvc 的集成,在web.xml中配置spring mvc的前端控制器。配置很简单,如下:
<!-- Loads the Spring web application context -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>mvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>mvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
由于我们是基于约定的编程,当mvc这个servlet加载时,会在/WEB-INF/目录下查找文件名为mvc-servlet.xml这个配置文件。下面我们就具体看看spring mvc的配置,首先,我们让spring 扫描web层的controller,代码如下:
<context:component-scan base-package="org.leochen.samples.web" />
第2步,充分利用mvc这个命名空间的作用,
<mvc:annotation-driven />
这个打包了一些列功能配置,比如支持JSR 303的验证,以及sprng 3的类型转换和字段的格式化等。
第3步,添加spring mvc对静态资源的处理,结合在web.xml中配置的url-pattern为”/“,
<mvc:resources mapping="/resources/**"
location="/resources/,classpath:/META-INF/web-resources/" />
<mvc:default-servlet-handler />
静态资源都放在web根目录下的resources文件下,对所有以/resources开头的请求,都会被映射到web根目录下的resources目录下和类路径下面的META-INF/web-resources(这个是spring-webflow 中一些jar包中的资源文件)目录下查找。
第4步,添加对国际化资源文件的处理,代码如下:
<mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor" />
</mvc:interceptors>
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"/>
<bean id="messageSource"
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<array>
<value>/WEB-INF/messages/globalMessage</value>
<value>/WEB-INF/messages/validationMessage</value>
</array>
</property>
<property name="defaultEncoding" value="UTF-8" />
<property name="cacheSeconds" value="0" />
</bean>
第5步,配置tiles
<bean class="org.springframework.web.servlet.view.tiles2.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/**/views.xml</value>
</list>
</property>
</bean>
<bean id="tilesViewResolver" class="org.springframework.js.ajax.AjaxUrlBasedViewResolver">
<property name="viewClass" value="org.springframework.webflow.mvc.view.FlowAjaxTilesView" />
</bean>
这里tilesViewResolver 使用的spring web flow中提供的实现类。
下面来讲讲spring web flow 需要配置的一些东西,我们把这些配置单独放到一个文件中,叫做mvc-webflows.xml,方便管理,最后会在mvc-servlet.xml中include这个spring web flow 的配置文件。
第1步,配置flow-registry,顾名思义,这是spring-webflow流的注册入口,代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:webflow="http://www.springframework.org/schema/webflow-config"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/webflow-config
http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.3.xsd">
<webflow:flow-registry id="flowRegistry" flow-builder-services="flowBuilderServices"
base-path="/WEB-INF/views">
<webflow:flow-location-pattern value="/**/*-flow.xml" />
</webflow:flow-registry>
在这里要着重讲几点:首先这个base-path,表示所有的flow都是在/WEB-INF/views目录下,flow-location-pattern 这个主要是用来查找各个流定义文件,关于流(flow)的id的确定,有以下两种分配算法,如果base-path存在,那么流的id就是从base-path到流的定义文件之间的目录路径,比如说流的定义文件为/WEB-INF/views/hotels/booking/booking-flow.xml,而base-path是/WEB-INF/views,所以flow的id就为hotels/booking.如果base-path不存在或者流的定义文件就在base-path目录下,那么这时flow的id就为流的定义文件名减去后缀(这里我们定义的后缀为-flow.xml),比如说我们的流定义文件叫booking-flow.xml,那么这时flow的id就为booking。
第2步,配置flow executor,流执行器
<webflow:flow-executor id="flowExecutor" flow-registry="flowRegistry">
<webflow:flow-execution-listeners>
<webflow:listener ref="securityFlowExecutionListener" />
</webflow:flow-execution-listeners>
</webflow:flow-executor>
这里flow-execution-listeners子元素是用来跟spring security 进行集成的,稍后会谈到。
第3步,补齐flow-registry的flow-builder-services属性的相关依赖配置。代码如下:
<webflow:flow-builder-services id="flowBuilderServices"
view-factory-creator="mvcViewFactoryCreator"
development="true"
validator="validator" />
<!-- Installs a listener to apply Spring Security authorities -->
<bean id="securityFlowExecutionListener"
class="org.springframework.webflow.security.SecurityFlowExecutionListener" />
<!-- Configures Web Flow to use Tiles to create views for rendering -->
<bean id="mvcViewFactoryCreator"
class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator">
<property name="viewResolvers">
<list>
<ref bean="tilesViewResolver" />
</list>
</property>
<property name="useSpringBeanBinding" value="true" />
</bean>
<bean id="validator"
class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />
简单说一下webflow:flow-builder-services 几个属性的含义,view-factory-creator表示使用的是哪个视图工厂,validator是用来在处理页面流的过程使用的是JSR303的验证,bean id为validator就是spring对jsr303的支持类。development可以扫描flow定义文件的变化,开发时这么用比较好。
好了,spring web flow 的定义文件就这么多了,下面来将spring mvc与spring web flow 集成。代码如下:
<!-- Spring MVC Integration With Spring Web Flow -->
<!-- 1.Registering the FlowHandlerAdapter, Enables FlowHandler URL mapping -->
<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter">
<property name="flowExecutor" ref="flowExecutor" />
</bean>
<!-- 2.Defining flow mappings, Maps request paths to flows in the flowRegistry -->
<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping">
<property name="flowRegistry" ref="flowRegistry" />
<property name="order" value="-1" />
</bean>
ok,spring mvc 和spring web flow 集成完毕,下面加入spring security 3.
第1步,当然是配置web.xml文件了,代码如下:
<!-- Enables Spring Security -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
注意这里的filter-name可不是随便起的,这个name为成为spring security filter chain中的一个filter相匹配上,从而完成spring security的功能。注意到filter-class只是spring-web中的一个类而已,并不是spring-security中的类。
第2步,配置spring-security,applicationContext-security.xml,全部配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.1.xsd">
<http security="none" pattern="/resources/**" />
<http auto-config="true" use-expressions="true">
<intercept-url pattern="/sec/**" access="hasRole('ROLE_USER')" />
<form-login login-page="/login"
login-processing-url="/loginProcess"
authentication-failure-url="/login?error=1"
default-target-url="/" />
<remember-me key="SpringWebFlowTutorial-rmkey-BUUttZnBJCa#U=4Dwg@%5_ptCC8wHtlY"
services-ref="ipTokenBasedRememberMeService" />
<logout logout-url="/logout" logout-success-url="/" invalidate-session="true" />
</http>
<authentication-manager alias="authenticationManager">
<authentication-provider user-service-ref="customJdbcDao">
<password-encoder ref="passwordEncoder">
<salt-source ref="saltSource" />
</password-encoder>
</authentication-provider>
</authentication-manager>
<beans:bean id="customJdbcDao" class="org.leochen.samples.dao.impl.CustomJdbcDaoImpl">
<beans:property name="dataSource" ref="dataSource" />
<beans:property name="enableGroups" value="true" />
<beans:property name="enableAuthorities" value="false" />
</beans:bean>
<!-- the property of 'key' must be have -->
<beans:bean id="ipTokenBasedRememberMeService"
class="org.leochen.samples.web.security.IPTokenBasedRememberMeService">
<beans:property name="userDetailsService" ref="customJdbcDao" />
<beans:property name="key"
value="SpringWebFlowTutorial-rmkey-BUUttZnBJCa#U=4Dwg@%5_ptCC8wHtlY" />
<beans:property name="tokenValiditySeconds" value="1209600" />
<beans:property name="parameter" value="_remember_me" />
<beans:property name="cookieName" value="LOGIN_REMEMBER_ME" />
</beans:bean>
</beans:beans>
spring security 可以参考spring security的文档和 Spring Security 3这本书来参考学习,很有帮助。
这样基本配置就这么着了,对于那些datasource,jpa 以及spring-jpa的配置会在附件中的代码中给出,附件中的代码可以完整运行。当然了,要熟悉maven。
下面说一下一个流的基本定义图,如下图:
还是看图一目了然吧,下面是flow定义文件,/WEB-INF/views/hotels/booking/booking-flow.xml,如下:
<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
<!--notice: 'attributes' property cann't use the form like hasRole('ROLE_USER'),
it's just the role name -->
<secured attributes="ROLE_USER"/>
<var name="searchCriteria" class="org.leochen.samples.web.controllers.hotels.SearchCriteria"/>
<input name="hotelId" required="true"/>
<on-start>
<evaluate expression="bookingService.createBooking(hotelId,currentUser.name)"
result="flowScope.booking"/>
</on-start>
<view-state id="enterBookingDetails" model="booking">
<binder>
<binding property="checkinDate" />
<binding property="checkoutDate" />
<binding property="beds" />
<binding property="smoking" />
<binding property="creditCard" />
<binding property="creditCardName" />
<binding property="creditCardExpiryMonth" />
<binding property="creditCardExpiryYear" />
<binding property="amenities" />
</binder>
<on-render>
<render fragments="main"/>
</on-render>
<transition on="submit" to="reviewBooking"/>
<transition on="cancel" to="bookingCancelled" bind="false" />
</view-state>
<view-state id="reviewBooking">
<on-render>
<render fragments="main"/>
</on-render>
<transition on="confirm" to="bookingConfirmed">
<evaluate expression="bookingService.save(booking)" />
</transition>
<transition on="revise" to="enterBookingDetails"/>
<transition on="cancel" to="bookingCancelled"/>
</view-state>
<end-state id="bookingConfirmed" />
<end-state id="bookingCancelled" />
</flow>
下面贴上E-R图,看得直观,如下:
最后,想说几点关于jsr303验证的几个注意点,使用hibernate validator(jsr 303的实现)为spring mvc提供验证
的时候,需要把org.hibernate.validator包下面的ValidationMessages.properties文件拷贝到类路径下,这样就可以
自定义验证消息了,对于spring web flow 的验证,只需在flow定义文件的相同目录下定义messages.properties文件
就可以添加验证消息了,spring web flow 对于验证消息key的生成遵循这么一个约定,key值有3部分组成,第一部分是model的名称,比如booking,第二部分是model的property,比如booking的checkinDate属性,第三部分是error
code,举个全的例子,booking.checkinDate.NotNull=,booking.creditCardExpiryMonth.typeMismatch=
Spring3.1新特性,配置JPA无需persistence.xml描述文件,SpringWebFlowTutorial-without-persistence.xml.7z这个是修改后的打包文件,里面还将SpringWebFlowTutorial.sql文件中的booking_amenities表的amenity 字段长度增大些,一个小bug修正了一下。原先的版本在应用服务器glassfish中还不能部署成功,是因为spring提供的
LocalContainerEntityManagerFactoryBean与Java EE应用服务器有些冲突,在没有Spring3.1新特性之前,
可以有其他的解决方案,具体请参考Spring官方文档。现在spring不用persistence.xml文件了,就可以避免掉冲突了。