Asynchrone HTTP-programmering met Play Framework

Java Top

Ik heb zojuist het nieuwe aangekondigd Leer de lente natuurlijk, gericht op de basisprincipes van Spring 5 en Spring Boot 2:

>> BEKIJK DE CURSUS

1. Overzicht

Vaak hebben onze webservices andere webservices nodig om hun werk te kunnen doen. Het kan moeilijk zijn om aan gebruikersverzoeken te voldoen terwijl de responstijd laag blijft. Een trage externe service kan onze responstijd verlengen en ervoor zorgen dat ons systeem verzoeken opstapelt en meer middelen gebruikt. Dit is waar een niet-blokkerende benadering erg nuttig kan zijn

In deze zelfstudie sturen we meerdere asynchrone verzoeken naar een service vanuit een Play Framework-toepassing. Door gebruik te maken van Java's niet-blokkerende HTTP-mogelijkheid, kunnen we probleemloos externe bronnen opvragen zonder onze eigen hoofdlogica te beïnvloeden.

In ons voorbeeld verkennen we de Play WebService-bibliotheek.

2. De Play WebService (WS) -bibliotheek

WS is een krachtige bibliotheek die asynchrone HTTP-oproepen biedt met behulp van Java Actie.

Met behulp van deze bibliotheek verzendt onze code deze verzoeken en gaat verder zonder te blokkeren. Om het resultaat van het verzoek te verwerken, bieden we een consumerende functie, dat wil zeggen een implementatie van de Klant koppel.

Dit patroon heeft overeenkomsten met de implementatie van callbacks in JavaScript, Beloften, en de async / wachten patroon.

Laten we een simpel bouwen Klant dat een aantal van de antwoordgegevens registreert:

ws.url (url) .thenAccept (r -> log.debug ("Thread #" + Thread.currentThread (). getId () + "Verzoek voltooid: Response code =" + r.getStatus () + "| Reactie: "+ r.getBody () +" | Huidige tijd: "+ System.currentTimeMillis ()))

Onze Klant is slechts inloggen in dit voorbeeld. De consument kan echter alles doen wat we met het resultaat moeten doen, zoals het resultaat opslaan in een database.

Als we dieper kijken naar de implementatie van de bibliotheek, kunnen we zien dat WS Java's omhult en configureert AsyncHttpClient, die deel uitmaakt van de standaard JDK en niet afhankelijk is van Play.

3. Bereid een voorbeeldproject voor

Laten we, om met het framework te experimenteren, enkele unit-tests maken om verzoeken te lanceren. We zullen een skeleton-webtoepassing maken om ze te beantwoorden en het WS-framework gebruiken om HTTP-verzoeken te doen.

3.1. De skeletwebapplicatie

Allereerst maken we het eerste project met behulp van de sbt nieuw opdracht:

sbt nieuw speelframe / play-java-seed.g8

In de nieuwe map, we dan bewerk het build.sbt bestand en voeg de WS-bibliotheekafhankelijkheid toe:

libraryDependencies + = javaWs

Nu kunnen we de server starten met de sbt uitvoeren opdracht:

$ sbt run ... --- (De applicatie wordt uitgevoerd, automatisch herladen is ingeschakeld) --- [info] pcsAkkaHttpServer - Luisteren naar HTTP op / 0: 0: 0: 0: 0: 0: 0: 0: 9000

Zodra de applicatie is gestart, kunnen we controleren of alles in orde is door te bladeren // localhost: 9000, waarmee de welkomstpagina van Play wordt geopend.

3.2. De testomgeving

Om onze applicatie te testen, gebruiken we de unit test class HomeControllerTest.

Ten eerste moeten we uitbreiden WithServer die de levenscyclus van de server zal bieden:

