software-development
March 11

Побег из IBM Websphere ESB

Рассказ про еще один наш проект категории «полный Пэ», от которого отказались обычные «соевые» разработчики.

Капитан Джек "Воробей" валит нах#й от тентаклей IBM увидев счет за услуги.

Ужасы из далекого прошлого

IBM Websphere ESB это некогда очень известная и популярная реализация «шины данных уровня предприятия» (Enterprise Service Bus). В настоящее время данный продукт является неподдерживаемым IBM и уже не продается.

Пик популярности шин данных как решения пришелся на начало 2000х, а вся популярность сошла совсем недавно — примерно к 2018му.

Разумеется в продакшне до сих пор осталось очень много интеграционных проектов на базе IBM Websphere ESB, которые никуда не делись и продолжают работать, гоняя XML-документы из одной кривой корпоративной системы в другую.

Один из таких проектов и достался нам для опытов.

Задача

Поскольку продукт Websphere ESB более не поддерживается, не выпускаются обновления, не исправляются ошибки и не решаются проблемы совместимости — у владельцев встал вопрос:

что делать с их (пока) работающим проектом, реализованным на базе IBM Websphere ESB.

Разумеется существует официальное решение для этой проблемы, от самой IBM, заключается в миграции клиентского проекта на новую платформу IBM Integration Bus — продолжателя идей Websphere ESB, но на другой технологической базе.

К сожалению поскольку IBM ушел из РФ и закрыл свои офисы, официально приобрести необходимые лицензии и проддержу не получится.

А для неофициальных вариантов оно все слишком дорогое. И это даже не наше личное мнение, а очень и очень небедного заказчика — со своим банком, самолетом и пароходом.

Разумеется при официльном решении еще необходимо мигрировать сам клиентский проект, чтобы он смог работать на новой IBM Integration Bus.

Своими силами и с божьей помощью.

Что ныне тоже проблематично, ввиду отсутствия и официальной поддержки и специалистов IBM и необходимых компетенций.

Поэтому было принято волевое решение о тотальной переделке проекта и переезда на более популярные и известные современным разработчикам технологии.

В качестве такой технологии был выбран Spring Framework, модули которого в совокупности закрывают все потребности клиентского проекта.

Хотя разумеется сам Spring был и остается технически сильно проще и меньше чем IBM Websphere ESB — это несравнимые технологии.

Среда разработки процессов для Mule ESB.

Обоснование

Полагаю подобная переделка вызовет определенные вопросы, особенно если у читающих есть опыт работы с ESB, SOA, SCA и всеми подобными технологиями.

Все же помимо IBM на свете есть и другие производители со своими интеграционными решениями, например очень популярные в мире MuleESB или Camunda.

Почему не мигрировать проект на подобную платформу, оставаясь в рамках концепции интеграционного решения?

На это есть весьма печальный и грустный ответ:

утрата компетенций.

Дело в том что специалистов с хоть каким-то опытом работы с любым ESB на рынке исчезающе мало, а различия между интеграционными платформами от разных вендоров — существенны.

Поэтому содержание подобного интеграционного проекта в перспективе 5-10 лет все также будет создавать проблемы с кадрами:

фактически компании придется сначала найти сотрудника, потом его дополнительно обучать и удерживать.

При этом стоит помнить что технологии BPM, BPEL, ESB — считаются устаревшими, поэтому никакого интереса у молодых разработчиков не вызывают.

Что касается вопросов производительности, то скорость работы решения на базе интеграционной шины очень сильно зависит от ее правильной настройки — т. е. напрямую от компетенций и специфических навыков администратора шины, а не разработчиков.

В случае же программного решения на Spring Framework — все вопросы производительности остаются на совести разработчиков и они в общем случае справляются лучше.

Словом одна из ключевых задач миграции:

продолжение развития интеграционного проекта, но силами обычных Java/Spring разработчиков и уход от закрытого решения крупного вендора (IBM).

Жизнь продолжается, вместе с ней продолжается и использование интеграции между системами.

Внешний вид запущенного BPEL-процесса в редакторе.

Прототип

Поскольку (с точки зрения владельцев проекта), подобная переделка является серьезным риском — прежде чем ее одобрить, нам было предложено реализовать прототип: 

