viernes, 12 de octubre de 2012

Tratamiento de errores en procesos de negocio con Activiti

Activiti
Al igual que en todo programa durante la ejecución de los procesos de negocio se pueden producir condiciones de error que deberemos tratar. Por ejemplo, supongamos un proceso de negocio que trata con la venta de diferentes artículos, probablemente una comprobación y condición de error será comprobar las existencias del artículo que se va a vender en el proceso. Este es un error lógico que se podría dar pero también se pueden dar errores técnicos como por ejemplo un error al actualizar las existencias en la base de datos o al acceder a un sistema externo como podría ser un web service o servicio REST.

Los errores lógicos se pueden tratar mediante ciertas construcciones de la notación BPMN, los errores técnicos podemos tratarlos dirigiendo el flujo del proceso. Estas dos formas de tratamiento de errores no son exclusivas sino que se pueden aplicar ambas en un mismo proceso. Veamos un ejemplo de ambos tipos de tratamientos en un proceso de negocio que comprueba las existencias de un producto.

Primero la definición del proceso de negocio en notación BPMN y el diagrama:



<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:activiti="http://activiti.org/bpmn" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL http://www.omg.org/spec/BPMN/2.0/20100501/BPMN20.xsd" targetNamespace="HelloWorldActiviti">
<process id="errores" name="Errores" isExecutable="true">
<startEvent id="start" />
<sequenceFlow id="flow1" sourceRef="start" targetRef="comprobarExistenciasSubprocess" />
<subProcess id="comprobarExistenciasSubprocess">
<startEvent id="subprocessStart" />
<sequenceFlow id="flow11" sourceRef="subprocessStart" targetRef="comprobarExistenciasServiceTask" />
<serviceTask id="comprobarExistenciasServiceTask" activiti:class="es.com.blogspot.elblogdepicodev.activiti.misc.ComprobarExistenciasServiceTask" />
<sequenceFlow id="flowHayExistencias" sourceRef="comprobarExistenciasServiceTask" targetRef="subprocessEnd" />
<sequenceFlow id="flowNoHayExistencias" sourceRef="comprobarExistenciasServiceTask" targetRef="subprocessError" />
<endEvent id="subprocessEnd" />
<endEvent id="subprocessError">
<errorEventDefinition errorRef="noHayExistencias" />
</endEvent>
</subProcess>
<sequenceFlow id="flow4" sourceRef="comprobarExistenciasSubprocess" targetRef="hayExistenciasTask" />
<boundaryEvent id="catchNoHayExistenciasBoundaryEvent" attachedToRef="comprobarExistenciasSubprocess">
<errorEventDefinition errorRef="noHayExistencias" />
</boundaryEvent>
<sequenceFlow id="flow5" sourceRef="catchNoHayExistenciasBoundaryEvent" targetRef="noHayExistenciasTask" />
<scriptTask id="hayExistenciasTask" scriptFormat="groovy">
<script>
println("Hay existencias de ${producto.nombre} (${producto.existencias})")
producto.existencias -= 1
</script>
</scriptTask>
<scriptTask id="noHayExistenciasTask" scriptFormat="groovy">
<script>println("No hay existencias de ${producto.nombre}")</script>
</scriptTask>
<sequenceFlow id="flow6" sourceRef="hayExistenciasTask" targetRef="end" />
<sequenceFlow id="flow7" sourceRef="noHayExistenciasTask" targetRef="end" />
<endEvent id="end" />
</process>
</definitions>

La tarea «comprobarExistenciasServiceTask» contiene la lógica para comprobar las existencias de un producto y en función de si hay existencias o no toma la decisión de dirigir el flujo a través de la transición adecuada, este será el tratamiento técnico que aunque en este caso no se produce un excepción por un error con un sistema externo para este proceso es una condición de error. En caso de que haya existencias el subproceso acabarán en el evento «end» del subproceso y en caso de que no haya existencias el subproceso se terminará con el evento «error» del subproceso, este sería el tratamiento lógico del error.

Veamos la clase ComprobarExistenciasServiceTask:

package es.com.blogspot.elblogdepicodev.activiti.misc;
import org.activiti.engine.impl.pvm.PvmTransition;
import org.activiti.engine.impl.pvm.delegate.ActivityBehavior;
import org.activiti.engine.impl.pvm.delegate.ActivityExecution;
public class ComprobarExistenciasServiceTask implements ActivityBehavior {
@Override
public void execute(ActivityExecution execution) throws Exception {
PvmTransition transition = null;
Producto producto = (Producto) execution.getVariable("producto");
if (producto.hayExistencias()) {
transition = execution.getActivity().findOutgoingTransition("flowHayExistencias");
} else {
transition = execution.getActivity().findOutgoingTransition("flowNoHayExistencias");
}
execution.take(transition);
}
}

