Розширення ядра Camunda
Розширення можливостей парсингу bpmn
Для розширення можливостей парсингу bpmn було імплементовано наступну ієрархію класів
-
TransientBpmnParse
- є наслідкомBpmnParse
і перевантажує методи парсингу bpmn -
TransientBpmnParseFactory
- є наслідкомBpmnParseFactory
та потрібен для створенняTransientBpmnParse
уSpringProcessEngineConfiguration
-
LowcodeSpringProcessEngineConfiguration
- є наслідкомSpringProcessEngineConfiguration
та потрібен для визначення параметрів кастомними значеннями (такими якbpmnParseFactory
) -
CamundaConfiguration
- базовий конфігураційний клас який створює та конфігурує екземпляр класуLowcodeSpringProcessEngineConfiguration
Заміщення парсингу input/output параметрів
У Camunda визначення input/output параметрів імплементовано з використанням AbstractVariableScope::setVariableLocal
(який не є transient, тобто створює запис для цього параметру в базі даних) для зберігання параметру у контексті виконання бізнес-процесу.
Тому було вирішено замістити InputParameter
за замовчуванням кастомними TransientInputParameter
-
Перевантажений метод
parseActivityOutput
виконує input/output маппинг за замовчуванням, отримує згенеровані input параметри та заміщує їх екземплярами класуTransientInputParameter
-
TransientInputParameter::execute
використовуєAbstractVariableScope::setVariableLocal
з ознакоюisTransient = true
для зберігання параметру якtransient
Заміщення логіки expression language resolver
У Camunda expression language resolver імплементовано так щоб повертати значення визначеної змінної.
Тобто якщо змінна variable
має значення value
, то вираз ${variable}
поверне тільки значення value
типу String
(навіть якщо змінна є transient).
Через що використання таких виразів не є захищеним (expression language resolver передає значення у camunda input parameter та "губить" інформацію про те чи transient ця змінна, а camunda input parameter своєю чергою не отримав інформації про те чи ця змінна transient та кладе цей параметр у контекст виконання бізнес-процесу без ознаки transient, через що ця змінна потрапляє до бази даних Camunda).
Тому було вирішено додати кастомний ElResolver
до контексту Camunda.
-
CamundaConfiguration
створює екземплярCamundaSpringExpressionManager
та кладе його уLowcodeSpringProcessEngineConfiguration
-
CamundaSpringExpressionManager
створює екземплярElResolver
за замовчуванням та екземплярTransientVariableScopeElResolver
, комбінує їх вCompositeElResolver
та повертає його -
TransientVariableScopeElResolver::getValue
у випадку якщо змінна не transient повертає значення змінної типу цієї самої змінної, але у випадку якщо змінна transient, то повертає екземпляр класуTypedValue
який зберігає ознаку transient.
Ці зміни спричинили зміни у самій expression language.
Відтепер якщо змінна є transient та потрібно повернути саме значення змінної у виразі то це можна зробити через .value .
Наприклад ${variable.toString()} стане ${variable.value.toString()}
|
Можливість додати системні змінні до контексту виконання бізнес-процесу
Щоб можна було змінні з bpms-camunda-global-system-vars config-map додати до контексту виконання бізнес-процесу було імплементовано наступну ієрархію класів.
-
CamundaProperties
містить список системних змінних -
CamundaEngineSystemVariablesSupportListener
містить екземпляр класуCamundaProperties
та у методі parseStartEvent додає системні змінні до контексту виконання бізнес-процесу -
CamundaEngineSystemVariablesSupportListenerPlugin
додає екземплярCamundaEngineSystemVariablesSupportListener
до загального спискуpreParseListeners
у Camunda
Збереження токену ініціатора бізнес-процесу у transient змінній бізнес-процесу
Для збереження токену ініціатора бізнес-процесу було імплементовано наступну ієрархію класів.
-
InitiatorTokenStartEventListener
бере об’єкт автентифікації зSecurityContextHolder
та зберігає токен у контексті Camunda в якості transient змінної з ім’ямinitiator_access_token
-
BpmSecuritySupportListener
містить екземпляр класуInitiatorTokenStartEventListener
та у методі parseStartEvent додає цей об’єкт до загального спискуexecutionListeners
стартового івенту
Оскільки змінна |
Збереження даних про фактичного виконавця задачі в змінних процесу
Для збереження токену та ім’я користувача фактичного виконавця задачі було імплементовано наступну ієрархію класів.
-
CompleterTaskEventListener
бере об’єкт автентифікації зSecurityContextHolder
та зберігає данні у контексті Camunda, а саме: ім’я користувача - в якості змінної з ім’ям<task_id>_completer
, токен користувача - в якості transient змінної з ім’ям<task_id>_completer_access_token
. -
BpmSecuritySupportListener
містить екземпляр класуCompleterTaskEventListener
та у методі parseUserTask додає цей об’єкт до загального спискуtaskListeners
користувацьких задач.
Оскільки змінна |
Збереження попередніх даних форми користувацьких задач у Ceph
Для Збереження попередніх даних форми користувацьких задач у Ceph було імплементовано наступну ієрархію класів
-
PutFormDataToStorageTaskListener
бере інпут параметр задачі з ім’ямuserTaskInputFormDataPrepopulate
та зберігає його у Ceph під ключем що має відношення до користувацької задачі -
StorageBpmnParseListener
містить екземпляр класуPutFormDataToStorageTaskListener
та у методіparseUserTask
додає цей об’єкт до загального спискуtaskListeners
кожної користувацької задачі
Якщо інпут параметр |
Видалення файлів з Ceph перед завершенням бізнес-процесу
Для видалення файлів з Ceph було імплементовано наступну ієрархію класів
-
FileCleanerEndEventListener
отримує ідентифікатор екземпляра бізнес-процесу та формує префікс форматуprocess/{processInstanceId}/
за яким отримує перелік ключів файлів збережених у Ceph, після чого видаляє файли за цим переліком. -
StorageBpmnParseListener
містить екземпляр класуFileCleanerEndEventListener
та у методіparseEndEvent
додає цей об’єкт до загального спискуexecutionListeners
кінцевого івенту.
Видалення користувацьких даних з Ceph перед завершенням бізнес-процесу
Для видалення користувацьких даних з Ceph було імплементовано наступну ієрархію класів
-
FormDataCleanerEndEventListener
отримує ідентифікатор екземпляра бізнес-процесу та формує префікс форматуprocess/{processInstanceId}/
за яким отримує перелік ключів користувацьких даних збережених у Ceph, та додає до цього переліку ключ користувацьких даних стартової форми якщо він присутній, після чого видаляє дані за цим переліком. -
StorageBpmnParseListener
містить екземпляр класуFormDataCleanerEndEventListener
та у методіparseEndEvent
додає цей об’єкт до загального спискуexecutionListeners
кінцевого івенту.
Маппинг виключень на HTTP відповідь
У разі виникнення виключної ситуації Camunda маппить це виключення використовуючи ExceptionMapper
-
ExceptionMapper<Throwable>
інтерфейс який містить методtoResponse
-
CamundaRestExceptionMapper<RestException>
клас який маппитьRestException
на HTTP відповідь з HTTP статусом який міститься уRestException
з тілом яке має наступну структуру
{
"traceId" : "traceId",
"code" : "code",
"message" : "message",
"localizedMessage" : "localizedMessage"
}
-
CamundaSystemExceptionMapper<SystemException>
- маппитьSystemException
на HTTP відповідь зі статусом 500 з тілом яке має наступну структуру
{
"traceId" : "traceId",
"code" : "code",
"message" : "message",
"localizedMessage" : "localizedMessage"
}
-
UserDataValidationExceptionMapper<ValidationException>
- маппитьValidationException
на HTTP відповідь зі статусом 422 з тілом яке має наступну структуру
{
"traceId" : "traceId",
"code" : "code",
"message" : "message",
"details" : {
"errors": [
{
"field": "fieldName",
"value": "fieldValue",
"message": "localizedMessage"
}
]
}
}
-
TaskAlreadyInCompletionExceptionMapper<TaskAlreadyInCompletionException>
- мапитьTaskAlreadyInCompletionException
на HTTP відповідь зі статусом 409 з тілом яке має наступну структуру
{
"traceId" : "traceId",
"code" : "code",
"message" : "message",
"localizedMessage" : "localizedMessage"
}
Синхронізоване виконання задачі
Для того, щоб не можна було виконати одну й ту ж задачу кілька разів, була реалізована можливість синхронізації по бізнес-ключу. Таким чином, наприклад, буде можливість виконувати декілька задач одразу, хоча не буде можливості виконувати одну й ту ж задачу в декілька потоків.
Було реалізовано SynchronizationService який у собі має кеш локів з weak-reference ключами, де ключем є будь-який бізнес-ключ (наприклад taskId
) а значенням є ReentrantLock
пов’язаний з цим бізнес ключем. Таким чином, якщо 2 потоки будуть використовувати лок отриманий з кешу по одному й тому ж ключу водночас вони будуть синхронізовані, хоча будь-які інші потоки з іншими ключами будуть використовувати вже інші локи тому й не будуть заблоковані між собою.
SynchronizationService
надає наступні можливості:-
ReentrantLock getLock(Object key)
— повертає лок пов’язаний з бізнес-ключемkey
для самостійного використання -
void execute(Object key, Runnable runnable)
— бере лок пов’язаний з бізнес-ключемkey
, по ньому синхронізується, виконуєrunnable.run()
та відпускає лок -
R evaluate(Object key, Supplier<R> supplier)
— бере лок пов’язаний з бізнес-ключемkey
, по ньому синхронізується, повертає результатsupplier.get()
та відпускає лок -
void executeOrThrow(Object key, Runnable runnable, Supplier<T extends Throwable> exceptionSupplier)
— бере лок пов’язаний з бізнес-ключемkey
, та намагається його заблокувати, якщо виходить заблокувати лок, то виконуєrunnable.run()
та відпускає лок, інакше — кидає виключення яке береться зexceptionSupplier
-
R evaluateOrThrow(Object key, Supplier<R> supplier, Supplier<T extends Throwable> exceptionSupplier)
— бере лок пов’язаний з бізнес-ключемkey
, та намагається його заблокувати, якщо виходить заблокувати лок, то повертає результат отриманій зsupplier.get()
та відпускає лок, інакше — кидає виключення яке береться зexceptionSupplier
Таким чином пара методів execute та evaluate по одному й тому ж ключу буде виконувати runnable.run() /supplier.get() по черзі, а executeOrThrow та evaluateOrThrow "відказуються" їх виконувати якщо щось вже заблокувало потрібній лок.
|
Також було реалізовано SynchronizedTaskServiceImpl
який розширює стандартний TaskServiceImpl
та перевантажує методи complete
та completeWithVariablesInReturn
додаючи синхронізацію за taskId
за допомогою SynchronizationService
з використанням executeOrThrow
та evaluateOrThrow
відповідно.