продемонстрировать миграцию на небольшом учебном проекте для Websphere ESB.

Что и было сделано.

Был взят учебный проект «Order Processing», из галереи примеров для IBM Websphere Process Server 7.0, от 2009го года что примерно соответствовало возрасту и степени устаревания основного проекта заказчика.

Полный исходный код нашего прототипа выложен на Github, там же лежат оригинальные исходники учебного проекта — для сравнения.

Данный проект представляет собой интеграционную схему для обработки заказов условного крупного интернет-магазина.

Блок-схема выглядит вот так:

Данная схема связывает между собой CRM, платежный шлюз, систему складского учета и систему доставки — разумеется все условно и упрощенно, без реальных интеграций.

Также в данном учебном проекте используется BPM-процесс, причем не один а несколько, вот так выглядит их общая схема:

Обратите внимание на две точки старта — это важно, будет раскрыто чуть ниже.

Особенности реализации

В учебном проекте используется как последовательное так и параллельное выполнение шагов, а также есть стадии с перекладыванием данных из одной структуры в другую и разумеется огромное количество XSD-схем.

Помимо этого, есть шаги с программной логикой (скриптлетами), где в качестве скриптового языка используется Java:

<wpc:script>
        <wpc:javaCode>
        <![CDATA[
        String kind = ProductData.getString("kind");
        int number = ProductData.getInt("number");
        double price = 25.0;
        if("mercedes".equals(kind)) price = 35000.0;
        else if("porsche".equals(kind)) price = 55000.0;
        TotalAmount = new Double(ShippingPrice.doubleValue() + price * number);
        System.out.println("CalculateTotalAmount: " + TotalAmount);
      ]]>
      </wpc:javaCode>
</wpc:script>

Однако этот код не компилируется при сборке а интерпретируется во время работы процесса.

Помимо скриптлетов, в учебном проекте присутствуют и полноценные SCA-компоненты на Java, которые компилируются при сборке проекта - как это происходит в самом обычном Java-приложении:

/* 
"This sample program is provided AS IS and may be used, executed, copied and modified without royalty payment by customer (a) for its own instruction and study, (b) in order to develop applications designed to run with an IBM WebSphere product, either for customer's own internal use or for redistribution by customer, as part of such an application, in customer's own products."
Product 5655-FLW,  (C) COPYRIGHT International Business Machines Corp., 2005
All Rights Reserved * Licensed Materials - Property of IBM
*/
package bpc.samples.order;
import commonj.sdo.DataObject;
import com.ibm.websphere.sca.ServiceManager;
public class StockManagerServiceImpl {
	/**
	 * Default constructor.
	 */
	public StockManagerServiceImpl() {
		super();
	}
	/**
	 * Return a reference to the component service instance for this implementation
	 * class.  This should be used when passing this service to another reference api
	 * or if you want to invoke yourself asynchonously.
	 *
	 * @generated (com.ibm.wbit.java)
	 */
	private Object getMyService() {
		return (Object) ServiceManager.INSTANCE.locateService("self");
	}
	/**
	 * Method generated to support implemention of operation "checkAvailability" defined for WSDL port type 
	 * named "interface.StockManagerService".
	 * 
	 * The presence of commonj.sdo.DataObject as the return type and/or as a parameter 
	 * type conveys that its a complex type. Please refer to the WSDL Definition for more information 
	 * on the type of input, output and fault(s).
	 */
	public Boolean checkAvailability(DataObject productData) {		
		System.out.println("checkAvailability(DataObject) - ENTRY.");		
		String kind = "";
		int number = 0;		
		if(productData != null)
		{
			System.out.println("productData != null");
			kind = productData.getString("kind");
			number = productData.getInt("number");
			System.out.println("requested number of kind "  + kind + ": " + number);
		}		
		if(number > 0)
		{
			if(!"Pizza Funghi".equals(kind))
			{
				System.out.println("checkAvailability(DataObject) - EXIT. true");
				return Boolean.TRUE;
			}			
		}
		System.out.println("checkAvailability(DataObject) - EXIT. false");		
		return Boolean.FALSE;
	}
}

