Статьи

Начало работы с Activiti и Spring Boot

Этот пост является гостевым постом соучредителя Activiti и члена сообщества  Joram Barrez (@jbarrez),  который работает на Alfresco. Спасибо, Джорам! Я хотел бы видеть больше этих гостевых постов сообщества, поэтому, как обычно, не стесняйтесь  пинговать меня (@starbuxman)  идеями и предложениями! -Josh


Github . Проект был основан и спонсируется  Alfresco , но пользуется вкладом со всего мира и отраслей.

Определение процесса обычно визуализируется в виде диаграммы, подобной блок-схеме. В последние годы стандарт BPMN 2.0 (стандарт OMG, например, UML) стал де-факто «языком» этих диаграмм. Этот стандарт определяет, как следует интерпретировать определенную фигуру на диаграмме, как технически, так и с точки зрения бизнеса, и как она хранится в виде XML-файла, который не так уж и сложен, но, к счастью, большинство инструментов скрывает это за вами. Это стандарт, и вы можете использовать любое количество совместимых инструментов для проектирования (и даже запуска) ваших BPMN-процессов. Тем не менее, если вы спрашиваете меня, нет лучшего выбора, чем Activiti!

Джошем Лонгом . Пару месяцев назад мы с Джошем  провели вебинар,  который должен дать вам хорошее представление об основах интеграции Activiti для Spring Boot. Раздел  руководства пользователя Activiti по Spring Boot  также является отличной отправной точкой для получения дополнительной информации.

найти в моем репозитории Github .

Процесс, который мы реализуем здесь, является процессом найма разработчика. Конечно, это упрощено (так как оно должно уместиться на этой веб-странице), но вы должны получить основные понятия. Вот схема:

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

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

Это  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.

[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());

    }

  • editor in the cloud (also included in the .zip download you can get from Activiti'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.