闲来无事,看看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文件了,就可以避免掉冲突了。