Еще одним важным моментом является использование внутренних вебсервисов на SOAP — они используются для взаимодействия между разными BPM-процессами.

И это все только учебный проект!

Диаграмма BPM-процесса в редакторе.

Миграция

Суть подобной миграции — не просто создание готового прототипа (он откровенно мало кому нужен), а выработка подходов и путей, которые будут применены для миграции уже большого интеграционного проекта.

Поэтому некоторые шаги могут показаться неоправданными:

слишком сложными, не масштабируемыми или потенциально проблемными для производительности.

Это нормально и ожидаемо, вопросы производительности вообще стоит решать только по мере их возникновения — во избежание повторения поговорки:

Premature optimization is the root of all evil.

Оно же «бессмысленная оптимизация — корень всех зол».

Также некоторые части были упрощены или удалены при переделке, в первую очередь это касается внутренних вебсервисов и всей BPM-обвязки (см. ниже).

Итоговый проект прототипа был выложен на Github.

Общие принципы

Опишу для начала общие принципы всего процесса переделки, чтобы было понимание происходящего.

  1. Результатом работы является веб-приложение на базе Spring Boot, реализующее входящий вебсервис SOAP (согласно спецификации в учебном проекте IBM Websphere ESB) и асинхронную обработку, полностью совпадающую с логикой оригинального проекта.
  2. SCA-компоненты и скриптлеты переделываются в стандартные бины Spring, с сохранением логики работы.
  3. Вместо BPM-процессов описываемых с помощью BPMN-нотации и запускаемых внутри BPM-движка, используется программная логика, зашитая непосредственно в код проекта. Вся «отделяемость» и стадия моделирования процессов полностью убирается.
  4. Все внутренние вебсервисы удаляются, бины Spring вызывают друг друга во время работы, в одном и том же контексте работающего приложения.
  5. Часть компонентов, используемых для передачи данных и в качестве обвязки к скриптлетам были удалены, вместо них используется прямой вызов скриптлета.

"Было-стало", слева код на Spring, а справа - оригинальный SCA-компонент.

Сборка и XSD-схемы

Болванка проекта была сгенерирована с помощью обычного Spring Initializr, с указанием использовать Apache Maven и последние стабильные версии Java и Spring.

После чего внутрь были перенесены все XSD и WSDL файлы из оригинального интеграционного проекта для IBM Websphere ESB.

Выглядит вот так:

Обратите внимание, что как в WSDL так и в XSD-схемах присутствуют относительные пути:

Поэтому схема размещения должна совпадать с таковой из оригинального проекта.

Для генерации POJO-классов на основе XSD-схем был добавлен плагин jaxb-maven-plugin, последней версии — с поддержкой Jakarta API:

<plugin>
	<groupId>org.jvnet.jaxb</groupId>
	<artifactId>jaxb-maven-plugin</artifactId>
	<version>4.0.3</version>
	<executions>
		<execution>
			<id>charging</id>
			<goals>
				<goal>generate</goal>
			</goals>
			<configuration>
				<generateDirectory>target/generated-sources/charging/</generateDirectory>
				<schemaDirectory>${basedir}/src/main/resources/xsd/ChargingLibrary/bpc/samples/charging</schemaDirectory>
			</configuration>
	    </execution>
...

Результатом его работы являются генерируемые .java файлы в каталоге target/classes/generated-sources,которые добавляются в classpath при сборке проекта.

Пример:

//
// This file was generated by the Eclipse Implementation of JAXB, v4.0.4 
// See https://eclipse-ee4j.github.io/jaxb-ri 
// Any modifications to this file will be lost upon recompilation of the source schema. 
//
package bpc.samples.charging.charging;

