Экспресс-сервис для параллельного вызова SOAP менее чем за 25 строк кода

ekspress servis dlya parallelnogo vyzova soap menee chem za 25 strok

Обзор

Предположим, есть сервис, который имеет следующие функции:

  1. Он предоставляет конечную точку REST, получающую список запросов.
  2. Он параллельно вызывает службу SOAP один раз на элемент в списке запросов.
  3. Он возвращает преобразованный результат из XML в JSON.

Исходный код этой службы может выглядеть примерно так, используя Node.js, Express и руководство по стилю JavaScript Airbnb:

'use strict';

const { soap } = require('strong-soap');
const expressApp = require('express')();
const bodyParser = require('body-parser');

const url="
const clientPromise = new Promise((resolve, reject) => (
    soap.createClient(url, {}, (err, client) => err ? reject(err) : resolve(client))
));

expressApp.use(bodyParser.json())
    .post('/parallel-soap-invoke', (req, res) => (clientPromise.then(client => ({ client, requests: req.body }))
        .then(invokeOperations)
        .then(results => res.status(200).send(results))
        .catch(({ message: error }) => res.status(500).send({ error }))
    ))
    .listen(3000, () => console.log('Waiting for incoming requests.'));

const invokeOperations = ({ client, requests }) => (Promise.all(requests.map(request => (
    new Promise((resolve, reject) => client.Add(request, (err, result) => (
        err ? reject(err) : resolve(result))
    ))
))));

Образец запроса:

POST /parallel-soap-invoke
[
  {
    "intA": 1,
    "intB": 2
  },
  {
    "intA": 3,
    "intB": 4
  },
  {
    "intA": 5,
    "intB": 6
  }
]

Образец ответа:

HTTP/1.1 200
[
  {
    "AddResult": 3
  },
  {
    "AddResult": 7
  },
  {
    "AddResult": 11
  }
]

Тесты показывают, что один прямой запрос в службу SOAP с помощью SOAPUI занимает ~430 мс (с того места, где я нахожусь в Чили). Отправка трех запросов (как показано выше) занимает ~400 мс для вызовов в службу Express (кроме первого, который получает WSDL и создает клиент).

Почему больше запросов занимает меньше времени? В основном потому, что XML не проверяется тщательно, как в обычном SOAP, поэтому если эта мягкая проверка не соответствует вашим ожиданиям, вам следует рассмотреть дополнительные функции или решения.

Интересно, как это будет выглядеть при использовании async/await? Вот так (результаты те же):

'use strict';

const { soap } = require('strong-soap');
const expressApp = require('express')();
const bodyParser = require('body-parser');

const url="
const clientPromise = new Promise((resolve, reject) => (
    soap.createClient(url, {}, (err, client) => err ? reject(err) : resolve(client))
));

expressApp.use(bodyParser.json())
    .post('/parallel-soap-invoke', async (req, res) => {
        try {
            res.status(200).send(await invokeOperations(await clientPromise, req.body));
        } catch ({message: error}) {
            res.status(500).send({ error });
        }
    })
    .listen(3000, () => console.log('Waiting for incoming requests.'));

const invokeOperations = (client, requests) => (Promise.all(requests.map(request => (
    new Promise((resolve, reject) => client.Add(request, (err, result) => (
        err ? reject(err) : resolve(result))
    ))
))));

На следующем изображении представлено представление о том, как работает код:

1*WFN937ih5z83fc_gNqWVqw

Целью этой статьи является показать простоту использования JavaScript для задач в корпоративном мире, например, для вызова служб SOAP. Если вы знакомы с JavaScript, это в основном просто a Promise.all в дополнение к нескольким обещанным обратным вызовам под конечной точкой Express. Вы можете перейти непосредственно в раздел 4 (Бонусный трек), если вы думаете, что это может быть полезно для вас.

Если вы находитесь за пределами мира JavaScript, я думаю, что 24 строки кода для трех функций, о которых я упоминал в начале, это очень хорошо. Теперь я перейду к деталям.

1. Раздел Экспресс

Начнём с кода, связанного с Express, минимальным и гибким фреймворком веб-приложений Node.js. Это довольно просто, и вы можете найти его где угодно, поэтому я дам краткое описание.

'use strict';

 // Express framework.
const express = require('express');
// Creates an Express application.
const app = express();

/**
 * Creates a GET (which is defined by the method invoked on 'app') endpoint,
 * having 'parallel-soap-invoke' as entry point.
 * Each time a GET request arrives at '/parallel-soap-invoke', the function passed
 * as the second parameter from app.get will be invoked.
 * The signature is fixed: the request and response objects.
 */
