Это вторая часть статьи, в которой мы продолжим разбираться с процессными переменными. Первую часть читайте здесь.
Области видимости
Подобно обычным переменным, процессные переменные имеют области видимости (scope). Но эта концепция работает в BPM совсем не так, как в языках программирования. Просто примите это как факт и не полагайтесь на интуицию — она легко может подвести.
В программировании область видимости переменной определяется лексически — в зависимости от того, в каком классе, методе или блоке она объявлена. При этом не важно, используется ли статическая типизация, как в Java, или динамическая, как в Python.
Процессные переменные — это нечто совершенно иное, как говорили Монти Пайтон. По сути, это не переменные, а объекты, которые существуют в runtime. Следовательно, лексический подход здесь не действует. И хотя вы можете объявить переменные в BPMN-модели, это не настоящая декларация, как в программировании. Это скорее описание намерений — движок не обязывает эти переменные существовать, пока они реально не будут установлены.
Например, в Jmix BPM есть возможность определить процессные переменные в стартовом событии. Подобное объявление полезно с целью документирования, чтобы каждый, кто читает модель, понимал, какие переменные в ней используются. И если процесс должен запускаться программно, явное перечисление переменных помогает разработчику понять, какие параметры нужны для запуска.

Но от этого они сами собой в процессе не появятся. Либо они должны быть переданы как параметры, либо на каком-то из следующих шагов созданы. В противном случае при попытке обращения к ним система выдаст ошибку, что такой объект не существует.
Как мы уже разбирали в первой части этой статьи, процессные переменные создаются в результате вызова метода setVariable
. А их область видимости определяется местом их “рождения”, почти как гражданство, — то есть контекстом выполнения (execution), в котором они созданы.
Когда запускается экземпляр процесса, движок создаёт корневой контекст выполнения (process instance). Затем по мере продвижения по процессу, эти контексты образуют иерархическую структуру. Например, когда на пути встречается вложенный (embedded) подпроцесс, параллельный шлюз, асинхронная задача и так далее, то образуется новый контекст выполнения. Далее, появляются новые контексты — дочерние по отношению к предыдущим. Таким образом, execution-контексты образуют дерево.
Соответственно, область видимости переменных определяется положением этого контекста в дереве. Переменные, созданные в верхнем контексте, видны на всех нижележащих уровнях.
Возьмем для примера процесс и обозначим все его контексты выполнения:

Тогда дерево контекстов выполнения будет выглядеть вот так:

То есть, если мы определим переменную orderId
на верхнем уровне процесса, она будет доступна везде. А переменная discount
, если она будет установлена в первой параллельной ветке, не будет видна нигде, кроме своего execution, и, если она вам понадобится позже, обратится к ней не получится. Так что надо планировать объявление переменных с учетом областей видимости.
Вложенный подпроцесс не только позволяет структурировать логику выполнения, но еще и создает новую область видимости (scope), и это может быть даже его более важное свойство.
Отдельная история с внешними подпроцессами (call activity). Каждый такой вызов оборачивается в отдельный контекст выполнения. Поэтому во второй параллельной ветке мы видим еще один вложенный execution. Но сам внешний подпроцесс выполняется вообще, как отдельный экземпляр и по умолчанию не видит переменных родительского процесса. Их надо явно замэппить, чтобы передать в дочерний процесс. И аналогичным образом получить обратно, если это нужно.