import bpc.samples.charging.ChargingRequest;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlType;
/**
 * <p>Java class for anonymous complex type</p>.
 * 
 * <p>The following schema fragment specifies the expected content contained within this class.</p>
 * 
 * <pre>{@code
 * <complexType>
 *   <complexContent>
 *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
 *       <sequence>
 *         <element name="chargingRequest" type="{http://bpc/samples/charging}ChargingRequest"/>
 *       </sequence>
 *     </restriction>
 *   </complexContent>
 * </complexType>
 * }</pre>   
 */
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
    "chargingRequest"
})
@XmlRootElement(name = "charge")
public class Charge {
    @XmlElement(required = true, nillable = true)
    protected ChargingRequest chargingRequest;
    /**
     * Gets the value of the chargingRequest property.
     * 
     * @return
     *     possible object is
     *     {@link ChargingRequest }
     *     
     */
    public ChargingRequest getChargingRequest() {
        return chargingRequest;
    }
    /**
     * Sets the value of the chargingRequest property.
     * 
     * @param value
     *     allowed object is
     *     {@link ChargingRequest }
     *     
     */
    public void setChargingRequest(ChargingRequest value) {
        this.chargingRequest = value;
    }
}

Обратите внимание на использование аннотаций из пакета jakarta.xml.bind.annotation а не javax.xml.bind.annotation, это и есть новое Jakarta API.

Данные классы будут использоваться в проекте в качестве DTO.

Входящий вебсервис

В оригинальном учебном интеграционном проекте, запуск процесса осуществлялся двумя способами:

вручную из интерфейса и путем вызова вебсервиса.

Чтобы не тратить время на разработку интерфейса, мы ограничились реализацией только входящего вебсервиса, с использованием Spring WS.

После запуска проекта, входящий вебсервис будет доступен по адресу:

http://localhost:8080/ws/order.wsdl

Настройка входящего вебсервиса осуществляется через специальный класс:

package com.Ox08.experiments.migrated.esb;
import jakarta.servlet.Servlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.ws.config.annotation.EnableWs;
import org.springframework.ws.config.annotation.WsConfigurerAdapter;
import org.springframework.ws.transport.http.MessageDispatcherServlet;
import org.springframework.ws.wsdl.wsdl11.SimpleWsdl11Definition;
import org.springframework.ws.wsdl.wsdl11.Wsdl11Definition;
/**
 * Spring WS Configuration
 */
@EnableWs
@Configuration
public class WebServiceConfig extends WsConfigurerAdapter {
    @Bean
    public ServletRegistrationBean<Servlet> messageDispatcherServlet(
            ApplicationContext applicationContext) {
        MessageDispatcherServlet servlet =
                new MessageDispatcherServlet();
        servlet.setApplicationContext(applicationContext);
        return new ServletRegistrationBean<>(servlet,
                "/ws/*");
    }
    @Bean(name = "order")
    public Wsdl11Definition defaultWsdl11Definition() {
        SimpleWsdl11Definition wsdl11Definition =
                new SimpleWsdl11Definition();
        wsdl11Definition
                .setWsdl(new ClassPathResource("/xsd/OrderLibrary/bpc/samples/order/Order.wsdl"));
        return wsdl11Definition;
    }
}

Тут задается общий контект для всех вебсервисов /ws и указывается что необходимо использовать WSDL-файл из ресурсов по адресу:

/xsd/OrderLibrary/bpc/samples/order/Order.wsdl

Помимо этого, необходимо создать класс с аннотацией @Endpoint с описанием методов вебсервиса:

