What becomes of a shared value - camunda-community-hub/vanillabp-camunda7-adapter GitHub Wiki
Your workflow aggregate shares values, and this engine has to store them. It stores most of them as they are. So
${amount > 1000} compares numbers, and Cockpit shows something an operator can read. The rest becomes an object
variable. Then a serialization format decides what a model reads. It also decides what happens to running workflows
when you rename a class.
Every adapter wiki answers the four questions below, in the same order. So you can compare two BPMS side by side: Camunda 8 and the Process-Engine-API.
-
What the platform decided before the BPMS sees anything
The flattening every adapter is handed the result of, and the shapes which are left after it. -
What this BPMS stores as itself
The variable types this engine has, and the two kinds of value which become an object variable. -
What your application can say about it
Theserialization-formatproperty, the dataformat it needs on the classpath, and where the format you chose loses something anyway. -
What it costs to configure nothing
Java serialization, what it charges for a blob in Cockpit, and the cheaper way round all of it.
This page is about the way out, from your aggregate to the engine. The way in is what a @TaskParam of your method
reads back. It is described in What a @TaskParam reads here.
When the values are handed over is described in What reaches the engine.
Sharing does not hand your objects to the engine. VanillaBP first flattens what your aggregate exposes. Enums, a
UUID and the java.time values arrive as text. Collections arrive as lists. A nested object arrives as a map of its
own readable attributes. This rule is described once, with examples, in
What is shared, and when.
It is the same for every BPMS.
So Camunda 7 only has to deal with a number, a text, a boolean, a character, a list and a map. The rest of this page says what it does with each of them.
Camunda 7 stores a short, an integer, a long, a double, a boolean, a string, a date and bytes as they are. So
${amount > 1000} compares numbers, and Cockpit shows a value. A Character is written as a string. EL (the
expression language) compares it to 'A' and to "A" in the same way.
Everything else becomes an object variable, and an object variable is stored in the serialization format the
engine is told to use. Two kinds of value end up there. One is a nested value, which is a map or a list after the
flattening above. The other is a number this engine has no type for: a BigDecimal, a BigInteger or a Float.
These keep their class and are not widened to a double. That is the value your code holds, and the value VanillaBP 1
gave your models (why).
Since VanillaBP 2, the platform checks the TYPE of every value which travels, while your application starts. Some types may not arrive as what they were. The platform refuses a value of such a type, unless you declared it. The rule and the property are on Which types may travel.
The platform asks this adapter what Camunda 7 does with a type. The answer follows the section above. A text, a
boolean, an enum and the numbers the engine has a type for survive. A BigDecimal and a BigInteger are reported
as changed. On the way out, an expression reads whatever the configured format produced. On the way back, the number
arrives as the declared type, but without the scale it was written with. For everything else the answer is "cannot
say", because the value of an object variable depends on a dataformat which VanillaBP does not own. That answer
costs a warning, never a failed start.
The same check has a second half. It is about the expressions in your models, not the values which travel.
Camunda 7 evaluates JUEL (Java Unified Expression Language). So an expression is what sits between ${ and } or
between #{ and }, and the rest of an attribute is text. R3/${firstRun}/PT${pauseHours}H has two expressions,
and PT1H has none.
The adapter reads the places where Camunda 7 lets an expression quietly decide something: the condition of a
sequence flow, the condition of a conditional event, the definition of a timer, and the cardinality, the collection
and the completion condition of a multi-instance element. It passes each one to the core, together with its element
and the text JUEL evaluates. The core says what it costs. An expression which walks a path or calls a method gets a
warning. An expression which computes something gets a note. An expression which names a single variable is what
VanillaBP recommends, and it is not quoted anywhere. A process whose expressions are all of that last kind gets no
message at all. The message asks for the pattern
Decoupling BPMN from the data model.
With accept-expressions-in-the-model you say that a process means its expressions exactly as they are written.
The check reads the model which this version of your application deploys. The engine still holds the older versions, and workflows still run on them. But the check does not read those models. A model in the engine cannot be changed any more, so a message about it would ask for work nobody can do.
Configure a serialization format, and put a Camunda dataformat on the classpath which provides it. The SPIN JSON
dataformat provides application/json. Start with that one. The second option is
camunda-xstream for application/xstream, which is XML. VanillaBP
applies the format to the engine (defaultSerializationFormat) and to the variables it writes.
The format is resolved per workflow, then per workflow module, then per adapter:
vanillabp:
adapters:
camunda7:
serialization-format: application/json
workflow-modules:
my-module:
adapters:
camunda7:
# a workflow module may deviate
serialization-format: application/xstream
workflows:
MyWorkflow:
adapters:
camunda7:
# and so may a single workflow
serialization-format: application/jsonThe format is read for the BPMN process which the values are written into. Your process service belongs to the
process you started. But a task, a waiting message or a call activity may put the values into a called process.
Then the entry of the called process counts: workflows.ShipmentProcess for a task of ShipmentProcess,
even if you call the process service of OrderProcess.
A dataformat comes as a Camunda engine plugin, and an engine which VanillaBP builds has to be told about it. Name
the plugin's class and its properties per adapter id. Camunda applies those properties itself, with the same code
which reads the <property> elements of a bpm-platform.xml. So they are converted to the types the plugin
declares. The keys are kebab-case, as everywhere in this configuration (allowed-types sets allowedTypes). The
plugin's documentation says which keys exist:
vanillabp:
adapters:
camunda7:
serialization-format: application/json
engine-plugins:
spin: # a name you choose
plugin-class: org.camunda.spin.plugin.impl.SpinProcessEnginePluginThe SPIN JSON dataformat needs no properties at all. The plugin class and the dependency on the classpath are enough. A plugin which does take properties is configured the same way:
vanillabp:
adapters:
camunda7:
serialization-format: application/xstream
engine-plugins:
xstream:
plugin-class: org.camunda.xstream.ProcessEnginePlugin
properties:
encoding: UTF-8
allowed-types: my.project.**,other.project.** # example, see belowConfigure the plugin here, not through whatever mechanism it brings along. This is the one place which works the same on Spring Boot and on Quarkus. And it works per adapter id. A side-by-side migration needs that, because each of the two embedded engines has the plugins its models require.
allowed-types is not optional decoration. XStream refuses to deserialize types it was not told about. So an
expression which reads a nested value fails until the aggregate's types are listed. List them as full class names,
type hierarchies (prefix <) or wildcards, as the plugin documents.
If a plugin needs more than a constructor without arguments, provide it as a bean instead. Every
ProcessEnginePlugin bean of the application is applied to every engine this adapter builds.
@Produces // Quarkus; a @Bean method on Spring Boot
@Singleton
ProcessEnginePlugin somePlugin() {
return new SomePlugin(somethingItNeeds);
}A format decides what your value reads as, and it may change the type. JSON has one number type. So a BigDecimal
of 120.50 is written as 120.5 and comes back as 120.5. One level down it loses more than a digit. Nothing in
the map records what its members were. So order.total comes back as a Double, and ${order.total.scale()}
raises an incident, where Java serialization gave an answer. A comparison works either way, because EL converts
both sides of ${total > 100} to BigDecimal. But a rendered value and a comparison for equality are affected.
You do not have to find that out in production. While your application starts, the adapter writes a sample of the type through the engine's own serializer for the format you configured, and reads it back. If the two differ, it warns once per attribute your models read. The warning names the attribute, the format and what the engine answered:
Camunda7[c7]: BPMN process 'LoanApproval' of workflow module 'loans' shares 'total' as a java.math.BigDecimal, and
the serialization format 'application/json' configured for it cannot carry that type without loss: the engine reads
a value of 120.50 back as 120.5. ...
It measures instead of reading from a list of types, because the dataformat is yours. If you register a JSON dataformat which keeps the scale, the warning goes away.
Without a format, the engine uses its own default, Java serialization. That is a bad place to end up (why):
- Cockpit shows a base64 blob instead of the data. So nobody who operates the workflows can read what a decision was based on.
- The engine's database then holds serialized instances of YOUR classes. If you rename an attribute, move a class
or change a
serialVersionUID, the running workflows hold values the new code cannot read any more. - An expression which navigates the value (
${order.customer.name}) only works while the class in the database and the class on the classpath still match.
The adapter warns once when it writes such a value while no format is configured. The round-trip check of the previous section stays silent then, because Java serialization keeps every one of these types exactly. The cost is the blob above.
The cheapest way around all of this is to share less. A workflow aggregate shares the results a model decides on and
the values an operator reads. It does not share everything a business object happens to hold. If your models never
read an attribute, one @NoSyncWithBPMS makes it nobody's problem.
