Визуальный конструктор бизнес-логики на основе Camunda BPM

от автора

Привет! Меня зовут Олег Гетманский, я – старший архитектор информационных систем. Сегодня расскажу, как мы упростили создание и управление бизнес-процесссами в IdM, оставив в прошлом жестко зашитые в систему правила и внедрив гибкий визуальный конструктор бизнес-логики Camunda BPM. Под катом краткое руководство по внедрению движка с моими комментариями – возможно, для кого-то оно сэкономит несколько рабочих часов или даже дней.

Автоматизация в IdM

Развитая IdM-система должна уметь автоматизировать процессы управления доступом даже в крупных компаниях с сотнями тысяч пользователей. Ее можно рассматривать как пункт управления: система интегрируется с другими информационными ресурсами организации и дает возможность централизованно управлять пользователями и их полномочиями. Процессы управления включают в себя такие операции, как предоставление доступа при приеме на работу, запрос дополнительных полномочий, приостановка доступа при увольнении, смена пароля и прочие действия, которые требуются по регламентам компаний.

С точки зрения разработчика самое сложное в системах класса IdM – это требование абсолютной кастомизируемости: заказчики считают, что в IdM все должно быть настраиваемо. Приведу несколько примеров из жизни:

  • Есть стандартная логика бизнес-процесса: IdM автоматически создает учетную запись для сотрудника при приеме на работу и блокирует УЗ и связанные с ней права при увольнении. Написали код, протестировали, выпустили фичу – все работает. Но у заказчика есть такое понятие, как перевод через увольнение, и теперь наша фича ломает этот выстроенный процесс перевода – IdM воспринимает его как просто прием на работу и создает новую учетную запись, а она сотруднику не нужна. Здесь важно, чтобы осталась прежняя учетка, и, уж тем более, чтобы она не заблокировалась.

  • IdM автоматически блокирует аккаунт на время отпуска пользователя. Эту фичу можно включить и выключить. Обязательно найдется заказчик, который попросит вместо блокировки отправлять уведомление, или перемещать учетную запись в отдельный каталог.

  • IdM автоматически назначает базовые роли новым пользователям. Внезапно найдутся «не такие как все» VIP-пользователи, которым нужно назначать роли в обход регламента – по разным алгоритмам в разных организациях.

  • В IdM на одного сотрудника приходится одна персональная учетная запись в домене организации. Это хорошо, но есть такие организации, в которых сотрудники работают на нескольких должностях по совместительству, и должны иметь несколько доменных аккаунтов.

Список потенциальных требований, которые нельзя предсказать заранее, со временем становится все больше и больше. Из-за этого у нас возникает ощущение, что новые фичи всегда получаются недоделанные, а программисты виноваты в том, что разрабатывают недостаточно гибкий продукт.

Настроение разработчика, когда появляется новое непредвиденное требование

Настроение разработчика, когда появляется новое непредвиденное требование

Как можно решить эту проблему? Мы пробовали разные варианты:

  1. Inline feature flags. Создаём множество параметров конфигурации и прямо в коде бизнес-логики пишем, как нужно поступать в том или ином случае, в зависимости от этих параметров. В отдельном интерфейсе можно включить те или иные флажки. Идея не взлетела, потому что мы все равно не знаем, какие фича-флаги понадобятся в будущем. К тому же чем больше в коде фича-флагов, тем дороже выходит реализация очередного такого флажка.

  2. Заскриптованная бизнес-логика. Оставить в стабильной ветке лишь общий workflow и архитектурный «каркас» в виде доступных сервисов и API, предоставив командам внедрения возможность самостоятельно закодировать специфичную для заказчика бизнес-логику. В таком случае объектом поставки является комбинация из стандартного продукта и множества скриптовых конструкций (в нашем случае это скрипты на языке Groovy). Кастомизировать и настраивать в такой модели можно практически всё, что угодно, однако при написании скрипта легко поломать то, что работало раньше. К тому же на практике оказалось довольно сложно поддерживать обратную совместимость для старых скриптов в новых версиях продукта.

  3. Плагины. Выделить сервисы-компоненты в виде простых Spring-бинов, каждый из которых отвечает за свой участок бизнес-логики. Реализацию компонента можно дополнить или вовсе заменить, если подложить в classpath приложения jar-файл, в котором есть альтернативная реализация бина. В таком случае мы получаем высокую кастомизируемость, в том смысле, что позволяем разработчикам самостоятельно написать плагин под конкретного заказчика. Однако при изменении кода оригинальных бинов нам придётся адаптировать все существующие плагины под новую версию продукта.

