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 символов.
Предположим, у нас есть сложный объект с массой свойств. Существует два пути его обработки: сериализация по умолчанию в байт-код, либо сериализация в 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; } }
Теперь запустим приложение и посмотрим на то как выглядят переменные в сериализованном виде, а также проверим маппинг:
Исходный код проекта на GitHub: https://github.com/mstislavm/camundaBattle (ветка exercise_3).
Презентации проекта на GitHub: Reunico Camunda Presentations
Все материалы цикла:
Camunda для разработчика: Часть 1Camunda для разработчика: Часть 2
Camunda для разработчика: Часть 4
Читайте также:
Все, что вы хотели знать о Camunda, но боялись спроситьдругие статьи