REUNICO | Разработка и сопровождение ПО: Camunda для разработчика - Часть 3

Camunda для разработчика - Часть 3

Материалы с третьего воркшопа "Camunda для разработчика". Продолжаем доработку процессного приложения Camunda BPM Spring Boot.

Вопросы, затронутые в рамках воркшопа:

  • Http Connector
  • Camunda Spin
  • Сложный объект в процессе
  • Сериализация / десериализация
  • Маппинг

Ссылка на видео и исходный код под катом.

Сегодня будем использовать сложные объекты в процессе, выполним сериализацию и десериализацию этих объектов, а также посмотрим на то, как использовать Camunda Spin и HTTP Connector.

Развитие проекта будет заключаться в следующем: каждый элемент булевой коллекции мы заменим на сложный объект. Чтобы поместить такие объекты в контекст процесса нам понадобится механизм сериализации - процесс превращения объекта в байт-код. Используя же Camunda Spin можно преобразовать объект не в байт-код, а в человекочитаемый формат JSON или XML и сохранить в переменную процесса. В Camunda есть ряд примитивных типов данных (с точки зрения Java это, конечно, не примитивы, а сложные объекты): boolean, bytes, short, integer, long, double, date, string, null. При сохранении состояния процесса, то есть при сохранении переменных в базу, под каждый тип данных отведено свое поле. Для String существует ограничение в 4000 символов.

Camunda для разработчика: Типы данных

Предположим, у нас есть сложный объект с массой свойств. Существует два пути его обработки: сериализация по умолчанию в байт-код, либо сериализация в JSON или XML с использованием Camunda Spin. Кроме этого следует отметить, что помещать целиком бизнес контекст в контекст процесса не рекомендуется. В процесс следует помещать только те свойства объекта, которые необходимы для принятия каких-либо решений в процессе, либо для связывания данных с бизнес сущностью. Например: есть заявление на выдачу кредита, которое обладает определенными атрибутами. Часть этих атрибутов, как допустим, идентификатор заемщика, сумма кредита - необходимы для принятия решения и вы их помещаете в процесс. Атрибут "номер заявления", который позволить связать конкретный экземпляр процесса с бизнес сущностью в базе, тоже следует поместить в процесс. Избыточные данные типа фотографий, вложенных документов и прочее категорически не рекомендуется помещать в контекст процесса.

Новые функции приложения:

Взаимодействие с HTTP/REST сервисом - HTTP connector
Сериализация, десериализация, маппинг POJO - Camunda Spin

Нам понадобятся следующие зависимости

Коннектор:

camunda-connect-core
camunda-connect-connectors-all

Spin:

camunda-spin-core
camunda-engine-plugin-spin
camunda-spin-dataformat-json-jackson

При использование camunda-spin-dataformat-all будет невозможно использование аннотации @JsonIgnoreProperties(value = { "myValue" }), она просто не сработает. Если необходима сериализация и в JSON, и в XML лучше подключать две зависимости по отдельности, чем одну camunda-spin-dataformat-all.

Перейдем к написанию класса Warrior. Класс должен имплементировать интерфейс Serializable. Зададим следующие свойства: имя, титул, количество жизней, статус (жив/мертв), а также общее для всех сериализуемых объектов поле. Для инициализации полей воспользуемся конструктором, но это приведет к некорректной сериализации объекта. Чтобы избежать этой проблемы зададим еще и пустой конструктор. Кроме этого сгенерируем геттеры и сеттеры. Для дальнейшего удобства маппинга тела GET-запроса на сущность воина добавим к полям аннотации @JsonAlias("fieldName"), а на класс добавим @JsonIgnoreProperties(ignoreUnknown = true) для игнорирования всех неизвестных свойств.

    package com.reunico.demo.domain;
 
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 
import java.io.Serializable;
 
@JsonIgnoreProperties(ignoreUnknown = true)
public class Warrior implements Serializable {
 
 
    private static final long serialVersionUID = 1L;
 
    @JsonAlias("name.findName")
    private String name;
 
    @JsonAlias("name.title")
    private String title;
 
    private Boolean isAlive;
 
    @JsonAlias("random.number")
    private Integer hp;
 
