Asynchrónne programovanie HTTP s rámcom Play

Java Top

Práve som oznámil nové Naučte sa jar kurz zameraný na základy jari 5 a Spring Boot 2:

>> SKONTROLUJTE KURZ

1. Prehľad

Naše webové služby často potrebujú na výkon svojej práce iné webové služby. Môže byť ťažké vyhovieť požiadavkám používateľov pri zachovaní nízkeho času odozvy. Pomalá externá služba môže zvýšiť náš čas odozvy a spôsobiť, že náš systém zhromažďuje požiadavky a využíva viac zdrojov. To je miesto, kde môže byť veľmi užitočný neblokujúci prístup

V tomto tutoriáli vypálime viac asynchrónnych požiadaviek na službu z aplikácie Play Framework. Využitím neblokujúcej schopnosti HTTP Java budeme schopní plynulo dopytovať externé zdroje bez ovplyvnenia našej vlastnej hlavnej logiky.

V našom príklade preskúmame knižnicu Play WebService.

2. Knižnica Play WebService (WS)

WS je výkonná knižnica poskytujúca asynchrónne volania HTTP pomocou Javy Akcia.

Pomocou tejto knižnice náš kód odosiela tieto požiadavky a pokračuje bez blokovania. Na spracovanie výsledku žiadosti poskytujeme náročnú funkciu, to znamená implementáciu Spotrebiteľ rozhranie.

Tento vzor zdieľa niektoré podobnosti s implementáciou spätných volaní JavaScriptom, Sľuby, a asynchronizovať / čakať vzor.

Postavme jednoduchý Spotrebiteľ ktorý zaznamenáva niektoré z údajov odpovede:

ws.url (url) .thenAccept (r -> log.debug ("Thread #" + Thread.currentThread (). getId () + "Žiadosť je dokončená: Kód odpovede =" + r.getStatus () + "| Odpoveď: "+ r.getBody () +" | Aktuálny čas: "+ System.currentTimeMillis ()))

Náš Spotrebiteľ sa v tomto príklade iba prihlasuje. Spotrebiteľ by mohol s výsledkom urobiť čokoľvek, čo potrebujeme, napríklad uložiť výsledok do databázy.

Ak sa pozrieme hlbšie na implementáciu knižnice, môžeme pozorovať, že WS obaľuje a konfiguruje Java AsyncHttpClient, ktorý je súčasťou štandardného JDK a nezávisí od Play.

3. Pripravte ukážkový projekt

Ak chcete experimentovať s rámcom, vytvorme niekoľko testov jednotiek na spustenie požiadaviek. Vytvoríme kostrovú webovú aplikáciu, ktorá im odpovie, a na vytváranie požiadaviek HTTP použijeme rámec WS.

3.1. Webová aplikácia Skeleton

Najskôr vytvoríme počiatočný projekt pomocou sbt nový príkaz:

sbt new playframework / play-java-seed.g8

V novom priečinku potom upraviť build.sbt súbor a pridajte závislosť knižnice WS:

libraryDependencies + = javaWs

Teraz môžeme server spustiť pomocou sbt beh príkaz:

$ sbt run ... --- (Spustenie aplikácie, automatické načítanie je povolené) --- [info] pcsAkkaHttpServer - počúvanie HTTP na / 0: 0: 0: 0: 0: 0: 0: 0: 9000

Po spustení aplikácie môžeme prehliadaním skontrolovať, či je všetko v poriadku // localhost: 9000, ktorá otvorí uvítaciu stránku Play.

3.2. Testovacie prostredie

Na testovanie našej aplikácie použijeme triedu testovania jednotiek HomeControllerTest.

Najprv musíme predĺžiť WithServer ktoré poskytnú životný cyklus servera:

verejná trieda HomeControllerTest rozširuje WithServer { 

Vďaka svojmu rodičovi táto trieda teraz spúšťa náš skeletový webový server v testovacom režime a na náhodnom porte, pred vykonaním testov. The WithServer trieda tiež zastaví aplikáciu po dokončení testu.

Ďalej musíme poskytnúť aplikáciu na spustenie.

Môžeme to vytvoriť pomocou Guice‘S GuiceApplicationBuilder:

@ Override chránená aplikácia provideApplication () {return new GuiceApplicationBuilder (). Build (); } 

A nakoniec sme nastavili adresu URL servera, ktorá sa má použiť v našich testoch, pomocou čísla portu poskytnutého testovacím serverom:

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

Teraz sme pripravení písať testy. Komplexný testovací rámec nám umožňuje sústrediť sa na kódovanie našich testovacích požiadaviek.

4. Pripravte požiadavku WSR

Pozrime sa, ako môžeme spustiť základné typy požiadaviek, napríklad GET alebo POST, a viacdielne žiadosti o nahranie súboru.

4.1. Inicializujte WSRequest Objekt

Najskôr musíme získať a WSClient napríklad na konfiguráciu a inicializáciu našich požiadaviek.

V aplikácii v reálnom živote môžeme získať klienta, ktorý je automaticky nakonfigurovaný s predvoleným nastavením, pomocou injekcie závislostí:

@Autowired WSClient ws;

V našej testovacej triede však používame WSTestClient, dostupné z rámca Play Test:

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

Keď už máme svojho klienta, môžeme inicializovať a WSRequest objekt zavolaním na url metóda:

ws.url (url)

The url metóda robí dosť na to, aby nám umožnila vyhovieť žiadosti. Môžeme ho však ďalej prispôsobiť pridaním niektorých vlastných nastavení:

ws.url (url) .addHeader ("kľúč", "hodnota") .addQueryParameter ("num", "" + num);

Ako vidíme, pridávanie hlavičiek a parametrov dotazu je celkom jednoduché.

Po úplnej konfigurácii našej žiadosti môžeme zavolať metódu na jej spustenie.

4.2. Všeobecná požiadavka GET

Na spustenie žiadosti GET musíme zavolať na dostať metóda na našom WSRequest objekt:

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

Pretože sa jedná o neblokujúci kód, spustí sa požiadavka a potom sa pokračuje v vykonávaní na ďalšom riadku našej funkcie.

Objekt vrátený používateľom dostať je a DokončenieStage inštancia, ktorá je súčasťou CompletableFuture API.

Po dokončení hovoru HTTP vykoná táto fáza iba niekoľko pokynov. Zabalí odpoveď do a WSResponse objekt.

Za normálnych okolností by sa tento výsledok preniesol do ďalšej fázy realizačného reťazca. V tomto príklade sme neposkytli žiadnu konzumačnú funkciu, takže sa výsledok stratí.

Z tohto dôvodu je táto žiadosť typu „oheň a zabudni“.

4.3. Odošlite formulár

Odoslanie formulára sa veľmi nelíši od dostať príklad.

Na spustenie žiadosti jednoducho zavoláme príspevok metóda:

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

V tomto scenári musíme odovzdať telo ako parameter. Môže to byť jednoduchý reťazec, napríklad súbor, dokument JSON alebo XML, a BodyWritable alebo a Zdroj.

4.4. Odošlite údaje o viacerých častiach / formulári

Viacdielny formulár vyžaduje, aby sme odoslali vstupné polia aj údaje z priloženého súboru alebo streamu.

Na implementáciu tohto rámca používame príspevok metóda s a Zdroj.

Vo vnútri zdroja môžeme zabaliť všetky rôzne typy údajov potrebné pre náš formulár:

Zdrojový súbor = FileIO.fromPath (Paths.get ("hello.txt")); FilePart súbor = nový FilePart ("fileParam", "myfile.txt", "text / plain", súbor); DataPart data = new DataPart ("kľúč", "hodnota"); ws.url (url) ... .post (Source.from (Arrays.asList (súbor, údaje))));

Aj keď tento prístup pridáva ďalšie konfigurácie, je stále veľmi podobný iným typom požiadaviek.

5. Spracujte asynchronnú odpoveď

Do tohto okamihu sme spustili iba žiadosti typu fire-and-forget, kde náš kód s údajmi odpovede nič neurobí.

Poďme teraz preskúmať dve techniky spracovania asynchrónnej odpovede.

Buď môžeme zablokovať hlavné vlákno, čakáme na a CompletableFuture, alebo konzumovať asynchrónne s a Spotrebiteľ.

5.1. Odozva procesu blokovaním pomocou CompletableFuture

Aj keď používame asynchrónny rámec, môžeme sa rozhodnúť zablokovať vykonávanie nášho kódu a čakať na odpoveď.

Pomocou CompletableFuture API, na implementáciu tohto scenára potrebujeme v našom kóde iba niekoľko zmien:

Odpoveď WSResponse = ws.url (url) .get () .toCompletableFuture () .get ();

To by mohlo byť užitočné napríklad na zabezpečenie silnej konzistencie údajov, ktorú nemôžeme dosiahnuť inými spôsobmi.

5.2. Odpovedať na proces asynchrónne

Ak chcete spracovať asynchrónnu odpoveď bez blokovania, poskytujeme a Spotrebiteľ alebo Funkcia ktorý je spustený asynchrónnym rámcom, keď je k dispozícii odpoveď.

Napríklad pridajme a Spotrebiteľ náš predchádzajúci príklad na zaznamenanie odpovede:

ws.url (url) .addHeader ("kľúč", "hodnota") .addQueryParameter ("num", "" + 1) .get () .thenAccept (r -> log.debug ("vlákno #" + vlákno. currentThread (). getId () + "Žiadosť bola dokončená: Kód odpovede =" + r.getStatus () + "| Odpoveď:" + r.getBody () + "| Aktuálny čas:" + System.currentTimeMillis ()));