Если у вас есть событийные подпроцессы, то каждый из них живет в своем execution и ждет, когда его активируют. Никаких особых хитростей тут нет — видны все переменные уровня процесса и свои.
Когда на пути встречается многоэкземплярная задача (multi-instance task), то сначала создается общий контекст выполнения (в нашем примере – execution 2), который отслеживает все экземпляры. А потом для каждого экземпляра создается еще и свой отдельный контекст. И здесь встречается довольно частая ошибка, когда из конкретного экземпляра пытаются записать значение переменной верхнего уровня, например, при параллельном согласовании документа. И получается, что все согласующие записывают свое решение в оду и ту же переменную, а в результате мы видим лишь мнение последнего участника согласования. Главное здесь — не запутаться во всех этих переменных, которые зачастую называются одинаково.
Локальные переменные
Если задать переменную обычным способом, она будет доступна всем дочерним узлам. Но если использовать метод setVariableLocal
, она «запрётся» в рамках текущего execution и не будет видна снаружи, в том числе и в нижележащих контекстах.
Хорошо. Но зачем может понадобиться гарантированно не передавать переменную вниз по иерархии?
На самом деле это не главная цель. Локальные переменные применяются для того, чтобы “навести порядок в процессе” — если она явно объявлена локальной, это значит вы точно не затрете значение какой-то вышестоящей переменной с тем же именем.
Если вернуться к нашему примеру с согласованием: когда мы в каждом экземпляре мульти-инстанс подпроцесса явно указываем, что поле для комментария и решение по документу это локальные переменные, то меньше вероятность, что возникнет какая-то путаница.
В общем, локальные переменные — это механизм изоляции и предотвращения ошибок, а не функциональная необходимость. Они не решают задач, которые нельзя было бы реализовать иначе, но делают это безопаснее и чище.
Затенение переменных
А что, если переменные с одинаковыми именами?
Такое бывает — вы создаёте переменную в дочернем контексте с тем же именем, что уже есть в родительском. В этом случае происходит затенение переменной (shadowing) — то есть становится недоступной в текущем контексте, даже если она всё ещё существует в дереве контекстов выполнения.
Как это работает
Каждый execution-контекст содержит собственный набор переменных. При обращении к переменной через метод getVariable(String name)
, движок сначала ищет её в текущем execution. Если переменная найдена — используется она, даже если переменная с таким же именем есть на уровне выше. Таким образом, переменная верхнего уровня «затеняется».
// Где-то в начале процесса
execution.setVariable("status", "CREATED");
// Внутри задачи или подпроцесса:
execution.setVariableLocal("status", "PROCESSING");
// Что увидит скрипт или сервис в этом execution?
String currentStatus = (String) execution.getVariable("status"); // "PROCESSING"
Несмотря на то, что родительская переменная всё ещё существует, дочерняя перекрывает её в рамках текущего execution. Как только вы выйдете из области действия дочернего контекста (например, закончится подпроцесс), снова станет доступна переменная верхнего уровня.
Затенение переменных может быть полезным при правильном использовании, но также представляет собой потенциальный источник ошибок. В некоторых сценариях оно даёт преимущество — например, позволяет временно переопределить значение переменной без изменения её оригинала. Это особенно удобно в мультиинстанс-конструкциях, где каждый экземпляр работает со своей копией данных.
Однако затенение способно привести к неожиданным результатам, если вы не уверены, в каком именно контексте находитесь. При этом отладка становится сложнее: в истории вы можете видеть переменную с тем же именем, но не всегда понятно, на каком уровне она была создана и почему её значение отличается.
Чтобы избежать подобных проблем, стоит придерживаться ряда рекомендаций. Лучше использовать разные имена для переменных, если они несут разный смысл или принадлежат разным уровням исполнения. Также важно осознанно управлять контекстом, в котором создаются переменные, и по возможности избегать setVariableLocal
, если в этом нет явной необходимости. При анализе состояния процесса полезно отдельно проверять переменные текущего и родительского уровней с помощью getVariableLocal()
и getParent().getVariable()
, чтобы получить полную картину.
Типы процессных переменных
Что касается типов переменных, их разнообразие зависит от разработчика движка — как мы уже говорили, в спецификации это не определено, поэтому каждый творит кто во что горазд. Конечно, есть общий джентльменский набор, который включает примитивные типы — строки, числа, логические значения, а также даты. Но даже здесь есть отличия — сравните две картинки и увидите сами.
В Camunda у нас один набор типов данных, а в Jmix BPM (с движком Flowable) — несколько иной.

В части базовых типов эта разница несущественна и может проявиться разве что при миграции с одного движка на другой. Но есть интересные различия, про которые стоит сказать.

