Storing Activiti process variables as JSON text

By default Activiti will use Java serialization to store non-primitive process variables in the database.
This is nice and mostly works but has a few disadvantages:

  • You have to be careful about your serialVersionUID fields on the object (you need it).
  • It is not refactor friendly. Change a class or field name creates havoc.
  • You cannot use a database query to search on property value.

Here is some code to use JSON as serialization format. The result is put in the text_ field to allow database searches. However, this does limit the maximum length of the string (you can increase the field length if you want – by default Activiti defines it as 4000 characters).

You have to create a VariableType implementation and pass that to your configuration. When initializing using Spring, this can be done using something like:

<bean id="processEngineConfiguration">
    <!-- your usual configuration -->
 
    <property name="customPreVariableTypes">
        <list>
            <bean class="mypackage.SampleAsJsontype" />
            <bean class="mypackage.SampleListAsJsontype" />
            <bean class="mypackage.SerializeAsJsonType" />
        </list>
    </property>
 
</bean>

VariableType for a specific object type.

public class SampleAsJsontype implements VariableType {
 
    private static final Logger LOG = LoggerFactory.getLogger(SampleAsJsontype.class);
 
    private DcJsonMapper mapper = new DcJsonMapper();
 
    @Override
    public String getTypeName() {
        return "sample";
    }
 
    @Override
    public boolean isCachable() {
        return true;
    }
 
    @Override
    public Object getValue(ValueFields valueFields) {
        try {
            return mapper.readValue(valueFields.getTextValue(), Sample.class);
        } catch (IOException ioe) {
            LOG.error("Kan object niet converteren naar Sample: " + valueFields.getTextValue(), ioe);
            return null;
        }
    }
 
    @Override
    public void setValue(Object value, ValueFields valueFields) {
        if (null == value) {
            valueFields.setTextValue(""); // needed to allow removing variables
        } else {
            try {
                valueFields.setTextValue(mapper.writeValueAsString(value));
            } catch (IOException ioe) {
                LOG.error("Kan Sample niet converteren o(JSON) string: " + value, ioe);
            }
        }
    }
 
    @Override
    public boolean isAbleToStore(Object value) {
        return value instanceof Sample;
    }
 
}

When it is a list of this type you can use something like:

public class SampleListAsJsontype implements VariableType {
 
    private static final Logger LOG = LoggerFactory.getLogger(SampleListAsJsontype.class);
 
    private DcJsonMapper mapper = new DcJsonMapper();
 
    @Override
    public String getTypeName() {
        return "sampleList";
    }
 
    @Override
    public boolean isCachable() {
        return true;
    }
 
    @Override
    public Object getValue(ValueFields valueFields) {
        try {
            return mapper.readValue(valueFields.getTextValue(), new TypeReference<List<Sample>>() { });
        } catch (IOException ioe) {
            LOG.error("Kan object niet converteren naar Sample: " + valueFields.getTextValue(), ioe);
            return null;
        }
    }
 
    @Override
    public void setValue(Object value, ValueFields valueFields) {
        if (null == value) {
            valueFields.setTextValue(""); // needed to allow removing variables
        } else {
            try {
                valueFields.setTextValue(mapper.writeValueAsString(value));
            } catch (IOException ioe) {
                LOG.error("Kan Sample niet converteren o(JSON) string: " + value, ioe);
            }
        }
    }
 
    @Override
    public boolean isAbleToStore(Object value) {
        return value instanceof Collection && containsAllLijnLocatie((Collection) value);
    }
 
    private boolean containsAllLijnLocatie(Collection collection) {
        for (Object object : collection) {
            if (!(object instanceof Sample)) {
                return false;
            }
        }
        return true;
    }
 
}

Much more practical is a generic solution which converts many object. Here is the marker interface to trigger the serialization.

public interface SerializeAsJson {
}

In this case, the JSON data is prepended with the Java type to assure the JSON mapper knows what to do. This code also assures the length of the string fits the field.

public class SerializeAsJsonType implements VariableType {
 
    private static final Logger LOG = LoggerFactory.getLogger(SerializeAsJsonType.class);
 
    private static final String SEPARATOR = "=>";
    private static final int SERIALIZED_MAX_LENGTH = 44000; // Activiti default field length
 