Potom uvidíme odpoveď v denníkoch:

[debug] c.HomeControllerTest - vlákno č. 30 Požiadavka dokončená: Kód odpovede = 200 | Odpoveď: {"Výsledok": "ok", "Params": {"num": ["1"]}, "Hlavičky": {"accept": ["* / *"], "host": [" localhost: 19001 "]," kľúč ": [" hodnota "]," user-agent ": [" AHC / 2.1 "]}} | Aktuálny čas: 1579303109613

Stojí za zmienku, že sme použili potom Prijať, ktorá vyžaduje a Spotrebiteľ funkcia, pretože po prihlásení nemusíme nič vracať.

Keď chceme, aby súčasná fáza niečo vrátila, aby sme to mohli použiť v ďalšej fáze, potrebujeme to potomPoužiť namiesto toho, ktorá trvá a Funkcia.

Tieto používajú konvencie štandardných funkčných rozhraní Java.

5.3. Veľké telo reakcie

Kód, ktorý sme doteraz implementovali, je dobrým riešením pre malé odpovede a väčšinu prípadov použitia. Ak však potrebujeme spracovať niekoľko stoviek megabajtov dát, budeme potrebovať lepšiu stratégiu.

Mali by sme poznamenať: Vyžiadajte si metódy ako dostať a príspevok načítať celú odpoveď do pamäte.

Aby sa zabránilo možnému OutOfMemoryError, môžeme použiť prúdy Akka na spracovanie odpovede bez toho, aby sme ju nechali vyplniť našu pamäť.

Napríklad môžeme napísať jeho telo do súboru:

ws.url (url) .stream () .thenAccept (odpoveď -> {vyskúšať {OutputStream outputStream = Files.newOutputStream (cesta); drez) outputWriter = Sink.foreach (bajty -> outputStream.write (bytes.toArray ())); response.getBodyAsSource (). runWith (outputWriter, materializer); } catch (IOException e) {log.error ("Pri otváraní výstupného toku sa vyskytla chyba", e); }});

The Prúd metóda vracia a DokončenieStage kde WSResponsegetBodyAsStream metóda, ktorá poskytuje a Zdroj.

Pomocou kódu Akka môžeme povedať kódu, ako spracovať tento typ tela drez, ktorý v našom príklade jednoducho zapíše všetky údaje prechádzajúce cez server OutputStream.

5.4. Časové limity

Pri vytváraní žiadosti môžeme tiež nastaviť konkrétny časový limit, takže pokiaľ nedostaneme včas úplnú odpoveď, žiadosť sa preruší.

Toto je obzvlášť užitočná funkcia, keď vidíme, že služba, na ktorú sa pýtame, je obzvlášť pomalá a mohla by spôsobiť hromadenie otvorených spojení uviaznutých pri čakaní na odpoveď.

Môžeme nastaviť globálny časový limit pre všetky naše požiadavky pomocou parametrov ladenia. Pre časový limit špecifický pre požiadavku môžeme pridať k žiadosti pomocou setRequestTimeout:

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

Stále je však potrebné vyriešiť jeden prípad: Možno sme dostali všetky údaje, ale naše Spotrebiteľ môže byť jeho spracovanie veľmi pomalé. To sa môže stať, ak dôjde k veľkému množstvu dát, volanie do databázy atď.

V systémoch s nízkou priepustnosťou môžeme jednoducho nechať bežať kód, kým sa nedokončí. Možno by sme však chceli prerušiť dlhotrvajúce aktivity.

Aby sme to dosiahli, musíme náš kód zabaliť do niekoľkých futures manipulácia.

Simulujme veľmi dlhý proces v našom kóde:

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

Týmto sa vráti Ok odpoveď po 10 sekundách, ale nechceme čakať tak dlho.

Namiesto toho s čas vypršal wrapper, dáme nášmu kódu pokyn, aby čakal nie viac ako 1 sekundu:

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

Teraz naša budúcnosť vráti výsledok v oboch smeroch: výsledok výpočtu, ak Spotrebiteľ hotové včas, alebo výnimka z dôvodu futures čas vypršal.

5.5. Spracovanie výnimiek

V predchádzajúcom príklade sme vytvorili funkciu, ktorá buď vráti výsledok, alebo zlyhá s výnimkou. Teraz teda musíme zvládnuť oba scenáre.

S nástrojom môžeme zvládnuť scenáre úspechu aj neúspechu handleAsync metóda.

Povedzme, že chceme vrátiť výsledok, ak ho máme, alebo prihlásiť chybu a vrátiť výnimku na ďalšie spracovanie:

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

Kód by teraz mal vrátiť a DokončenieStage obsahujúci Výnimka časového limitu vyhodený.