Наверное, вы обратили внимание, что Camunda поддерживает типы JSON и XML. Но это не встроенная фича движка, чтобы работать с ними вам потребуется специальная библиотека Camunda Spin, предназначенная для работы со структурированными данными внутри бизнес-процессов. Она обеспечивает удобный API для парсинга, навигации и изменения данных, а также автоматически сериализует и десериализует данные при передаче между задачами. Это может быть особенно удобно при обработке ответов от REST-сервисов, хранения сложных структур как переменных и формирования документов XML/JSON.
В свою очередь, Jmix BPM позволяет использовать в качестве процессных переменных элементы модели данных платформы Jmix — сущности (Entity), списки сущностей (Entity list) и перечисляемые типы (Enum). Это особенно помогает, когда в процессе вам нужно манипулировать сложными бизнес-объектами, содержащими десятки атрибутов. Например, заявки, договора, тикеты техподдержки и тому подобное.
Entity Data task — доступ из процесса к модели данных
В Jmix BPM есть специальный тип задач — Entity Data task. С его помощью вы прямо из процесса можете создавать новые экземпляры сущностей, модифицировать их и загружать в процессные переменные отдельные экземпляры или коллекции сущностей, полученные в результате выполнения JPQL-запросов. Причем это не является каким-то расширением нотации BPMN. Технически это обычные сервисные задачи, но со специфическим набором параметров.
Таким образом, вы можете моделировать процесс в стиле лоукод — у вас есть User task для действий пользователей и Entity Data task для манипуляций с данными. И если не требуется каких-то интеграций или сложной логики, то этого вполне достаточно.
Рассмотрим некий условный пример. Предположим, нам поступили данные. Система проверяет, относятся ли они к существующему заказу клиента или нет. В зависимости от этого, выполняется одна, либо другая задача — создать новую сущность Order
или загрузить или найти имеющуюся. Далее, сотрудник выполняет какую-то работу, а Entity Data task обновляет измененные атрибуты сущности.

