Use Spring para crear un motor de flujo de trabajo simple

Muchas aplicaciones empresariales de Jave requieren que el procesamiento se ejecute en un contexto separado del del sistema principal. En muchos casos, estos procesos de backend realizan varias tareas, y algunas dependen del estado de una tarea anterior. Con el requisito de tareas de procesamiento interdependientes, una implementación que utilice un único conjunto de llamadas a métodos de estilo procedimental generalmente resulta inadecuada. Con Spring, un desarrollador puede separar fácilmente un proceso de backend en una agregación de actividades. El contenedor Spring se une a esas actividades para formar un flujo de trabajo simple.

Para los propósitos de este artículo, el flujo de trabajo simple se define como cualquier conjunto de actividades realizadas en un orden predeterminado sin la interacción del usuario. Sin embargo, este enfoque no se sugiere como reemplazo de los marcos de flujo de trabajo existentes. Para escenarios en los que se necesitan interacciones más avanzadas, como bifurcaciones, uniones o transiciones basadas en la entrada del usuario, un motor de flujo de trabajo comercial o de código abierto independiente está mejor equipado. Un proyecto de código abierto ha integrado con éxito un diseño de flujo de trabajo más complejo con Spring.

Si las tareas de flujo de trabajo en cuestión son simplistas, el enfoque de flujo de trabajo simple tiene sentido en comparación con un marco de flujo de trabajo independiente completamente funcional, especialmente si Spring ya está en uso, ya que se garantiza una implementación rápida sin incurrir en tiempo de aceleración. Además, dada la naturaleza del contenedor ligero de inversión de control de Spring, Spring reduce la sobrecarga de recursos.

Este artículo presenta brevemente el flujo de trabajo como tema de programación. Utilizando conceptos de flujo de trabajo, Spring se emplea como marco para impulsar un motor de flujo de trabajo. Luego, se discuten las opciones de implementación de producción. Comencemos con la idea de un flujo de trabajo simple centrándonos en los patrones de diseño del flujo de trabajo y la información de fondo relacionada.

Flujo de trabajo simple

El flujo de trabajo de modelado es un tema que se ha estudiado desde la década de 1970, y muchos desarrolladores han intentado crear una especificación de modelado de flujo de trabajo estandarizado. Patrones de flujo de trabajo , un documento técnico de WHM van der Aalst et al. (Julio de 2003), ha logrado clasificar un conjunto de patrones de diseño que modelan con precisión los escenarios de flujo de trabajo más comunes. Entre los patrones de flujo de trabajo más triviales se encuentra el patrón de secuencia. El patrón de flujo de trabajo de secuencia se ajusta a los criterios de un flujo de trabajo simple y consta de un conjunto de actividades ejecutadas en secuencia.

Los diagramas de actividades UML (Lenguaje de modelado unificado) se utilizan comúnmente como un mecanismo para modelar el flujo de trabajo. La Figura 1 muestra un proceso de flujo de trabajo de Secuencia básico modelado utilizando un diagrama de actividad UML estándar.

El flujo de trabajo de secuencia es un patrón de flujo de trabajo estándar que prevalece en las aplicaciones J2EE. Una aplicación J2EE generalmente requiere que ocurra una secuencia de eventos en un hilo de fondo o de forma asincrónica. El diagrama de actividad de la Figura 2 ilustra un flujo de trabajo simple para notificar a los viajeros interesados ​​que la tarifa aérea a su destino favorito ha disminuido.

El flujo de trabajo de la aerolínea en la Figura 1 es responsable de crear y enviar notificaciones dinámicas por correo electrónico. Cada paso del proceso representa una actividad. Debe ocurrir algún evento externo antes de que el flujo de trabajo se ponga en marcha. En este caso, ese evento es una disminución de la tarifa para la ruta de vuelo de una aerolínea.

Analicemos la lógica empresarial del flujo de trabajo de la aerolínea. Si la primera actividad no encuentra usuarios interesados ​​en notificaciones de caída de tasas, se cancela todo el flujo de trabajo. Si se descubren usuarios interesados, se completan las actividades restantes. Posteriormente, una transformación XSL (Extensible Stylesheet Language) genera el contenido del mensaje, después de lo cual se registra la información de auditoría. Finalmente, se intenta enviar el mensaje a través de un servidor SMTP. Si el envío se completa sin errores, se registra el éxito y el proceso finaliza. Pero, si ocurre un error mientras se comunica con el servidor SMTP, se hará cargo de una rutina especial de manejo de errores. Este código de manejo de errores intentará reenviar el mensaje.

