Код делегации
|
Этот раздел перенесён из документации Camunda 7 и в дальнейшем будет доработан с учётом особенностей OpenBPM Engine |
Код делегации (Delegation Code) позволяет вам выполнять внешний код на Java, скрипты или вычислять выражения при определенных событиях, случающихся при выполнении процесса.
Существуют разные типы делегации кода:
-
Java Delegates могут быть прикреплены к сервисной задаче BPMN.
-
Delegate Variable Mapping (делегация маппинга переменной) может быть прикреплена к активности вызова.
-
Execution Listeners (слушатели выполнения) могут быть прикреплены к любому событию в пределах нормального потока токенов, например, к запуску экземпляра процесса или к входу в активность.
-
Task Listeners (слушатели задач) могут быть прикреплены к событиям в пределах цикла жизни пользовательской задачи, например, к созданию или завершению пользовательской задачи.
Вы можете создать generic код делегации и сконфигурировать его через BPMN 2.0 XML, используя так называемый Field Injection.
Java-делегация
Чтобы реализовать класс, который может быть вызван во время выполнения процесса, этот класс должен реализовывать интерфейс io.openbpm.bpm.engine.delegate.JavaDelegate и предоставлять требуемую логику в методе execute. Когда выполнение процесса доходит до этого конкретного шага, он выполнит логику, заданную в этом методе, и выйдет из активности способом по умолчанию для BPMN 2.0.
В качестве примера давайте создадим Java-класс, который можно использовать для изменения значения процессной переменной типа стринг на верхний регистр. Этот класс должен реализовывать интерфейс io.openbpm.bpm.engine.delegate.JavaDelegate, что требует, чтобы мы реализовали метод execute(DelegateExecution). Эта операция будет вызываться движком и должна содержать бизнес-логику. Информация по экземпляру процесса, такая как процессные переменные и другая информация будет доступна для просмотра и манипуляций через интерфейс {{< javadocref page="org/camunda/bpm/engine/delegate/DelegateExecution.html" text="DelegateExecution" >}} (нажмите на ссылку, чтобы получить детальный Javadoc по работе с ним).
public class ToUppercase implements JavaDelegate {
public void execute(DelegateExecution execution) throws Exception {
String var = (String) execution.getVariable("input");
var = var.toUpperCase();
execution.setVariable("input", var);
}
}
|
Каждый раз, когда выполняется класс делегации, ссылающийся на активность, создается отдельный экземпляр этого класса. Это означает, что каждый раз, когда выполняется активность, для вызова |
Классы, на которые идет ссылка из определения процесса (с использованием camunda:class) НЕ инстанциируются во время деплоймента. Только когда выполнение процесса достигает точки в процессе, где класс используется в первый раз, будет создаваться экземпляр этого класса. Если класс не найден, будет выбрасываться исключение типа ProcessEngineException. Причина такого поведения заключается в том, что окружение (а точнее, его classpath) во время деплоймента часто отличается от того, что мы увидим в реальном рантайм-окружении.
Поведение активности
Вместо написания Java Delegate, можно также предоставить класс, который реализует интерфейс io.openbpm.bpm.engine.impl.pvm.delegate.ActivityBehavior. Эти реализации могут затем получить доступ к более мощному ActivityExecution, который, например, также позволяет влиять на управление течением процесса. Однако, обратите внимание на то, что это не очень хорошая практика, и ее следует по возможности избегать. Поэтому рекумендуется использовать интерфейс ActivityBehavior только для самых сложных случаев и только если вы точно знаете, что делаете.
Инъекция полей (Field Injection)
Существует возможность внедрять значения в поля делегируемых классов. Поддерживаются следующие формы инъекции:
-
Фиксированные строковые значения
-
Выражения
При наличии публичного setter-метода в делегированном классе, значение моет инъектироваться через него, следуя соглашениям по именования для Java-бинов (например, поле firstName имеет setter-метод setFirstName(…)). Если такой метод недоступен для этого поля, значение приватного поля будат установлено на делегате (но использование приватных полей не рекомендуется — см. предупреждение ниже).
Независимо от типа значения, задекларированного в process-definition, тип setter/приватного поля, являющегося объектом инъекции, должен всегда быть io.openbpm.bpm.engine.delegate.Expression.
|
Приватные поля можно модифицировать не всегда! Это не работает, например, с CDI-бинами (потому что вы работаете с прокси, а не с реальными объектами) или с некоторыми |
Следующий отрывок кода показывает, как внедрить значение константы в поле. Инъекция полей поддерживается при использовании атрибута class или delegateExpression. Обратите внимание, что нам необходимо задекларировать XML-элемент extensionElements до того, как реальзо выполнять инъекцию поля, что является требованием XML-схемы стандарта BPMN 2.0.
<serviceTask id="javaService"
name="Java service invocation"
camunda:class="io.openbpm.bpm.examples.bpmn.servicetask.ToUpperCaseFieldInjected">
<extensionElements>
<camunda:field name="text" stringValue="Hello World" />
</extensionElements>
</serviceTask>
Класс ToUpperCaseFieldInjected имеет поле text типа io.openbpm.bpm.engine.delegate.Expression. При вызове text.getValue(execution), будет возвращено сконфигурированное значение строки Hello World.
Альтернативно для длинных текстов (например, для текста емейла в одну строку) может использоваться суб-элемент camunda:string :
<serviceTask id="javaService"
name="Java service invocation"
camunda:class="io.openbpm.bpm.examples.bpmn.servicetask.ToUpperCaseFieldInjected">
<extensionElements>
<camunda:field name="text">
<camunda:string>
Hello World
</camunda:string>
</camunda:field>
</extensionElements>
</serviceTask>
Для инъекции значений, которые динамически вычисляются в рантайме, можно использовать выражения. Эти выражения могут использовать процессные переменные, CDI или Spring-бины. Как уже отмечалось, каждый раз, когда задача выполняется, будет создаваться отдельный экземпляр Java-класса. Для динамической инъекции значений в поля вы можете внедрить значение полей и выражения метода в io.openbpm.bpm.engine.delegate.Expression, которое можно рассчитать или вызвать с использованием DelegateExecution, переданного через метод execute.
<serviceTask id="javaService" name="Java service invocation"
camunda:class="io.openbpm.bpm.examples.bpmn.servicetask.ReverseStringsFieldInjected">
<extensionElements>
<camunda:field name="text1">
<camunda:expression>${genderBean.getGenderString(gender)}</camunda:expression>
</camunda:field>
<camunda:field name="text2">
<camunda:expression>Hello ${gender == 'male' ? 'Mr.' : 'Mrs.'} ${name}</camunda:expression>
</camunda:field>
</extensionElements>
</serviceTask>
Пример класса, приведенный ниже, использует внедренные выражения и вычисляет их, используя текущий DelegateExecution.
public class ReverseStringsFieldInjected implements JavaDelegate {
private Expression text1;
private Expression text2;
public void execute(DelegateExecution execution) {
String value1 = (String) text1.getValue(execution);
execution.setVariable("var1", new StringBuffer(value1).reverse().toString());
String value2 = (String) text2.getValue(execution);
execution.setVariable("var2", new StringBuffer(value2).reverse().toString());
}
}
В качестве альтернативы вы можете также установить выражения как атрибуты вместо дочерних элементов, чтобы сделать XML менее подробным.
<camunda:field name="text1" expression="${genderBean.getGenderString(gender)}" />
<camunda:field name="text2" expression="Hello ${gender == 'male' ? 'Mr.' : 'Mrs.'} ${name}" />
|
Инъекция происходит каждый раз, когда вызывается сервисная задача, поскольку будет создаваться отдельный экземпляр класса. Когда поля меняются вашим кодом, значения будут внедрены заново, когда активность будет выполнена в следующий раз. |
|
По тем же причинам, что и упомянутые выше, инъекция полей как правило, не должны использоваться со Spring-бинами, которые следуют паттерну singleton по умолчанию. В пртивном случае вы можете столкнуться с нестабильностью из-за параллельных модификаций полей бинов. |
Делегирование маппинга переменных
Чтобы реализовать класс, который делегирует маппинг входных и выходных переменных для активности вызова, этот класс должен реализовывать интерфейс io.openbpm.bpm.engine.delegate.DelegateVariableMapping. Реализация должна предоставлять методы mapInputVariables(DelegateExecution, VariableMap) и mapOutputVariables(DelegateExecution, VariableScope).
См. следующий пример:
public class DelegatedVarMapping implements DelegateVariableMapping {
@Override
public void mapInputVariables(DelegateExecution execution, VariableMap variables) {
variables.putValue("inputVar", "inValue");
}
@Override
public void mapOutputVariables(DelegateExecution execution, VariableScope subInstance) {
execution.setVariable("outputVar", "outValue");
}
}
Метод mapInputVariables вызывается перед тем, как выполнить активность вызова, чтобы осуществить маппинг входных переменных. Входные переменные необходимо положить в заданную карту переменных. Метод mapOutputVariables вызывается после того, как выполнится активность вызова, чтобы осуществить маппинг выходных переменных. Выходные переменные могут быть установлены напрямую внутри вызывающей сущности. Поведение, связанное с загрузкой классов, похоже на загрузку классов в
Java-делегациях.
Слушатель выполнения
Слушатели выполнения (Execution listeners) позволяют выполнять внешний Java-код или вычислять выражение при наступлении определенных событий во время выполнения процесса. События, которые могут быть перехвачены, следующие:
-
Начало и конец экземпляра процесса.
-
Переход между состояниями.
-
Начало и конец активности.
-
Начало и конец шлюза.
-
Начало и конец промежуточного события.
-
Окончание стартового события или старт события окончания.
Следующее определение процесса содержит три слушателя выполнений:
<process id="executionListenersProcess">
<extensionElements>
<camunda:executionListener
event="start"
class="io.openbpm.bpm.examples.bpmn.executionlistener.ExampleExecutionListenerOne" />
</extensionElements>
<startEvent id="theStart" />
<sequenceFlow sourceRef="theStart" targetRef="firstTask" />
<userTask id="firstTask" />
<sequenceFlow sourceRef="firstTask" targetRef="secondTask">
<extensionElements>
<camunda:executionListener>
<camunda:script scriptFormat="groovy">
println execution.eventName
</camunda:script>
</camunda:executionListener>
</extensionElements>
</sequenceFlow>
<userTask id="secondTask">
<extensionElements>
<camunda:executionListener expression="${myPojo.myMethod(execution.eventName)}" event="end" />
</extensionElements>
</userTask>
<sequenceFlow sourceRef="secondTask" targetRef="thirdTask" />
<userTask id="thirdTask" />
<sequenceFlow sourceRef="thirdTask" targetRef="theEnd" />
<endEvent id="theEnd" />
</process>
Первый слушатель выполнений получает уведомление, когда процесс стартует. Слушатель — это внешний Java-класс (например, ExampleExecutionListenerOne), который должен реализовывать интерфейс io.openbpm.bpm.engine.delegate.ExecutionListener. Когда происходит событие (в данном случае, событие окончания), вызывается метод notify(DelegateExecution execution).
public class ExampleExecutionListenerOne implements ExecutionListener {
public void notify(DelegateExecution execution) throws Exception {
execution.setVariable("variableSetInExecutionListener", "firstValue");
execution.setVariable("eventReceived", execution.getEventName());
}
}
Также существует возможность использовать класс делегации, реализующий интерфейс io.openbpm.bpm.engine.delegate.JavaDelegate. Эти классы делегации могут затем повторно использоваться в других конструктах, таких как делегация для сервисной задачи.
Второй слушатель выполнения вызывается, когда осуществляется переход между состояниями (transition is taken). Обратите внимание, что элемент слушателя не определяет событие, поскольку для переходов возможны только события типа take. При задании слушателя на переходе значения атрибутов события игнорируются. Также оно содержит дочерний элемент camunda:script, задающий скрипт, который будет выполнен как слушатель выполнения. В качестве альтернативы можно задать источник скрипта как внешний ресурс (см. документацию по источникам скриптов для скриптовых задач).
Последний слушатель выполнения вызывается, когда заканчивается активность secondTask. Вместо использования класса на декларации слушателя, задается выражение, которое вычисляется/вызывается при срабатывании события.
<camunda:executionListener expression="${myPojo.myMethod(execution.eventName)}" event="end" />
|
Событие |
Как и с другими выражениями, переменные выполнения вычисляются и могут использоваться. Поскольку объект реализации выполнения имеет свойство, дающее доступ извне к имени события, появляется возможность передать имя события в ваши методы, используя execution.eventName.
Слушатели выполнения также поддерживают использование delegateExpression, подобно сервисной задаче.
<camunda:executionListener event="start" delegateExpression="${myExecutionListenerBean}" />
Слушатель задачи
Слушатель задач используется для выполнения кастомной Java-логики при срабатывании определенного относящегося к задаче события. Он может добавляться только к определению процесса как дочерний элемент пользовательской задачи. Обратите внимание, что он также должен быть потомком extensionElements из стандарта BPMN 2.0 и находится в пространстве имен, поскольку слушатель задач является конструктом, конкретно предназначенным для движка Camunda.
<userTask id="myTask" name="My Task" >
<extensionElements>
<camunda:taskListener event="create" class="io.openbpm.bpm.MyTaskCreateListener" />
</extensionElements>
</userTask>
Жизненный цикл события слушателя задачи
Работа слушателей задачи зависит от порядка, в котором срабатывают следующие относящиеся к задаче события:
Событие create срабатывает, когда задача создается как транзиентный объект со всеми свойствами задачи. Никакое другое относящееся к задаче событие не может сработать раньше, чем событие create. Событие позволяет инспектировать все свойства задачи, когда мы получаем его в слушателе созданий.
Событие update происходит, когда меняется свойство задачи (например, assignee, owner, priority и т.д.) на уже созданной задаче. Сюда включаются атрибуты задачи (например, assignee, owner, priority и т.д.), а также зависимые сущности (например, аттачменты, комментарии, локальные переменные задачи). Обратите внимание, что инициализация задачи не вызывает срабатывание события update (задача только создается). Это также означает, что событие update всегда происходит после события create, которое уже произошло ранее.
Событие assignment отслеживает конкретно изменения свойства задачи assignee (назначенный исполнитель). Это событие может сработать в двух случаях:
-
Когда создается задача, у которой атрибут
assigneeявно задается в определении процесса. В этом случае событие assignment сработает после события create. -
Когда на уже созданную задачу назначается исполнитель, то ест свойство задачи
assigneeменяется. В этом случае событие assignment последует за событием update, поскольку изменение свойстваassigneeприводит к обновлению задачи.
Событие assignment может использоваться для более точной инспекции после реальной установки свойства assignee.
Событие timeout происходит, когда таймер, ассоциированный с текущим слушателем задачи, срабатывает. Обратите внимание, что это требует задания таймера. Событие timeout может произойти после того, как задача была создана (created), но до того, как она была завершена (completed).
Событие complete происходит, когда задача завершается успешно и непосредственно перед тем, как она удаляется из данных рантайма. Успешное завершение слушателя задачи при наступлении события complete приводит к окончанию жизненного цикла события.
Событие delete происходит непосредственно перед тем, как задача удаляется из данных рантайма из-за:
-
Прерывающего граничного события;
-
Прерывающего подпроцесса события;
-
Удаления экземпляра процесса;
-
Ошибки BPMN, выброшенной внутри слушателя задачи.
Никакие другие события не происходят после того, как произошло событие delete, поскольку оно приводи к окончанию жизненного цокла события задачи. Это означает, что события delete и complete являются взаимоисключающими.
Цепочки событий задачи
Приведенные выше описания дают представление о порядке, в котором срабатывают события задачи. Однако, этот порядок может быть нарушен при следующих условиях:
-
При вызове
Task#complete()внутри слушателя задачи событие complete сработает сразу же. Связанные с этой задачей слушатели будут немедленно вызваны, после чего оставшиеся слушатели задачи для предыдущего события будут обработаны. -
При использовании методов
TaskServiceвнутри слушателя задач, что может стать причиной срабатывания дополнительных событий задачи. Так же, как и с вышеупомянутым событием complete, эти события задач немедленно вызывают относящием=ся к ним слушатели, после чего будут обработаны оставшиеся слушатели задач. Однако, следует отметить, что цепочка событий, запущенная внутри слушателя задачи через вызов методаTaskService, будет следовать описанному выше порядку. -
При выбрасывании события ошибки BPMN внутри слушателя задачи (например, слушатель задачи события complete). Это отменит задачу и вызовет срабатывание события delete.
При упомянутых выше условиях пользователи должны быть особенно осторожны, чтобы случайно не создать бесконечный цикл событий задачи.
Задание слушателя задачи
Слушатель задачи поддерживает следующие атрибуты:
-
event (обязательный): тип события задачи, при котором будет вызван слушатель задачи. Возможные варианты событий включают: create, assignment, update, complete, delete и timeout;
Обратите внмание, что событие timeout требует дочернего элемента timerEventDefinition в слушателе задачи и сработает только если Job Executor включен.
-
class: класс делегации, который необходимо вызвать. Этот класс должен реализовывать интерфейс
io.openbpm.bpm.engine.impl.pvm.delegate.TaskListener.
public class MyTaskCreateListener implements TaskListener {
public void notify(DelegateTask delegateTask) {
// Custom logic goes here
// The task object is persisted in the database after this method has finished
}
}
+ Также можно использовать инъекцию полей для передачи процессных переменных или выполнения в класс делегации. ОБратите внимание, что каждый раз, когда выполняется активность, ссылающаяся на класс делегации, будет создаваться отдельный экземпляр этого класса.
-
expression: (не может использоваться вместе с атрибутами класса): задает выражение, которое будет выполняться, когда случается событие. Можно передать обхект типа
DelegateTaskи имя события (используяtask.eventName) вызванному объекту в качестве параметров.
<camunda:taskListener event="create" expression="${myObject.callMethod(task, task.eventName)}" />
-
delegateExpression: позволяет задавать выражение, которое разрешается в обхект, реализующий интерфейс
TaskListenerподобно сервисной задаче.
<camunda:taskListener event="create" delegateExpression="${myTaskListenerBean}" />
-
id: уникальный идентификатор слушателя внутри области видимости пользовательской задачи, требуется только когда
eventустановлено вtimeout.
Помимо атрибутов class, expression и delegateExpression, дочерний элемент camunda:script может использоваться для задания скрипта как слушатель задачи. Ресурс внешнего скрипта может декларироваться также через атрибут ресурса элемента camunda:script (см. документацию по источникам скриптов для скриптовых задач).
<userTask id="task">
<extensionElements>
<camunda:taskListener event="create">
<camunda:script scriptFormat="groovy">
println task.eventName
</camunda:script>
</camunda:taskListener>
</extensionElements>
</userTask>
Более того, дочерний элемент timerEventDefinition может использоваться в совокупности с типом event-а timeout, чтобы задать ассоциированный таймер. Заданный делегат будет вызываться Job Executor-ом при истечении таймера. Это не прервет выполнение пользовательской задачи.
<userTask id="task">
<extensionElements>
<camunda:taskListener event="timeout" delegateExpression="${myTaskListenerBean}" id="friendly-reminder" >
<timerEventDefinition>
<timeDuration xsi:type="tFormalExpression">PT1H</timeDuration>
</timerEventDefinition>
</camunda:taskListener>
</extensionElements>
</userTask>
Инъекция полей на слушателе
При использовании слушателей, сконфигурированных с атрибутом класса может применяться инъекция полей. Это в точности тот же механизм, который описан для Java-делегатов и содержит обзор возможностей, предоставляемых для инъекции полей.
Приведенный ниже фрагмент показывает простой пример процесса со слушателем выполнения с внедренными (инъектированными) полями:
<process id="executionListenersProcess">
<extensionElements>
<camunda:executionListener class="io.openbpm.bpm.examples.bpmn.executionListener.ExampleFieldInjectedExecutionListener" event="start">
<camunda:field name="fixedValue" stringValue="Yes, I am " />
<camunda:field name="dynamicValue" expression="${myVar}" />
</camunda:executionListener>
</extensionElements>
<startEvent id="theStart" />
<sequenceFlow sourceRef="theStart" targetRef="firstTask" />
<userTask id="firstTask" />
<sequenceFlow sourceRef="firstTask" targetRef="theEnd" />
<endEvent id="theEnd" />
</process>
Реальная реализация слушателя может выглядеть вот так:
public class ExampleFieldInjectedExecutionListener implements ExecutionListener {
private Expression fixedValue;
private Expression dynamicValue;
public void notify(DelegateExecution execution) throws Exception {
String value =
fixedValue.getValue(execution).toString() +
dynamicValue.getValue(execution).toString();
execution.setVariable("var", value);
}
}
Класс ExampleFieldInjectedExecutionListener производит конкатенацию двух внедренных полей (одно фиксированное, другое динамическое) и сохраняет их в процессной переменной var.
@Deployment(resources = {
"org/camunda/bpm/examples.adoc/bpmn/executionListener/ExecutionListenersFieldInjectionProcess.bpmn20.xml"
})
public void testExecutionListenerFieldInjection() {
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("myVar", "listening!");
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("executionListenersProcess", variables);
Object varSetByListener = runtimeService.getVariable(processInstance.getId(), "var");
assertNotNull(varSetByListener);
assertTrue(varSetByListener instanceof String);
// Result is a concatenation of fixed injected field and injected expression
assertEquals("Yes, I am listening!", varSetByListener);
}
Доступ к сервисам движка управления процессами
Из кода делегаций можно получить доступ к сервисам публичного API (RuntimeService, TaskService, RepositoryService …). Далее приведен пример, показывающий, как получать доступ к TaskService из реализации JavaDelegate.
public class DelegateExample implements JavaDelegate {
public void execute(DelegateExecution execution) throws Exception {
TaskService taskService = execution.getProcessEngineServices().taskService();
taskService.createTaskQuery()...;
}
}
Выбрасывание BPMN-ошибок из кода делегации
Из кода делегации (Java-делегата, выполнения и слушателей задач) можно бросить BpmnError. Это делается с использованием предоставленного Java-класса исключения изнутри вашего кода на Java (например, в JavaDelegate):
public class BookOutGoodsDelegate implements JavaDelegate {
public void execute(DelegateExecution execution) throws Exception {
try {
...
} catch (NotOnStockException ex) {
throw new BpmnError(NOT_ON_STOCK_ERROR);
}
}
}
Выбрасывание BPMN ошибок из слушателей
При реализации события поимки ошибки учитывайте тот факт, что BpmnError будут пойманы, когда они окажутся выброшены в нормальном течении процесса в следующих слушателях:
-
слушатели старта и окончания выполнения на активности, шлюзе и промежуточных событиях
-
слушатели передачи выполнения на переходах
-
слушатели событий create, assign, и complete на задачах
BpmnError не будет поймана для следующих слушателей:
-
слушателей начала и окончания процесса
-
слушателей удаления задачи
-
слушателей, вызванных извне нормального течения процесса:
-
модификация процесса произведена, что вызывает инициализацию скоупа подпроцесса, и некоторые из его слушателей выбрасывают ошибку
-
удаление экземпляра процесса вызывает слушатель его окончания, который выбрасывает ошибку
-
слушатель отрабатывает по причине выполнения прерывающего граничного события, например, корреляции сообщений на подпроцессе вызывает слушатель его окончания, который выбрасывает ошибку
-
|
Выбрасывание |
Установка бизнес-ключа из кода делегации
Опция установки нового значения бизнес-ключа на уже запущенном экземпляре процесса показана в приведенном ниже примере:
public class BookOutGoodsDelegate implements JavaDelegate {
public void execute(DelegateExecution execution) throws Exception {
...
String recalculatedKey = (String) execution.getVariable("recalculatedKeyVariable");
execution.setProcessBusinessKey(recalculatedKey);
...
}
}
Коды исключений
Вы можете выбросить ProcessEngineException из вашего кода делегации и задать ваш кастомный код ошибки, передав его через конструктор или через вызов ProcessEngineException#setCode.
Вы также можете создать класс кастомного исключения, который расширяет ProcessEngineException:
// Defining a custom exception.
public class MyException extends ProcessEngineException {
public MyException(String message, int code) {
super(message, code);
}
}
// Delegation code that throws MyException with a custom error code.
public class MyJavaDelegate implements JavaDelegate {
@Override
public void execute(DelegateExecution execution) {
String myErrorMessage = "My error message.";
int myErrorCode = 22_222;
throw new MyException(myErrorMessage, myErrorCode);
}
}
Установка кастомного кода ошибки через код делегации позволяет вашей бизнес-логике отреагировать на это, получив код через ProcessEngineException#getCode при вызове Camunda Java API или через вычисление свойства code в ответе на ошибочный вызов REST API.
Если вы не видите кода, движок присваивает 0, который может быть переопределен кастомным или встроенным провайдером кода ошибки.
Таке вы можете зарегистрировать ваш кастомный провайдер кода исключения, чтобы назначить коды ошибок на исключения, которые вы не можете контролировать через ваш код делегации.
|
Лицензия и атрибуция
Эта документация была создана на базе материала "Camunda 7 Docs" от Camunda, находится под лицензией Creative Commons Attribution-ShareAlike 3.0 Unported License .
Оригинал документации: https://docs.camunda.org