openbare klasse HomeControllerTest breidt WithServer {uit 

Dankzij zijn ouder, deze klasse start nu onze skeleton-webserver in testmodus en op een willekeurige poort, voordat u de tests uitvoert. De WithServer class stopt ook de toepassing wanneer de test is voltooid.

Vervolgens moeten we een applicatie bieden om uit te voeren.

We kunnen het maken met Guice‘S GuiceApplicationBuilder:

@Override beschermde applicatie offerApplication () {retourneer nieuwe GuiceApplicationBuilder (). Build (); } 

En tot slot hebben we de server-URL ingesteld om in onze tests te gebruiken, met behulp van het poortnummer dat door de testserver is verstrekt:

@Override @Before public void setup () {OptionalInt optHttpsPort = testServer.getRunningHttpsPort (); if (optHttpsPort.isPresent ()) {poort = optHttpsPort.getAsInt (); url = "// localhost:" + poort; } anders {poort = testServer.getRunningHttpPort () .getAsInt (); url = "// localhost:" + poort; }}

Nu zijn we klaar om tests te schrijven. Dankzij het uitgebreide testraamwerk kunnen we ons concentreren op het coderen van onze testverzoeken.

4. Bereid een WSRequest voor

Laten we eens kijken hoe we basistypen verzoeken kunnen activeren, zoals GET of POST, en meervoudige verzoeken voor het uploaden van bestanden.

4.1. Initialiseer het WSRequest Voorwerp

Allereerst moeten we een WSClient instantie om onze verzoeken te configureren en te initialiseren.

In een real-life applicatie kunnen we een client krijgen, automatisch geconfigureerd met standaardinstellingen, via afhankelijkheidsinjectie:

@Autowired WSClient ws;

In onze testklas gebruiken we echter WSTestClient, beschikbaar via Play Test framework:

WSClient ws = play.test.WSTestClient.newClient (poort);

Zodra we onze klant hebben, kunnen we een WSRequest object door het url methode:

ws.url (url)

De url methode doet genoeg om ons in staat te stellen een verzoek in te dienen. We kunnen het echter verder aanpassen door enkele aangepaste instellingen toe te voegen:

ws.url (url) .addHeader ("sleutel", "waarde") .addQueryParameter ("num", "" + num);

Zoals we kunnen zien, is het vrij eenvoudig om headers en queryparameters toe te voegen.

Nadat we ons verzoek volledig hebben geconfigureerd, kunnen we de methode aanroepen om het te starten.

4.2. Generiek GET-verzoek

Om een ​​GET-verzoek te activeren, hoeven we alleen maar het krijgen methode op onze WSRequest voorwerp:

ws.url (url) ... .get ();

Omdat dit een niet-blokkerende code is, start het het verzoek en gaat het verder met de uitvoering op de volgende regel van onze functie.

Het object is geretourneerd door krijgen is een Voltooiingsfase voorbeeld, dat deel uitmaakt van de CompletableFuture API.

Zodra de HTTP-aanroep is voltooid, voert deze fase slechts een paar instructies uit. Het verpakt het antwoord in een WSResponse voorwerp.

Normaal gesproken wordt dit resultaat doorgegeven aan de volgende fase van de uitvoeringsketen. In dit voorbeeld hebben we geen verbruiksfunctie voorzien, dus het resultaat gaat verloren.

Om deze reden is dit verzoek van het type "vuur-en-vergeten".

4.3. Dien een formulier in

Het indienen van een formulier verschilt niet veel van het krijgen voorbeeld.

Om het verzoek te activeren, bellen we gewoon het post methode:

ws.url (url) ... .setContentType ("application / x-www-form-urlencoded") .post ("key1 = waarde1 & key2 = waarde2");

In dit scenario moeten we een body als parameter doorgeven. Dit kan een eenvoudige tekenreeks zijn, zoals een bestand, een json- of xml-document, een BodyWritable of een Bron.

4.4. Dien een meerdelige / formuliergegevens in

Een meerdelig formulier vereist dat we zowel invoervelden als gegevens verzenden vanuit een bijgevoegd bestand of stream.

Om dit in het framework te implementeren, gebruiken we de post methode met een Bron.

Binnen de bron kunnen we alle verschillende gegevenstypen die nodig zijn voor ons formulier, verpakken:

Bronbestand = FileIO.fromPath (Paths.get ("hello.txt")); FilePart file = nieuw FilePart ("fileParam", "myfile.txt", "text / plain", bestand); DataPart data = nieuw DataPart ("sleutel", "waarde"); ws.url (url) ... .post (Source.from (Arrays.asList (bestand, data)));

Hoewel deze benadering wat meer configuratie toevoegt, lijkt het nog steeds sterk op de andere soorten verzoeken.

5. Verwerk de asynchrone respons

Tot nu toe hebben we alleen fire-and-forget-verzoeken geactiveerd, waarbij onze code niets doet met de responsgegevens.

Laten we nu twee technieken onderzoeken voor het verwerken van een asynchrone respons.

We kunnen ofwel de hoofdthread blokkeren, wachtend op een Toekomstige, of asynchroon consumeren met een Klant.

5.1. Verwerk de reactie door te blokkeren met CompletableFuture

Zelfs als we een asynchroon raamwerk gebruiken, kunnen we ervoor kiezen om de uitvoering van onze code te blokkeren en op het antwoord te wachten.

De ... gebruiken CompletableFuture API, we hebben slechts een paar wijzigingen in onze code nodig om dit scenario te implementeren:

WSResponse-antwoord = ws.url (url) .get () .toCompletableFuture () .get ();

Dit kan bijvoorbeeld nuttig zijn om een ​​sterke gegevensconsistentie te bieden die we op andere manieren niet kunnen bereiken.

5.2. Verwerk de reactie asynchroon

Om een ​​asynchrone reactie te verwerken zonder te blokkeren, wij bieden een Klant of Functie dat wordt uitgevoerd door het asynchrone raamwerk wanneer het antwoord beschikbaar is.

Laten we bijvoorbeeld een Klant naar ons vorige voorbeeld om het antwoord te loggen:

ws.url (url) .addHeader ("sleutel", "waarde") .addQueryParameter ("num", "" + 1) .get () .thenAccept (r -> log.debug ("Thread #" + Thread. currentThread (). getId () + "Verzoek voltooid: antwoordcode =" + r.getStatus () + "| Antwoord:" + r.getBody () + "| Huidige tijd:" + System.currentTimeMillis ()));

We zien dan het antwoord in de logboeken:

[debug] c.HomeControllerTest - Thread # 30 Verzoek voltooid: Response code = 200 | Reactie: {"Resultaat": "ok", "Params": {"num": ["1"]}, "Headers": {"accept": ["* / *"], "host": [" localhost: 19001 "]," key ": [" waarde "]," user-agent ": [" AHC / 2.1 "]}} | Huidige tijd: 1579303109613

Het is vermeldenswaard dat we Accepteer dan, waarvoor een Klant functie omdat we na het loggen niets hoeven te retourneren.

Als we willen dat de huidige fase iets teruggeeft, zodat we het in de volgende fase kunnen gebruiken, hebben we nodig dan toepassen in plaats daarvan, waarvoor een Functie.

Deze gebruiken de conventies van de standaard Java Functional Interfaces.

5.3. Groot responslichaam

De code die we tot nu toe hebben geïmplementeerd, is een goede oplossing voor kleine reacties en de meeste use-cases. Als we echter enkele honderden megabytes aan gegevens moeten verwerken, hebben we een betere strategie nodig.

We moeten opmerken: Verzoek methoden zoals krijgen en post laad het volledige antwoord in het geheugen.

Om een ​​mogelijke Onvoldoende geheugen foutkunnen we Akka Streams gebruiken om het antwoord te verwerken zonder dat het ons geheugen vult.

We kunnen bijvoorbeeld de hoofdtekst in een bestand schrijven:

ws.url (url) .stream () .thenAccept (antwoord -> {probeer {OutputStream outputStream = Files.newOutputStream (pad); Sink outputWriter = Sink.foreach (bytes -> outputStream.write (bytes.toArray ())); response.getBodyAsSource (). runWith (outputWriter, materializer); } catch (IOException e) {log.error ("Er is een fout opgetreden bij het openen van de uitvoerstroom", e); }});

De stroom methode retourneert een Voltooiingsfase waar de WSResponse heeft een getBodyAsStream methode die een Bron.

We kunnen de code vertellen hoe dit type lichaam moet worden verwerkt door Akka's te gebruiken Wastafel, die in ons voorbeeld gewoon alle gegevens wegschrijven die passeren in de OutputStream.

5.4. Time-outs

Bij het maken van een verzoek kunnen we ook een specifieke time-out instellen, zodat het verzoek wordt onderbroken als we niet op tijd het volledige antwoord ontvangen.

Dit is een bijzonder handige functie als we zien dat een service die we opvragen bijzonder traag is en kan leiden tot een opeenstapeling van open verbindingen die wachten op het antwoord.

We kunnen een algemene time-out instellen voor al onze verzoeken met behulp van afstemmingsparameters. Voor een verzoekspecifieke time-out kunnen we een verzoek toevoegen met setRequestTimeout:

ws.url (url) .setRequestTimeout (Duration.of (1, SECONDS));

Er moet echter nog één geval worden afgehandeld: we hebben misschien alle gegevens ontvangen, maar onze Klant kan erg traag zijn bij het verwerken ervan. Dit kan gebeuren als er veel gegevens worden gekraakt, database-oproepen, enz.

In systemen met een lage doorvoersnelheid kunnen we de code gewoon laten draaien totdat deze is voltooid. Het is echter mogelijk dat we langlopende activiteiten willen afbreken.

Om dat te bereiken, moeten we onze code met enkele toekomsten behandeling.

Laten we een heel lang proces simuleren in onze code:

ws.url (url) .get () .thenApply (resultaat -> {probeer {Thread.sleep (10000L); return Results.ok ();} catch (InterruptedException e) {return Results.status (SERVICE_UNAVAILABLE);}} );

Dit zal een OK reactie na 10 seconden, maar we willen niet zo lang wachten.

In plaats daarvan met de time-out wrapper, geven we onze code de opdracht om niet langer dan 1 seconde te wachten:

CompletionStage f = futures.timeout (ws.url (url) .get () .thenApply (result -> {probeer {Thread.sleep (10000L); return Results.ok ();} catch (InterruptedException e) {return Results. status (SERVICE_UNAVAILABLE);}}), 1L, TimeUnit.SECONDS); 

Nu zal onze toekomst hoe dan ook een resultaat retourneren: het berekeningsresultaat als de Klant op tijd klaar, of de uitzondering vanwege de toekomsten time-out.

5.5. Uitzonderingen afhandelen

In het vorige voorbeeld hebben we een functie gemaakt die een resultaat retourneert of mislukt met een uitzondering. Dus nu moeten we beide scenario's afhandelen.

We kunnen zowel succes- als faalscenario's aan met de handleAsync methode.

Laten we zeggen dat we het resultaat willen retourneren, als we het hebben, of de fout willen loggen en de uitzondering willen retourneren voor verdere afhandeling:

CompletionStage res = f.handleAsync ((result, e) -> {if (e! = Null) {log.error ("Uitzondering gegooid", e); return e.getCause ();} else {resultaat teruggeven;}} ); 

De code zou nu een Voltooiingsfase met de TimeoutException gegooid.

We kunnen het verifiëren door simpelweg een assertEquals op de klasse van het uitzonderingsobject geretourneerd:

Klasse clazz = res.toCompletableFuture (). Get (). GetClass (); assertEquals (TimeoutException.class, clazz);

Bij het uitvoeren van de test wordt ook de uitzondering geregistreerd die we hebben ontvangen:

[fout] c.HomeControllerTest - Uitzondering opgetreden java.util.concurrent.TimeoutException: time-out na 1 seconde ...

6. Verzoek om filters

Soms moeten we wat logica uitvoeren voordat een verzoek wordt geactiveerd.

We kunnen de WSRequest object eenmaal geïnitialiseerd, maar een elegantere techniek is om een WSRequestFilter.

Een filter kan worden ingesteld tijdens de initialisatie, voordat de triggermethode wordt aangeroepen, en wordt aan de verzoeklogica gekoppeld.

We kunnen ons eigen filter definiëren door het WSRequestFilter interface, of we kunnen een kant-en-klare interface toevoegen.

Een veelvoorkomend scenario is het vastleggen van hoe het verzoek eruitziet voordat het wordt uitgevoerd.

In dit geval hoeven we alleen de AhcCurlRequestLogger:

ws.url (url) ... .setRequestFilter (nieuwe AhcCurlRequestLogger ()) ... .get ();

Het resulterende logboek heeft een krullen-achtig formaat:

[info] p.l.w.a.AhcCurlRequestLogger - curl \ --verbose \ --request GET \ --header 'key: waarde' \ '// localhost: 19001'

We kunnen het gewenste logniveau instellen door onze logback.xml configuratie.

7. Reacties in cache plaatsen

WSClient ondersteunt ook het cachen van reacties.

Deze functie is vooral handig wanneer hetzelfde verzoek meerdere keren wordt geactiveerd en we niet elke keer de nieuwste gegevens nodig hebben.

Het helpt ook als de service die we bellen tijdelijk niet beschikbaar is.

7.1. Voeg caching-afhankelijkheden toe

Om caching te configureren, moeten we eerst de afhankelijkheid toevoegen in onze build.sbt:

libraryDependencies + = ehcache

Dit configureert Ehcache als onze cachelaag.

Als we Ehcache niet specifiek willen, kunnen we elke andere JSR-107 cache-implementatie gebruiken.

7.2. Forceer caching heuristiek

Standaard zal Play WS geen HTTP-reacties cachen als de server geen cacheconfiguratie retourneert.

Om dit te omzeilen, kunnen we de heuristische caching forceren door een instelling toe te voegen aan onze application.conf:

play.ws.cache.heuristics.enabled = true

Dit zal het systeem configureren om te beslissen wanneer het nuttig is om een ​​HTTP-antwoord in de cache op te slaan, ongeacht de geadverteerde caching van de externe service.

8. Aanvullende afstemming

Het indienen van verzoeken bij een externe service vereist mogelijk enige clientconfiguratie. Het kan zijn dat we omleidingen, een trage server of wat filtering moeten afhandelen, afhankelijk van de user-agent-header.

Om dat aan te pakken, kunnen we onze WS-client afstemmen met behulp van eigenschappen in onze application.conf:

play.ws.followRedirects = false play.ws.useragent = MyPlayApplication play.ws.compressionEnabled = true # tijd om te wachten tot de verbinding tot stand is gebracht play.ws.timeout.connection = 30 # tijd om te wachten op gegevens nadat de verbinding is gemaakt open play.ws.timeout.idle = 30 # max. beschikbare tijd om het verzoek te voltooien play.ws.timeout.request = 300

Het is ook mogelijk om het onderliggende te configureren AsyncHttpClient direct.

De volledige lijst met beschikbare eigenschappen kan worden gecontroleerd in de broncode van AhcConfig.

9. Conclusie

In dit artikel hebben we de Play WS-bibliotheek en de belangrijkste functies ervan onderzocht. We hebben ons project geconfigureerd, geleerd hoe we veelvoorkomende verzoeken kunnen activeren en hoe we hun reactie kunnen verwerken, zowel synchroon als asynchroon.

We werkten met grote datadownloads en zagen hoe we korte langlopende activiteiten konden verminderen.

Ten slotte hebben we gekeken naar caching om de prestaties te verbeteren en hoe de client kan worden afgestemd.

Zoals altijd is de broncode voor deze tutorial beschikbaar op GitHub.

Java onderkant

Ik heb zojuist het nieuwe aangekondigd Leer de lente natuurlijk, gericht op de basisprincipes van Spring 5 en Spring Boot 2:

>> BEKIJK DE CURSUS