app.get('/parallel-soap-invoke', (_, res) => {
    // HTTP status of the response is set first and then the result to be sent.
    res.status(200).send('Hello!');
});

// Starts 'app' and sends a message when it's ready.
app.listen(3000, () => console.log('Waiting for incoming requests.'));

Результат:

GET /parallel-soap-invoke
HTTP/1.1 200
Hello!

Теперь нам нужно обработать объект, отправленный через POST. Экспресс body-parser позволяет легко получить доступ к тексту запроса:


'use strict';

const expressApp = require('express')(); // Compressing two lines into one.
const bodyParser = require('body-parser'); // Several parsers for HTTP requests.

expressApp.use(bodyParser.json()) // States that 'expressApp' will use JSON parser.
    // Since each Express method returns the updated object, methods can be chained.
    .post('/parallel-soap-invoke', (req, res) => { 
        /**
         * As an example, the same request body will be sent as response with
         * a different HTTP status code.
         */
        res.status(202).send(req.body); // req.body will have the parsed object 
    })
    .listen(3000, () => console.log('Waiting for incoming requests.'));
POST /parallel-soap-invoke
content-type: application/json

[
  {
    "intA": 1,
    "intB": 2
  },
  {
    "intA": 3,
    "intB": 4
  },
  {
    "intA": 5,
    "intB": 6
  }
]

HTTP/1.1 202

[
  {
    "intA": 1,
    "intB": 2
  },
  {
    "intA": 3,
    "intB": 4
  },
  {
    "intA": 5,
    "intB": 6
  }
]

Итак, короче говоря: настройте программу Express и, как только получите результат, отправьте его через res и вуаль.

2. Раздел SOAP

Это будет немного больше шагов, чем предыдущий раздел. Основная идея состоит в том, что для параллельных вызовов SOAP я буду использовать Promise.all. В состоянии использовать Promise.allвызов в службы SOAP нужно обрабатывать в рамках Promise, не касающегося strong-soap. В этом разделе будет показано, как конвертировать обычные обратные вызовы. strong-soap в Promises, а затем положить a Promise.all к тому же.

В следующем коде будет использован самый простой пример strong-soap‘s документации. Я просто немного упросту это и использую тот самый WSDL, который мы видим (я не использовал тот же WSDL, как указано в strong-soap‘s документация, поскольку этот WSDL больше не работает):

'use strict';

// The SOAP client library.
var { soap } = require('strong-soap');
// WSDL we'll be using through the article.
var url="

// Hardcoded request
var requestArgs = {
    "intA": 1,
    "intB": 2,
};

// Creates the client which is returned in the callback.
soap.createClient(url, {}, (_, client) => (
    // Callback delivers the result of the SOAP invokation.
    client.Add(requestArgs, (_, result) => (
        console.log(`Result: ${"\n" + JSON.stringify(result)}`)
    ))
));
$ node index.js
Result:
{"AddResult":3}

Я превращу это в Promises и просмотрю все обратные вызовы, один за другим, для примера. Таким образом, процесс перевода будет для вас кристально понятен:

'use strict';

var { soap } = require('strong-soap');
var url="

var requestArgs = {
    "intA": 1,
    "intB": 2,
};

/**
 * A function that will return a Promise which will return the SOAP client.
 * The Promise receives as parameter a function having two functions as parameters:
 * resolve & reject.
 * So, as soon as you got a result, call resolve with the result,
 * or call reject with some error otherwise.
 */
const createClient = () => (new Promise((resolve, reject) => (
    // Same call as before, but I'm naming the error parameter since I'll use it.
    soap.createClient(url, {}, (err, client) => (
        /**
         * Did any error happen? Let's call reject and send the error.
         * No? OK, let's call resolve sending the result. 
         */
        err ? reject(err) : resolve(client)
    ))))
);

/**
 * The above function is invoked.
 * The Promise could have been inlined here, but it's more understandable this way.
 */
createClient().then(
    /**
     * If at runtime resolve is invoked, the value sent through resolve
     * will be passed as parameter for this function.
     */
    client => (client.Add(requestArgs, (_, result) => (
        console.log(`Result: ${"\n" + JSON.stringify(result)}`)
    ))),
    // Same as above, but in this case reject was called at runtime.
    err => console.log(err),
);

Звонок node index.jsполучает тот же результат, что и раньше. Следующий обратный звонок:

'use strict';

var { soap } = require('strong-soap');
var url="