BPM-движок в основе бизнес-логики

На самом деле продвинутая IdM-система должна уметь  управлять аккаунтами и привилегиями, назначать и отзывать роли, посылать уведомления, выявлять нарушения и много другого полезного. Остается только уточнить, когда именно и как именно это всё нужно делать в отдельно взятой организации: когда создавать аккаунт для сотрудника (и сколько их будет), когда назначать роли и какие они будут, когда посылать уведомления, и о чем они должны быть и тому подобное.

Если посмотреть на IdM как на систему, автоматизирующую бизнес-процессы, то получается, что:

  • Можно выделить контекстно-независимые компоненты для проведения атомарных операций (создать аккаунт, назначить роль, отправить уведомление). Эти компоненты простые, они очень редко изменяются, и их можно переиспользовать. В своей системе мы иногда создаем новые компоненты, когда нужна новая функциональность.

  • Задача располагает к выстраиванию событийно-ориентированной архитектуры. При различных действиях (автоматических или пользовательских) порождаются события. В ответ на различные события происходят автоматические действия, определяемые бизнес-процессами организации («Новый пользователь? Создать доменный аккаунт», «Сотрудник ушел в отпуск? Временно заблокировать аккаунт», и много другого, что может потребоваться в отдельно взятой организации). Любую автоматику можно представить как последовательность простых шагов.

  • Максимальная гибкость настройки требуется именно на уровне принятия решений. Это не касается компонентов, выполняющих атомарные операции. Очень редко нужно изменять реализацию конкретных операций, и наоборот — очень часто мы будем изменять алгоритмы принятия решений – то есть добавлять условия на выполнение тех или иных операций, менять их последовательность, убирать из процессов ненужные шаги и добавлять нужные.

Так почему бы не возложить автоматизацию бизнес-процессов на предназначенный для этого движок BPM, оставив непосредственно в IdM только компоненты атомарных операций? Давайте посмотрим, что из этого получилось у нас в нашей IdM-системе.

В качестве BPM-системы мы выбрали Camunda v.7 – это open-source движок, его можно встроить в Java-приложение, он хорошо интегрируется со Spring. Есть визуальный редактор Camunda Modeller, в котором можно рисовать бизнес-процессы (БП). Сами БП у нас будут в формате BPMN (Business Process Management Notation).

Ниже под спойлером будет немного кода в технологическом стеке Java 17 + Maven + Spring Framework + Hibernate.

Компоненты атомарных операций

Сначала создадим атомарные компоненты. Каждый компонент – это небольшой stateless-бин, который отвечает за свою ограниченную область применения. Методы бина должны быть максимально простые и как можно более универсальные, мы должны иметь возможность вызвать их без знания контекста.

// Все атомарные компоненты называются по шаблону [Область применения]Ops  // Так проще отличить их от любых других компонентов  @Component  public class RoleOps {  // Компоненты полагаются на другие готовые сервисы из инфраструктуры системы      @Autowired      protected RoleService roleService;        @Autowired      protected UserRepository userRepository;        // Методы принимают и возвращают простые типы      /**       * Назначить роль на пользователя       * @param roleId id роли       * @param userId id пользователя       * @return id назначения       */      public String assignRoleToUser(String roleId, String userId) {          var role = roleService.getRole(roleId);          var user = userRepository.getUser(userId);          return roleService.createAssignment(role, user).getId();      }        /**       * Отозвать назначение       * @param assignmentId Id назначения       */      public void removeAssignment(String assignmentId) {          roleService.removeAssignment(assignmentId);      }  }

Теперь мы можем вызывать компонент RoleOps в скриптовом обработчике Camunda:

roleOps.assignRoleToUser("id-role-admin", userId)

Получается лаконично, такие однострочники мы затем будем использовать в BPMN-диаграммах.

Интегрируем Camunda в проект

Создаем новый Maven проект, добавляем зависимости на Camunda, Spring и сервисы инфраструктуры IdM:

<!-- Camunda, Spring --> <dependency>     <groupId>org.camunda.bpm</groupId>     <artifactId>camunda-engine</artifactId> </dependency> <dependency>     <groupId>org.camunda.bpm</groupId>     <artifactId>camunda-engine-spring</artifactId> </dependency> <dependency>     <groupId>org.springframework</groupId>     <artifactId>spring-context</artifactId> </dependency> <!-- Инфраструктура IdM --> <dependency>     <groupId>ru.solarsecurity.inRights.model</groupId>     <artifactId>model-common</artifactId>     <scope>provided</scope> </dependency>

В нашем случае мы создаем отдельный Maven-модуль, который живет в составе проекта, однако можно сделать отдельный BPM-микросервис. Сегодня это даже предпочтительно: принятие решений – отдельно, исполнители – отдельно.

Создаем конфигурацию Camunda:

@Configuration public class BpmBeanConfiguration {      @Qualifier("transactionManager")     @Autowired     protected PlatformTransactionManager transactionManager;      @Bean     public SpringProcessEngineConfiguration engineConfiguration(DataSource dataSource) {         SpringProcessEngineConfiguration cfg = new SpringProcessEngineConfiguration();         cfg.setProcessEngineName("engine");                  // Используем стандартные dataSource и transactionManager из проекта         // Camunda будет использовать ту же БД, что и остальная система         cfg.setDataSource(dataSource);         cfg.setDatabaseSchemaUpdate("true");         cfg.setTransactionManager(transactionManager);         cfg.setScriptEngineResolver(new DefaultScriptEngineResolver(new ScriptEngineManager()));         cfg.setInitializeTelemetry(false); // Отключаем телеметрию Camunda                return cfg;     }      @Bean     public ProcessEngineFactoryBean engineFactory(SpringProcessEngineConfiguration engineConfiguration) {         ProcessEngineFactoryBean factoryBean = new ProcessEngineFactoryBean();         factoryBean.setProcessEngineConfiguration(engineConfiguration);         return factoryBean;     } }

Итак, сейчас у нас есть движок Camunda, в нем можно развернуть свои бизнес-процессы и стартовать их.

В нашей IdM бизнес-процессы будут стартовать по сигналу, где сигнал – это какое-либо пользовательское действие или внешнее событие.

Все сигналы наследуются от нашего интерфейса CamundaSignal:

public interface CamundaSignal extends Serializable {     String getSignalName(); }

Для Camunda важно, чтобы сигналы были сериализуемыми и имели название.

Пример сигнала, который говорит о том, что в IdM появился новый пользователь:

public record UserCreatedEvent( String id,     String login,     String name,     Map<String, Serializable> attributes ) implements CamundaSignal {     @Override     public String getSignalName() {         return "userCreated";     } }

Создаем сервис для запуска бизнес-процессов по сигналам. Поскольку запуск БП и выполнение всех его шагов – это дорогостоящая операция, то лучше не блокировать поток обработки пользовательских запросов и сделать запуск БП асинхронным. Мы отправляем сигналы для Camunda в отдельном ThreadPoolExecutor:

@Component public class CamundaSignalService {      @Autowired     protected RuntimeService runtimeService;      // 128 потоков выполнения БП по умолчанию.     // Так много, потому что внутри них IdM занята в основном ожиданием I/O:     //  обращения к СУБД, запросы к внешним системам, отправка уведомлений     @Value("${inRights.camunda.threadPoolSize:128}")     protected int threadPoolSize;      protected ThreadPoolExecutor singalSubmitterThreadExecutor;      @PostConstruct     protected void init() {         var threadFactory = new CustomizableThreadFactory();         threadFactory.setDaemon(true);         threadFactory.setThreadNamePrefix("camunda-signal-thread-");          singalSubmitterThreadExecutor = new ThreadPoolExecutor(             threadPoolSize, threadPoolSize, 60, TimeUnit.SECONDS,             new LinkedBlockingQueue<>(Integer.MAX_VALUE), threadFactory         );     }      /**      * Отправить сигнал в Camunda. Это запустит БП, которые должны отреагировать на данный сигнал.      * @param camundaSignal сигнал      */     public void sendSignal(CamundaSignal camundaSignal) {         singalSubmitterThreadExecutor.execute(() ->             runtimeService.createSignalEvent(camundaSignal.getSignalName())                 .setVariables(Map.of("signal", camundaSignal))                 .send()         );     } }

Теперь отправляем сигналы в Camunda там, где возникают соответствующие события в системе:

// В БД сохранен новый пользователь userRepository.save(user); // Сообщаем об этом Camunda camundaSignalService.sendSignal(new UserCreatedEvent(user.getId(), user.getLogin(), user.getName(), user.getAttributes()));

Далее — разворачиваем в Camunda наш стандартный БП, чтобы наполнить уже движок каким-нибудь полезным функционалом. В идеале мы хотим сделать отдельную административную web-страничку для управления бизнес-процессами – разворачивать новые БП, скачивать / включать / выключать существующие. Интерфейсы Camunda для этого не подходят, поэтому создадим свой простой репозиторий сущностей БП.

Сущность бизнес-процесса имеет название, случайный id как UUID, флаг развернут / не развернут, и контент в формате BPMN:

@Entity @Table(name = "bpm_process") public class BusinessProcessEntity {      @GeneratedValue(generator = "custom-generator", strategy = GenerationType.IDENTITY)     @GenericGenerator(name = "custom-generator", strategy = UuidGenerator.STRATEGY_NAME)     @Id     protected String uuid;      @Column     protected String name;      @Column     protected boolean deployed;      @Version     @Column     protected Integer version;      @Column     protected byte[] content;      // getters, setters }

Репозиторий для сущностей БП:

public interface BusinessProcessRepository extends CrudRepository<BusinessProcessEntity, String> {     Optional<BusinessProcessEntity> findByName(String name); }

BusinessProcessDeploymentService отвечает за фактическое соответствие сущностей BusinessProcessEntity реальному состоянию БП в Camunda BPM. С помощью него и только него мы будем управлять БП на инсталляциях нашей IdM:

@Component  public class BusinessProcessDeploymentService {        @Autowired      protected BusinessProcessRepository bpRepo;        @Autowired      protected RepositoryService camundaRepo;        /**       * Активировать БП в Camunda BPM       */      @Transactional      public void deploy(BusinessProcessEntity bp) {          bp.setDeployed(true);          bpRepo.save(bp);          deleteDeployments(bp.getName()); // Удаляем старую версию деплоймента          camundaRepo.createDeployment()   // Создаем новый деплоймент              .source(bp.getName())              .addInputStream(bp.getName(), new ByteArrayInputStream(bp.getContent()))              .deploy();      }        /**       * Деактивировать БП в Camunda BPM       */      @Transactional      public void undeploy(BusinessProcessEntity bp) {          bp.setDeployed(false);          bpRepo.save(bp);          deleteDeployments(bp.getName());      }        protected void deleteDeployments(String deploymentSource) {          camundaRepo.createDeploymentQuery().deploymentSource(deploymentSource).list().stream()              .map(Deployment::getId)              .forEach(camundaRepo::deleteDeployment);      }  }

Установка флага deployed = true и деплоймент в Camunda происходят в одной транзакции, поэтому если в процессе деплоя что-то пойдет не так, то сущность BusinessProcessEntity не будет считаться активным процессом.

Представление архитектуры модуля BPM

Представление архитектуры модуля BPM

Для краткости я пропущу написание REST-контроллера и детали реализации фронтенда.

Итоговый вид административной странички управления БП:

Бизнес-процессы разворачиваются в Camunda сразу после загрузки файла. Если снять флажок «Активен» с бизнес-процесса, то деплоймент будет удален из Camunda, но сама сущность BusinessProcessEntity останется в таблице. Так можно включать и отключать БП, не удаляя их из системы. Если загрузить БП с именем, которое уже есть в таблице, тогда старая версия БП будет обновлена в Camunda.

Пишем простой бизнес-процесс

Для составления БП будем использовать редактор Camunda Modeller. Создаем новый файл в формате BPMN diagram (Camunda Platform 7).

Добавляем сигнал начала процесса — userCreatedEvent (как написано в коде: UserCreatedEvent#getSignalName). Добавляем в процесс шаги типа Script Task. Внутри шагов — скриптовые выражения, вызывающие методы атомарных компонентов. В результате получится примерно так:

Пример бизнес-процесса в IdM-системе

Пример бизнес-процесса в IdM-системе

Сохраняем файл bpmn, загружаем в IdM и таким образом реализуем то, что нужно организации.

Итак, составляя BPMN, можно быстро набросать работающую фичу по желаниям заказчика, возможно, даже сидя вместе с ним за одним столом. Нужно сделать перевод через увольнение? Отлично, только расскажите, с чего оно начинается, и что IdM должна при этом сделать. Если известны стартовые события и правила формирования атрибутов учетных записей, то поддержать множественные доменные аккаунты также будет довольно просто.

В целом, чем больше атомарных бинов, которые можно использовать в БП, – тем более функциональные процессы можно строить в визуальном редакторе.

Теперь мы можем выстроить такой процесс реализации новых фич:

  1. Системный аналитик (или заказчик, архитектор, разработчик) представляет схематичное изображение нового бизнес-процесса. Это может быть BPMN-диаграмма или просто иллюстрация с разноцветными элементами. Самое главное, чтобы было понятно, какие события должны происходить и как на них нужно реагировать.

  2. Превращаем схему в валидную BPMN-диаграмму.

  3. Наполняем диаграмму обращениями к атомарным компонентам через скриптовые выражения.

  4. Реализуем в IdM новые сигналы (события, запросы) и атомарные компоненты, если существующих недостаточно.

  5. Получаем рабочий бизнес-процесс в виде файла с расширением bpmn, проверяем работоспособность.

  6. Устанавливаем готовый bpmn у заказчика.

Готовый БП – это легковесный переносимый артефакт. Файлы bpmn можно копировать, изменять, можно положить их в систему контроля версий и использовать как основу для других кастомных БП.

В заключение

BPM-движок, такой как Camunda, вполне может использоваться как визуальный конструктор в окружении, где требуется 100% кастомизируемость алгоритмов и сложно предсказать, в какую сторону пойдет развитие той или иной фичи.

Плюсы этого решения:

  • Это «микросервисно»! Модуль BPM взаимодействует с остальной системой посредством обмена сигналами и событиями, это позволяет выделить его в отдельный микросервис. Инфраструктура исполнения команд, поступающих от BPM, может быть масштабирована отдельно.

  • Поощряет написание простого кода. BPMN-диаграммы будут простые и понятные, если таковыми будут атомарные компоненты, которые их поддерживают. Желательно, чтобы Ops-бины принимали и возвращали простые типы данных: строки, числа, enum, record.

  • Наглядность. Бизнес-логика в BPMN отображена визуально. Не требуется умение программировать, чтобы понимать диаграммы бизнес-процессов.

  • Изменения on-the-fly. Для изменения бизнес-логики на конкретной инсталляции достаточно только загрузить новый файл bpmn.

  • Обратная совместимость. Поскольку БП работают только с Ops-компонентами, то для поддержки обратной совместимости нам достаточно того, что публичное API Ops-компонентов не изменяется от версии к версии. Если нам нужны более продвинутые возможности от операционных компонентов, тогда мы просто напишем новые бины.

  • Производительность. Мы не используем в Camunda асинхронные процессы, таймеры и пользовательские задачи. Выполнение всего БП укладывается в одну транзакцию. Это значит, что не требуется сохранять промежуточное состояние процесса в базе. Скорость выполнения кода бизнес-логики практически равна скорости выполнения обычного Java-кода. Подробнее о транзакциях в Camunda.

На что нужно обратить внимание, если вы решитесь на внедрение движка BPM:

  • Тяжеловесность. BPM-движок усложняет систему как минимум одним своим присутствием. Поэтому если ваша предметная область достаточно предсказуема, и вы можете сохранить гибкость вашего продукта просто посредством написания качественного кода, тогда вам не нужен BPM.

  • Нужно знать нотацию. Для того, чтобы грамотно составлять бизнес-процессы, кто-то в команде должен изучить нотацию BPMN 2.0.

  • BPM – это автоматические действия. Описанный способ применения движка не подходит для решения общих проблем в области кастомизации. К примеру, если нужно сделать настраиваемые графические формы или слегка изменить сценарий взаимодействия программы с пользователем – тогда BPM не поможет, а более подходящим инструментом будет модульный конструктор фронтенда. Наиболее сильная сторона движка BPM – это автоматизированные процессы и действия, происходящие без участия человека.

  • Обязательное протоколирование. Довольно сложно проводить отладку кода бизнес-логики, когда он заключен в BPMN-диаграммы. Крайне желательно делать так, чтобы любое совершенное действие было зафиксировано в файлах логов или в журнале событий, или в системе аудита. По этим данным позже можно будет отследить, почему движок BPM принял то или иное решение на конкретном шаге. Без подробного протоколирования бизнес-процессы – это черный ящик.


ссылка на оригинал статьи https://habr.com/ru/articles/726774/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *