Asynkron HTTP-programmering med Play Framework

Java Top

Jeg kunngjorde nettopp det nye Lær våren kurs, med fokus på det grunnleggende i vår 5 og vårstøvel 2:

>> KONTROLLER KURSET

1. Oversikt

Ofte trenger våre webtjenester å bruke andre webtjenester for å gjøre jobben sin. Det kan være vanskelig å betjene brukerforespørsler samtidig som det holder en lav responstid. En langsom ekstern tjeneste kan øke responstiden vår og føre til at systemet vårt hoper opp forespørsler ved å bruke flere ressurser. Det er her en ikke-blokkerende tilnærming kan være veldig nyttig

I denne opplæringen vil vi skyte ut flere asynkrone forespørsler til en tjeneste fra et Play Framework-program. Ved å utnytte Javas ikke-blokkerende HTTP-evne, vil vi kunne stille spørsmål om eksterne ressurser uten å påvirke vår egen hovedlogikk.

I vårt eksempel vil vi utforske Play WebService Library.

2. Play WebService (WS) -biblioteket

WS er ​​et kraftig bibliotek som gir asynkrone HTTP-anrop ved bruk av Java Handling.

Ved hjelp av dette biblioteket sender koden vår disse forespørslene og fortsetter uten å blokkere. For å behandle resultatet av forespørselen, tilbyr vi en forbruksfunksjon, det vil si en implementering av Forbruker grensesnitt.

Dette mønsteret deler noen likheter med JavaScript-implementeringen av tilbakeringinger, Løfter, og asynkronisere / vente mønster.

La oss bygge en enkel Forbruker som logger noen av svardataene:

ws.url (url) .thenAccept (r -> log.debug ("Thread #" + Thread.currentThread (). getId () + "Request complete: Response code =" + r.getStatus () + "| Svar: "+ r.getBody () +" | Nåværende tid: "+ System.currentTimeMillis ()))

Våre Forbruker logger bare på dette eksemplet. Forbrukeren kan gjøre alt vi trenger å gjøre med resultatet, som å lagre resultatet i en database.

Hvis vi ser dypere på bibliotekets implementering, kan vi observere at WS bryter inn og konfigurerer Java AsyncHttpClient, som er en del av standard JDK og ikke er avhengig av Play.

3. Forbered et eksempel på prosjekt

For å eksperimentere med rammeverket, la oss lage noen enhetstester for å starte forespørsler. Vi oppretter et skjelettapplikasjon for å svare på dem og bruke WS-rammeverket til å komme med HTTP-forespørsler.

3.1. Skjelettnettapplikasjonen

Først og fremst lager vi det opprinnelige prosjektet ved å bruke sbt ny kommando:

sbt ny playframework / play-java-seed.g8

I den nye mappen, vi da redigere build.sbt fil og legg til WS-bibliotekets avhengighet:

bibliotekavhengigheter + = javaWs

Nå kan vi starte serveren med sbt kjøre kommando:

$ sbt run ... --- (Kjører applikasjonen, automatisk omlasting er aktivert) --- [info] pcsAkkaHttpServer - Lytter etter HTTP på / 0: 0: 0: 0: 0: 0: 0: 0: 9000

Når applikasjonen har startet, kan vi sjekke at alt er ok ved å bla // lokal vert: 9000, som åpner Play velkomstside.

3.2. Testmiljøet

For å teste søknaden vår, bruker vi enhetstestklassen HomeControllerTest.

Først må vi utvide WithServer som vil gi serverens livssyklus:

offentlig klasse HomeControllerTest utvider WithServer { 

Takk til foreldrene, denne klassen starter nå skjelettwebserveren vår i testmodus og i en tilfeldig port, før du kjører testene. De WithServer klasse stopper også søknaden når testen er ferdig.

Deretter må vi gi et program for å kjøre.

Vi kan lage det med Guice‘S GuiceApplicationBuilder:

@ Override-beskyttet applikasjon supplyApplication () {returner nye GuiceApplicationBuilder (). Build (); } 

Og til slutt konfigurerte vi server-URL-en som skal brukes i testene våre, ved hjelp av portnummeret som tilbys av testserveren:

@ Override @ Før offentlig ugyldig oppsett () {OptionalInt optHttpsPort = testServer.getRunningHttpsPort (); hvis (optHttpsPort.isPresent ()) {port = optHttpsPort.getAsInt (); url = "// localhost:" + port; } annet {port = testServer.getRunningHttpPort () .getAsInt (); url = "// localhost:" + port; }}

Nå er vi klare til å skrive tester. Det omfattende testrammeverket lar oss konsentrere oss om å kode testforespørslene våre.

4. Forbered en WSRequest

La oss se hvordan vi kan utløse grunnleggende typer forespørsler, for eksempel GET eller POST, og forespørsler om flerdelt for filopplasting.

4.1. Initialiser WSRequest Gjenstand

Først og fremst må vi skaffe oss en WSClient forekomst for å konfigurere og initialisere forespørslene våre.

I en applikasjon fra virkeligheten kan vi få en klient, automatisk konfigurert med standardinnstillinger, via avhengighetsinjeksjon:

@Autowired WSClient ws;

I testklassen bruker vi imidlertid WSTestClient, tilgjengelig fra Play Test-rammeverket:

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

Når vi har fått vår klient, kan vi initialisere en WSRequest objektet ved å ringe url metode:

ws.url (url)

De url metoden gjør nok for at vi kan avfire en forespørsel. Vi kan imidlertid tilpasse det ytterligere ved å legge til noen tilpassede innstillinger:

ws.url (url) .addHeader ("nøkkel", "verdi") .addQueryParameter ("num", "" + num);

Som vi kan se, er det ganske enkelt å legge til overskrifter og spørringsparametere.

Etter at vi har konfigurert forespørselen vår fullt ut, kan vi ringe metoden for å starte den.

4.2. Generisk GET-forespørsel

For å utløse en GET-forespørsel må vi bare ringe metode på vår WSRequest gjenstand:

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

Siden dette er en ikke-blokkerende kode, starter den forespørselen og fortsetter deretter kjøringen på neste linje i vår funksjon.

Objektet returnerte av er en CompletionStage forekomst, som er en del av Fullførbar fremtid API.

Når HTTP-samtalen er fullført, utfører dette trinnet bare noen få instruksjoner. Den bryter responsen inn i en WSResponse gjenstand.

Normalt vil dette resultatet videreføres til neste trinn i kjøringskjeden. I dette eksemplet har vi ikke gitt noen forbrukende funksjon, så resultatet går tapt.

Av denne grunn er denne forespørselen av typen "fyr og glem".

4.3. Send inn et skjema

Å sende inn et skjema er ikke veldig forskjellig fra eksempel.

For å utløse forespørselen ringer vi bare post metode:

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

I dette scenariet må vi sende en kropp som en parameter. Dette kan være en enkel streng som en fil, et json- eller xml-dokument, en BodyWritable eller a Kilde.

4.4. Send inn en flerdelt / skjemadata

Et flerdelt skjema krever at vi sender både inndatafelt og data fra en vedlagt fil eller strøm.

For å implementere dette i rammeverket bruker vi post metode med en Kilde.

Inne i kilden kan vi pakke inn alle de forskjellige datatypene som skjemaet vårt trenger:

Kildefil = FileIO.fromPath (Paths.get ("hallo.txt")); FilePart file = new FilePart ("fileParam", "myfile.txt", "text / plain", file); DataPart data = ny DataPart ("nøkkel", "verdi"); ws.url (url) ... .post (Source.from (Arrays.asList (file, data)));

Selv om denne tilnærmingen legger til litt mer konfigurasjon, er den likevel veldig lik de andre typer forespørsler.

5. Behandle Async-svaret

Fram til dette har vi bare utløst forespørsler om brann og glem, der koden vår ikke gjør noe med svardataene.

La oss nå utforske to teknikker for behandling av et asynkront svar.

Vi kan enten blokkere hovedtråden og vente på en Fullførbar fremtid eller konsumere asynkront med en Forbruker.

5.1. Behandle svar ved å blokkere med Fullførbar fremtid

Selv når vi bruker et asynkront rammeverk, kan vi velge å blokkere kodens utførelse og vente på svaret.

Bruker Fullførbar fremtid API, vi trenger bare noen få endringer i koden vår for å implementere dette scenariet:

WSResponse respons = ws.url (url) .get () .toCompletableFuture () .get ();

Dette kan for eksempel være nyttig for å gi en sterk datakonsistens som vi ikke kan oppnå på andre måter.

5.2. Behandle svar asynkront

For å behandle et asynkront svar uten å blokkere, vi gir en Forbruker eller Funksjon som drives av det asynkrone rammeverket når svaret er tilgjengelig.

La oss for eksempel legge til en Forbruker til vårt forrige eksempel for å logge svaret:

ws.url (url) .addHeader ("key", "value") .addQueryParameter ("num", "" + 1). get () .thenAccept (r -> log.debug ("Thread #" + Thread.) currentThread (). getId () + "Forespørsel fullført: Svarskode =" + r.getStatus () + "| Svar:" + r.getBody () + "| Nåværende tid:" + System.currentTimeMillis ()));

Vi ser da svaret i loggene:

[debug] c.HomeControllerTest - Tråd # 30 Forespørsel fullført: Svarskode = 200 | Svar: {"Resultat": "ok", "Params": {"num": ["1"]}, "Headers": {"accept": ["* / *"], "host": [" localhost: 19001 "]," key ": [" value "]," user-agent ": [" AHC / 2.1 "]}} | Nåværende tid: 1579303109613

Det er verdt å merke seg at vi brukte deretterAksepter, som krever en Forbruker funksjon siden vi ikke trenger å returnere noe etter loggingen.

Når vi vil at den nåværende fasen skal returnere noe, slik at vi kan bruke det i neste trinn, trenger vi deretterSøk i stedet, som tar en Funksjon.

Disse bruker konvensjonene til standard Java Functional Interfaces.

5.3. Stor respons kropp

Koden vi har implementert så langt, er en god løsning for små svar og de fleste brukssaker. Men hvis vi trenger å behandle noen hundre megabyte med data, trenger vi en bedre strategi.

Vi bør merke oss: Be om metoder som og post last hele responsen i minnet.

For å unngå en mulig OutOfMemoryError, kan vi bruke Akka Streams til å behandle svaret uten å la det fylle minnet vårt.

For eksempel kan vi skrive kroppen i en fil:

ws.url (url) .stream () .thenAccept (respons -> {prøv {OutputStream outputStream = Files.newOutputStream (sti); Vask outputWriter = Sink.foreach (bytes -> outputStream.write (bytes.toArray ())); response.getBodyAsSource (). runWith (outputWriter, materializer); } catch (IOException e) {log.error ("Det oppstod en feil under åpning av utgangsstrømmen", e); }});

De strøm metoden returnerer a CompletionStage hvor i WSResponse har en getBodyAsStream metode som gir en Kilde.

Vi kan fortelle koden hvordan vi behandler denne typen kropp ved å bruke Akkas Synke, som i vårt eksempel ganske enkelt vil skrive data som går gjennom i OutputStream.

5.4. Tidsavbrudd

Når vi bygger en forespørsel, kan vi også angi en bestemt tidsavbrudd, slik at forespørselen blir avbrutt hvis vi ikke mottar det komplette svaret i tide.

Dette er en spesielt nyttig funksjon når vi ser at en tjeneste vi spør etter, er spesielt treg og kan føre til en samling av åpne forbindelser som sitter og venter på svaret.

Vi kan angi en global tidsavbrudd for alle forespørslene våre ved hjelp av innstillingsparametere. For en forespørselsspesifikk tidsavbrudd kan vi legge til en forespørsel ved hjelp av setRequestTimeout:

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

Det er fremdeles ett tilfelle å håndtere, skjønt: Vi har kanskje mottatt alle dataene, men vår Forbruker kan være veldig treg å behandle den. Dette kan skje hvis det er mye dataknusing, databasesamtaler, etc.

I systemer med lav gjennomstrømning kan vi bare la koden kjøre til den er fullført. Vi kan imidlertid ønske å avbryte langvarige aktiviteter.

For å oppnå det, må vi pakke koden med noen futures håndtering.

La oss simulere en veldig lang prosess i koden vår:

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

Dette vil returnere en OK svar etter 10 sekunder, men vi vil ikke vente så lenge.

I stedet for med pause innpakning, vi instruerer koden vår om å vente i ikke mer enn 1 sekund:

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

Nå vil fremtiden vår returnere et resultat på begge måter: beregningsresultatet hvis Forbruker ferdig i tide, eller unntaket på grunn av futures pause.

5.5. Håndtering av unntak

I forrige eksempel opprettet vi en funksjon som enten returnerer et resultat eller mislykkes med unntak. Så nå må vi håndtere begge scenariene.

Vi kan håndtere både suksess- og fiaskoscenarier med handleAsync metode.

La oss si at vi vil returnere resultatet, hvis vi har fått det, eller logge feilen og returnere unntaket for videre håndtering:

CompletionStage res = f.handleAsync ((resultat, e) -> {hvis (e! = Null) {log.error ("Unntatt kastet", e); returner e.getCause ();} annet {returresultat;}} ); 

Koden skal nå returnere a CompletionStage inneholder TimeoutException kastet.

Vi kan bekrefte det ved å ringe en hevderEquals på klassen for unntaksobjektet som returneres:

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

Når du kjører testen, vil den også logge unntaket vi mottok:

[feil] c.HomeControllerTest - Unntak kastet java.util.concurrent.TimeoutException: Timeout etter 1 sekund ...

6. Be om filtre

Noen ganger må vi kjøre litt logikk før en forespørsel utløses.

Vi kunne manipulere WSRequest objektet en gang initialisert, men en mer elegant teknikk er å sette et WSRequestFilter.

Et filter kan stilles inn under initialisering før utkallingsmetoden kalles, og er knyttet til forespørsellogikken.

Vi kan definere vårt eget filter ved å implementere WSRequestFilter grensesnitt, eller vi kan legge til en ferdig.

Et vanlig scenario er å logge hvordan forespørselen ser ut før den kjøres.

I dette tilfellet trenger vi bare å stille inn AhcCurlRequestLogger:

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

Den resulterende loggen har en krølle-liknende format:

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

Vi kan stille inn ønsket loggnivå ved å endre vårt logback.xml konfigurasjon.

7. Caching-svar

WSClient støtter også caching av svar.

Denne funksjonen er spesielt nyttig når den samme forespørselen utløses flere ganger, og vi ikke trenger de ferskeste dataene hver gang.

Det hjelper også når tjenesten vi ringer midlertidig er nede.

7.1. Legg til Caching-avhengigheter

For å konfigurere caching må vi først legge til avhengigheten i vår build.sbt:

biblioteksavhengighet + = ehcache

Dette konfigurerer Ehcache som vårt cachelag.

Hvis vi ikke vil ha Ehcache spesifikt, kan vi bruke hvilken som helst annen JSR-107 cache-implementering.

7.2. Force Caching Heuristic

Som standard vil ikke Play WS cache HTTP-svar hvis serveren ikke returnerer noen bufringskonfigurasjon.

For å omgå dette, kan vi tvinge heuristisk caching ved å legge til en innstilling til vår application.conf:

play.ws.cache.heuristics.enabled = true

Dette vil konfigurere systemet til å bestemme når det er nyttig å cache et HTTP-svar, uavhengig av fjerntjenestens annonserte hurtigbufring.

8. Ekstra innstilling

Forespørsel til en ekstern tjeneste kan kreve noen klientkonfigurasjon. Vi kan trenge å håndtere viderekoblinger, en treg server eller noe filtrering avhengig av brukeragentens overskrift.

For å løse det kan vi stille inn WS-klienten vår ved å bruke egenskaper i vår application.conf:

play.ws.followRedirects = falsk play.ws.useragent = MyPlayApplication play.ws.compressionEnabled = true # tid å vente på at tilkoblingen blir opprettet play.ws.timeout.connection = 30 # tid å vente på data etter at tilkoblingen er åpen play.ws.timeout.idle = 30 # maks tid tilgjengelig for å fullføre forespørselen play.ws.timeout.request = 300

Det er også mulig å konfigurere det underliggende AsyncHttpClient direkte.

Den komplette listen over tilgjengelige eiendommer kan sjekkes i kildekoden til AhcConfig.

9. Konklusjon

I denne artikkelen utforsket vi Play WS-biblioteket og dets hovedtrekk. Vi konfigurerte prosjektet vårt, lærte hvordan vi kan utløse vanlige forespørsler og behandle svaret deres, både synkront og asynkront.

Vi jobbet med store data nedlastinger og så hvordan vi kunne kutte korte langvarige aktiviteter.

Til slutt så vi på caching for å forbedre ytelsen, og hvordan du kan stille inn klienten.

Som alltid er kildekoden for denne opplæringen tilgjengelig på GitHub.

Java bunn

Jeg kunngjorde nettopp det nye Lær våren kurs, med fokus på det grunnleggende i vår 5 og vårstøvel 2:

>> KONTROLLER KURSET