Експрес-служба для паралельного виклику SOAP у 25 рядках коду

Огляд

Припустимо, існує служба, яка має такі особливості:

  1. Він виставляє кінцеву точку REST, яка отримує список запитів.
  2. Він паралельно викликає службу SOAP, один раз на елемент у списку запитів.
  3. Він повертає перетворений результат із XML у JSON.

Вихідний код цієї служби може виглядати приблизно так, використовуючи Node.js, Express та Airbnb JavaScript Guide:

'use strict'; const { soap } = require('strong-soap'); const expressApp = require('express')(); const bodyParser = require('body-parser'); const url = '//www.dneonline.com/calculator.asmx?WSDL'; 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 = '//www.dneonline.com/calculator.asmx?WSDL'; 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)) )) ))));

Наступне зображення надає уявлення про те, як працює код:

Ця стаття має на меті показати простоту використання JavaScript для завдань у корпоративному світі, таких як виклик служб SOAP. Якщо ви знайомі з JavaScript, це, по суті, лише 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і voilà.

2. Розділ SOAP

Це матиме кілька кроків, ніж попередній розділ. Основна ідея полягає в тому, що для паралельного виклику SOAP я буду використовувати Promise.all. Щоб мати можливість використовувати Promise.all, виклик служб SOAP повинен оброблятися в рамках обіцянки, що не стосується strong-soap. У цьому розділі буде показано, як перетворити звичайні зворотні дзвінки з strong-soapобіцянок, а потім поставити знак Promise.allзверху.

У наступному коді буде використаний найосновніший приклад із strong-soapдокументації. Я просто спрощу це трохи і використаю той самий WSDL, який ми бачили (я не використовував той самий WSDL, зазначений у strong-soapдокументації, оскільки WSDL більше не працює):

'use strict'; // The SOAP client library. var { soap } = require('strong-soap'); // WSDL we'll be using through the article. var url = '//www.dneonline.com/calculator.asmx?WSDL'; // 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 = '//www.dneonline.com/calculator.asmx?WSDL'; 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 = '//www.dneonline.com/calculator.asmx?WSDL'; 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 = '//www.dneonline.com/calculator.asmx?WSDL'; 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 = '//www.dneonline.com/calculator.asmx?WSDL'; 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є очікуване значення, або вам доведеться перевірити, чи обіцянка вирішено чи ні, або вам доведеться поставити якийсь випромінювач подій, щоб знати, коли cachedClientготовий. Перший раз, коли я писав код для цього, я використовував такий підхід, і в підсумку я жив з тим фактом, що кожен окремий дзвінок, зроблений до першого, createClient().resolveзамінив cachedClient. Якщо проблема не така зрозуміла, дайте мені знати, і я напишу код та приклади.
  2. Promises have a very cool feature (see MDN documentation, “Return value” section): if you call .then() on a resolved/rejected Promise, it will return the very same value that was resolved/rejected, without processing again. In fact, very technically, it will be the very same object reference.

The second approach is much simpler to implement, so the related code is the following:

'use strict'; var { soap } = require('strong-soap'); var url = '//www.dneonline.com/calculator.asmx?WSDL'; 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}

Finally for this section, let’s make the code handle several parallel calls. This will be easy:

  1. For handling several parallel calls, we’ll need Promise.all.
  2. Promise.all has a single parameter: an array of Promises. So we’ll be converting the list of requests into a list of Promises. The code currently converts a single request into a single Promise (invokeOperation), so the code just needs a .map to achieve this.
'use strict'; var { soap } = require('strong-soap'); var url = '//www.dneonline.com/calculator.asmx?WSDL'; // 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. Putting it all together

This is fairly easy — it’s just assembling the last code from each previous section:

'use strict'; const { soap } = require('strong-soap'); const expressApp = require('express')(); const bodyParser = require('body-parser'); const url = '//www.dneonline.com/calculator.asmx?WSDL'; 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" }

Hmmm… Not a good result, since I did not expect an error at all. The problem is that invokeOperations doesn’t have req in its scope. The first thought could be “Just add it to the signature.” But that’s not possible, as that signature matches the result from the previous Promise, and that promise doesn’t return req, it only returns client. But, what if we add an intermediate Promise whose only purpose is injecting this value?

'use strict'; const { soap } = require('strong-soap'); const expressApp = require('express')(); const bodyParser = require('body-parser'); const url = '//www.dneonline.com/calculator.asmx?WSDL'; 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)) )) )));

The results are exactly the same as the ones at the summary.

4. Bonus track

A generic SOAP to JSON converter for parallel SOAP invoking. The code is familiar, based on what you saw in the former sections. How about that?

'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)) )) ))));

First use example:

POST /parallel-soap-invoke content-type: application/json { "wsdlUrl": "//www.dneonline.com/calculator.asmx?WSDL", "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 } ] 

Second use example:

POST /parallel-soap-invoke content-type: application/json { "wsdlUrl": "//ws.cdyne.com/ip2geo/ip2geo.asmx?wsdl", "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" } } ]

Are you going through Digital Decoupling? In a JavaScript full-stack architecture on top of the old services, this artifact could help you encapsulate all SOAP services, extend them, and expose only JSON. You could even modify this code a bit to call several different SOAP services at the same time (that should be just an additional .map and .reduce, as I see it right now). Or you could encapsulate your enterprise’s WSDLs in a database and invoke them based on a code or some identifier. That would be just one or two additional promises to the chain.