    private DcJsonMapper mapper = new DcJsonMapper();
 
    @Override
    public String getTypeName() {
        return "serializeAsJson";
    }
 
    @Override
    public boolean isCachable() {
        return true;
    }
 
    @Override
    public Object getValue(ValueFields valueFields) {
        try {
            String value = valueFields.getTextValue();
            int pos = value.indexOf(SEPARATOR);
            if (pos < 1) {
                throw new IllegalArgumentException("Value for serializeAsJson does not contain type indicator.");
            }
            String className = value.substring(0, pos);
            String json = value.substring(pos + SEPARATOR.length());
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            if (null == classLoader) {
                classLoader = this.getClass().getClassLoader();
            }
            try {
                Class clazz = classLoader.loadClass(className);
                return mapper.readValue(json, clazz);
            } catch (ClassNotFoundException cnfe) {
                throw new IllegalArgumentException("Cannot find class  " + className + ".", cnfe);
            }
        } catch (IOException ioe) {
            LOG.error("Cannot convert JSON to object: " + valueFields.getTextValue(), ioe);
            return null;
        }
    }
 
    @Override
    public void setValue(Object value, ValueFields valueFields) {
        if (null == value) {
            valueFields.setTextValue(""); // needed to allow removing variables
        } else {
            try {
                String serialized = value.getClass().getName() + SEPARATOR + mapper.writeValueAsString(value);
                if (serialized.length() > SERIALIZED_MAX_LENGTH) {
                    throw new IllegalArgumentException("Serialized value for object of type " +
                            value.getClass().getName() + " is too long to store as JSON object.");
                }
                valueFields.setTextValue(serialized);
            } catch (IOException ioe) {
                LOG.error("Cannot convert " + value.getClass().getName() + " to (JSON) string: " + value, ioe);
            }
        }
    }
 
    @Override
    public boolean isAbleToStore(Object value) {
        return value instanceof SerializeAsJson;
    }
 
}

If you want the JSON length limit to be increased you can alter the SERIALIZED_MAX_LENGTH constant in the code above and increase the field size in your database using something like (PostgreSQL sample):

ALTER TABLE act_ru_variable ALTER text_ TYPE VARCHAR(16000);
ALTER TABLE act_hi_varinst ALTER text_ TYPE VARCHAR(16000);
ALTER TABLE act_hi_detail ALTER text_ TYPE VARCHAR(16000);

If you already have process variables in your database which did not use these serializers, then you will need some migration code.
The bg trick is to delete the process variable before storing it again. If you don’t delete it first Activiti will try to store the updated value using the same serializer (if that serializer can store the updated value).

This code handles the migration when the system starts. It is implemented as a Spring service.

@Component
public class MigrateProcessVariables {
 
    @Autowired
    private TaskService taskService;
 
    /**
     * Migrate existing variables to store again using the JSON serializers.
     */
    @PostConstruct
    public void fixSerializedVariables() {
        List<Task> tasks = taskService.createTaskQuery().list();
        for (Task task : tasks) {
            Map<String, Object> vars = taskService.getVariables(task.getId());
            for (Map.Entry<String, Object> entry : vars.entrySet()) {
                Object value = entry.getValue();
                if (value instanceof Sample || value instanceof SerializeAsJson) {
                    taskService.removeVariable(task.getId(), entry.getKey());
                    taskService.setVariable(task.getId(), entry.getKey(), value);
                }
                if (value instanceof List) {
                    // convert possible List<Sample> objects
                    List list = (List) value;
                    boolean changed = list.size() > 0;
                    for (int i = 0; i < list.size(); i++) {
                        Object item = list.get(i);
                        if (!(item instanceof Sample)) {
                            changed = false;
                        }
                    }
                    if (changed) {
                        taskService.removeVariable(task.getId(), entry.getKey());
                        taskService.setVariable(task.getId(), entry.getKey(), list);
                    }
                }
            }
        }
    }
 
}

Leave a Reply

Your email address will not be published. Required fields are marked *

question razz sad evil exclaim smile redface biggrin surprised eek confused cool lol mad twisted rolleyes wink idea arrow neutral cry mrgreen

*