Usa Spring per creare un semplice motore di flusso di lavoro

Molte applicazioni aziendali Jave richiedono che l'elaborazione venga eseguita in un contesto separato da quello del sistema principale. In molti casi, questi processi di backend eseguono diverse attività, alcune delle quali dipendono dallo stato di un'attività precedente. Con il requisito di attività di elaborazione interdipendenti, un'implementazione che utilizza un unico insieme di chiamate di metodo in stile procedurale di solito si rivela inadeguata. Utilizzando Spring, uno sviluppatore può facilmente separare un processo di backend in un'aggregazione di attività. Il contenitore Spring si unisce a queste attività per formare un semplice flusso di lavoro.

Ai fini di questo articolo, per flusso di lavoro semplice si intende qualsiasi insieme di attività eseguite in un ordine predeterminato senza l'interazione dell'utente. Questo approccio, tuttavia, non è suggerito come sostituto dei framework di flusso di lavoro esistenti. Per gli scenari in cui sono necessarie interazioni più avanzate, come fork, join o transizioni basate sull'input dell'utente, è meglio equipaggiato un motore di workflow commerciale o open source autonomo. Un progetto open source ha integrato con successo un design del flusso di lavoro più complesso con Spring.

Se le attività del flusso di lavoro a portata di mano sono semplicistiche, l'approccio del flusso di lavoro semplice ha senso al contrario di un framework di flusso di lavoro autonomo completamente funzionale, soprattutto se Spring è già in uso, poiché l'implementazione rapida è garantita senza incorrere in tempi di avvio. Inoltre, data la natura del leggero contenitore di inversione di controllo di Spring, Spring riduce il sovraccarico di risorse.

Questo articolo introduce brevemente il flusso di lavoro come argomento di programmazione. Utilizzando i concetti del flusso di lavoro, Spring viene utilizzato come struttura per guidare un motore del flusso di lavoro. Quindi, vengono discusse le opzioni di distribuzione della produzione. Cominciamo con l'idea di un flusso di lavoro semplice concentrandoci sui modelli di progettazione del flusso di lavoro e sulle relative informazioni di base.

Flusso di lavoro semplice

La modellazione del flusso di lavoro è un argomento che è stato studiato fin dagli anni '70 e molti sviluppatori hanno tentato di creare una specifica di modellazione del flusso di lavoro standardizzata. Workflow Patterns , un white paper di WHM van der Aalst et al. (Luglio 2003), è riuscito a classificare una serie di modelli di progettazione che modellano accuratamente gli scenari di flusso di lavoro più comuni. Uno dei pattern di flusso di lavoro più banali è il pattern Sequence. Conforme ai criteri di un flusso di lavoro semplice, il modello di flusso di lavoro Sequence consiste in un insieme di attività eseguite in sequenza.

I diagrammi di attività UML (Unified Modeling Language) sono comunemente usati come meccanismo per modellare il flusso di lavoro. La Figura 1 mostra un processo di flusso di lavoro Sequence di base modellato utilizzando un diagramma di attività UML standard.

Il flusso di lavoro Sequence è un modello di flusso di lavoro standard prevalente nelle applicazioni J2EE. Un'applicazione J2EE di solito richiede che una sequenza di eventi si verifichi in un thread in background o in modo asincrono. Il diagramma delle attività della Figura 2 illustra un semplice flusso di lavoro per informare i viaggiatori interessati che il biglietto aereo per la loro destinazione preferita è diminuito.

Il flusso di lavoro della compagnia aerea nella Figura 1 è responsabile della creazione e dell'invio di notifiche e-mail dinamiche. Ogni fase del processo rappresenta un'attività. Prima che il flusso di lavoro venga avviato, deve verificarsi un evento esterno. In questo caso, tale evento è una diminuzione della tariffa per la rotta di volo di una compagnia aerea.