Môžeme to overiť jednoduchým zavolaním na assertEquals v triede vráteného objektu výnimky:

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

Pri spustení testu sa zaznamená aj výnimka, ktorú sme dostali:

[chyba] c.HomeControllerTest - vyvolaná výnimka java.util.concurrent.TimeoutException: časový limit po 1 sekunde ...

6. Vyžiadajte si filtre

Niekedy je potrebné pred spustením požiadavky spustiť určitú logiku.

Mohli by sme manipulovať s WSRequest objekt po inicializácii, ale elegantnejšou technikou je nastavenie a WSRequestFilter.

Počas inicializácie, pred volaním metódy spúšťania, je možné nastaviť filter, ktorý je pripojený k logike požiadavky.

Náš vlastný filter môžeme definovať implementáciou WSRequestFilter rozhranie, alebo môžeme pridať už hotové.

Bežným scenárom je zaznamenávanie toho, ako vyzerá žiadosť, pred jej vykonaním.

V takom prípade stačí nastaviť AhcCurlRequestLogger:

ws.url (url) ... .setRequestFilter (nový AhcCurlRequestLogger ()) ... .get ();

Výsledný protokol má a zvlneniepodobný formát:

[info] p.l.w.a.AhcCurlRequestLogger - curl \ --verbose \ --request GET \ --header 'kľúč: hodnota' \ '// localhost: 19001'

Môžeme nastaviť požadovanú úroveň denníka zmenou našej logback.xml konfigurácia.

7. Odpovede do pamäte cache

WSClient podporuje tiež ukladanie odpovedí do pamäte cache.

Táto funkcia je obzvlášť užitočná, keď sa rovnaká požiadavka spustí viackrát a nepotrebujeme zakaždým najaktuálnejšie údaje.

Pomáha tiež vtedy, keď je služba, na ktorú voláme, dočasne nefunkčná.

7.1. Pridajte závislosti na medzipamäti

Ak chcete nakonfigurovať ukladanie do medzipamäte, musíme najskôr pridať závislosť do nášho build.sbt:

libraryDependencies + = ehcache

Týmto sa konfiguruje Ehcache ako naša vrstva ukladania do pamäte cache.

Ak konkrétne nechceme Ehcache, môžeme použiť akúkoľvek inú implementáciu vyrovnávacej pamäte JSR-107.

7.2. Force Caching heuristický

Ak server nevráti žiadnu konfiguráciu ukladania do pamäte, služba Play WS predvolene nebude ukladať odpovede HTTP do vyrovnávacej pamäte.

Aby sme to obišli, môžeme vynútiť heuristické ukladanie do medzipamäte pridaním nastavenia do nášho application.conf:

play.ws.cache.heuristics.enabled = true

Toto nakonfiguruje systém tak, aby rozhodoval, kedy je užitočné uložiť odpoveď HTTP do pamäte cache, bez ohľadu na inzerované ukladanie do pamäte cache vzdialenej služby.

8. Dodatočné ladenie

Zadávanie požiadaviek na externú službu môže vyžadovať určitú konfiguráciu klienta. Možno budeme musieť zvládnuť presmerovania, pomalý server alebo nejaké filtrovanie v závislosti od hlavičky agenta používateľa.

Aby sme to vyriešili, môžeme vyladiť nášho klienta WS pomocou vlastností v našom application.conf:

play.ws.followRedirects = false play.ws.useragent = MyPlayApplication play.ws.compressionEnabled = true # čas čakania na nadviazanie spojenia play.ws.timeout.connection = 30 # čas čakania na dáta po pripojení otvorený play.ws.timeout.idle = 30 # maximálneho času, ktorý je k dispozícii na dokončenie žiadosti play.ws.timeout.request = 300

Je tiež možné nakonfigurovať podklad AsyncHttpClient priamo.

Celý zoznam dostupných vlastností je možné skontrolovať v zdrojovom kóde servera AhcConfig.

9. Záver

V tomto článku sme preskúmali knižnicu Play WS a jej hlavné funkcie. Nakonfigurovali sme náš projekt, naučili sme sa, ako spúšťať bežné požiadavky a spracovávať ich odpovede, synchrónne aj asynchrónne.

Pracovali sme s veľkým objemom sťahovania údajov a zistili sme, ako obmedziť krátke dlhodobé aktivity.

Nakoniec sme sa pozreli na ukladanie do vyrovnávacej pamäte na zlepšenie výkonu a na to, ako vyladiť klienta.

Ako vždy, zdrojový kód tohto tutoriálu je k dispozícii na GitHub.

Java dole

Práve som oznámil nové Naučte sa jar kurz zameraný na základy jari 5 a Spring Boot 2:

>> SKONTROLUJTE KURZ

$config[zx-auto] not found$config[zx-overlay] not found