
Привет! Меня зовут Олег Гетманский, я – старший архитектор информационных систем. Сегодня расскажу, как мы упростили создание и управление бизнес-процесссами в IdM, оставив в прошлом жестко зашитые в систему правила и внедрив гибкий визуальный конструктор бизнес-логики Camunda BPM. Под катом краткое руководство по внедрению движка с моими комментариями – возможно, для кого-то оно сэкономит несколько рабочих часов или даже дней.
Автоматизация в IdM
Развитая IdM-система должна уметь автоматизировать процессы управления доступом даже в крупных компаниях с сотнями тысяч пользователей. Ее можно рассматривать как пункт управления: система интегрируется с другими информационными ресурсами организации и дает возможность централизованно управлять пользователями и их полномочиями. Процессы управления включают в себя такие операции, как предоставление доступа при приеме на работу, запрос дополнительных полномочий, приостановка доступа при увольнении, смена пароля и прочие действия, которые требуются по регламентам компаний.
С точки зрения разработчика самое сложное в системах класса IdM – это требование абсолютной кастомизируемости: заказчики считают, что в IdM все должно быть настраиваемо. Приведу несколько примеров из жизни:
-
Есть стандартная логика бизнес-процесса: IdM автоматически создает учетную запись для сотрудника при приеме на работу и блокирует УЗ и связанные с ней права при увольнении. Написали код, протестировали, выпустили фичу – все работает. Но у заказчика есть такое понятие, как перевод через увольнение, и теперь наша фича ломает этот выстроенный процесс перевода – IdM воспринимает его как просто прием на работу и создает новую учетную запись, а она сотруднику не нужна. Здесь важно, чтобы осталась прежняя учетка, и, уж тем более, чтобы она не заблокировалась.
-
IdM автоматически блокирует аккаунт на время отпуска пользователя. Эту фичу можно включить и выключить. Обязательно найдется заказчик, который попросит вместо блокировки отправлять уведомление, или перемещать учетную запись в отдельный каталог.
-
IdM автоматически назначает базовые роли новым пользователям. Внезапно найдутся «не такие как все» VIP-пользователи, которым нужно назначать роли в обход регламента – по разным алгоритмам в разных организациях.
-
В IdM на одного сотрудника приходится одна персональная учетная запись в домене организации. Это хорошо, но есть такие организации, в которых сотрудники работают на нескольких должностях по совместительству, и должны иметь несколько доменных аккаунтов.
Список потенциальных требований, которые нельзя предсказать заранее, со временем становится все больше и больше. Из-за этого у нас возникает ощущение, что новые фичи всегда получаются недоделанные, а программисты виноваты в том, что разрабатывают недостаточно гибкий продукт.
Как можно решить эту проблему? Мы пробовали разные варианты:
-
Inline feature flags. Создаём множество параметров конфигурации и прямо в коде бизнес-логики пишем, как нужно поступать в том или ином случае, в зависимости от этих параметров. В отдельном интерфейсе можно включить те или иные флажки. Идея не взлетела, потому что мы все равно не знаем, какие фича-флаги понадобятся в будущем. К тому же чем больше в коде фича-флагов, тем дороже выходит реализация очередного такого флажка.
-
Заскриптованная бизнес-логика. Оставить в стабильной ветке лишь общий workflow и архитектурный «каркас» в виде доступных сервисов и API, предоставив командам внедрения возможность самостоятельно закодировать специфичную для заказчика бизнес-логику. В таком случае объектом поставки является комбинация из стандартного продукта и множества скриптовых конструкций (в нашем случае это скрипты на языке Groovy). Кастомизировать и настраивать в такой модели можно практически всё, что угодно, однако при написании скрипта легко поломать то, что работало раньше. К тому же на практике оказалось довольно сложно поддерживать обратную совместимость для старых скриптов в новых версиях продукта.
-
Плагины. Выделить сервисы-компоненты в виде простых 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 не будет считаться активным процессом.
Для краткости я пропущу написание REST-контроллера и детали реализации фронтенда.
Итоговый вид административной странички управления БП:

Бизнес-процессы разворачиваются в Camunda сразу после загрузки файла. Если снять флажок «Активен» с бизнес-процесса, то деплоймент будет удален из Camunda, но сама сущность BusinessProcessEntity останется в таблице. Так можно включать и отключать БП, не удаляя их из системы. Если загрузить БП с именем, которое уже есть в таблице, тогда старая версия БП будет обновлена в Camunda.
Пишем простой бизнес-процесс
Для составления БП будем использовать редактор Camunda Modeller. Создаем новый файл в формате BPMN diagram (Camunda Platform 7).
Добавляем сигнал начала процесса — userCreatedEvent (как написано в коде: UserCreatedEvent#getSignalName). Добавляем в процесс шаги типа Script Task. Внутри шагов — скриптовые выражения, вызывающие методы атомарных компонентов. В результате получится примерно так:
Сохраняем файл bpmn, загружаем в IdM и таким образом реализуем то, что нужно организации.
Итак, составляя BPMN, можно быстро набросать работающую фичу по желаниям заказчика, возможно, даже сидя вместе с ним за одним столом. Нужно сделать перевод через увольнение? Отлично, только расскажите, с чего оно начинается, и что IdM должна при этом сделать. Если известны стартовые события и правила формирования атрибутов учетных записей, то поддержать множественные доменные аккаунты также будет довольно просто.
В целом, чем больше атомарных бинов, которые можно использовать в БП, – тем более функциональные процессы можно строить в визуальном редакторе.
Теперь мы можем выстроить такой процесс реализации новых фич:
-
Системный аналитик (или заказчик, архитектор, разработчик) представляет схематичное изображение нового бизнес-процесса. Это может быть BPMN-диаграмма или просто иллюстрация с разноцветными элементами. Самое главное, чтобы было понятно, какие события должны происходить и как на них нужно реагировать.
-
Превращаем схему в валидную BPMN-диаграмму.
-
Наполняем диаграмму обращениями к атомарным компонентам через скриптовые выражения.
-
Реализуем в IdM новые сигналы (события, запросы) и атомарные компоненты, если существующих недостаточно.
-
Получаем рабочий бизнес-процесс в виде файла с расширением bpmn, проверяем работоспособность.
-
Устанавливаем готовый 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/
Добавить комментарий