Esaminiamo la logica aziendale del flusso di lavoro della compagnia aerea. Se la prima attività non trova utenti interessati alle notifiche di calo dei tassi, l'intero flusso di lavoro viene annullato. Se vengono scoperti utenti interessati, le attività rimanenti vengono completate. Successivamente, una trasformazione XSL (Extensible Stylesheet Language) genera il contenuto del messaggio, dopodiché vengono registrate le informazioni di controllo. Infine, viene effettuato un tentativo di inviare il messaggio tramite un server SMTP. Se l'invio viene completato senza errori, il successo viene registrato e il processo termina. Tuttavia, se si verifica un errore durante la comunicazione con il server SMTP, subentra una speciale routine di gestione degli errori. Questo codice di gestione degli errori tenterà di inviare nuovamente il messaggio.

Dato l'esempio della compagnia aerea, una domanda è evidente: come potresti suddividere in modo efficiente un processo sequenziale in singole attività? Questo problema viene gestito in modo eloquente utilizzando Spring. Parliamo rapidamente della primavera come framework di inversione del controllo.

Controllo invertente

Spring ci consente di rimuovere la responsabilità di controllare le dipendenze di un oggetto spostando questa responsabilità nel contenitore Spring. Questo trasferimento di responsabilità è noto come Inversion of Control (IoC) o Dependency Injection. Vedere "Inversion of Control Containers and the Dependency Injection Pattern" di Martin Fowler (martinfowler.com, gennaio 2004) per una discussione più approfondita su IoC e Dependency Injection. Gestendo le dipendenze tra gli oggetti, Spring elimina la necessità di codice collante , codice scritto al solo scopo di far collaborare le classi tra loro.

Componenti del flusso di lavoro come fagioli primaverili

Prima di andare troppo lontano, ora è un buon momento per esaminare i concetti principali alla base della primavera. L' ApplicationContextinterfaccia, ereditando BeanFactorydall'interfaccia, si impone come l'effettiva entità di controllo o contenitore all'interno di Spring. Il ApplicationContextè responsabile per esemplificazione, la configurazione e la gestione del ciclo di vita di un insieme di fagioli conosciuti come bean Spring. Il ApplicationContextè configurato per il cablaggio bean Spring in un file di configurazione basato su XML. Questo file di configurazione determina la natura in cui i bean Spring collaborano tra loro. Pertanto, in Spring speak, i fagioli Spring che interagiscono con gli altri sono noti come collaboratori. Per impostazione predefinita, i bean Spring esistono come singleton inApplicationContext, ma l'attributo singleton può essere impostato su false, cambiandoli in modo efficace per comportarsi in quella che Spring chiama modalità prototipo .

Tornando al nostro esempio, nella diminuzione del biglietto aereo, un'astrazione di una routine di invio SMTP è cablata come ultima attività nell'esempio di processo del flusso di lavoro (codice di esempio disponibile in Risorse). Essendo la quinta attività, questo fagiolo è giustamente chiamato activity5. Per inviare un messaggio, activity5richiede un collaboratore delegato e un gestore degli errori:

L'implementazione dei componenti del flusso di lavoro come bean Spring si traduce in due sottoprodotti desiderabili, facilità di test unitario e un grande grado di riutilizzabilità. Un test di unità efficiente è evidente data la natura dei contenitori IoC. Utilizzando un contenitore IoC come Spring, le dipendenze dei collaboratori possono essere facilmente sostituite con finte sostituzioni durante i test. Nell'esempio della compagnia aerea, un Activitybean Spring come activity5può essere facilmente recuperato da un test autonomo ApplicationContext. La sostituzione di un delegato SMTP fittizio in activity5rende possibile lo unit test activity5separatamente.

Il secondo sottoprodotto, la riusabilità, è realizzato da attività del flusso di lavoro come una trasformazione XSL. Una trasformazione XSL, astratta in un'attività del flusso di lavoro, può ora essere riutilizzata da qualsiasi flusso di lavoro che si occupa di trasformazioni XSL.

Cablaggio del flusso di lavoro

