Asynkron HTTP-programmering med Play Framework
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 KURSET1. 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 få 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 få 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 få 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
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 få 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