Этот пост является гостевым постом соучредителя Activiti и члена сообщества Joram Barrez (@jbarrez), который работает на Alfresco. Спасибо, Джорам! Я хотел бы видеть больше этих гостевых постов сообщества, поэтому, как обычно, не стесняйтесь пинговать меня (@starbuxman) идеями и предложениями! -Josh
Вступление
Activiti — это лицензированный Apache механизм управления бизнес-процессами (BPM). Такой механизм имеет основной целью взять определение процесса, состоящее из задач и вызовов службы, и выполнить их в определенном порядке, одновременно предоставляя различные API для запуска, управления и запроса данных об экземплярах процесса для этого определения. В отличие от многих своих конкурентов, Activiti легок и легко интегрируется с любой технологией Java или проектом. Все это работает в любом масштабе — от нескольких десятков до многих тысяч или даже миллионов процессов.
Исходный код Activiti можно найти на Github . Проект был основан и спонсируется Alfresco , но пользуется вкладом со всего мира и отраслей.
Определение процесса обычно визуализируется в виде диаграммы, подобной блок-схеме. В последние годы стандарт BPMN 2.0 (стандарт OMG, например, UML) стал де-факто «языком» этих диаграмм. Этот стандарт определяет, как следует интерпретировать определенную фигуру на диаграмме, как технически, так и с точки зрения бизнеса, и как она хранится в виде XML-файла, который не так уж и сложен, но, к счастью, большинство инструментов скрывает это за вами. Это стандарт, и вы можете использовать любое количество совместимых инструментов для проектирования (и даже запуска) ваших BPMN-процессов. Тем не менее, если вы спрашиваете меня, нет лучшего выбора, чем Activiti!
Интеграция Spring Boot
Activiti и Spring прекрасно играют вместе. Подход, основанный на соглашении о конфигурации в Spring Boot, прекрасно работает с процессором Activiti, который настраивает и использует. Из коробки вам нужна только база данных, поскольку выполнение процесса может длиться от нескольких секунд до нескольких лет. Очевидно, что неотъемлемой частью определения процесса является вызов и потребление данных в различные системы и из них с использованием всех видов технологий. Простота добавления необходимых зависимостей и интеграции различных элементов (логической схемы) с Spring Boot действительно делает эту детскую игру.
Использование Spring Boot и Activiti в микросервисном подходе также имеет большой смысл. Spring Boot позволяет легко и быстро запустить готовый к работе сервис и — в архитектуре распределенного микросервиса — процессы Activiti могут склеивать различные микросервисы, а также переплетаться в человеческом рабочем процессе (задачах и формах) для достижения определенной цели.
Интеграция Spring Boot в Activiti была создана экспертом Spring Джошем Лонгом . Пару месяцев назад мы с Джошем провели вебинар, который должен дать вам хорошее представление об основах интеграции Activiti для Spring Boot. Раздел руководства пользователя Activiti по Spring Boot также является отличной отправной точкой для получения дополнительной информации.
Начиная
Код для этого примера можно найти в моем репозитории Github .
Процесс, который мы реализуем здесь, является процессом найма разработчика. Конечно, это упрощено (так как оно должно уместиться на этой веб-странице), но вы должны получить основные понятия. Вот схема:
Как сказано во введении, все формы здесь имеют очень специфическую интерпретацию благодаря стандарту BPMN 2.0. Но даже без знания BPMN процесс довольно прост для понимания:
- Когда процесс начинается, резюме соискателя сохраняется во внешней системе.
- Затем процесс ожидает, пока не будет проведено телефонное интервью. Это делает пользователь (см. Маленькую иконку человека в углу).
- Если телефонное интервью не было всем этим, отправляется вежливое письмо с отказом. В противном случае должно состояться как техническое интервью, так и финансовые переговоры.
- Обратите внимание, что в любой момент заявитель может отменить. Это показано на диаграмме как событие на границе большого прямоугольника. Когда происходит событие, все внутри будет убито, и процесс останавливается.
- Если все идет хорошо, отправляется приветственное письмо.
Давайте создадим новый проект Maven и добавим зависимости, необходимые для получения Spring Boot, Activiti и базы данных. Мы будем использовать базу данных в памяти, чтобы все было просто.
<dependency> <groupId>org.activiti</groupId> <artifactId>spring-boot-starter-basic</artifactId> <version>${activiti.version}</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.185</version> </dependency>
Таким образом, для создания самого первого приложения Spring Boot + Activiti требуется только две зависимости:
@SpringBootApplication public class MyApp { public static void main(String[] args) { SpringApplication.run(MyApp.class, args); } }
Вы уже можете запустить это приложение, оно не будет ничего делать функционально, но за кулисами оно уже
- создает базу данных H2 в памяти
- создает механизм процессов Activiti, используя эту базу данных
- выставляет все сервисы Activiti как Spring Beans
- здесь и там настраиваются лакомые кусочки, такие как исполнитель асинхронных заданий Activiti, почтовый сервер и т. д.
Давайте что-нибудь запустим. Перетащите определение процесса BPMN 2.0 в src/main/resources/processes
папку. Все процессы, размещенные здесь, будут автоматически развернуты (т. Е. Проанализированы и сделаны исполняемыми) на движке Activiti. Давайте начнем с простого и создадим его, CommandLineRunner
который будет выполняться при загрузке приложения:
@Bean CommandLineRunner init( final RepositoryService repositoryService, final RuntimeService runtimeService, final TaskService taskService) { return new CommandLineRunner() { public void run(String... strings) throws Exception { Map<String, Object> variables = new HashMap<String, Object>(); variables.put("applicantName", "John Doe"); variables.put("email", "[email protected]"); variables.put("phoneNumber", "123456789"); runtimeService.startProcessInstanceByKey("hireProcess", variables); } }; }
Итак, здесь происходит то, что мы создаем карту всех переменных, необходимых для запуска процесса, и пропускаем ее при запуске процесса. Если вы проверите определение процесса, то увидите, что мы ссылаемся на эти переменные ${variableName}
во многих местах (например, в описании задачи).
Первым шагом процесса является автоматический шаг (см. Маленький значок зубчатого колеса), реализованный с использованием выражения, использующего Spring Bean:
который реализован с
activiti:expression="${resumeService.storeResume()}"
Конечно, нам нужен этот компонент, иначе процесс не запустится. Итак, давайте создадим это:
@Component public class ResumeService { public void storeResume() { System.out.println("Storing resume ..."); } }
Запустив приложение сейчас, вы увидите, что бин называется:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.2.0.RELEASE) 2015-02-16 11:55:11.129 INFO 304 --- [ main] MyApp : Starting MyApp on The-Activiti-Machine.local with PID 304 ... Storing resume ... 2015-02-16 11:55:13.662 INFO 304 --- [ main] MyApp : Started MyApp in 2.788 seconds (JVM running for 3.067)
Вот и все! Поздравляю с запуском первого экземпляра процесса с помощью Activiti в Spring Boot!
Давайте немного оживим и добавим следующую зависимость в наш pom.xml:
<dependency> <groupId>org.activiti</groupId> <artifactId>spring-boot-starter-rest-api</artifactId> <version>${activiti.version}}</version> </dependency>
Наличие этого в classpath делает изящную вещь: он берет API Activiti REST (который написан на Spring MVC) и полностью раскрывает это в вашем приложении. REST API Activiti полностью документирован в Руководстве пользователя Activiti .
API REST защищен базовой аутентификацией и по умолчанию не имеет пользователей. Давайте добавим администратора в систему, как показано ниже (добавьте это в класс MyApp). Конечно, не делайте этого в производственной системе, там вы захотите подключить аутентификацию к LDAP или что-то еще.
@Bean InitializingBean usersAndGroupsInitializer(final IdentityService identityService) { return new InitializingBean() { public void afterPropertiesSet() throws Exception { Group group = identityService.newGroup("user"); group.setName("users"); group.setType("security-role"); identityService.saveGroup(group); User admin = identityService.newUser("admin"); admin.setPassword("admin"); identityService.saveUser(admin); } }; }
Запустите приложение. Теперь мы можем запустить экземпляр процесса, как мы это делали в CommandLineRunner, но теперь, используя REST:
curl -u admin:admin -H "Content-Type: application/json" -d '{"processDefinitionKey":"hireProcess", "variables": [ {"name":"applicantName", "value":"John Doe"}, {"name":"email", "value":"[email protected]"}, {"name":"phoneNumber", "value":"1234567"} ]}' http://localhost:8080/runtime/process-instances
Что возвращает нам представление json экземпляра процесса:
{ "tenantId": "", "url": "http://localhost:8080/runtime/process-instances/5", "activityId": "sid-42BAE58A-8FFB-4B02-AAED-E0D8EA5A7E39", "id": "5", "processDefinitionUrl": "http://localhost:8080/repository/process-definitions/hireProcess:1:4", "suspended": false, "completed": false, "ended": false, "businessKey": null, "variables": [], "processDefinitionId": "hireProcess:1:4" }
Я просто хочу на минуту остановиться, как это круто. Просто добавив одну зависимость, вы получаете весь API Activiti REST, встроенный в ваше приложение!
Давайте сделаем это еще круче и добавим следующую зависимость
<dependency> <groupId>org.activiti</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>${activiti.version}</version> </dependency>
Это добавляет конечную точку привода Spring Boot для Activiti. Если мы перезапустим приложение и нажмем http://localhost:8080/activiti/
, мы получим некоторую базовую статистику о наших процессах. С некоторым воображением, что в реальной системе вы развернули и выполняете намного больше определений процессов, вы можете увидеть, насколько это полезно.
Этот же исполнительный механизм также зарегистрирован как компонент JMX, предоставляющий аналогичную информацию.
{ completedTaskCountToday: 0, deployedProcessDefinitions: [ "hireProcess (v1)" ], processDefinitionCount: 1, cachedProcessDefinitionCount: 1, runningProcessInstanceCount: { hireProcess (v1): 0 }, completedTaskCount: 0, completedActivities: 0, completedProcessInstanceCount: { hireProcess (v1): 0 }, openTaskCount: 0 }
To finish our coding, let’s create a dedicated REST endpoint for our hire process, that could be consumed by for example a javascript web application (out of scope for this article). So most likely, we’ll have a form for the applicant to fill in the details we’ve been passing programmatically above. And while we’re at it, let’s store the applicant information as a JPA entity. In that case, the data won’t be stored in Activiti anymore, but in a separate table and referenced by Activiti when needed.
You probably guessed it by now, JPA support is enabled by adding a dependency:
<dependency> <groupId>org.activiti</groupId> <artifactId>spring-boot-starter-jpa</artifactId> <version>${activiti.version}</version> </dependency>
and add the entity to the MyApp class:
@Entity class Applicant { @Id @GeneratedValue private Long id; private String name; private String email; private String phoneNumber; // Getters and setters
We’ll also need a Repository for this Entity (put this in a separate file or also in MyApp). No need for any methods, the Repository magic from Spring will generate the methods we need for us.
public interface ApplicantRepository extends JpaRepository<Applicant, Long> { // .. }
And now we can create the dedicated REST endpoint:
@RestController public class MyRestController { @Autowired private RuntimeService runtimeService; @Autowired private ApplicantRepository applicantRepository; @RequestMapping(value="/start-hire-process", method= RequestMethod.POST, produces= MediaType.APPLICATION_JSON_VALUE) public void startHireProcess(@RequestBody Map<String, String> data) { Applicant applicant = new Applicant(data.get("name"), data.get("email"), data.get("phoneNumber")); applicantRepository.save(applicant); Map<String, Object> variables = new HashMap<String, Object>(); variables.put("applicant", applicant); runtimeService.startProcessInstanceByKey("hireProcessWithJpa", variables); } }
Note we’re now using a slightly different process called ‘hireProcessWithJpa’, which has a few tweaks in it to cope with the fact the data is now in a JPA entity. So for example, we can’t use ${applicantName} anymore, but we now have to use ${applicant.name}.
Let’s restart the application and start a new process instance:
curl -u admin:admin -H "Content-Type: application/json" -d '{"name":"John Doe", "email": "[email protected]", "phoneNumber":"123456789"}' http://localhost:8080/start-hire-process
We can now go through our process. You could create a custom endpoints for this too, exposing different task queries with different forms … but I’ll leave this to your imagination and use the default Activiti REST end points to walk through the process.
Let’s see which task the process instance currently is at (you could pass in more detailed parameters here, for example the ‘processInstanceId’ for better filtering):
curl -u admin:admin -H "Content-Type: application/json" http://localhost:8080/runtime/tasks
which returns
{ "order": "asc", "size": 1, "sort": "id", "total": 1, "data": [{ "id": "14", "processInstanceId": "8", "createTime": "2015-02-16T13:11:26.078+01:00", "description": "Conduct a telephone interview with John Doe. Phone number = 123456789", "name": "Telephone interview" ... }], "start": 0 }
So, our process is now at the Telephone interview
. In a realistic application, there would be a task list and a form that could be filled in to complete this task. Let’s complete this task (we have to set the telephoneInterviewOutcome
variable as the exclusive gateway uses it to route the execution):
curl -u admin:admin -H "Content-Type: application/json" -d '{"action" : "complete", "variables": [ {"name":"telephoneInterviewOutcome", "value":true} ]}' http://localhost:8080/runtime/tasks/14
When we get the tasks again now, the process instance will have moved on to the two tasks in parallel in the subprocess (big rectangle):
{ "order": "asc", "size": 2, "sort": "id", "total": 2, "data": [ { ... "name": "Tech interview" }, { ... "name": "Financial negotiation" } ], "start": 0 }
We can now continue the rest of the process in a similar fashion, but I’ll leave that to you to play around with.
Testing
One of the strengths of using Activiti for creating business processes is that everything is simply Java. As a consequence, processes can be tested as regular Java code with unit tests. Spring Boot makes writing such test a breeze.
Here’s how the unit test for the “happy path” looks like (while omitting @Autowired
fields and test e-mail server setup). The code also shows the use of the Activiti API’s for querying tasks for a given group and process instance.
@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = {MyApp.class}) @WebAppConfiguration @IntegrationTest public class HireProcessTest { @Test public void testHappyPath() { // Create test applicant Applicant applicant = new Applicant("John Doe", "[email protected]", "12344"); applicantRepository.save(applicant); // Start process instance Map<String, Object> variables = new HashMap<String, Object>(); variables.put("applicant", applicant); ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("hireProcessWithJpa", variables); // First, the 'phone interview' should be active Task task = taskService.createTaskQuery() .processInstanceId(processInstance.getId()) .taskCandidateGroup("dev-managers") .singleResult(); Assert.assertEquals("Telephone interview", task.getName()); // Completing the phone interview with success should trigger two new tasks Map<String, Object> taskVariables = new HashMap<String, Object>(); taskVariables.put("telephoneInterviewOutcome", true); taskService.complete(task.getId(), taskVariables); List<Task> tasks = taskService.createTaskQuery() .processInstanceId(processInstance.getId()) .orderByTaskName().asc() .list(); Assert.assertEquals(2, tasks.size()); Assert.assertEquals("Financial negotiation", tasks.get(0).getName()); Assert.assertEquals("Tech interview", tasks.get(1).getName()); // Completing both should wrap up the subprocess, send out the 'welcome mail' and end the process instance taskVariables = new HashMap<String, Object>(); taskVariables.put("techOk", true); taskService.complete(tasks.get(0).getId(), taskVariables); taskVariables = new HashMap<String, Object>(); taskVariables.put("financialOk", true); taskService.complete(tasks.get(1).getId(), taskVariables); // Verify email Assert.assertEquals(1, wiser.getMessages().size()); // Verify process completed Assert.assertEquals(1, historyService.createHistoricProcessInstanceQuery().finished().count()); }
Next steps
- We haven’t touched any of the tooling around Activiti. There is a bunch more than just the engine, like the Eclipse plugin to design processes, a free web editor in the cloud (also included in the
.zip
download you can get fromActiviti's
site, a web application that showcases many of the features of the engine, … - The current release of Activiti (version 5.17.0) has integration with Spring Boot 1.1.6. However, the current master version is compatible with 1.2.1.
- Using Spring Boot 1.2.0 brings us sweet stuff like support for XA transactions with JTA. This means you can hook up your processes easily with JMS, JPA and Activiti logic all in the same transaction! ..Which brings us to the next point …
- In this example, we’ve focussed heavily on human interactions (and barely touched it). But there’s many things you can do around orchestrating systems too. The Spring Boot integration also has Spring Integration support you could leverage to do just that in a very neat way!
- And of course there is much much more about the BPMN 2.0 standard. Read more about itin the Activiti docs.