var requestArgs = {
    "intA": 1,
    "intB": 2,
};

const createClient = () => (new Promise((resolve, reject) => (
    soap.createClient(url, {}, (err, client) => (
        err ? reject(err) : resolve(client)
    ))))
);

/**
 * Same as before: do everything you need to do; once you have a result,
 * resolve it, or reject some error otherwise.
 * invokeOperation will replace the first function of .then from the former example,
 * so the signatures must match.
 */
const invokeOperation = client => (new Promise((resolve, reject) => (
    client.Add(requestArgs, (err, result) => (
        err ? reject(err) : resolve(result)
    ))
)));

/**
 * .then also returns a Promise, having as result the value resolved or rejected
 * by the functions that were passed as parameters to it. In this case, the second .then
 * will receive the value resolved/rejected by invokeOperation.
 */
createClient().then(
    invokeOperation,
    err => console.log(err),
).then(
    result => console.log(`Result: ${"\n" + JSON.stringify(result)}`),
    err => console.log(err),
);

node index.js? Все то же самое. Давайте обратим эти Promises в функцию, чтобы подготовить код для ее вызова внутри конечной точки Express. Это также немного упрощает обработку ошибок:

'use strict';

var { soap } = require('strong-soap');
var url="

var requestArgs = {
    "intA": 1,
    "intB": 2,
};

const createClient = () => (new Promise((resolve, reject) => (
    soap.createClient(url, {}, (err, client) => (
        err ? reject(err) : resolve(client)
    ))))
);

const invokeOperation = client => (new Promise((resolve, reject) => (
    client.Add(requestArgs, (err, result) => (
        err ? reject(err) : resolve(result)
    ))
)));

const processRequest = () => createClient().then(invokeOperation);

/**
 * .catch() will handle any reject not handled by a .then. In this case,
 * it will handle any reject called by createClient or invokeOperation.
 */
processRequest().then(result => console.log(`Result: ${"\n" + JSON.stringify(result)}`))
    .catch(({ message }) => console.log(message));

Бьюсь об заклад, вы можете угадать результат node index.js.

Что произойдет, если совершить несколько следующих вызовов? Мы узнаем по следующему коду:

'use strict';

var { soap } = require('strong-soap');
var url="

var requestArgs = {
    "intA": 1,
    "intB": 2,
};

const createClient = () => (new Promise((resolve, reject) => (
    soap.createClient(url, {}, (err, client) => {
        if (err) {
            reject(err);
        } else {
            // A message is displayed each time a client is created.
            console.log('A new client is being created.');
            resolve(client);
        }
    })))
);

const invokeOperation = client => (new Promise((resolve, reject) => (
    client.Add(requestArgs, (err, result) => (
        err ? reject(err) : resolve(result)
    ))
)));

const processRequest = () => createClient().then(invokeOperation)

processRequest().then(result => console.log(`Result: ${"\n" + JSON.stringify(result)}`))
    .catch(({ message }) => console.log(message));
processRequest().then(result => console.log(`Result: ${"\n" + JSON.stringify(result)}`))
    .catch(({ message }) => console.log(message));
processRequest().then(result => console.log(`Result: ${"\n" + JSON.stringify(result)}`))
    .catch(({ message }) => console.log(message));
$ node index.js
A new client is being created.
A new client is being created.
Result:
{"AddResult":3}
A new client is being created.
Result:
{"AddResult":3}
Result:
{"AddResult":3}

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

  1. Вы можете создать переменную вне Promise и кэшировать клиента, как только она у вас будет (непосредственно перед ее решением). Назовем это cachedClient. Но в таком случае вам придется вручную обрабатывать звонки на createClient() сделано между первым вызовом и перед разрешением первого клиента. Вам придется проверить, если cachedClient является ожидаемым значением, или вам придется проверить, решен ли Promise или нет, или вам придется установить какой-то источник событий, чтобы знать, когда cachedClient готов. Когда я впервые написал код для этого, я использовал этот подход, и в результате я жил с тем фактом, что каждый отдельный вызов, сделанный до первого createClient().resolve перезаписан cachedClient. Если проблема не так ясна, дайте мне знать, и я напишу код и примеры.
  2. У Promises есть очень крутая функция (см. документацию MDN, раздел «Вернутое значение»): если вы позвоните по телефону .then() для решенного/отклоненного обещания он вернет то самое значение, которое было решено/отклонено, без повторной обработки. На самом деле, очень технически, это будет та же ссылка на объект.

Второй подход гораздо проще реализовать, поэтому связанный код такой:

'use strict';

var { soap } = require('strong-soap');
var url="

var requestArgs = {
    "intA": 1,
    "intB": 2,
};

// createClient function is removed.
const clientPromise = (new Promise((resolve, reject) => (
    soap.createClient(url, {}, (err, client) => {
        if (err) {
            reject(err);
        } else {
            console.log('A new client is being created.');
            resolve(client);
        }
    })))
);

const invokeOperation = client => (new Promise((resolve, reject) => (
    client.Add(requestArgs, (err, result) => (
        err ? reject(err) : resolve(result)
    ))
)));

// clientPromise is called instead getClient().
clientPromise.then(invokeOperation)
    .then(result => console.log(`Result: ${"\n" + JSON.stringify(result)}`))
    .catch(({ message }) => console.log(message));
clientPromise.then(invokeOperation)
    .then(result => console.log(`Result: ${"\n" + JSON.stringify(result)}`))
    .catch(({ message }) => console.log(message));
clientPromise.then(invokeOperation)
    .then(result => console.log(`Result: ${"\n" + JSON.stringify(result)}`))
    .catch(({ message }) => console.log(message));
$ node index.js
A new client is being created.
Result:
{"AddResult":3}
Result:
{"AddResult":3}
Result:
{"AddResult":3}

Наконец, для этого раздела давайте заставим код обрабатывать несколько параллельных вызовов. Это будет легко:

  1. Для обработки нескольких параллельных вызовов нам понадобится Promise.all.
  2. Promise.all имеет один параметр: массив Promises. Итак, мы превратим список запросов в список Promises. На данный момент код превращает один запрос в один Promise (invokeOperation), поэтому коду требуется только a .map чтобы добиться этого.
'use strict';

var { soap } = require('strong-soap');
var url="

// Hardcoded list of requests.
var requestsArgs = [
    {
        "intA": 1,
        "intB": 2,
    },
    {
        "intA": 3,
        "intB": 4,
    },
    {
        "intA": 5,
        "intB": 6,
    },
];

const clientPromise = (new Promise((resolve, reject) => (
    soap.createClient(url, {}, (err, client) => err ? reject(error) : resolve(client))
)));

// Promise.all on top of everything.
const invokeOperation = client => (Promise.all(
    // For each request, a Promise is returned.
    requestsArgs.map(requestArgs => new Promise((resolve, reject) => (
        // Everything remains the same here.
        client.Add(requestArgs, (err, result) => (
            err ? reject(err) : resolve(result)
        ))
    )))
));

clientPromise.then(invokeOperation)
    .then(result => console.log(`Result: ${"\n" + JSON.stringify(result)}`))
    .catch(({ message }) => console.log(message));
$ node index.js
Result:
[{"AddResult":3},{"AddResult":7},{"AddResult":11}]

3. Соединить все это вместе

Это достаточно просто – нужно просто собрать последний код из каждого предыдущего раздела:

'use strict';

const { soap } = require('strong-soap');
const expressApp = require('express')();
const bodyParser = require('body-parser');

const url="
const clientPromise = new Promise((resolve, reject) => (
    soap.createClient(url, {}, (err, client) => err ? reject(err) : resolve(client))
));

expressApp.use(bodyParser.json())
    .post('/parallel-soap-invoke', (req, res) => (clientPromise.then(invokeOperations)
        .then(results => res.status(200).send(results))
        .catch(({ message: error }) => res.status(500).send({ error }))
    ))
    .listen(3000, () => console.log('Waiting for incoming requests.'));

// Adding req.body instead of hardcoded requests.
const invokeOperations = client => Promise.all(req.body.map(request => (
    new Promise((resolve, reject) => client.Add(request, (err, result) => (
        err ? reject(err) : resolve(result))
    ))
)));
POST /parallel-soap-invoke

[
  {
    "intA": 1,
    "intB": 2
  },
  {
    "intA": 3,
    "intB": 4
  },
  {
    "intA": 5,
    "intB": 6
  }
]
 
HTTP/1.1 500

{
  "error": "req is not defined"
}

Хм… Не очень хороший результат, потому что я вообще не ожидал ошибки. Проблема в том invokeOperations нету req в своей сфере. Первым мнением может быть «Просто добавьте это к подписи». Но это невозможно, поскольку эта подпись соответствует результату предыдущего Promise, и это обещание не возвращается reqэто только возвращается client. Но что если мы добавим промежуточное обещание, единственной целью которого является введение этого значения?

'use strict';