En función de si hay existencias toma un transición u otra. Ahora la clase Producto.

package es.com.blogspot.elblogdepicodev.activiti.misc;
import java.io.Serializable;
public class Producto implements Serializable {
private static final long serialVersionUID = -2340188855153751355L;
private String nombre;
private Long existencias;
public Producto() {
}
public Producto(String nombre, Long existencias) {
this.nombre = nombre;
this.existencias = existencias;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public Long getExistencias() {
return existencias;
}
public void setExistencias(Long existencias) {
this.existencias = existencias;
}
public boolean hayExistencias() {
return existencias > 0;
}
}
view raw Producto.java hosted with ❤ by GitHub

Los teses unitarios para probar el proceso de negocio:

package es.com.blogspot.elblogdepicodev.activiti;
import java.util.HashMap;
import java.util.Map;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.test.ActivitiRule;
import org.activiti.engine.test.Deployment;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import es.com.blogspot.elblogdepicodev.activiti.misc.Producto;
@SuppressWarnings({ "rawtypes", "unchecked" })
public class ErroresTest extends ActivitiAbstractTest {
@Rule
public ActivitiRule activitiRule = new ActivitiRule("activiti-h2.cfg.xml");
@Test
@Deployment(resources = "bpmn/Errores.bpmn20.xml")
public void testHayExistencias() {
Producto producto = new Producto("Arch Linux T-Shirt", 10l);
Map variables = new HashMap();
variables.put("producto", producto);
RuntimeService rs = activitiRule.getRuntimeService();
rs.startProcessInstanceByKey("errores", variables);
Assert.assertEquals(new Long(9), producto.getExistencias());
}
@Test
@Deployment(resources = "bpmn/Errores.bpmn20.xml")
public void testNoHayExistencias() {
Producto producto = new Producto("Arch Linux Mug", 0l);
Map variables = new HashMap();
variables.put("producto", producto);
RuntimeService rs = activitiRule.getRuntimeService();
rs.startProcessInstanceByKey("errores", variables);
Assert.assertEquals(new Long(0), producto.getExistencias());
}
}

El test testHayExistencias comprueba la ejecución correcta para un producto con existencias, también que se resta una unidad a las existencias. El test testNoHayExistencias comprueba la ejecución en el caso de un producto sin existencias.

Finalmente, el programa que ejecuta el proceso de negocio:

package es.com.blogspot.elblogdepicodev.activiti;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngineConfiguration;
import org.activiti.engine.ProcessEngines;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.runtime.ProcessInstance;
import org.h2.tools.Server;
import es.com.blogspot.elblogdepicodev.activiti.misc.Producto;
public class Errores {
public static void main(String[] args) throws Exception {
Server server = null;
try {
server = Server.createTcpServer().start();
ProcessEngines.init();
ProcessEngine processEngine = ProcessEngineConfiguration.createProcessEngineConfigurationFromResource("activiti-mysql.cfg.xml").buildProcessEngine();
RuntimeService runtimeService = processEngine.getRuntimeService();
RepositoryService repositoryService = processEngine.getRepositoryService();
repositoryService.createDeployment().addClasspathResource("bpmn/Errores.bpmn20.xml").deploy();
Producto producto = new Producto("Arch Linux T-Shirt", 10l);
Map variables = new HashMap();
variables.put("producto", producto);
ProcessInstance pi = runtimeService.startProcessInstanceByKey("errores", variables);
System.out.println(MessageFormat.format("Las nuevas existencias de {0} son {1}", producto.getNombre(), producto.getExistencias()));
} finally {
ProcessEngines.destroy();
if (server != null)
server.stop();
}
}
}
view raw Errores.java hosted with ❤ by GitHub


En el apartado referencia puedes consultar el código fuente de este y otros ejemplos de procesos de negocio.

Referencia:
Conceptos sobre procesos de negocio (BP, BPM, BPMS, ...)
Procesos de negocio con Activiti
Usar variables en un proceso de negocio con Activiti
Reglas de negocio con Drools y Activiti
Código fuente del ejemplo tratamiento de errores con Activiti