Asinkrono HTTP programiranje s Play Frameworkom

Java Top

Upravo sam najavio novo Uči proljeće tečaj, usredotočen na osnove Spring 5 i Spring Boot 2:

>> PROVJERITE TEČAJ

1. Pregled

Često naše web usluge trebaju koristiti druge web usluge da bi obavljale svoj posao. Može biti teško uslužiti korisničke zahtjeve, a zadržati kratko vrijeme odgovora. Spora vanjska usluga može povećati vrijeme odziva i uzrokovati da naš sustav gomila zahtjeve, koristeći više resursa. Tu pristup bez blokiranja može biti od velike pomoći

U ovom ćemo uputstvu aktivirati više asinkronih zahtjeva za uslugu iz aplikacije Play Framework. Koristeći Javinu neblokirajuću HTTP sposobnost, moći ćemo glatko ispitivati ​​vanjske resurse bez utjecaja na našu glavnu logiku.

U našem ćemo primjeru istražiti Play WebService Library.

2. Biblioteka Play WebService (WS)

WS je moćna knjižnica koja pruža asinkrone HTTP pozive pomoću Jave Akcijski.

Koristeći ovu knjižnicu, naš kod šalje te zahtjeve i nastavlja bez blokiranja. Da bismo obradili rezultat zahtjeva, pružamo konzumirajuću funkciju, odnosno provedbu Potrošač sučelje.

Ovaj obrazac dijeli neke sličnosti s JavaScript implementacijom povratnih poziva, Obećanja, i asinkronizirati / čekati uzorak.

Izgradimo jednostavan Potrošač koji bilježi neke podatke o odgovoru:

ws.url (url) .thenAccept (r -> log.debug ("Tema #" + Thread.currentThread (). getId () + "Zahtjev dovršen: Kôd odgovora =" + r.getStatus () + "| Odgovor: "+ r.getBody () +" | Trenutno vrijeme: "+ System.currentTimeMillis ()))

Naše Potrošač se samo prijavljuje u ovaj primjer. Potrošač bi mogao učiniti sve što trebamo s rezultatom, na primjer, pohraniti rezultat u bazu podataka.

Ako dublje pogledamo u implementaciju knjižnice, možemo primijetiti da WS obavija i konfigurira Javinu AsyncHttpClient, koji je dio standardnog JDK i ne ovisi o Playu.

3. Pripremite primjer projekta

Da bismo eksperimentirali s okvirom, stvorimo neke jedinične testove za pokretanje zahtjeva. Izradit ćemo skeletnu web aplikaciju koja će odgovoriti na njih i upotrijebiti WS okvir za izradu HTTP zahtjeva.

3.1. Web aplikacija Skeleton

Prije svega, inicijalni projekt kreiramo pomoću sbt novo naredba:

sbt novi playframework / play-java-seed.g8

Tada smo u novoj mapi uredi graditi.sbt datoteku i dodajte ovisnost WS knjižnice:

libraryDependencies + = javaWs

Sada možemo pokrenuti poslužitelj s sbt trčanje naredba:

$ sbt run ... --- (pokretanje aplikacije, omogućeno je automatsko ponovno učitavanje) --- [info] pcsAkkaHttpServer - Slušanje HTTP-a uključeno / 0: 0: 0: 0: 0: 0: 0: 0: 9000

Nakon pokretanja aplikacije, pregledavanjem možemo provjeriti je li sve u redu // localhost: 9000, koja će otvoriti Playovu stranicu dobrodošlice.

3.2. Ispitno okruženje

Da bismo testirali našu aplikaciju, koristit ćemo klasu unit test HomeControllerTest.

Prvo, moramo produžiti WithServer koji će pružiti životni ciklus poslužitelja:

