How to use transient variables in Activiti

A feature that has been requested quite a bit – transient variables – has landed in the Beta3 of Activiti v6 released yesterday. In this post, I’ll show you an example on how transient variables can be used to cover some advanced use cases that weren’t possible (or optimal) before.

So far, all variables in Activiti were persistent. This means the variable and value are stored in the data store and historical audit data is kept. Transient variables on the other hand act and behave like a regular variable, but they are not persisted. Beyond not being persisted, the following is special to transient variables:

  • a transient variable only survives until the next ‘wait state’, when the state of the process instance is persisted to the database.
  • a transient variable shadows a persistent variable with the same name.

More detailed information about transient variables and the API’s can be found in the documentation.

Example

The process definition that we’ll use to demo some bits of the transient variables is shown below. It’s a fairly simple process: the idea is that we’ll ask some things like keyword and language from the user and use it to do a GitHub API call. If successful, the results are shown to the user. It’s easy to write a UI for this (or use the new forms in the Beta3 angularJS app), but in this post we’ll focus on the code only.

The BPMN 2.0 xml and code can be found on this Github repo: https://github.com/jbarrez/transient-vars-example

Screenshot from 2016-09-01 11:44:50

Let’s walk through the process together. The process starts by providing some input from the user about what should be searched on (usually this would be done using a start form).

repositoryService.createDeployment().addClasspathResource("process.bpmn20.xml").deploy();

Map<String, Object> variables = new HashMap<String, Object>();
variables.put("keyWord", "workflow");
variables.put("language", "java");
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("githubsearch", variables);

The variables we pass when starting the process instance are regular variables. They are persisted and audit history will be kept, as there is no reason why this shouldn’t be the case.

The first step that is executed is the ‘execute HTTP call’ step, which is a Service Task with a Java delegate:

<serviceTask name="Execute HTTP call" activiti:class="org.activiti.ExecuteHttpCallDelegate"></serviceTask>

Java code:

public class ExecuteHttpCallDelegate implements JavaDelegate {

    public void execute(DelegateExecution execution) {

        String keyword = (String) execution.getVariable("keyWord");
        String language = (String) execution.getVariable("language");

        String url = "https://api.github.com/search/repositories?q=%s+language:%s&sort=stars&order=desc";
        url = String.format(url, keyword, language);
        HttpGet httpget = new HttpGet(url);

        CloseableHttpClient httpclient = HttpClients.createDefault();
        try {
            CloseableHttpResponse response = httpclient.execute(httpget);

            execution.setTransientVariable("response", IOUtils.toString(response.getEntity().getContent(), "UTF-8"));
            execution.setTransientVariable("responseStatus", response.getStatusLine().getStatusCode());

            response.close();

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}

Here, we’re doing a simple HTTP get against the GitHub API, using the ‘keyword’ and ‘language’ variables we’ve passed on process instance start. Special here is on line 16 and 17 that we’re storing the response and response status in transient variables (that’s the setTransientVariable() call). The reasons for choosing transient variables here are

  • The json response from the Github API is very large. It can be stored in a persistent way of course, but this won’t be good for performance.
  • From an audit point of view, the whole response matters very little. We’ll extract the important bits later from that response, and those will be stored in the historical data.

After getting the response and storing it in a transient variable, we pass the exclusive gateway. The sequenceflow looks like this:

<sequenceFlow ... >
  <extensionElements>
    <activiti:executionListener event="take" class="org.activiti.ProcessResponseExecutionListener"></activiti:executionListener>
  </extensionElements>
  <conditionExpression xsi:type="tFormalExpression"><![CDATA[${responseStatus == 200}]]></conditionExpression>
</sequenceFlow>

Note that for the sequence flow condition there is no difference when it comes to using a transient or non-transient variable. A regular getVariable will also return the transient variable with the name, if set (this is the shadowing part in the docs mentioned above). A getTransientVariable also exists, when only the transient set of variables should be consulted. Anyway: for the condition: no difference at all.

You can also see that the sequence flow has a (hidden in the diagram) execution listener. The execution listener will parse the json response, select the relevant bits and store these in a transient array list. This is important, as you’ll read below the code.

public class ProcessResponseExecutionListener implements ExecutionListener {

    private ObjectMapper objectMapper = new ObjectMapper();

    public void notify(DelegateExecution execution) {

        List<String> searchResults = new ArrayList<String>();

        String response = (String) execution.getVariable("response");
        try {
            JsonNode jsonNode = objectMapper.readTree(response);
            JsonNode itemsNode = jsonNode.get("items");
            if (itemsNode != null && itemsNode.isArray()) {
                for (JsonNode itemNode : (ArrayNode) itemsNode) {
                    String url = itemNode.get("html_url").asText();
                    searchResults.add(url);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        execution.setTransientVariable("searchResults", searchResults);
    }

}

The reason for storing the list as a transient variable is an important one. As you can see in the diagram, a multi instance subprocess follows. A subprocess often takes a collection variable to create the instances. So far, this was a persistent variable, in the form of a java-serialized ArrayList. I’ve never liked that (I’ve always used delegateExpressions with a bean if I had to do it before). This has always bothered me a bit. Now, the arraylist is transient and won’t be stored in the data store:

  <subProcess name="subProcess">
    <multiInstanceLoopCharacteristics isSequential="false" 
          activiti:collection="searchResults" activiti:elementVariable="searchResult" />   

Do note that the ‘searchResult’ variable above will be a persistent variable.

Note that the transient variables will be there until the user task is reached and the state is stored in the data store. It’s also possible to pass transient variables when starting a process instance (which could have been an option here, but I think storing user input is a thing you’d want in your audit data).

If you’d run the process instance like this for example:

Map<String, Object> variables = new HashMap<String, Object>();
variables.put("keyWord", "workflow");
variables.put("language", "java");
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("githubsearch", variables);

List<Task> tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list();
for (Task task : tasks) {
  System.out.println("Current task : " + task.getName());
}

Which gives as example output (limited to 5 results):

Current task : Review result https://github.com/Activiti/Activiti
Current task : Review result https://github.com/twitter/ambrose
Current task : Review result https://github.com/azkaban/azkaban
Current task : Review result https://github.com/romannurik/AndroidDesignPreview
Current task : Review result https://github.com/spring-projects/spring-xd

The user could now look into the details of each of the results and continue the process.

Last Words

As you can imagine, there are quite a few use cases for transient variables. I know that for many people this was an important feature, so I’m glad it’s out there now. Feedback and comments of course, as usual, always welcome!

One Comment

  1. […] 引入 临时变量(transient variables),即仅在工作流执行过程中的单个事务中使用,而不保存到 Activiti 变量表中的变量。参见 http://www.jorambarrez.be/blog/2016/09/01/how-to-use-transient-variables-in-activiti/ […]

Leave a Reply

Your email address will not be published.