Recently, in one of my projects, there was a requirement for simple workflow around some background processing tasks. Most of the tasks were already implemented so we had to just implement the workflow around those.
Available solutions
There are many workflow frameworks available for Java. Even though they met the expectations, they seemed to be too heavy for a simple requirement. Next, there were blogs around how to use Spring framework for creating simple workflows using Spring beans. This indeed was what I was looking for. However, something was missing here. Each action in the workflow can be a Spring bean with a simple interface having an execute method as follow:
But how do we pass parameters to the actions. Most of the implementations would pass a map between actions to the
execute() method. This means an action would add it's result to the map using a key and the next action would read the value from this map. It would make the actions aware of each other or at least aware of the keys for passing information. If the key or the data type of value changes then the next action reading the value would break.
Expected behavior
It would have been far better to implement an action as a normal class with its dependencies being injected using, say, Spring dependency injection. Then during workflow execution the
execute() method can be invoked to get the desired output. We should be able to pass the output from one action to the next action without the actions being aware of each other.
Solution
Using the Spring Expression Language
The
Spring Expression Language allows to have simple expressions defined and evaluated against specified object instance. The bean XML allows using such expression to inject property values. The idea was to use SpEL for defining the values to be injected into the workflow action. Let's take an example of a simple action which takes a string as an input and simply returns reverse string as output. The action is implemented as follows:
It simply takes a string value as an input in the constructor itself and in the
execute() call returns the reverse string. The instance of the above action will be created within the code using the application context's
getBean() method. But how do we inject the required value? For that first let's have a look at how to define the bean for this action class.
Above is a simple bean definition mentioning the implementation class and the parameters. In this case we are indicating the value to be passed to the constructor. Notice, that the value is actually an expression starting with
%{ and ending with
}. We have defined our custom expression prefix using
% because Spring already uses
# and
$ for identifying it's expressions. The variable name
source can be anything you want. For our example, the
source refers to the original input to the workflow. It can be anything from a simple string, integer, class instance etc. to array, set, map etc.
For our workflow, we will have list of beans representing the actions. The workflow will be responsible to create instance of the actions and execute them but while doing so it should be able to pass the parameters defined by our custom expression. Let's see how we can do this.
Using custom expression resolver
Spring allows to have custom expression resolver implementing the
BeanExpressionResolver interface. Following is the snippet for our
BeanInstantiator class implementing this interface as well as the
BeanFactoryPostProcessor interface.
The
postProcessBeanFactory() method at
Line 4 is for the
BeanFactoryPostProcessor interface. Here, we are going to register our custom resolver which is the same class. Also, we are preserving the existing resolver.
The
evaluate() method at
Line 10 is for the
BeanExpressionResolver interface. Whenever, Spring encounters an expression then this method will be called. Here, we are checking if the expression is our custom expression. The expression string can be enclosed in
%{} e.g.
%{source} or it can be part of any other string value e.g.
This will have %{source} expression embedded inside. If we have a custom expression then:
- we create instance of SpelExpressionParser.
- parse the expression.
- invoke the parsed expression and pass in the required data.
In above case,
Line 16 delegates the evaluation call to
doEvaluate() method which does above steps. One thing abstracted here currently on
Line 16 is the
EXPRESSION_EVALUATION_CONTEXT which is instance of
EvaluationContext. We are storing this value in thread local because the
evaluate() method is called internally by Spring and there is no other way for us to pass the context.
Passing the context to Spring expression resolver
In above snippet we have seen how to add our own expression resolver. But how do we pass in the required data to the resolver? In our case we have use the expression as
%{source} saying pass in the original source input to the action instance through constructor. Following is the additional snippet for the same
BeanInstantiator class above (previous methods removed for readability).
The
BeanInstantiator class also implements the
ApplicationContextAware interface. So we get the application context passed into the
setApplicationContext() method at
Line 4.
The
BeanInstantiator class exposes the
getAction() method. This method takes the bean id of the workflow action to be instantiated. It then uses the
applicationContext.getBean() method to create instance of the action class. But how will our custom expression be resolved to inject the required value specified by the expression
%{source}.
Line 11 in
getAction() method creates an instance
rootEvalObject of anonymous class having getters getContext(), getSource(), getOutput() returning the original values.
Line 16 in
getAction() creates the instance of
StandardEvaluationContext passing the
rootEvalObject.
But
getAction() is our method and we are just calling the
applicationContext.getBean() to get the instance. Spring will internally call the
evaluate() method on our class since it also implements the
BeanExpressionResolver interface.
Line 17 sets the
StandardEvaluationContext instance to thread local variable
EXPRESSION_EVALUATION_CONTEXT and then calls
applicatinContext.getBean() to create the instance. In the
evaluate() method called by Spring we retrieve this context and then evaluate the expression.
Since we have used the getters as getContext(), getSource(), getOutput() you can refer to them as properties or getters directly in expressions e.g. as
%{source} or
%{getSource()}
You can add as many properties you want like getContext(), getOutput() etc. returning whatever values you want. You can even add methods to be used in expression e.g. to transform data, to get some configuration etc.
Workflow-lite implementation
It allows one to define & execute the workflow using UML activity diagram. Mostly useful for implementing predefined workflows in a product.