const { soap } = require('strong-soap');
const expressApp = require('express')();
const bodyParser = require('body-parser');

const url="
const clientPromise = new Promise((resolve, reject) => (
    soap.createClient(url, {}, (err, client) => err ? reject(err) : resolve(client))
));

expressApp.use(bodyParser.json())
    .post('/parallel-soap-invoke', (req, res) => (
        /**
         * After clientPromise.then, where client is received, a new Promise is
         * created, and that Promise will resolve an object having two properties:
         * client and requests.
         */
        clientPromise.then(client => ({ client, requests: req.body }))
            .then(invokeOperations)
            .then(results => res.status(200).send(results))
            .catch(({ message: error }) => res.status(500).send({ error }))
    ))
    .listen(3000, () => console.log('Waiting for incoming requests.'));

/**
 * Since the shape of the object passed to invokeOperations changed, the signature has
 * to change to reflect the shape of the new object.
 */
const invokeOperations = ({ client, requests }) => Promise.all(requests.map(request => (
    new Promise((resolve, reject) => client.Add(request, (err, result) => (
        err ? reject(err) : resolve(result))
    ))
)));

Результаты точно такие же, как у резюме.

4. Бонусный трек

Общий конвертер SOAP в JSON для параллельного вызова SOAP. Код знаком на основе того, что вы видели в предыдущих главах. Как о том, что?

'use strict';

const { soap } = require('strong-soap');
const expressApp = require('express')();
const bodyParser = require('body-parser');

const clientPromises = new Map();

expressApp.use(bodyParser.json())
    .post('/parallel-soap-invoke', ({ body: { wsdlUrl, operation, requests } }, res) => (
        getClient(wsdlUrl).then(client => ({ client, operation, requests }))
            .then(invokeOperations)
            .then(results => res.status(200).send(results))
            .catch(({ message: error }) => res.status(500).send({ error }))
    ))
    .listen(3000, () => console.log('Waiting for incoming requests.'));

const getClient = wsdlUrl => clientPromises.get(wsdlUrl)
    || (clientPromises.set(wsdlUrl, new Promise((resolve, reject) => (
        soap.createClient(wsdlUrl, {}, (err, client) => err ? reject(err) : resolve(client))
    ))).get(wsdlUrl));

const invokeOperations = ({ client, operation, requests }) => (Promise.all(requests.map(request => (
    new Promise((resolve, reject) => client[operation](request, (err, result) => (
        err ? reject(err) : resolve(result))
    ))
))));

Пример первого использования:

POST /parallel-soap-invoke
content-type: application/json

{
  "wsdlUrl": "
  "operation": "Add",
  "requests": [
    {
      "intA": 1,
      "intB": 2
    },
    {
      "intA": 3,
      "intB": 4
    },
    {
      "intA": 5,
      "intB": 6
    }
  ]
}

HTTP/1.1 200

[
  {
    "AddResult": 3
  },
  {
    "AddResult": 7
  },
  {
    "AddResult": 11
  }
]

Второй пример использования:

POST /parallel-soap-invoke
content-type: application/json

{
  "wsdlUrl": "
  "operation": "ResolveIP",
  "requests": [
    {
      "ipAddress": "8.8.8.8",
      "licenseKey": ""
    },
    {
    	"ipAddress": "8.8.4.4",
    	"licenseKey": ""
    }
  ]
}

HTTP/1.1 200

[
  {
    "ResolveIPResult": {
      "Country": "United States",
      "Latitude": 37.75101,
      "Longitude": -97.822,
      "AreaCode": "0",
      "HasDaylightSavings": false,
      "Certainty": 90,
      "CountryCode": "US"
    }
  },
  {
    "ResolveIPResult": {
      "Country": "United States",
      "Latitude": 37.75101,
      "Longitude": -97.822,
      "AreaCode": "0",
      "HasDaylightSavings": false,
      "Certainty": 90,
      "CountryCode": "US"
    }
  }
]

Вы проходите цифровое решение? В архитектуре полного стека JavaScript поверх старых служб этот артефакт может помочь вам инкапсулировать все службы SOAP, расширить их и предоставить только JSON. Вы можете даже немного изменить этот код, чтобы одновременно вызывать несколько различных служб SOAP (это должно быть только дополнительным. .map и .reduce, как я сейчас вижу). Или вы можете инкапсулировать WSDL вашего предприятия в базу данных и вызвать их на основе кода или какого-либо идентификатора. Это было бы только одно-два дополнительных обещания цепочке.

Добавить комментарий

Ваш адрес email не будет опубликован.