|
<!-- .style1 {color: #0000FF} -->
May 2005 Discuss this Article<!-- .style1 {color: #0000FF} --> IntroductionHave you found as your web application gets more complex, understanding and managing the page flow – the orchestration that drives your application use cases – gets harder and harder? Are you tired of being forced into very particular ways of doing things that don’t give you much reuse? Do you feel you’re spending too much time developing your own approaches to generic problems like session state management? Enter Spring Web Flow. What is Spring Web Flow?Spring Web Flow (SWF) is an emerging module of The Spring Framework. The module is part of Spring’s web application development stack, which includes Spring MVC. Spring Web Flow aims to be the best solution for the management of web application page flow. It is a powerful controller for use when your applications demand complex controlled navigations, such as wizards, to guide the user through a series of steps within a larger application transaction. An example of such a controlled navigation is illustrated as a UML State Diagram below: Figure 1 - An Example Flight Booking Flow Astute readers will recognize this as a typical flight booking flow – the kind you participate every time you book an airline reservation on-line. Why Does Spring Web Flow Exist?In traditional web applications, page flows like the one above are not explicit—they are not first class citizens. Take a webapp built on Struts, for example. To implement a page flow in Struts, most developers build on what the framework provides them: actions and views. In this case, a single action is associated with a specific request URL. When a request comes in at that URL, the action is executed. During execution, the action performs some processing and then selects an appropriate result view for display. It’s that simple. So to implement a multi-step page flow in Struts, individual actions are chained together through the various views. Action URLs to process different events like “back” or “submit” are hard-coded into each view. Some form of ad-hoc session storage is used to manage flow state. Redirect after post is used to prevent duplicate submissions, etc. Although this is a simple and functional approach, it has a major disadvantage: the overall page flow of the web application is not clear from looking at the action definitions in the struts-config.xml file. You can’t see the forest – the flow – from the trees – the many action and view definitions. Flexibility also suffers since actions and views cannot be easily reused. Finally, you simply have to do too much work—it should be easier! Spring MVC offers a slightly higher level of functionality: form controllers that implement a predefined page flow. Two such controllers are provided out of the box: SimpleFormController and AbstractWizardFormController. However, these are still specific examples of a more general page flow concept. Tapestry and JSF use an event-driven approach at the page level, rather than the request level, where each page and its backing controller logic are kept together. However, neither provides first-class support for a logical page flow with a well-defined lifecycle that spans several pages and potentially different paths. As you’ll see, the lifecycle of such a page flow is longer than a single request, but shorter than a session. This is where Spring Web Flow comes in, allowing you to represent the page flow of a web application in a clear and simple way, and reuse it anywhere, including environments like Struts, Spring MVC, Tapestry, JSF, and even Portlets. AdvantagesAs you will see, Spring Web Flow offers several advantages:
How does Spring Web Flow Work?For now it suffices to say that a web flow is composed of a set of states . A state is a point in the flow where something happens; for instance, displaying a view or executing an action. Each state has one or more transitions that are used to move to another state. A transition is triggered by an event . The Book Flight Sample Web FlowTo demonstrate what a web flow definition looks like, the following piece of XML captures the flight booking process illustrated in the UML state diagram above: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE webflow PUBLIC "-//SPRING//DTD WEBFLOW//EN" "http://www.springframework.org/dtd/spring-webflow.dtd"> <webflow id="bookflight" start-state="obtainTripInfo"> <action-state id="obtainTripInfo"> <action bean="bookingActions" method="bindAndValidate"/> <transition on="success" to="suggestItineraries"/> <transition on="error" to="tryAgain"/> </action-state> <action-state id="suggestItineraries"> <action bean="bookingActions"/> <transition on="success" to="displaySuggestedItineraries"/> </action-state> <view-state id="displaySuggestedItineraries" view="suggestedItenaries"> <transition on="startOver" to="cancel"/> <transition on="select" to="selectItinerary"/> </view-state> <action-state id="selectItinerary"> <action bean="bookingActions"/> <transition on="success" to="isPassengerInfoRequired"/> </action-state> <decision-state id="isPassengerInfoRequired"> <if test="${requestScope.passenger == null}" then="enterPassengerInformation"/> <if test="${requestScope.passenger.preferences.alwaysConfirmPassengerInfo}" then="enterPassengerInformation" else="displayReservationVerification"/> </decision-state> <subflow-state id="enterPassengerInformation" flow="passenger"> <attribute-mapper> <input value="${requestScope.passenger.id}" as="passengerId"/> </attribute-mapper> <transition on="finish" to="displayReservationVerification"/> </subflow-state> <view-state id="displayReservationVerification" view="reservationVerification"> <transition on="startOver" to="cancel"/> <transition on="assignSeats" to="chooseSeatAssignments"/> <transition on="book" to="book"/> </view-state> <subflow-state id="chooseSeatAssignments" flow="seatAssignments"> <attribute-mapper> <input value="${requestScope.passenger.id}" as="passengerId"/> <input name="itinerary"/> </attribute-mapper> <transition on="finish" to="displayReservationVerification"/> </subflow-state> <action-state id="book"> <action bean="bookingActions"/> <transition on="success" to="displayConfirmation"/> </action-state> <end-state id="displayConfirmation" view="reservationConfirmation"/> <end-state id="tryAgain" view="tryAgain"/> <end-state id="cancel" view="home"/> </webflow> Figure 2 – A XML-based Flight Booking Flow definition As you can see, just from scanning the XML definition, the logical flow driving the booking process is clearly discernable, even if you don’t yet know about Spring Web Flow implementation details. And if you look a bit closer, you’ll see two subflows that spawn child processes of the booking flow. The first subflow guides the user through entering his passenger information. The second has the user make his seat assignments. The ability to nest flows that act as “mini application modules” is one of the most powerful capabilities of Spring Web Flow. You could show the definition above to a business analyst and she’d probably get it. Better yet, you could engineer a visual diagram from this definition and present that to a business analyst for review. Tools to do exactly this are already appearing. The Book Flight Flow ExplainedThe next part of this article breaks down the key parts of the above Book flight definition, and provides supporting dialog that illustrates how Spring Web Flow works. The Flow DefinitionStarting with line 1 of the XML-based flow definition: <webflow id="bookflight" start-state="obtainTripInfo"> ... </webflow> The webflow element defines the flow, specifying its id and start-state . The id is simply a unique identifier. The start state is the first state to transition to when a new flow session is activated at runtime. So for this business case, when a new bookflight session is activated , it transitions to the obtainTripInfo state. The Obtain Trip Info Action StateMoving on to the obtainTripInfo state definition. <action-state id="obtainTripInfo"> <action bean="bookingActions" method="bindAndValidate"/> <transition on="success" to="suggestItineraries"/> <transition on="error" to="tryAgain"/> </action-state> Recall that when states are entered, behavior happens. As you’ll see, there are different state types that execute different behaviors. An action state , like obtainTripInfoabove, executes an action when entered. That action returns the logical result of its execution, and that result is mapped to a state transition. It’s that simple. So for this business case, obtainTripInfo, when entered, executes the bindAndValidate method on the Action implementation with the bookingActions identifier. This method binds form input from the browser to a Trip domain object and validates it. If that process is successful, the suggestItineraries state is entered. If an error occurs, the tryAgain state is entered. The Booking ActionWhen using Spring Web Flow with Spring IoC, the bean attribute of the action element refers to the name of an Action implementation exported in the Spring Application Context. Here, the bookingActions bean definition looks like this: web-context.xml <bean id="bookingActions" class="org.springframework.samples.bookflight.BookingActions"> <property name="bookingAgent" ref="myBookingAgent"/> </bean> This allows our action implementation to be managed by Spring and configured via dependency injection. The Suggest Itineraries Action StateNow take a look at the next action state that, given a bound and validated Trip object as input, returns a collection of suggested itineraries: <action-state id="suggestItineraries"> <action bean="bookingActions"/> <transition on="success" to="displaySuggestedItineraries"/> </action-state> The actual implementation code required to make this happen is straightforward: public class BookingActions extends FormAction { ... public Event suggestItineraries(RequestContext context) { Trip trip = (Trip)context.getRequestScope().getAttribute("trip"); Collection<Itinerary> itineraries = bookingAgent.suggestItineraries(trip); context.getRequestScope().setAttribute("itineraries", itineraries); return success(); } } When the suggestItineraries state is entered, the suggestItineraries method is invoked. The other action states work in exactly the same way: entering the state invokes a method on the target action bean. The Display Suggested Itineraries View StateOnce a collection of suggested itineraries is returned, the next step has the user review them so she may select the best one. This is accomplished by the following state definition: <view-state id="displaySuggestedItineraries" view="suggestedItenaries"> <transition on="startOver" to="cancel"/> <transition on="select" to="selectItinerary"/> </view-state> As you can see, displaySuggestedItineraries is a view state —a state type we have not yet discussed. A view state, when entered, causes the executing flow to pause , and returns control back to the client with instruction to render the configured view . Later, after some user think-time, the client signals an event describing what action the user took. That resumes the flow, and the event that occurred is mapped to a state transition, which takes the user to the next step in the flow. Again, it’s that simple. So for this business case, when the displaySuggestedItineraries state is entered the suggestedIteneraries view is rendered and control returns to the browser. The user then decides which itinerary she wants and clicks the “select” button. That signals the select event, passing in the id of the selected itinerary as an event parameter. The user may also choose to startOver, at which time the flow transitions to the cancel state. Note it is the responsibility of the client environment the flow is hosted in to map the requested view name, like suggestedItineraries, to a renderable view template , like /WEB-INF/jsp/suggestedIternaries.jsp. For example, in Spring MVC, the FlowController does this using the familiar ModelAndView and ViewResolver constructs. In Struts, the FlowAction does this using the familiar ActionForward. Client Side StateAt this point you might ask: “… since the executing flow is paused when a ViewState is entered, and control is returned to the browser, how is the same flow picked up and resumed on subsequent events?” The answer is the client tracks the unique id of the executing flow, and provides it as input when the next event is signaled. This is typically done using a hidden form field. For example, in a jsp: <input type="hidden" value="<c:out value="${flowExecution.id}"/>">The “Is Passenger Info Required?” Decision State After the user selects the Itinerary she wants, the flow has to make a contextual decision about where to go next. Specifically, if the user has not logged in, or she has logged in but wishes to confirm her passenger information – like the credit card she will use – the flow should allow her to enter that information. On the other hand, if she has already logged in and wishes to go straight to the booking page, the flow should skip this optional step. Basically, a dynamic decision has to be made that takes into account the user’s information and preferences. The decision state is perfect for this. See the definition below: <decision-state id="isPassengerInfoRequired"> <if test="${requestScope.passenger == null}" then="enterPassengerInformation"/> <if test="${requestScope.passenger.preferences.alwaysConfirmPassengerInfo}" then="enterPassengerInformation" else="displayReservationVerification"/> </decision-state>The Enter Passenger Information SubFlow State The process of managing passenger information is logically independent of the booking process. It is one part within that process, yes, but it certainly makes sense a user would want to edit her information outside of the booking context. Subflow states facilitate this. When a subflow state is entered, a child flow is spawned. The parent flow is suspended until the child flow ends. This lets you view your application as a set of self-contained modules – flows – that you can easily embed in multiple situations in a consistent manner. Take a look at the enterPassengerInformation subflow state: <subflow-state id="enterPassengerInformation" flow="passenger"> <attribute-mapper> <input value="${requestScope.passenger.id}" as="passengerId"/> </attribute-mapper> <transition on="finish" to="displayReservationVerification"/> </subflow-state> The flow attribute is the id of the flow to spawn when this state is entered. The attribute-mapper element maps attributes to and from the subflow. Input mappings map attributes down to the subflow. Output mappings map attributes back up to the parent flow when the subflow ends. As you can see, expressions (in this case OGNL) are also supported. So for this business case, when the enterPassengerInformation state is entered, the passenger flow is spawned. The passengerId attribute is passed down to the flow as input. From there, the subflow does whatever it wants. It’s a black box as far the parent flow is concerned. When the subflow ends, the parent flow resumes, responding to the ending result to determine where to go next—in this case, to reservation verification. The Display Confirmation End StateThere is one last core state type that has yet to be discussed: the end state. When an end state is entered, the active flow session terminates. Upon termination, all resources associated with the flow are cleaned up for you automatically. Below is the displayConfirmation end state that displays confirmation after an itinerary is successfully booked: <end-state id="displayConfirmation" view="reservationConfirmation"/> When this state is entered, the bookflight flow ends and the reservationConfirmation view displays. Because the bookflight flow was acting as the root flow, and not a subflow, it and any allocated resources are automatically cleaned up. Note: had the ending flow been acting as a subflow, the entered end state is treated as a subflow result the resuming parent flow can respond to. More specifically, the entered end state ID is used as grounds for a state transition in the resuming parent flow's subflow state. You can see this in action by taking a look at the "enterPassengerInformation" subflow state definition. Note how it responds to the "finish" result of the subflow, which corresponds to a "finish" end state within the passenger flow. Flow DeploymentSo far you’ve learned what Spring Web Flow is all about, and you’ve seen an example of a realistic flow definition. What you haven’t seen yet is how to deploy that flow definition for execution in a particular environment, like Spring MVC in a servlet environment. Doing this is a cinch. Here’s all you have to do with Spring MVC: <bean name="/booking.htm" class="org.springframework.web.flow.mvc.FlowController"> <property name="flow"> <ref bean="bookingFlow"/> </property> </bean> <bean id="bookingFlow" class="org.springframework.web.flow.config.XmlFlowFactoryBean"> <property name="location" value="classpath:bookflight-flow.xml"/> </bean> This automatically exports the bookingFlow at the /booking.htm URL for use in a servlet environment. Advanced TopicsThe following section introduces some of the more advanced features of Spring Web Flow. Flow Execution ListenersThe FlowExecutionListener construct is an observer that allows you to listen and respond to the lifecycle of an executing flow. You can use this feature to do anything from state precondition and post condition checks, to auditing and security. Flow Execution Storage StrategiesThe mechanism by which the state of an executing flow is saved and restored is fully pluggable. HttpSession-based storage is the default, and SWF provides two other storage strategies out of the box: one using server-side continuation-based session storage, another using full client-side serialization. Defining your own custom storage, for example to store flow state in a Database, is trivial. When Is Spring Web Flow Right For You?It should be noted that Spring Web Flow is not a one-size-fits-all solution. As you’ve seen, it’s a stateful system that automates the management of page flows that drive business processes. It should not be used when simpler, stateless solutions are more appropriate. For example, it should not be used where sites require free navigations , where the user is free to “click around” anywhere they please. Spring Web Flow is designed to power controlled navigations , where the user is guided through a process with a clear business goal and lifecycle. To further make the use case more concrete, here are some examples of “good flows”, where the SWF system would be appropriate:
Here are some examples where Spring Web Flow would not be appropriate:
Spring Web Flow is meant to be used as a compliment to traditional controllers within any web environment, such as Spring MVC, Struts, Tapestry, Web Work, JSF, or Portlets. A single site should combine use of simple controllers with web flows where appropriate. Road MapSpring Web Flow 1.0 final will be released with Spring 1.3, scheduled right before JavaOne in June. Between now and then, expect regular, stable-for-development-use preview releases. The offering is already quite mature in terms of feature set and sample applications. As the development team pushes closer to a final release, here are some of the most important features we will be working on: IntegrationAs a standalone library, Spring Web Flow is a strong fit for integration with other frameworks. Out of the box Spring MVC, Struts, and Portlet MVC integration is already provided. JSF and Tapestry integration are expected by the final release. Flow ManagementWith Spring 1.2, exporting beans in an MBeanServer for management and monitoring is easy. A strongly typed FlowExecutionMBean management interface already exists, and we plan to extend that so global statistics on all flows executing on the server can be centrally monitored through a JMX console. PluggabilityEvery construct in the system will be made pluggable for easy extension and customization, even from the xml definition. This includes States and Transitions, among other concepts. Compensating TransactionsSupporting features and sample applications demonstrating use of compensating transactions to rollback previously committed work during execution of a flow is of high interest to us. ConclusionSpring Web Flow is a powerful solution for managing controlled navigations that drive business processes. And it’s fun to work with. If you haven’t tried it already, what are you waiting for? ReferencesSpring Web Flow is covered in the Core Spring training course offered by Interface21 - http://www.springframework.com/training The Spring Framework, http://www.springframework.org The Spring Web Flow Wiki, http://opensource.atlassian.com/confluence/spring/display/WEBFLOW/Home The kdonald blog, http://www.jroller.com/page/kdonald Struts, http://struts.apache.org Java Server Faces, http://java.sun.com/j2ee/javaserverfaces/ Tapestry, http://jakarta.apache.org/tapestry WebWork, http://www.opensymphony.com/webwork/ JavaOne, http://java.sun.com/javaone/ BiographiesKeith Donald is an Interface21 principal and a core Spring Framework project member. An experienced developer and mentor, Keith has built applications for customers spanning a diverse set of industries including banking, network management, information assurance, education, and retail. He specializes in translating business requirements into technical solutions. Keith is the founder of the Spring Rich Client Project and co-lead of Spring Web Flow with Erwin Vervaet. Lately you can find him leading Spring Training courses across the US and abroad, and as a guest speaker on Spring with the NoFluffJustStuff (NFJS) tour. Erwin Vervaet is a software engineer with a keen interest in applying modern IT concepts and tools. He has been using the Java language since 1996, and has a master's degree in computer science from the Katholieke Universiteit Leuven in Belgium. He has been involved in IT research, e-commerce projects, open source initiatives, and industrial software systems. Erwin currently works as an independent consultant. |