package com.Ox08.experiments.migrated.esb;
import bpc.samples.order.CheckCustomerResponse;
import bpc.samples.order.PersonalData;
import bpc.samples.order.ProductData;
import bpc.samples.order.order.EnterPersonalData;
import bpc.samples.order.order.EnterProductData;
import com.Ox08.experiments.migrated.esb.order.CustomerVerificationService;
import com.Ox08.experiments.migrated.esb.order.FileOrder;
import com.Ox08.experiments.migrated.esb.order.StockManagerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;
import org.springframework.ws.server.endpoint.annotation.ResponsePayload;
@Endpoint
public class OrderEndpoint {
    private static final Logger LOGGER = LoggerFactory.getLogger(OrderEndpoint.class);
    @Autowired
    private StockManagerService sms;
    @Autowired
    private CustomerVerificationService cvs;
    @Autowired
    private ProcessService ps;
    @Autowired
    private InputRequestService rs;
    @PayloadRoot(
            namespace = "http://bpc/samples/order/Order",
            localPart = "enterPersonalData")
    @ResponsePayload
    public void enterPersonalData(@RequestPayload EnterPersonalData personalData) {
        LOGGER.info("Entering personal data");
        PersonalData pd = personalData.getPersonalData();
        ProcessContext ctx = new ProcessContext();
        ctx.set("OrderNo",pd.getOrderNo());
        ctx.set("PersonalData", pd);
        CheckCustomerResponse resp = cvs.checkCustomer(pd);
        /**
         *   <bpws:source linkName="Link3">
         *           <bpws:transitionCondition><![CDATA[return CheckCustomerResponse.getBoolean("isCustomerOk");]]></bpws:transitionCondition>
         *         </bpws:source>
         */
        if (resp.isIsCustomerOk()) {
            ctx.set("CheckCustomerResponse",resp);
            rs.checkAdd(pd.getOrderNo(),ctx);
        } else {
            ps.call("FileOrder",ctx);
        }
    }
    @PayloadRoot(
            namespace = "http://bpc/samples/order/Order",
            localPart = "enterProductData")
    @ResponsePayload
    public void enterProductData(@RequestPayload EnterProductData productData) {
        LOGGER.info("Entering product data");
        ProductData pd = productData.getProductData();
        ProcessContext ctx = new ProcessContext();
        ctx.set("OrderNo",pd.getOrderNo());
        ctx.set("ProductData", pd);
        /**
         *   <bpws:source linkName="Link6">
         *           <bpws:transitionCondition><![CDATA[return IsProductAvailable.booleanValue();
         * ]]></bpws:transitionCondition>
         */

        boolean isAvailable = sms.checkAvailability(pd);
        ctx.set("IsProductAvailable",isAvailable);
        if (isAvailable) {
            rs.checkAdd(pd.getOrderNo(),ctx);
        } else {
            ps.call("FileOrder",ctx);
        }
    }
}

Обратите внимание на довольно сложный код внутри — тут начинается программная логика обработки процесса, которую я раскрою чуть ниже.

Две точки запуска процесса

В оригинальном учебном процессе используется несколько нестандартный механизм запуска:

нужно вызвать два разных метода входящего вебсервиса, каждый из которых добавит свою часть данных в контекст процесса.

Только после формирования полного набора данных происходит переход на следующий шаг.

Для реализации этой логики, был реализован отдельный сервис

com.Ox08.experiments.migrated.esb.InputRequestService

с «in-memory» хранилищем входящих запросов:

// in-memory request storage
private final Map<String, InputRequest> requests = new HashMap<>();

При обработке входящего запроса, присходит вызов метода этого сервиса, который проверяет по номеру заказа (orderNo) были ли уже загружены другие части данных и если были то добавляет в существующий запрос новую часть:

/**
 * Add part of request
 * @param orderNo
 *          order number
 * @param ctx
*/
public void checkAdd(String orderNo, ProcessContext ctx) {
 final InputRequest r;
 if (requests.containsKey(orderNo)) {
      r = requests.get(orderNo);
      // copies all variables into existing request's context
      r.ctx.fillFrom(ctx);
 } else {
      r = new InputRequest(ctx);
      requests.put(orderNo, r);
 }
 LOGGER.info("add part for order: {}  info: {}" , orderNo, ctx.dumpInfo());
}

А в фоне с заданной переодичностью происходит вызов другого метода:

/**
* Background processing, that fires actual process if both parts of request received
*/
@Scheduled(initialDelay = 0, fixedDelay = 5000)
void checkProcess() {
        if (requests.isEmpty()) {
            return;
        }
        LOGGER.info("found requests: {}" , requests.size());
        for (String orderNo : requests.keySet()) {
            InputRequest request = requests.get(orderNo);
            if (System.currentTimeMillis() - request.createdAt > EXPIRATION_TIME) {
                requests.remove(orderNo);
                LOGGER.info("order {} expired",orderNo);
                continue;
            }
            if (request.ctx.contains("PersonalData") && request.ctx.contains("ProductData")) {
                LOGGER.info("both personalData and productData has been added, start processing order {}",orderNo);
                ps.call("PreparePriceRequest",request.ctx);
                requests.remove(orderNo);
            }
        }
}

Данный метод проверяет все запросы в хранилище и если они не устарели — проверяет наличие всех частей данных:

if (request.ctx.contains("PersonalData") && request.ctx.contains("ProductData")) {
        ..

Если все нужные данные присутствуют — происходит вызов следующего шага обработки:

 ps.call("PreparePriceRequest",request.ctx);

Миграция с BPM

Следующим важным этапом является уход от BPM на полностью программную логику, что безусловно технологически является серьезным упрощением проекта.

Для того чтобы сохранить логику внутри скриптлетов, были реализованы некоторые элементы BPM-движка, в частности контекст выполнения:

package com.Ox08.experiments.migrated.esb;
import java.util.HashMap;
import java.util.Map;
/**
 * This class is used as BPM process context storage, to keep properties changes between steps. *
 * Similar to BPM variables, defined in .bpel file.
 */
public class ProcessContext {
    // in-memory storage for variables
    private final Map<String, Object> context = new HashMap<>();
    /**
     * Set process property/variable
     *
     * @param name
     * @param value
     */
    public void set(String name, Object value) {
        this.context.put(name, value);
    }
    /**
     * Checks if property/variable is defined
     * @param name
     * @return
     */
    public boolean contains(String name) {
        return name != null && context.containsKey(name);
    }
    /**
     * Gets process variable with specified type
     * @param name
     * @param clazz
     * @return
     * @param <T>
     */
    public <T> T get(String name, Class<T> clazz) {
        return contains(name) ? clazz.cast(context.get(name)) : null;
    }
    /**
     * Dumps all variables names into string
     * @return
     */
    public String dumpInfo() {
        return String.join(",", context.keySet());
    }
    /**
     * Fills variables from another context
     * @param another
     */
    public void fillFrom(ProcessContext another) {
        this.context.putAll(another.context);
    }
}

Для понимания механизма работы, взгляните вот на такой оригинальный код скриптлета:

String kind = ProductData.getString("kind");
int number = ProductData.getInt("number");
double price = 25.0;
if("mercedes".equals(kind)) price = 35000.0;
else if("porsche".equals(kind)) price = 55000.0;
TotalAmount = new Double(ShippingPrice.doubleValue() + price * number);
System.out.println("CalculateTotalAmount: " + TotalAmount);

Обратите внимание что тут отсутствует инициализация переменных ProductData, ShippingPrice и TotalAmount — они являются глобальными для всего BPM-процесса:

<bpws:variables>
    <bpws:variable name="ProductData" type="ns1:ProductData" wpc:id="3"/>
    <bpws:variable name="ShippingPrice" type="xsd:double" wpc:id="9"/>
    <bpws:variable name="TotalAmount" type="xsd:double" wpc:id="10"/>
</bpws:variables>

С помощью сервиса выше, код скриптлета изменяется следующим образом:

public void exec(ProcessContext ctx) {
        ProductData productData = ctx.get("ProductData", ProductData.class);
        String kind = productData.getKind();
        int number = productData.getNumber();
        double price = 25.0;
        if ("mercedes".equals(kind)) price = 35000.0;
        else if ("porsche".equals(kind)) price = 55000.0;
        Double shippingPrice = ctx.get("ShippingPrice", Double.class);
        Double totalAmount = shippingPrice + price * number;
        ctx.set("TotalAmount", totalAmount);
        LOGGER.info("CalculateTotalAmount: {}" , totalAmount);
        nextStep(ctx);
}

Как видите код находится внутри специального метода, в который передается экземпляр объекта ProcessContext, с уже заполненными данными времени выполнения — это частичная реализация механизма работы BPM-движка, скрытая от разработчика BPMN-процесса в IBM WESB.

Обратите внимание на вызов метода nextStep() , который является программным вызовом следующего шага процесса.

Это отличается от типичной для BPM-движка логики state-machine, когда сам движок занимается вызовом каждого шага и переходами.

Однако такой подход с программным вызовом является более гибким в полностью управляемой среде Spring Framework:

можно делать повторные вызовы, обрабатывать ошибки и делать дополнительные вызовы внешних сервисов.

Без необходимости оповещать об этом BPM-движок и фиксировать состояние процесса.

Для связывания отдельных шагов в процесс, был реализован еще один элемент BPM-движка, отвечающий за хранение метаданных:

package com.Ox08.experiments.migrated.esb;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class ProcessService {
     // in-memory хранилище меданных о шагах процесса
    private final Map<String, ProcessDefinition> defs = new HashMap<>();
    /**
       executes next step of a process
    */
    public void nextStep(String current, ProcessContext ctx) {
        if (!defs.containsKey(current)) return;        
        ProcessDefinition d = defs.get(current);
        if (d.nextSteps.isEmpty()) return;        
        for (String s : d.nextSteps) {
            if (!defs.containsKey(s)) continue;            
            ProcessDefinition nd = defs.get(s);
            if (d.async) 
                runAsync(nd.step, ctx);
             else 
                nd.step.exec(ctx);            
        }
    }
    /**
       вызов произвольного(!) шага процесса
    */
    public void call(String name, ProcessContext ctx) {
        if (!defs.containsKey(name)) return;        
        ProcessDefinition d = defs.get(name);
        d.step.exec(ctx);
    }
    /**
       adds process step metadata
    */
    void addStep(ProcessStep step, boolean async, List<String> nextSteps) {
        defs.put(step.getClass().getSimpleName(), new ProcessDefinition(step, async, nextSteps));
    }    
    @Async
    void runAsync(ProcessStep step, ProcessContext ctx) {
        step.exec(ctx);
    }
    static class ProcessDefinition {
        private final ProcessStep step;
        private final boolean async;
        private final List<String> nextSteps;
        ProcessDefinition(ProcessStep step, boolean async, List<String> nextSteps) {
            this.step = step; this.async = async; this.nextSteps = nextSteps;
        }
    }
}

С его помощью программное связывание шагов в процесс выглядит вот так:

@PostConstruct
void onInit() {
		ps.addStep(applicationContext.getBean(FileOrder.class), false, Collections.emptyList());
		ps.addStep(applicationContext.getBean(PreparePriceRequest.class), false, List.of("Calculate"));
		ps.addStep(applicationContext.getBean(Calculate.class), false, List.of("CalculateTotalAmount"));
		ps.addStep(applicationContext.getBean(CalculateTotalAmount.class), true, List.of("PrepareCharging", "PrepareShippingOrder"));
		ps.addStep(applicationContext.getBean(PrepareCharging.class), false, List.of("ChargeService"));
		ps.addStep(applicationContext.getBean(PrepareShippingOrder.class), false, List.of("PrepareAcknowledgement"));
		ps.addStep(applicationContext.getBean(PrepareAcknowledgement.class), false, List.of("PrepareReport"));
		ps.addStep(applicationContext.getBean(PrepareReport.class), false, List.of("FileOrder"));
}

Последний агрумент в вызове это названия следующих шагов для перехода, а булевая переменная отвечает за то будет вызов следующего шага асинхронным или нет.

Для приведения к единообразию был реализован интерфейс, декларирующий единственный метод:

package com.Ox08.experiments.migrated.esb;
public interface ProcessStep {
    void exec(ProcessContext ctx);
}

Этот метод является точкой входа в каждый компонент, участвующий в бизнес-процессе, используется для универсального вызова шага:

public void call(String name, ProcessContext ctx) {
        if (!defs.containsKey(name)) {
            return;
        }
        ProcessDefinition d = defs.get(name);
        // вызов выполнения
        d.step.exec(ctx);
}

Скриптлеты и SCA-компоненты

Как уже было упомянуто выше, все скриптлеты и полноценные SCA-компоненты с программной логикой были перенесены в отдельные бины Spring с сохранением именования.

Но для сохранения визуального разделения между скриптлетом и SCA — чтобы потом можно было найти концы, вторые были переделаны в сервисы (аннотация @Service) и без использования универсального интерфейса ProcessStep.

Неповторимый оригинал SCA-сервиса StockManagerServiceImpl:

/* 
"This sample program is provided AS IS and may be used, executed, copied and modified without royalty payment by customer (a) for its own instruction and study, (b) in order to develop applications designed to run with an IBM WebSphere product, either for customer's own internal use or for redistribution by customer, as part of such an application, in customer's own products."
Product 5655-FLW,  (C) COPYRIGHT International Business Machines Corp., 2005
All Rights Reserved * Licensed Materials - Property of IBM
*/
package bpc.samples.order;
import commonj.sdo.DataObject;
import com.ibm.websphere.sca.ServiceManager;
public class StockManagerServiceImpl {
	/**
	 * Default constructor.
	 */
	public StockManagerServiceImpl() {
		super();
	}
	/**
	 * Return a reference to the component service instance for this implementation
	 * class.  This should be used when passing this service to another reference api
	 * or if you want to invoke yourself asynchonously.
	 *
	 * @generated (com.ibm.wbit.java)
	 */
	private Object getMyService() {
		return (Object) ServiceManager.INSTANCE.locateService("self");
	}
	/**
	 * Method generated to support implemention of operation "checkAvailability" defined for WSDL port type 
	 * named "interface.StockManagerService".
	 * 
	 * The presence of commonj.sdo.DataObject as the return type and/or as a parameter 
	 * type conveys that its a complex type. Please refer to the WSDL Definition for more information 
	 * on the type of input, output and fault(s).
	 */
	public Boolean checkAvailability(DataObject productData) {		
		System.out.println("checkAvailability(DataObject) - ENTRY.");		
		String kind = "";
		int number = 0;		
		if(productData != null)
		{
			System.out.println("productData != null");
			kind = productData.getString("kind");
			number = productData.getInt("number");
			System.out.println("requested number of kind "  + kind + ": " + number);
		}		
		if(number > 0)
		{
			if(!"Pizza Funghi".equals(kind))
			{
				System.out.println("checkAvailability(DataObject) - EXIT. true");
				return Boolean.TRUE;
			}			
		}
		System.out.println("checkAvailability(DataObject) - EXIT. false");		
		return Boolean.FALSE;
	}
}

Жалкая копия (на Spring):

package com.Ox08.experiments.migrated.esb.order;
import bpc.samples.order.ProductData;
import com.Ox08.experiments.migrated.esb.charging.ChargeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class StockManagerService {
    private static final Logger LOGGER = LoggerFactory.getLogger(StockManagerService.class);
    /**
     * Method generated to support implemention of operation "checkAvailability" defined for WSDL port type
     * named "interface.StockManagerService".
     *
     * The presence of commonj.sdo.DataObject as the return type and/or as a parameter
     * type conveys that its a complex type. Please refer to the WSDL Definition for more information
     * on the type of input, output and fault(s).
     */
    public Boolean checkAvailability(ProductData productData) {
        LOGGER.info("checkAvailability(DataObject) - ENTRY.");
        String kind = "";
        int number = 0;
        if(productData != null)
        {
            LOGGER.info("productData != null");
            kind = productData.getKind();//.getString("kind");
            number = productData.getNumber();//.getInt("number");
            LOGGER.info("requested number of kind {} : {}"  , kind , number);
        }
        if(number > 0)
        {
            if(!"Pizza Funghi".equals(kind))
            {
                LOGGER.info("checkAvailability(DataObject) - EXIT. true");
                return Boolean.TRUE;
            }
        }
        LOGGER.info("checkAvailability(DataObject) - EXIT. false");
        return Boolean.FALSE;
    }
}

Как видите ключевое отличие тут — отказ от использования DataObject, который является частью SCA API и переход на обычные POJO.

Метод:

private Object getMyService() {
		return (Object) ServiceManager.INSTANCE.locateService("self");
}

в данном скриптлете (и во всех остальных) не используется, поэтому не был перенесен в версию на Spring.

Но сам этот паттерн является откровенно устаревшим сам по-себе, поэтому места где используется такая «ссылка на себя» точно стоит переработать.

Эпилог

Эта статья призвана показать что даже столь устаревшие интеграционные проекты — не приговор, даже без возможности получить официальную поддержку от вендора.

Мы показали лишь один из возможных, но далеко не единственный вариант миграции интеграционного проекта на базе шины данных, подошедший для конкретной ситуации.

Разумеется способов и путей сделать подобное много, разной сложности и упоротости — пишите если есть необходимость в подобной миграции, проконсультируем и поможем.

P.S.

Наш прототип был принят заказчиком.

По итогам его изучения был дан зеленый свет и теперь мы занимаемся миграцией уже большого проекта с IBM Websphere ESB, используя описанный выше путь в качестве образца.