Dado el ejemplo de la aerolínea, una pregunta es evidente: ¿Cómo podría dividir de manera eficiente un proceso secuencial en actividades individuales? Este problema se maneja elocuentemente con Spring. Analicemos rápidamente Spring como un marco de inversión de control.

Inversión de control

Spring nos permite eliminar la responsabilidad de controlar las dependencias de un objeto moviendo esta responsabilidad al contenedor Spring. Esta transferencia de responsabilidad se conoce como Inversión de control (IoC) o Inyección de dependencia. Consulte "Inversion of Control Containers and the Dependency Injection Pattern" de Martin Fowler (martinfowler.com, enero de 2004) para una discusión más profunda sobre IoC e Inyección de dependencias. Al administrar las dependencias entre los objetos, Spring elimina la necesidad de un código adhesivo , código escrito con el único propósito de hacer que las clases colaboren entre sí.

Componentes del flujo de trabajo como Spring beans

Antes de llegar demasiado lejos, ahora es un buen momento para analizar los conceptos principales detrás de Spring. La ApplicationContextinterfaz, heredada de la BeanFactoryinterfaz, se impone como la entidad o contenedor de control real dentro de Spring. El ApplicationContextes responsable de la creación de instancias, la configuración y la gestión del ciclo de vida de un conjunto de beans conocidos como Spring beans. El ApplicationContextse configura conectando Spring beans en un archivo de configuración basado en XML. Este archivo de configuración dicta la naturaleza en la que Spring beans colaboran entre sí. Así, en Spring speak, los Spring beans que interactúan con otros se conocen como colaboradores. Por defecto, los frijoles Spring existen como singletons en elApplicationContext, pero el atributo singleton se puede establecer en falso, cambiándolos de manera efectiva para que se comporten en lo que Spring llama modo prototipo .

Volviendo a nuestro ejemplo, en la disminución de la tarifa aérea, una abstracción de una rutina de envío SMTP se conecta como la última actividad en el ejemplo del proceso de flujo de trabajo (código de ejemplo disponible en Recursos). Siendo la quinta actividad, este frijol tiene un nombre apropiado activity5. Para enviar un mensaje, se activity5requiere un colaborador delegado y un controlador de errores:

La implementación de los componentes del flujo de trabajo como Spring beans da como resultado dos subproductos deseables, la facilidad de las pruebas unitarias y un alto grado de reutilización. Las pruebas unitarias eficientes son evidentes dada la naturaleza de los contenedores de IoC. Al utilizar un contenedor de IoC como Spring, las dependencias de los colaboradores se pueden intercambiar fácilmente con reemplazos simulados durante las pruebas. En el ejemplo de la aerolínea, un Activitybean Spring como el que activity5se puede recuperar fácilmente de una prueba independiente ApplicationContext. La sustitución de un delegado SMTP simulado en activity5permite realizar pruebas unitarias por activity5separado.

El segundo subproducto, la reutilización, se realiza mediante actividades de flujo de trabajo como una transformación XSL. Una transformación XSL, resumida en una actividad de flujo de trabajo, ahora puede ser reutilizada por cualquier flujo de trabajo que trate con transformaciones XSL.

Conectando el flujo de trabajo

En la API proporcionada (descargable desde Recursos), Spring controla un pequeño conjunto de jugadores para interactuar de una manera que constituye un flujo de trabajo. Las interfaces clave son:

  • Activity: Encapsula la lógica empresarial de un solo paso en el proceso de flujo de trabajo.
  • ProcessContext: Los objetos de tipo ProcessContextse pasan entre actividades en el flujo de trabajo. Los objetos que implementan esta interfaz son responsables de mantener el estado a medida que el flujo de trabajo pasa de una actividad a la siguiente.
  • ErrorHandler: Proporciona un método de devolución de llamada para manejar errores.
  • Processor: Describe un bean que actúa como ejecutante del hilo principal del flujo de trabajo.

El siguiente extracto del código de muestra es una configuración de Spring Bean que vincula el ejemplo de la aerolínea como un proceso de flujo de trabajo simple.

             /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

Ahora que sabemos cómo armar un flujo de trabajo simple usando Spring, centrémonos en la creación de instancias usando datos semilla. Para comprender cómo sembrar el flujo de trabajo, veamos los métodos expuestos en la Processorinterfaz real :

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); }

En la mayoría de los casos, los procesos de flujo de trabajo requieren algunos estímulos iniciales para comenzar. Existen dos opciones para iniciar un procesador: el doActivities(Object seedData)método o su alternativa sin argumentos. El siguiente listado de código es la doAcvtivities()implementación para el SequenceProcessorincluido con el código de muestra:

 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); } }