Найти тему
REUNICO

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;
}
}

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

-2

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

Оригинал статьи и видео доступны по ссылке

Подписывайтесь на нас в соц. сетях, ставьте лайки и задавайте вопросы. 😊