Nell'API fornita (scaricabile da Risorse), Spring controlla un piccolo gruppo di giocatori per interagire in un modo che costituisce un flusso di lavoro. Le interfacce chiave sono:

  • Activity: Incapsula la logica aziendale di un singolo passaggio nel processo del flusso di lavoro.
  • ProcessContext: Gli oggetti di tipo ProcessContextvengono passati tra le attività nel flusso di lavoro. Gli oggetti che implementano questa interfaccia sono responsabili del mantenimento dello stato durante la transizione del flusso di lavoro da un'attività all'altra.
  • ErrorHandler: Fornisce un metodo di callback per la gestione degli errori.
  • Processor: Descrive un bean che funge da esecutore del thread del flusso di lavoro principale.

Il seguente estratto dal codice di esempio è una configurazione del bean Spring che lega l'esempio della compagnia aerea come un semplice processo di flusso di lavoro.

             /property>  org.iocworkflow.test.sequence.ratedrop.RateDropContext  

The SequenceProcessor class is a concrete subclass that models a Sequence pattern. Wired to the processor are five activities that the workflow processor will execute in order.

When compared with most procedural backend process, the workflow solution really stands out as being capable of highly robust error handling. An error handler may be separately wired for each activity. This type of handler provides fine-grained error handling at the individual activity level. If no error handler is wired for an activity, the error handler defined for the overall workflow processor will handle the problem. For this example, if an unhandled error occurs any time during the workflow process, it will propagate out to be handled by the ErrorHandler bean, which is wired up using the defaultErrorHandler property.

More complex workflow frameworks persist state to a datastore between transitions. In this article, we're only interested in simple workflow cases where state transition is automatic. State information is only available in the ProcessContext during the actual workflow's runtime. Having only two methods, you can see the ProcessContext interface is on a diet:

public interface ProcessContext extends Serializable { public boolean stopProcess(); public void setSeedData(Object seedObject); }

The concrete ProcessContext class used for the airline example workflow is the RateDropContext class. The RateDropContext class encapsulates the data necessary to execute an airline rate drop workflow.

Until now, all bean instances have been singletons as per the default ApplicationContext's behavior. But we must create a new instance of the RateDropContext class for every invocation of the airline workflow. To handle this requirement, the SequenceProcessor is configured, taking a fully qualified class name as the processContextClass property. For every workflow execution, the SequenceProcessor retrieves a new instance of ProcessContext from Spring using the class name specified. For this to work, a nonsingleton Spring bean or prototype of type org.iocworkflow.test.sequence.simple.SimpleContext must exist in the ApplicationContext (see rateDrop.xml for the entire listing).

Seeding the workflow

Ora che sappiamo come mettere insieme un semplice flusso di lavoro utilizzando Spring, concentriamoci sulla creazione di istanze utilizzando i dati seed. Per capire come eseguire il seeding del flusso di lavoro, esaminiamo i metodi esposti sull'interfaccia effettiva Processor:

public interface Processor { public boolean supports(Activity activity); public void doActivities(); public void doActivities(Object seedData); public void setActivities(List activities); public void setDefaultErrorHandler(ErrorHandler defaultErrorHandler); }

Nella maggior parte dei casi, i processi del flusso di lavoro richiedono alcuni stimoli iniziali per l'avvio. Esistono due opzioni per avviare un processore: il doActivities(Object seedData)metodo o la sua alternativa senza argomenti. Il listato di codice seguente è l' doAcvtivities()implementazione per il SequenceProcessorcodice di esempio incluso:

 public void doActivities(Object seedData) { if (logger.isDebugEnabled()) logger.debug(getBeanName() + " processor is running.."); //retrieve injected by Spring List activities = getActivities(); //retrieve a new instance of the Workflow ProcessContext ProcessContext context = createContext(); if (seedData != null) context.setSeedData(seedData); for (Iterator it = activities.iterator(); it.hasNext();) { Activity activity = (Activity) it.next(); if (logger.isDebugEnabled()) logger.debug("running activity:" + activity.getBeanName() + " using arguments:" + context); try { context = activity.execute(context); } catch (Throwable th) { ErrorHandler errorHandler = activity.getErrorHandler(); if (errorHandler == null) { logger.info("no error handler for this action, run default error" + "handler and abort processing "); getDefaultErrorHandler().handleError(context, th); break; } else { logger.info("run error handler and continue"); errorHandler.handleError(context, th); } }