    public Warrior() {
 
    }
 
 
    public Warrior(String name, String title, Boolean isAlive, Integer hp) {
        this.name = name;
        this.title = title;
        this.isAlive = isAlive;
        this.hp = hp;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getTitle() {
        return title;
    }
 
 
    public void setTitle(String title) {
        this.title = title;
    }
 
    public Boolean getAlive() {
        return isAlive;
    }
 
    public void setAlive(Boolean alive) {
        isAlive = alive;
    }
 
    public Integer getHp() {
        return hp;
    }
 
    public void setHp(Integer hp) {
        this.hp = hp;
    }
}

Что заполнить сущности воинов используем faker.js. Фейкер выполнен в виде REST-сервиса к которому мы посылаем GET-запрос с требуемыми параметрами. ULR фейкера добавим в файл свойств application.yaml:

url: https://demo.reunico.com/faker/api/generate?property=name.findName&property=name.title&property=random.number&locale=tr

Для генерации сущностей будем использовать класс PrepareToBattle. Модифицируем его: добавим новое поле с названием url. Сделаем отдельный метод, который будет создавать воинов. Сначала инициализируем http-connector. Сконструируем запрос, указав его тип и url с помощью методов объекта httpConnector. Также можно задать необходимые header-ы. Для получения тела ответа сделаем проверку на statusCode и наличие контента ответа. response.getResponse() - вернет JSON объект, который можно распарсить с помощью Camunda Spin. Инициализацию воина можно провести двумя способами: с помощью геттеров и сеттеров (не очень удобный способ), с помощью Camunda Spin. В классе Warrior мы подготовили все необходимое для маппинга, а именно: игнорирование всех неизвестных свойств входящего JSON, соответствие названий атрибутов JSON-объекта и сущности воина.

    private Warrior create() {
 
        Warrior warrior = null;
        HttpConnector httpConnector = Connectors.getConnector(HttpConnector.ID);
        HttpRequest request = httpConnector.createRequest()
                .url(url)
                .get();
        Map headers = new HashMap<>();
        headers.put("Content-type", "application/json");
 
        request.setRequestParameter("headers", headers);
 
        HttpResponse response = request.execute();
 
        if (response.getStatusCode() == 200 || !response.getResponse().isEmpty()) {
            SpinJsonNode node = JSON(response.getResponse());
            warrior.setAlive(true);
            /*
            Первый способ инициализировать воина.
            warrior.setTitle(node.prop("name.title").stringValue());
            warrior.setName(node.prop("name.findName").stringValue());
            warrior.setHp(Integer.parseInt(node.prop("random.number").stringValue()));
            */
 
            warrior = JSON(response.getResponse()).mapTo(Warrior.class);
        }
        response.close();
        return warrior;
    }

Чтобы сериализация объекта происходила в формат JSON нужно дописать в класс ObjectValue jsonArmy = Variables.objectValue(army).serializationDataFormat("application/json").create(); Формат сериализации по-умолчанию также можно задать через файл application.yaml:

    camunda:
    bpm:
        default-serialization-format: application/json

Финальная версия класса PrepareToBattle:

    package com.reunico.demo;
 
import com.reunico.demo.domain.Warrior;
import org.camunda.bpm.engine.delegate.BpmnError;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.camunda.bpm.engine.variable.Variables;
import org.camunda.bpm.engine.variable.value.ObjectValue;
import org.camunda.connect.Connectors;
import org.camunda.connect.httpclient.HttpConnector;
import org.camunda.connect.httpclient.HttpRequest;
import org.camunda.connect.httpclient.HttpResponse;
import org.camunda.spin.json.SpinJsonNode;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
 
import java.util.*;
import static org.camunda.spin.Spin.JSON;
 
@Component
public class PrepareToBattle implements JavaDelegate {
 
    @Value("${maxWarriors}")
    private int maxWarriors;
 
    @Value("${url}")
    private String url;
 
    @Override
    public void execute(DelegateExecution delegateExecution) throws Exception {
 
        int warriors = (int) delegateExecution.getVariable("warriors");
        int enemyWarriors = (int) (Math.random() * 100);
        maxWarriors = maxWarriors == 0 ? 100 : maxWarriors;
 
        if (warriors < 1 || warriors > maxWarriors) {
            throw new BpmnError("warriorsError");
        }
 
        List army = new ArrayList<>();
 
        for(int i = 0; i <= warriors; i++) {
            army.add(create());
        }
 
        System.out.println("Prepare to battle! Enemy army = " + enemyWarriors + " vs. our army: " + warriors);
 
 
        ObjectValue jsonArmy = Variables.objectValue(army).serializationDataFormat("application/json").create();
 
        delegateExecution.setVariable("army", army);
        delegateExecution.setVariable("jsonArmy", jsonArmy);
        delegateExecution.setVariable("enemyWarriors", enemyWarriors);
    }
 
    private Warrior create() {
        Warrior warrior = null;
 
        HttpConnector httpConnector = Connectors.getConnector(HttpConnector.ID);
        HttpRequest request = httpConnector.createRequest()
                .url(url)
                .get();
        
        Map headers = new HashMap<>();
        headers.put("Content-type", "application/json");
        request.setRequestParameter("headers", headers);
 
        HttpResponse response = request.execute();
 
 
        if (response.getStatusCode() == 200 || !response.getResponse().isEmpty()) {
            SpinJsonNode node = JSON(response.getResponse());
            warrior = JSON(response.getResponse()).mapTo(Warrior.class);
        }
        response.close();
        return warrior;
    }
}

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

Camunda для разработчика: Маппинг и сериализация данных

Исходный код проекта на GitHub: https://github.com/mstislavm/camundaBattle (ветка exercise_3).

Презентации проекта на GitHub: Reunico Camunda Presentations