javna klasa HomeControllerTest proširuje se s serverom { 

Zahvaljujući svom roditelju, ova klasa sada pokreće naš web poslužitelj skeleta u testnom načinu i na slučajnom priključku, prije pokretanja testova. The WithServer klasa također zaustavlja prijavu kada je test završen.

Dalje, moramo pružiti aplikaciju za pokretanje.

Možemo ga stvoriti pomoću Guice‘S GuiceApplicationBuilder:

@Override zaštićena aplikacija provideApplication () {vratiti novi GuiceApplicationBuilder (). Build (); } 

I na kraju, postavili smo URL poslužitelja za upotrebu u našim testovima, koristeći broj porta koji je pružio testni poslužitelj:

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

Sada smo spremni za pisanje testova. Sveobuhvatni testni okvir omogućuje nam da se koncentriramo na kodiranje zahtjeva za testiranje.

4. Pripremite WSRequest

Pogledajmo kako možemo pokrenuti osnovne vrste zahtjeva, poput GET ili POST, i višedijelne zahtjeve za prijenos datoteka.

4.1. Inicijalizirajte WSRequest Objekt

Prije svega, moramo dobiti a WSClient instanci za konfiguriranje i inicijalizaciju naših zahtjeva.

U aplikaciji iz stvarnog života putem injekcije ovisnosti možemo dobiti klijenta, automatski konfiguriranog sa zadanim postavkama:

@Autowired WSClient ws;

Međutim, u našoj testnoj klasi koristimo WSTestClient, dostupno iz okvira Play Test:

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

Jednom kada imamo svog klijenta, možemo inicijalizirati a WSRequest objekt pozivanjem url metoda:

ws.url (url)

The url metoda čini dovoljno da nam omogući pokretanje zahtjeva. Međutim, možemo ga dodatno prilagoditi dodavanjem nekih prilagođenih postavki:

ws.url (url) .addHeader ("key", "value") .addQueryParameter ("num", "" + num);

Kao što vidimo, prilično je jednostavno dodati zaglavlja i parametre upita.

Nakon što u potpunosti konfiguriramo svoj zahtjev, možemo pozvati metodu kako bismo ga pokrenuli.

4.2. Generički zahtjev za GET

Da bismo pokrenuli GET zahtjev, jednostavno moramo nazvati dobiti metoda na našem WSRequest objekt:

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

Budući da je ovo neblokirajući kôd, započinje zahtjev, a zatim nastavlja izvršenje u sljedećem retku naše funkcije.

Predmet vratio dobiti je CompletionStage primjer, koji je dio CompletableFuture API.

Nakon završetka HTTP poziva, ova faza izvršava samo nekoliko uputa. Omotava odgovor u WSResponse objekt.

Obično bi se ovaj rezultat prenio u sljedeću fazu ovršnog lanca. U ovom primjeru nismo osigurali nijednu konzumnu funkciju, pa se rezultat gubi.

Iz tog je razloga ovaj zahtjev tipa "vatri i zaboravi".

4.3. Pošaljite obrazac

Slanje obrasca ne razlikuje se puno od dobiti primjer.

Da bismo pokrenuli zahtjev, samo pozivamo post metoda:

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

U ovom scenariju trebamo proslijediti tijelo kao parametar. To može biti jednostavan niz poput datoteke, json ili xml dokumenta, a BodyWritable ili a Izvor.

4.4. Pošaljite podatke s više dijelova / obrazaca

Višedijelni obrazac zahtijeva da polja za unos i podatke šaljemo iz priložene datoteke ili streama.

Da bismo to implementirali u okvir, koristimo post metoda s a Izvor.

Unutar izvora možemo umotati sve različite vrste podataka potrebne našem obrascu:

Izvorna datoteka = FileIO.fromPath (Paths.get ("hello.txt")); FilePart datoteka = novi FilePart ("fileParam", "myfile.txt", "text / plain", datoteka); DataPart podaci = novi DataPart ("ključ", "vrijednost"); ws.url (url) ... .post (Source.from (Arrays.asList (datoteka, podaci)));

Iako ovaj pristup dodaje još neke konfiguracije, ipak je vrlo sličan ostalim vrstama zahtjeva.

5. Obradite asinkroni odgovor

Do ovog trenutka pokrenuli smo samo zahtjeve za vatru i zaboravi, gdje naš kod ne radi ništa s podacima o odgovoru.

Istražimo sada dvije tehnike za obradu asinkronog odgovora.

Možemo blokirati glavnu nit, čekajući a CompletableFuture, ili konzumirati asinkrono s a Potrošač.

5.1. Obradite odgovor blokiranjem s CompletableFuture

Čak i kada koristimo asinkroni okvir, možemo odlučiti blokirati izvršavanje našeg koda i pričekati odgovor.

Koristiti CompletableFuture API, treba nam samo nekoliko promjena u našem kodu da bismo implementirali ovaj scenarij:

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

To bi moglo biti korisno, na primjer, za pružanje snažne dosljednosti podataka koju ne možemo postići na druge načine.

5.2. Obradite odgovor asinkrono

Za obradu asinkronog odgovora bez blokiranja, pružamo a Potrošač ili Funkcija koji pokreće asinkroni okvir kad je odgovor dostupan.

Na primjer, dodajmo a Potrošač na naš prethodni primjer za bilježenje odgovora:

ws.url (url) .addHeader ("key", "value") .addQueryParameter ("num", "" + 1) .get (). thenAccept (r -> log.debug ("Thread #" + Thread. currentThread (). getId () + "Zahtjev završen: Odgovor koda =" + r.getStatus () + "| Odgovor:" + r.getBody () + "| Trenutno vrijeme:" + System.currentTimeMillis ()));

Tada vidimo odgovor u zapisnicima:

[otklanjanje pogrešaka] c.HomeControllerTest - Tema # 30 Zahtjev dovršen: Kôd odgovora = 200 | Odgovor: {"Rezultat": "ok", "Params": {"num": ["1"]}, "Headers": {"accept": ["* / *"], "host": [" localhost: 19001 "]," key ": [" value "]," user-agent ": [" AHC / 2.1 "]}} | Trenutno vrijeme: 1579303109613

Vrijedno je napomenuti da smo koristili ondaprihvati, što zahtijeva a Potrošač funkcija jer ne moramo ništa vraćati nakon prijave.

Kad želimo da trenutna faza nešto vrati, tako da to možemo koristiti u sljedećoj fazi, trebamo ondaPrimijeni umjesto toga, što traje a Funkcija.

Oni koriste konvencije standardnih Java funkcionalnih sučelja.

5.3. Veliko tijelo odgovora

Kôd koji smo do sada implementirali dobro je rješenje za male odgovore i većinu slučajeva korištenja. Međutim, ako trebamo obraditi nekoliko stotina megabajta podataka, trebat će nam bolja strategija.

Treba napomenuti: Metode zahtjeva poput dobiti i post učitati cijeli odgovor u memoriju.

Da bi se izbjegla moguća OutOfMemoryError, možemo koristiti Akka Streams za obradu odgovora, a da ne dopustimo da nam ispuni sjećanje.

Na primjer, možemo zapisati njegovo tijelo u datoteku:

ws.url (url) .stream (). thenAccept (response -> {try {OutputStream outputStream = Files.newOutputStream (path); Sink outputWriter = Sink.foreach (bajtovi -> outputStream.write (bajtovi.toArray ())); response.getBodyAsSource (). runWith (outputWriter, materijalizator); } catch (IOException e) {log.error ("Dogodila se pogreška prilikom otvaranja izlaznog toka", e); }});

The potok metoda vraća a CompletionStage gdje je WSResponse ima getBodyAsStream metoda koja pruža a Izvor.

Kodu možemo reći kako se obrađuje ova vrsta tijela pomoću Akka-e Umivaonik, koji će u našem primjeru jednostavno zapisati sve podatke koji prolaze kroz Izlazni tok.

5.4. Vremenska ograničenja

Prilikom izrade zahtjeva možemo postaviti i određeno vremensko ograničenje, pa se zahtjev prekida ako ne dobijemo potpuni odgovor na vrijeme.

Ovo je posebno korisna značajka kada vidimo da je usluga koju ispitujemo posebno spora i može prouzročiti gomilu otvorenih veza zaglavljenih u čekanju odgovora.

Pomoću parametara za podešavanje možemo postaviti globalno vremensko ograničenje za sve svoje zahtjeve. Za vremensko ograničenje određenog za zahtjev možemo ga dodati pomoću setRequestTimeout:

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

Ipak, još uvijek postoji jedan slučaj: možda smo dobili sve podatke, ali svoje podatke Potrošač može biti vrlo spora obrada. To bi se moglo dogoditi ako postoji puno drobljenja podataka, poziva baze podataka itd.

U sustavima s niskom propusnošću, možemo jednostavno pustiti da se kôd pokreće dok se ne dovrši. Međutim, možda bismo htjeli prekinuti dugotrajne aktivnosti.

Da bismo to postigli, svoj kod moramo omotati nekima budućnosti rukovanje.

Simulirajmo vrlo dug proces u našem kodu:

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

Ovo će vratiti u redu odgovor nakon 10 sekundi, ali ne želimo čekati toliko dugo.

Umjesto toga, s pauza omot, naređujemo našem kodu da pričeka ne više od 1 sekunde:

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

Sada će naša budućnost vratiti rezultat u bilo kojem slučaju: rezultat izračunavanja ako je Potrošač završen na vrijeme ili izuzetak zbog budućnosti pauza.

5.5. Rukovanje iznimkama

U prethodnom smo primjeru stvorili funkciju koja vraća rezultat ili ne uspije s iznimkom. Dakle, sada moramo riješiti oba scenarija.

Možemo se nositi sa scenarijima uspjeha i neuspjeha handleAsync metoda.

Recimo da želimo vratiti rezultat, ako ga imamo, ili prijaviti pogrešku i vratiti iznimku za daljnje rukovanje:

CompletionStage res = f.handleAsync ((rezultat, e) -> {if (e! = Null) {log.error ("Izbačen izuzetak", e); return e.getCause ();} else {povratni rezultat;}} ); 

Kôd bi sada trebao vratiti a CompletionStage koji sadrže TimeoutException bačen.

To možemo provjeriti jednostavnim pozivanjem assertEquals na klasi vraćenog objekta iznimke:

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

Tijekom izvođenja testa, također će zabilježiti iznimku koju smo dobili:

[pogreška] c.HomeControllerTest - Iznimka bačena java.util.concurrent.TimeoutException: Istek nakon 1 sekunde ...

6. Zatraži filtre

Ponekad moramo pokrenuti logiku prije pokretanja zahtjeva.

Mogli bismo manipulirati WSRequest objekt jednom inicijaliziran, ali elegantnija tehnika je postavljanje a WSRequestFilter.

Filtar se može postaviti tijekom inicijalizacije, prije pozivanja metode aktiviranja, i pridružen je logici zahtjeva.

Svoj vlastiti filtar možemo definirati primjenom WSRequestFilter sučelje, ili možemo dodati gotovo.

Uobičajeni scenarij je bilježenje kako izgleda zahtjev prije izvršenja.

U ovom slučaju, samo trebamo postaviti AhcCurlRequestLogger:

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

Rezultirajući zapisnik ima kovrčasličan formatu:

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

Možemo postaviti željenu razinu dnevnika, promjenom našeg logback.xml konfiguracija.

7. Keširanje odgovora

WSClient također podržava predmemoriranje odgovora.

Ova je značajka posebno korisna kada se isti zahtjev pokreće više puta i ne trebaju nam svaki put najsvježiji podaci.

Također pomaže kada usluga koju zovemo privremeno ne radi.

7.1. Dodajte ovisnosti o predmemoriranju

Da bismo konfigurirali predmemoriranje, prvo moramo dodati ovisnost u naš graditi.sbt:

libraryDependencies + = ehcache

Ovo konfigurira Ehcache kao naš predmemorijski sloj.

Ako posebno ne želimo Ehcache, možemo koristiti bilo koju drugu implementaciju predmemorije JSR-107.

7.2. Heurističko prisilno hvatanje

Prema zadanim postavkama, Play WS neće predmemorirati HTTP odgovore ako poslužitelj ne vrati nijednu konfiguraciju predmemoriranja.

Da bismo to zaobišli, možemo prisiliti heurističko predmemoriranje dodavanjem postavke našem prijava.konf:

play.ws.cache.heuristics.enabled = true

Ovo će konfigurirati sustav da odluči kada je korisno predmemorirati HTTP odgovor, bez obzira na predmemorirano oglašavanje udaljene usluge.

8. Dodatno ugađanje

Izrada zahtjeva za vanjsku uslugu može zahtijevati određenu konfiguraciju klijenta. Možda ćemo trebati rukovati preusmjeravanjima, sporim poslužiteljem ili nekim filtriranjem, ovisno o zaglavlju korisničkog agenta.

Da bismo to riješili, možemo prilagoditi našeg WS klijenta, koristeći svojstva u našem prijava.konf:

play.ws.followRedirects = false play.ws.useragent = MyPlayApplication play.ws.compressionEnabled = true # vrijeme čekanja da se uspostavi veza play.ws.timeout.connection = 30 # vrijeme čekanja podataka nakon uspostavljanja veze otvori play.ws.timeout.idle = 30 # maksimalno vrijeme dostupno za dovršenje zahtjeva play.ws.timeout.request = 300

Također je moguće konfigurirati osnovno AsyncHttpClient direktno.

Cjelovit popis dostupnih svojstava može se provjeriti u izvornom kodu AhcConfig.

9. Zaključak

U ovom smo članku istražili Play WS knjižnicu i njene glavne značajke. Konfigurirali smo naš projekt, naučili kako aktivirati uobičajene zahtjeve i obraditi njihov odgovor, i sinkrono i asinkrono.

Radili smo s velikim preuzimanjem podataka i vidjeli kako skratiti dugotrajne aktivnosti.

Konačno, pogledali smo predmemoriranje kako bismo poboljšali izvedbu i kako prilagoditi klijenta.

Kao i uvijek, izvorni kod za ovu lekciju dostupan je na GitHubu.

Dno Java

Upravo sam najavio novo Uči proljeće tečaj, usredotočen na osnove Spring 5 i Spring Boot 2:

>> PROVJERITE TEČAJ