En ekspresservice til parallel SOAP-påkaldelse på under 25 linjer kode

Oversigt

Lad os antage, at der er en tjeneste, der har følgende funktioner:

  1. Det udsætter et REST-slutpunkt, der modtager en liste med anmodninger.
  2. Den påberåber sig parallelt en SOAP-tjeneste en gang pr. Element i listen over anmodninger.
  3. Det returnerer det konverterede resultat fra XML til JSON.

Denne tjenestes kildekode kunne se sådan ud ved hjælp af Node.js, Express og Airbnb JavaScript Style 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)) )) ))));

Eksempel på anmodning:

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

Prøvesvar:

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

Test viser, at en enkelt direkte anmodning til SOAP-tjenesten ved hjælp af SOAPUI tager ~ 430 ms (hvorfra jeg befinder mig, i Chile). Afsendelse af tre anmodninger (som vist ovenfor) tager ~ 400 ms for opkald til Express-tjenesten (bortset fra den første, der får WSDL og bygger klienten).

Hvorfor tager flere anmodninger kortere tid? For det meste fordi XML ikke er stærkt valideret, som det er i almindelig SOAP, så hvis denne bløde validering ikke svarer til dine forventninger, bør du overveje yderligere funktioner eller løsninger.

Gad vide hvordan det ville se ud ved hjælp af async/await? Her går du (resultaterne er de samme):

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

Følgende billede giver et koncept for, hvordan koden fungerer:

Denne artikel har til formål at vise enkelheden ved at bruge JavaScript til opgaver i enterprise World, såsom påberåbning af SOAP-tjenester. Hvis du er fortrolig med JavaScript, er dette dybest set bare et Promise.alloven på et par lovede tilbagekald under et Express-slutpunkt. Du kan gå direkte til sektion 4 ( bonusspor ), hvis du tror, ​​det kan være nyttigt for dig.

Hvis du er uden for JavaScript-verdenen, synes jeg, at 24 linjer kode for de tre funktioner, jeg nævnte i starten, er en meget god handel. Jeg vil nu gå i detaljerne.

1. Express sektionen

Lad os starte med koden relateret til Express, et minimalt og fleksibelt Node.js webapplikationsramme. Det er ret simpelt, og du kan finde det overalt, så jeg giver en sammenfattende beskrivelse.

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

Resultat:

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

Nu skal vi håndtere et objekt sendt via POST. Express body-parsergiver let adgang til selve anmodningen:

 '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 } ] 

Så kort historie: opsæt Express-appen, og så snart du har resultatet, skal du sende den via resog voilà.

2. SOAP-sektionen

Dette vil have nogle flere trin end det foregående afsnit. Hovedideen er, at jeg bruger til at foretage SOAP-invokationer parallelt Promise.all. For at kunne bruge Promise.allskal påkaldelsen til SOAP-tjenesterne håndteres inden for et løfte, hvilket ikke er tilfældet for strong-soap. Dette afsnit viser, hvordan man konverterer de almindelige tilbagekald fra strong-soaptil løfter og derefter sætter en Promise.alloven på det.

Den følgende kode bruger det mest basale eksempel fra strong-soapdokumentationen. Jeg vil bare forenkle det lidt og bruge den samme WSDL, som vi har set (jeg brugte ikke den samme WSDL, der er angivet i strong-soapdokumentationen, da WSDL ikke fungerer længere):

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

Jeg konverterer dette til løfter, og jeg gennemgår alle tilbagekald, en efter en, for eksemplets skyld. På den måde bliver oversættelsesprocessen krystalklar for dig:

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

Opkald node index.jsfår det samme resultat som før. Næste tilbagekald:

'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? Stadig det samme. Lad os pakke disse løfter ind i en funktion for at forberede koden til at kalde den inde i Express-slutpunktet. Det forenkler også fejlhåndteringen lidt:

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

Jeg vedder på, at du kan gætte resultatet af node index.js.

Hvad sker der, hvis der foretages flere efterfølgende opkald? Vi finder ud af det med følgende kode:

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

Ikke godt, da flere klienter oprettes. Ideelt set skal klienten cachelagres og genbruges. Der er to hovedmåder at opnå dette:

  1. Du kan oprette en variabel uden for løftet og cache klienten, så snart du har den (lige før du løser den). Lad os kalde det cachedClient. Men i så fald skal du manuelt håndtere opkald, der createClient()foretages mellem den første gang det kaldes, og før den første klient løses. Du bliver nødt til at inspicere, om cachedClientden forventede værdi er, eller du skal kontrollere, om løftet er løst eller ej, eller du skal sætte en slags begivenhedsudsender for at vide, hvornår den cachedClienter klar. Første gang jeg skrev kode til dette, brugte jeg denne tilgang, og jeg endte med at leve med det faktum, at hvert eneste opkald, der blev foretaget før den første createClient().resolveoverskrev cachedClient. Hvis problemet ikke er så klart, så lad mig det vide, så skriver jeg koden og eksemplerne.
  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" } } ]

Går du igennem digital afkobling? I en JavaScript-full-stack-arkitektur oven på de gamle tjenester kan denne artefakt hjælpe dig med at indkapsle alle SOAP-tjenester, udvide dem og kun udsætte JSON. Du kan endda ændre denne kode lidt for at ringe til flere forskellige SOAP-tjenester på samme tid (det skal bare være en ekstra, .mapog .reducesom jeg ser det lige nu). Eller du kan indkapsle din virksomheds WSDL'er i en database og påberåbe dem baseret på en kode eller en eller anden identifikator. Det ville kun være et eller to yderligere løfter til kæden.