Материалы с третьего воркшопа "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).
Оригинал статьи и видео доступны по ссылке
Подписывайтесь на нас в соц. сетях, ставьте лайки и задавайте вопросы. 😊