Spring Web Flow: un’introduzione
Un’introduzione al framework Spring Web Flow che ci permette di definire un flusso di navigazione nella nostra applicazione web
Spring Web Flow
Fino ad oggi il pattern MVC è stato alla base della grande maggioranza delle web application sviluppate in ambito Java. Su questo pattern molti framework hanno costruito il loro successo, tra i quali Struts e successivamente SpringMVC. Questi framework si pongono come primo obiettivo quello di rendere modulari gli strati di Model, View e Controller.
Spesso però, durante lo sviluppo delle nostre applicazioni, ci siamo trovati di fronte alla gestione di un flusso applicativo, cioè un insieme di pagine che determinano il flusso della nostra applicazione. E’ proprio in questa problematica che s’inserisce un framework come Spring Web Flow. SWF nasce da un’idea di Keith Donald e oggi è sviluppato dalla SpringSource, società che sviluppa l’intero portfolio di Spring.
Concetti base
Come accennato precedentemente, Spring Web Flow mira alla gestione dei flussi applicativi. Ma cos’è un flusso? Un flusso, che nella terminologia di SWF è chiamato Flow, consiste in una sequenza di passi che possono essere eseguiti in diversi contesti e possono essere riutilizzati e composti a piacimento.
I passi che compongono il Flow vengono chiamati States, che possono essere del tipo: view-state, decision-state, subflow-state ed end-state. Lo state che generalmente viene più utilizzato è il view-state e corrisponde a una view che può generare eventi. Un evento, gestito dallo stesso state, dà il via a una Transition, che permette di passare dallo stato attuale del flusso a quello successivo. Durante l’esecuzione di un Flow è possibile definire delle azioni che possono essere eseguite in qualsiasi momento all’interno dello stesso e dei suoi state o transition. Queste sono:
• On flow start: viene eseguita prima che il flusso abbia inizio (on-start)
• On state entry: prima dell’entrata in uno state (on-entry)
• On view render: prima del rendering della view (on-render)
• On transition execution: prima che la transizione venga eseguita
• On state exit: prima dell’uscita dallo state corrente (on-exit)
• On flow end: prima che il flow corrente finisca (on-end)
Ma come si compone un Flow? Un flusso è normalmente gestito attraverso un file XML o per specifiche necessità anche attraverso le API messe a disposizione dal framework.
Scriviamo la prima applicazione
Iniziamo a sviluppare la prima, semplice, applicazione, utilizzando SWF integrato con SpringMVC (SWF può essere integrato anche con Struts, JSF, Portlet, etc.). L’applicazione si baserà sul flusso definito in figura1. Prima d’iniziare a scrivere il file per la definizione del flusso, è necessario procedere con la configurazione dell’applicazione, partendo da SpringMVC.
Estratto del web.xml:
<?xml version="1.0" encoding="UTF-8"?> <web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> … <!-- File di configurazione di Spring per l'applicazione LibrarySWF --> <context-param> <param-name>contextConfigLocation</param-name> <param-value> /WEB-INF/config/web-application-config.xml </param-value> </context-param> <!-- Carica il Spring web application context --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- Front Controller che gestisce tutte le richieste --> <servlet> <servlet-name>Spring MVC Dispatcher Servlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value></param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <!-- Mappa tutti gli url /spring/* per essere gestiti da Spring e SWF --> <servlet-mapping> <servlet-name>Spring MVC Dispatcher Servlet</servlet-name> <url-pattern>/spring/*</url-pattern> </servlet-mapping> … </web-app>
Quanto definito nel web.xml, è semplicemente la configurazione di SpringMVC, che si occupa di gestire tutte le chiamate effettuate verso l’url pattern “/spring/*”, attraverso la servlet front controller messa a disposizione dal framework MVC. Le prime tracce di SWF, le troviamo nella seguente configurazione
mvc-config.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="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-2.5.xsd"> <!-- regole di mapping per i flows basate su URL --> <bean id="flowMappings" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="mappings"> <value> /library/bookCreation=flowController </value> </property> <property name="order" value="0"/> </bean> <!-- Configurazione per Tiles --> <bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles2.TilesConfigurer"> <property name="definitions"> <list> <value>/WEB-INF/config/tiles-defs.xml</value> </list> </property> </bean> <!-- Resolver per la risoluzione dei nomi delle view. Questo resolver delega la risoluzione a Tiles. Il nome da risolvere corrisponde a una 'definition' di Tiles --> <bean id="tilesViewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.tiles2.TilesView"/> </bean> <!-- Gestisce le esecuzioni dei flows definiti precedentemente in "flowMappings" --> <bean id="flowController" class="org.springframework.webflow.mvc.servlet.FlowController"> <property name="flowExecutor" ref="flowExecutor"/> </bean> </beans>
Analizzando il file mvc-config.xml, la prima cosa che notiamo è la definizione di un bean con id “flowMappings” che fa riferimento alla classe org.springframework.web.servlet.handler.SimpleUrlHandlerMapping. Questa classe si occupa di mappare, tramite la property “mappings”, tutti gli URL e il corrispondente Flow Handler per gestire la richiesta di esecuzione di un flusso. La configurazione è del tipo PATH=HANDLER_BEAN_NAME. Nel nostro caso al path /library/bookCreation corrisponde il controller flowController, definito come:
<bean id="flowController"> <property name="flowExecutor" ref="flowExecutor"/> </bean>
Il FlowController è un adapter che dovrebbe essere sufficiente per gestire le richieste più comuni e che non hanno bisogno di gestioni mirate per l’esecuzione di un flow. Per esigenze specifiche, per cui le regole di default non sono più sufficienti, SWF ci mette a disposizione anche la possibilità di creare un proprio Flow Handler, che verrà poi configurato per uno specifico path.
Questo Flow Handler dovrà implementare org.springframework.webflow.mvc.servlet.FlowHandler, e anche in questo caso il framework ci facilita nell’implementazione di questa interfaccia grazie alla classe org.springframework.webflow.mvc.servlet.AbstractFlowHandler. Questa classe, implementa tutti i metodi dell’interfaccia lasciandoli vuoti, evitando in questo modo di dovere fare l’override di tutti i metodi dell’interfaccia FlowHandler, anche di quelli il cui comportamento di default è sufficiente. I metodi che è possibile implementare sono i seguenti:
- getFlowId(HttpServletRequest) : per definire l’id del Flow da eseguire. Di default viene recuperato direttamente dall’URL, come ultimo elemento dell’URI, prima dei parametri. Se con le servlet questo metodo è del tutto facoltativo, con le portlet dev’essere implementato obbligatoriamente, poiché attualmente è l’unico modo per risalire all’id del flow che si vuole eseguire. Questa “limitazione” è dovuta al meccanismo dei portlet container che si occupano direttamente della gestione dell’url
- createExecutionInputMap(HttpServletRequest) : per estrarre alcuni parametri dalla request e utilizzarli come parametri di input del flow. Per default tutti i parametri della request vengono utilizzati come parametri di input
- handleExecutionOutcome: se si vuole effettuare una redirect ad una particolare risorsa alla fine dell’esecuzione di un flow
- handleException: per un miglior controllo delle eccezioni non gestite all’interno del flow
Il FlowController, funge da adapter tra SpringMVC e il FlowExecutor. Il FlowExecutor è il vero e proprio engine dell’esecuzione dei flows. Questa interfaccia, la cui implementazione è nella classe org.springframework.webflow.executor.FlowExecutorImpl, contiene tutti i riferimenti per iniziare l’esecuzione di un nuovo flow (launchExecution()), per metterlo in pausa (createPausedResult()) o per riprenderne l’esecuzione (resumeExecution())
Di seguito il file di configurazione di Spring Web Flow
<?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-2.5.xsd http://www.springframework.org/schema/webflow-config http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.0.xsd"> <!-- Il vero motore di SWF. Esegue i flows, gestisce le transitions, etc. --> <webflow:flow-executor id="flowExecutor"/> <!-- Registro di tutti i flows --> <webflow:flow-registry id="flowRegistry" flow-builder-services="flowBuilderServices"> <webflow:flow-location path="/WEB-INF/flows/bookCreation.xml" /> <webflow:flow-location path="/WEB-INF/flows/addressCreation.xml" /> </webflow:flow-registry> <!-- Container per i servizi utilizzati durante l'esecuzione dei flows --> <webflow:flow-builder-services id="flowBuilderServices" view-factory-creator="mvcViewFactoryCreator"/> <!-- In questo caso è configurata la factory che si occupa di creare il ViewResolver per Tiles --> <bean id="mvcViewFactoryCreator" class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator"> <property name="viewResolvers" ref="tilesViewResolver"/> </bean> </beans>
In questo file, oltre a trovare la definizione per la creazione di un FlowExecutor, creato direttamente dal framework e injected direttamente nel nostro FrontController, troviamo anche la configurazione per il org.springframework.webflow.definition.registry.FlowDefinitionRegistry, che funziona da memory repository per le definizioni dei flussi dell’applicazione. Oltre ai file di configurazione dei flussi, è possibile indicare l’attributo flow-builder-services per definire i servizi da utilizzare per la costruzione e l’esecuzione dei flows. E’ possibile configurare quattro tipi di servizi:
- conversion-service
- formatter-registry
- expression-parser
- view-factory-creator
Il view-factory-creator si occupa di creare view factories e di conseguenza il sottostrato ViewResolver e View per la risoluzione e il rendering del nostro view name. Nella nostra applicazione d’esempio, la visualizzazione delle pagine jsp e della loro composition è gestita da Tiles. Infine, di seguito, troviamo il file parent di tutte le configurazioni che verrà caricato, come parametro nel ServletContext, allo startup della nostra applicazione. In questo modo il framework, avrà accesso a tutte le configurazioni finora descritte.
web-application-config.xml:
<?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:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"> <!-- Import di tutti i file di configurazione di Spring + SWF --> <import resource="mvc-config.xml" /> <import resource="webflow-config.xml" /> </beans>
File per la configurazione di Tiles:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE tiles-definitions PUBLIC "-//Apache Software Foundation//DTD Tiles Configuration 2.0//EN" "http://tiles.apache.org/dtds/tiles-config_2_0.dtd"> <tiles-definitions> <definition name="layout" template="/WEB-INF/jsp/layout.jsp"/> <definition name="bookData" extends="layout"> <put-attribute name="body" value="/WEB-INF/jsp/bookCreation.jsp"/> </definition> <definition name="authorForm" extends="layout"> <put-attribute name="body" value="/WEB-INF/jsp/authorCreation.jsp"/> </definition> <definition name="confirmation" extends="layout"> <put-attribute name="body" value="/WEB-INF/jsp/confirmationPage.jsp"/> </definition> <definition name="addressData" extends="layout"> <put-attribute name="body" value="/WEB-INF/jsp/addressCreation.jsp"/> </definition> </tiles-definitions>
Il Flow
Dopo aver esplorato le configurazioni preliminari, necessarie al funzionamento della nostra prima applicazione SWF, scendiamo nel dettaglio di una definizione di un flusso. Il flow utilizzato per la nostra applicazione (vedi figura1), si presenterà come la navigazione delle pagine mostrata in figura3.
Di seguito il flusso in formato xml (utilizzando SpringIDE è possibile definire un flusso anche graficamente, ma al momento della scrittura di questo articolo, la versione 2.x di Web Flow non è supportata)
bookCreation.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"> <view-state id="enterBookData" view="bookData"> <transition on="save" to="enterAuthorData"></transition> </view-state> <view-state id="enterAuthorData" view="authorForm"> <transition on="save" to="addressInformation"></transition> </view-state> <decision-state id="addressInformation"> <if test="requestParameters.addressInfo" then="enterAddressData" else="confirmation"></if> </decision-state> <subflow-state id="enterAddressData" subflow="addressCreation"> <transition on="addressSaved" to="confirmation"></transition> </subflow-state> <view-state id="confirmation"> <transition on="confirm" to="bookingCreationConfirmed"></transition> </view-state> <end-state id="bookingCreationConfirmed"></end-state> </flow>
addressCreation.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"> <view-state id="enterAddressData" view="addressData"> <transition on="save" to="addressSaved"></transition> </view-state> <end-state id="addressSaved"></end-state> </flow>
In questa prima applicazione si è scelto di concentrarsi soprattutto sull’aspetto di Flow, senza occuparci del binding con un model, della validazione o di altri aspetti che verranno trattati in articoli successivi. Una caratteristica importante da sottolineare è che il flow addressCreation.xml non ha nessun legame con il flow bookCreation.xml, ed esso stesso può essere eseguito come flusso indipendente da altri o ancora una volta come sub-flow di un altro flusso.
Questo ci da un’idea di quanto possa essere conveniente creare dei flussi facilmente riusabili all’interno della nostra applicazione. Con la versione 2.x del framework è stata aggiunto il concetto di ereditarietà tra le configurazioni dei flussi, aumentando le possibilità di riuso. Tornando alla definizione del nostro flusso, iniziamo subito con il dire che lo > di figura1, a partire da SWF 2.0 (nella versione precedente esisteva un tag ) viene considerato il primo > del flusso, in questo caso “enterBookData”, se non indicato come attributo del tag .
Da qui ha inizio il nostro flow. L’attributo view sta ad indicare il nome della view che il sottostrato composto dalle classi di tipo ViewResolver e View, andranno a tradurre con il nome della pagina da visualizzare. In questo caso, avendo deciso di utilizzare Tiles per la composizione delle nostre jsp, il file delle definitions ci indica che la pagina da visualizzare è la layout.jsp, che a sua volta include la bookCreation.jsp. A questo punto il nostro FlowExecutor, dopo aver visualizzato la pagina indicata dall’attributo view, mette in pausa il flow in attesa del verificarsi di un evento
<div> … <div> <form method="post"> <div class="label">Titolo:</div> <div class="input"><input type="text" id="title" name="title"></div> … <div> <input type="submit" name="_eventId_save" value="save"> </div> </form> </div> </div>
Come possiamo vedere da questo estratto di codice della nostra pagina jsp, l’evento viene eseguito dal submit di un form (ma è possibile farlo anche con un semplice href) e l’evento è indicato dal _eventId_save. A questo punto l’engine riprende l’esecuzione del flusso invocando il metodo resumeExecution() e dallo stato attuale, sull’evento save, effettua una transition verso il nuovo stato enterAuthorData
<transition on="save" to="enterAuthorData"></transition>
Il view-state enterAuthorData si comporta esattamente come il precedente. Quello che è interessante notare, invece, è lo stato a cui si arriva in seguito alla transition save dello state enterAuthorData. In questo caso ci troviamo di fronte a un >. Per questo tipo di state non viene definita una transition, ma il flusso viene indirizzato dal risultato della if. Nella nostra applicazione si è deciso di controllare il parametro che si trova nella request, popolata con i parametri della pagina authorCreation.jsp
<div> … <div> <form method="post"> … <div class="label">Acquisisci indirizzo dell'autore? </div> <div class="input"> si<input type="radio" id="addressInfo" name="addressInfo" value="true"> no<input type="radio" id="addressInfo" name="addressInfo" checked="checked" value="false"> </div> <div><input type="submit" name="_eventId_save" value="save"> </div> </form> </div> </div>
Nel caso si scelga di acquisire anche i dati dell’indirizzo dell’autore, si procederà verso lo state enterAddressData altrimenti verso lo state confirmation
<if test="requestParameters.addressInfo" then="enterAddressData" else="confirmation"></if>
A questo punto rimangono da analizzare gli ultimi due state che troviamo nella nostra applicazione: sub flow-state ed end-state. Nel caso il parametro nella request addressInfo sia true, il FlowExecutor ci rimanderà allo state enterAddressData, che abbiamo deciso di rappresentare come sotto-flusso. In questo modo è possibile riutilizzarlo in qualsiasi flusso abbia bisogno dell’acquisizione di un indirizzo. E’ importante notare come l’annidamento dei sotto flussi sia libero, non ci sono pertanto limitazioni sul numero dei sub-flow nested.
L’esecuzione di un sub-flow avviene esattamente con avviene quella del flusso chiamante, a differenza che quando questo flusso raggiunge l’end-state il flusso padre riprende l’esecuzione esattamente nello state a cui era rimasto. Nel sub-flow è possibile anche definire dei parametri di input e/o di output per e dal sub flow, attraverso i tag input e output definiti rispettivamente nel flusso padre al momento dell’invocazione al sub-flow e nel end-state quando il sotto flusso termina l’esecuzione. Come appena accennato, quando unend-state viene raggiunto, il flusso in questione è terminato.
Nel caso di un flusso padre di tutti i flussi, siamo al termine del nostro flusso applicativo e tutte le variabili mantenute nel flowScope, il cui ciclo di vita è valido dallo start-state al end-state del flusso padre, vengono rimosse automaticamente dal framework, senza nessun intervento di codice da parte dello sviluppatore.
Conclusioni
In questo primo articolo, abbiamo visto quali sono le caratteristiche fondamentali di SWF e abbiamo lasciato fuori quelle che sono le sue vere potenzialità (persistenza, binding, validation, ajax, security, etc.). In definitiva SWF s’integra facilmente con tutti i framework basati sul pattern MVC e su quelli più recenti come JSF. Dalla versione 2 del framework, sono stati aggiunti due nuovi moduli, quali Spring Javascript (attualmente si basa solo sul framewok Dojo, ma sono in cantiere anche implementazioni per altri js frameworks) e Spring Faces che contiene il supporto di Spring per JSF.
Risorse
http://en.wikipedia.org/wiki/Model-view-controller
http://www.springframework.org/webflow
http://www.springframework.org/
http://tiles.apache.org/
Una risposta
[…] Link al primo articolo della serie […]