Конечно, реальный процесс сложнее, эта схема лишь иллюстрирует идею, как можно использовать Entity Data task для работы с данными.
Ограничения
В этом разделе рассмотрим ограничения процессных переменных в разных BPM-продуктах.
Camunda
Ограничение длины строки
В Camunda значения типа String
хранятся в базе данных в колонке типа varchar(n)
с ограничением длины 4000 символов (2000 — для Oracle). В зависимости от используемой базы данных и настроенной кодировки, это ограничение может соответствовать разному количеству реальных символов. Движок Camunda не выполняет валидацию длины строки — значения передаются в базу данных "как есть". Если превышено допустимое значение, то произойдёт ошибка на уровне базы данных. То есть, контроль за длиной строковых переменных лежит на ответственности разработчика.
Ограничение на размер контекста
Хотя переменные процесса хранятся в отдельной таблице базы данных, на практике существует ограничение на общий размер данных, связанных с экземпляром процесса. Это ограничение связано не столько с физическим хранилищем, сколько с механизмами сериализации, загрузки в память, транзакционной обработки и другими внутренними аспектами работы движка процессов. Обычно безопасным пределом считается около 3–4 МБ на весь контекст процесса. Это включает сериализованные переменные, внутренние ссылки, события и другую служебную информацию. Точное значение зависит от конкретной СУБД, типа сериализации и конфигурации движка.
Если хранить много переменных или большие документы в контексте процесса, то можно неожиданно натолкнуться на ProcessEngineException
из-за превышения допустимого размера сериализованного объекта. Поэтому при работе с крупными переменными рекомендуется не приближаться к этому пределу и при необходимости проводить нагрузочное тестирование.
В Camunda 8 вообще существует жёсткое ограничение на размер данных (payload), которые можно передать в экземпляр процесса. Максимальный размер всех переменных процесса составляет 4 МБ, включая внутренние данные движка. Однако, учитывая накладные расходы, безопасным пределом считается около 3 МБ.
Flowable/Jmix BPM
Ограничение длины строки
Flowable работает со строками несколько иначе: если длина переменной типа String
превышает 4000 символов, то она автоматически получает внутренний тип longString
и тогда ее значение в виде байтового массива записывается в таблицу ACT_GE_BYTEARRAY
, а в ACT_RE_VARIABLE
хранится ссылка на него. Поэтому явных ограничений да длину строк у Flowable нет.
То есть, теоретически длина строковой переменной ограничена только длиной переменной в Java, где теоретический максимум составляет
Integer.MAX_VALUE = 2_147_483_647
(примерно 2.1 миллиарда символов).
Однако, в реальности всё ограничивается доступной оперативной памятью (heap).
Ограничение на размер списка сущностей
Для переменных типа Entity list
тоже есть ограничение, связанное со способом их хранения. При записи в БД они сериализуются в строку формата
<имя класса сущности>;.”UUID”
,
например так: jal_User."60885987-1b61-4247-94c7-dff348347f93"
А записывается эта строка в текстовое поле с предельной длиной 4000 символов. В итоге получается, что максимальная длина списка может быть порядка 80 элементов. Но это ограничение проявляет себя только в момент записи в БД, в памяти списки могут быть любого размера. То есть, вы можете использовать такие переменные либо в границах транзакции, либо объявлять их транзиентными — и тогда проблем с длинными списками не возникнет.
Ограничение на размер контекста
У Flowable (и Jmix BPM, соответственно) нет жёстко заданного ограничения на размер контекста процесса, но все равно рекомендуется контекст минимизировать, потому что большой объем процессных переменных негативно сказывается на производительности. Или можно упереться в ограничения самой СУБД.
Процессные переменные в Groovy-скриптах
Скрипты в BPMN часто недооценивают, а это очень мощный инструмент, особенно для быстрых, легковесных действий прямо внутри процесса. Их используют, чтобы инициализировать переменные, выполнить простые вычисления, записать сообщение в лог и так далее. В качестве скриптового языка в BPM-движках обычно применяется Groovу или JavaScript. Но больше все-таки Groovy, потому что он позволяет писать лаконичный код, использовать Java-библиотеки и легко работать с объектами процесса. Самый полезный из них это объект execution, который содержит контекст процесса и позволяет манипулировать и процессными переменными.
Две наши рабочие лошадки — это уже упомянутые выше методы setVariable
и getVariable
, используемые для записи и чтения процессных переменных. Однако, есть одна особенность, на первый взгляд очень удобная, но она может стать источником ошибок, которые трудно диагностировать. Дело в том, что процессные переменные доступны в скриптах просто по имени. То есть, вам необязательно сначала читать значение переменной, и только потом использовать. Можно сразу включать ее в выражение:
amount = price * quantity
Однако, есть нюансы: оператор присваивания на процессные переменные не действует, так что значение переменной amount
после выполнения этого выражения не изменится. Чтобы присвоить ей новое значение, обязательно нужно вызвать метод setVariable
, например, так:
execution.setVariable("amount", price * quantity)
Все потому, что процессная переменная — это сущность, и с ней надо обращаться особым образом.
Добавляет путаницы наличие собственно скриптовых переменных. Поскольку Groovy это динамический язык, переменные в нем создаются автоматически даже без объявления. Так что если вы не создали процессную переменную amount
, выражение ниже будет вполне валидным, Groovy создаст для вас скриптовую переменную, которую затем можно сохранить в контекст процесса:
amount = price * quantity
execution.setVariable("amount", amount)
И это еще не все! Хотя язык этого не требует, переменную в скрипте лучше объявить явно, это считается лучшей практикой предотвращения ошибок. Затем можете ее сохранить, если нужно:
def amount = price * quantity
. . .
execution.setVariable("amount", amount)Вот теперь полный порядок! — Точно? — Не совсем.
Когда скриптовая задача отмечена как асинхронная, то эта простота доступа к процессным переменным по имени без вызова соответствующего метода может сыграть с вами злую шутку. Например, вполне безобидное выражение для инкремента счетчика
Вот теперь полный порядок! — Точно? — Не совсем.
Когда скриптовая задача отмечена как асинхронная, то эта простота доступа к процессным переменным по имени без вызова соответствующего метода может сыграть с вами злую шутку. Например, вполне безобидное выражение для инкремента счетчика
execution.setVariable("counter", counter + 1L)
может внезапно выбросить ошибку
groovy.lang.MissingPropertyException: No such property: counter for class: Script2
Это означает, что в момент выполнения выражения переменная counter недоступна в контексте Groovy-скрипта. Во время выполнения, Flowable (и другие BPM движки) автоматически прокидывают процессные переменные в скрипт, но с асинхронными задачам все работает по-другому и возможны ситуации, когда движок не успевает это сделать до выполнения скрипта.
Подстраховаться от этого можно если явно выполнить чтение процессных переменных в скрипте:
def counter = execution.getVariable("counter")
execution.setVariable("counter", counter + 1L)
И тогда никаких исключений не вылетит!
Лучшие практики
Работа с процессными переменными — важный аспект построения надёжных бизнес-процессов. Именно через переменные процесс "помнит" данные, передаёт их между задачами и принимает решения. Однако без дисциплины в их использовании переменные легко становятся источником ошибок: от трудноуловимых багов до критических сбоев. В этом разделе мы собрали проверенные практики, которые помогут избежать типичных проблем и выстроить чистую, предсказуемую модель работы с данными в процессе.
1. Следите за именами переменных
Выбирайте осмысленные и уникальные имена переменных. Избегайте дублирования. Хороший пример: orderApprovalStatus
, плохой пример: status
.
2. Чем меньше переменных, тем лучше
Нужно постоянно помнить, что контекст процесса не резиновый и не плодить переменные без необходимости. Даже если нет строгих ограничений на размер, разрастание контекста снижает производительность.
Особенно стоит избегать огромных переменных — больших JSON’ов или документов. Лучше выгрузить их в файл, а в процессе иметь только ссылку на него.
3. Используйте transient-переменные для временных данных
Когда данные нужны только внутри одного действия или выражения, задавайте их как transient. Они не сохраняются в БД и не попадают в историю.
4. Аккуратнее с переменными-сущностями
BPM-платформы позволяют использовать сложные объекты в качестве переменных. Да, это удобно — можно манипулировать отдельными атрибутами сущностей в бизнес-логике.
Однако, если вы загрузили сущность в переменную на старте процесса, а потом прошло несколько дней и вы показываете ее пользователю через процессную форму, можно ли быть уверенным, что сущность не изменилась? — Конечно, нет!
Поэтому в переменной лучше хранить ID этой сущности и считывать ее всякий раз, когда она нужна.
5. Сериализация может подкинуть сюрпризы
При сохранении сложных объектов, движок выполняет сериализацию, прежде чем отправить данные в БД. И это тонкий момент. Нельзя взять и положить в процессную переменную любой Java-объект: сохраняемый объект должен либо реализовывать интерфейс Serializable
, либо вы можете написать свой сериализатор и зарегистрировать его в движке.
Также, сериализация может работать по-разному в обычных задачах и в асинхронных. Потому что асинхронные задачи выполняет JobExecutor, а у него своя кухня, и то, что сериализуется нормально самим движком, может дать сбой в асинхронном режиме.
6. Свяжите сущность с процессом
Часто бывает, что процесс запускается по какой-то сущности — заказу, документу, обращению клиента и так далее. И все время требуется со стороны сущности узнать, как там дела у процесса. Например, чтобы из карточки заказа быстро перейти в процесс или даже к задаче пользователя.
Вот простой лайфхак: добавьте в сущность поле processInstanceId
и будет вам счастье: больше не надо искать нужный процесс сложными запросами через API.
7. Не передавайте в Сall Activity все подряд
Конечно, соблазнительно нажать один чек-бокс и передать все переменные вызывающего процесса в подпроцесс. Но лучше явно настроить маппинг и передавать только нужные данные. Иначе это может привести к утечке данных или ошибкам сериализации.
8. Настройте историю
Чем больше переменных, тем быстрее растут таблицы с историческими данными. BPM-движки имеют разные механизмы для очистки истории, просто надо взять и настроить.
А если вам таки нужно хранить эти данные, разработайте механизм их выгрузки из процессной базы в свое хранилище. Вариантов тут множество, трудно советовать какой-то конкретный.
Еще один момент: следите, чтобы чувствительные данные не попадали в историю — логины-пароли к разным сервисам, ключи и так далее. Они бывают нужны по ходу процесса, а потом скидываются в историю вместе со всеми переменными. Но если в процессе мы строго следим за безопасностью, то доступ к историческим данным может контролироваться слабее.
В общем, от греха подальше такие переменные лучше не сливать в историю.
9. Используйте локальные переменные, где это уместно
Как мы обсудили, технически можно обойтись без локальных переменных. Однако, они помогают навести порядок. Если переменная явно объявлена локальной, меньше вероятность ошибки.
Но и усердствовать не стоит: необязательно все переменные в дочерних контекстах объявлять локальными. Иногда нужно, чтобы они протекали на нижние уровни.
10. Не используйте без надобности затенение переменных
Основной кейс, где этот механизм применяется — это мульти-инстанс активности. Там это действительно помогает. В остальных случаях лучше давать переменным неперекрывающиеся имена.
11. Документируйте переменные
BPM-движки не управляют переменными, в IDE тоже нет для этого инструментов. Заведите свой реестр переменных, описывайте их назначение и где они используются. Это поможет поддерживать процесс в дальнейшем.
Подписывайтесь на наш телеграм-канал
BPM Developers — про бизнес-процессы: новости, гайды, полезная информация и юмор.