Aszinkron HTTP programozás a Play keretrendszerrel

Java Top

Most jelentettem be az újat Tanulj tavaszt tanfolyam, amelynek középpontjában az 5. tavasz és a tavaszi bakancs 2 alapjai állnak:

>> ELLENŐRIZZE A FOLYAMATOT

1. Áttekintés

Webszolgáltatásainknak gyakran más webszolgáltatásokat kell használniuk munkájuk elvégzéséhez. Nehéz lehet kiszolgálni a felhasználói kéréseket, miközben alacsony a válaszidő. A lassú külső szolgáltatás megnövelheti a válaszidőnket, és rendszerünket több erőforrás felhasználásával halmozza fel a kéréseket. Ebben az esetben a blokkolás nélküli megközelítés nagyon hasznos lehet

Ebben az oktatóanyagban több aszinkron kérést indítunk egy szolgáltatáshoz egy Play Framework alkalmazásból. A Java nem blokkoló HTTP képességének kihasználásával zökkenőmentesen tudunk lekérdezni a külső erőforrásokat anélkül, hogy befolyásolnánk saját fő logikánkat.

Példánkban a Play WebService könyvtárat tárjuk fel.

2. A Play WebService (WS) könyvtár

A WS egy erőteljes könyvtár, amely aszinkron HTTP hívásokat biztosít Java használatával Akció.

A könyvtár használatával kódunk elküldi ezeket a kéréseket, és blokkolás nélkül folytatja. A kérés eredményének feldolgozásához fogyasztási funkciót biztosítunk, vagyis a Fogyasztó felület.

Ez a minta bizonyos hasonlóságot mutat a JavaScript visszahívásokkal történő megvalósításával, Ígéretek, és a aszinkron / vár minta.

Építsünk egy egyszerűt Fogyasztó amely naplózza a válaszadatok egy részét:

ws.url (url) .thenAccept (r -> log.debug ("Thread #" + Thread.currentThread (). getId () + "A kérés teljes: Válaszkód =" + r.getStatus () + "| Válasz: "+ r.getBody () +" | Jelenlegi idő: "+ System.currentTimeMillis ())}

A mi Fogyasztó csupán bejelentkezik ebben a példában. A fogyasztó bármit megtehet, amit nekünk kell tennie az eredménnyel, például az eredményt adatbázisban tárolhatja.

Ha mélyebben áttekintjük a könyvtár megvalósítását, megfigyelhetjük, hogy a WS beburkolja és konfigurálja a Java-kat AsyncHttpClient, amely a standard JDK része, és nem függ a Play-től.

3. Készítsen elő egy példa projektet

A keretrendszerrel való kísérletezéshez hozzunk létre néhány egységtesztet a kérések indításához. Létrehozunk egy csontváz webalkalmazást, hogy megválaszoljuk őket, és a WS keretrendszer segítségével HTTP-kéréseket küldünk.

3.1. A csontváz webalkalmazás

Először a kezdő projektet hozzuk létre a sbt új parancs:

sbt új playframework / play-java-seed.g8

Az új mappában akkor szerkesztése build.sbt fájlt, és adja hozzá a WS-könyvtár függőségét:

libraryDependencies + = javaWs

Most elindíthatjuk a szervert a sbt futni parancs:

$ sbt futtatás ... --- (Az alkalmazás futtatása, az automatikus újratöltés engedélyezett) --- [info] pcsAkkaHttpServer - HTTP-figyelés / 0: 0: 0: 0: 0: 0: 0: 0: 9000

Miután elindult az alkalmazás, böngészéssel ellenőrizhetjük, hogy minden rendben van-e // localhost: 9000, amely megnyitja a Play üdvözlő oldalát.

3.2. A tesztelő környezet

Alkalmazásunk teszteléséhez az egység teszt osztályt fogjuk használni HomeControllerTest.

Először is ki kell terjesztenünk A Szerverrel amely biztosítja a szerver életciklusát:

a public class HomeControllerTest kiterjeszti a WithServer { 

Köszönet a szülőjének, ez az osztály most teszt üzemmódban és véletlenszerű porton indítja el a csontváz webszerverünket, mielőtt lefuttatná a teszteket. A A Szerverrel osztály a teszt befejezése után is leállítja az alkalmazást.

Ezután meg kell adnunk egy alkalmazást a futtatáshoz.

Ezzel létrehozhatjuk Guice’S GuiceApplicationBuilder:

@Orride védett alkalmazás biztosítjaApplication () {return new GuiceApplicationBuilder (). Build (); } 

Végül beállítottuk a teszt során használandó kiszolgáló URL-jét a tesztkiszolgáló által megadott portszám felhasználásával:

@ Felülírás @ A nyilvános érvénytelen beállítás előtt () {OpcionálisInt optHttpsPort = testServer.getRunningHttpsPort (); if (optHttpsPort.isPresent ()) {port = optHttpsPort.getAsInt (); url = "// localhost:" + port; } else {port = testServer.getRunningHttpPort () .getAsInt (); url = "// localhost:" + port; }}

Most készen állunk tesztek írására. Az átfogó tesztkeret lehetővé teszi, hogy koncentráljunk a tesztkéréseink kódolására.

4. Készítse elő a WSR kérést

Nézzük meg, hogyan indíthatjuk el az alapvető típusú kéréseket, például a GET vagy a POST, és a többrészes kéréseket a fájl feltöltésére.

4.1. Inicializálja a WSRequest Tárgy

Először is meg kell szereznünk a WSClient példány a kéréseink konfigurálásához és inicializálásához.

Egy valós alkalmazásban az alapértelmezett beállításokkal automatikusan konfigurált klienst kaphatunk függőségi injekcióval:

@Autowired WSClient ws;

Tesztosztályunkban mégis használjuk WSTestClient, elérhető a Play Test keretrendszerről:

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

Miután megvan az ügyfelünk, inicializálhatjuk a WSRequest objektumot a url módszer:

ws.url (url)

A url módszer elég ahhoz, hogy egy kérést el tudjuk indítani. Néhány egyéni beállítás hozzáadásával azonban tovább testre szabhatjuk:

ws.url (url) .addHeader ("kulcs", "érték") .addQueryParameter ("num", "" + num);

Mint láthatjuk, fejlécek és lekérdezési paraméterek hozzáadása nagyon egyszerű.

Miután teljesen konfiguráltuk kérésünket, felhívhatjuk a módszert annak kezdeményezésére.

4.2. Általános GET kérés

A GET kérés kiváltásához csak felhívnunk kell a kap módszer a mi WSRequest tárgy:

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

Mivel ez egy nem blokkoló kód, elindítja a kérést, majd a funkciónk következő sorában folytatja a végrehajtást.

Az objektum visszatért kap egy CompletionStage példa, amely része a CompletableFuture API.

A HTTP hívás befejezése után ez a szakasz csak néhány utasítást hajt végre. A választ beburkolja a WSResponse tárgy.

Normális esetben ez az eredmény a végrehajtási lánc következő szakaszába kerül. Ebben a példában nem adtunk meg fogyasztó funkciót, így az eredmény elvész.

Ezért ez a kérés a „tűz és felejtsd el” típusú.

4.3. Küldjön űrlapot

Az űrlap benyújtása nem nagyon különbözik a kap példa.

A kérés kiváltásához csak hívjuk a post módszer:

ws.url (url) ... .setContentType ("application / x-www-form-urlencoded") .post ("kulcs1 = érték1 & kulcs2 = érték2");

Ebben a forgatókönyvben egy testet kell átadnunk paraméterként. Ez lehet egy egyszerű karakterlánc, például egy fájl, egy json vagy xml dokumentum, a BodyWritable vagy a Forrás.

4.4. Többrészes / űrlapadatok benyújtása

A többrészes űrlap megköveteli, hogy mind a bemeneti mezőket, mind az adatokat csatolt fájlból vagy adatfolyamból küldjük el.

Ennek a keretrendszerben történő megvalósításához a post módszer a Forrás.

A forrás belsejében be tudjuk csomagolni az űrlapunkhoz szükséges összes adattípust:

Forrásfájl = FileIO.fromPath (Paths.get ("hello.txt")); FilePart file = new FilePart ("fileParam", "myfile.txt", "text / plain", fájl); DataPart data = új DataPart ("kulcs", "érték"); ws.url (url) ... .post (Source.from (Arrays.asList (fájl, adatok)));

Bár ez a megközelítés ad még egy kis konfigurációt, mégis nagyon hasonló a többi típusú kéréshez.

5. Feldolgozza az Async Response-t

Eddig csak tűz és felejtés kéréseket váltottunk ki, ahol kódunk nem tesz semmit a válaszadatokkal.

Most vizsgáljuk meg az aszinkron válasz feldolgozásának két technikáját.

Vagy blokkolhatjuk a fő szálat, várva a CompletableFuture, vagy aszinkron módon fogyasztanak a Fogyasztó.

5.1. Folyamat válasz a blokkolással CompletableFuture

Még aszinkron keretrendszer használata esetén is dönthetünk úgy, hogy blokkoljuk a kód végrehajtását, és megvárjuk a választ.

Használni a CompletableFuture API, csak néhány változásra van szükség a kódban a forgatókönyv megvalósításához:

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

Ez hasznos lehet például olyan erős adatkonzisztencia biztosítására, amelyet más módon nem tudunk elérni.

5.2. A folyamat válasza aszinkron módon

Az aszinkron válasz blokkolás nélküli feldolgozásához biztosítjuk a Fogyasztó vagy Funkció amelyet az aszinkron keretrendszer futtat, amikor a válasz rendelkezésre áll.

Például tegyünk hozzá egy a-t Fogyasztó előző példánkra a válasz naplózásához:

ws.url (url) .addHeader ("kulcs", "érték") .addQueryParameter ("num", "" + 1) .get () .thenAccept (r -> log.debug ("Thread #" + Thread. currentThread (). getId () + "A kérés teljes: Válaszkód =" + r.getStatus () + "| Válasz:" + r.getBody () + "| Jelenlegi idő:" + System.currentTimeMillis ()));

Ezután látjuk a választ a naplókban:

[hibakeresés] c.HomeControllerTest - 30. szál kérése kész: Válaszkód = 200 | Válasz: {"Eredmény": "ok", "Paraméterek": {"num": ["1"]}, "Fejlécek": {"accept": ["* / *"], "host": [" localhost: 19001 "]," key ": [" value "]," user-agent ": [" AHC / 2.1 "]}} | Jelenlegi idő: 1579303109613

Érdemes megjegyezni, hogy használtuk akkor fogadja el, amelyhez a Fogyasztó függvény, mivel naplózás után nem kell semmit visszaküldenünk.

Amikor azt akarjuk, hogy az aktuális szakasz visszaadjon valamit, hogy a következő szakaszban felhasználhassuk, akkor szükségünk van rá akkor Alkalmazzon ehelyett, amely a Funkció.

Ezek a szokásos Java funkcionális interfészek konvencióit használják.

5.3. Nagy válaszú test

Az általunk eddig bevezetett kód jó megoldás kis válaszok és a legtöbb felhasználási eset esetén. Ha azonban néhány száz megabájtos adatot kell feldolgoznunk, jobb stratégiára van szükségünk.

Meg kell jegyeznünk: Kérjen olyan módszereket, mint kap és post töltse be a teljes választ a memóriába.

Az esetleges elkerülése érdekében OutOfMemoryError, használhatjuk az Akka Streameket a válasz feldolgozására anélkül, hogy hagynánk kitölteni a memóriánkat.

Például beírhatjuk a törzsét egy fájlba:

ws.url (url) .stream () .thenAccept (response -> {try {OutputStream outputStream = Files.newOutputStream (elérési út); Sink outputWriter = Sink.foreach (byte -> outputStream.write (bytes.toArray ())); response.getBodyAsSource (). runWith (outputWriter, materializer); } catch (IOException e) {log.error ("Hiba történt a kimeneti adatfolyam megnyitásakor", e); }});

A folyam metódus a CompletionStage hol a WSResponse van egy getBodyAsStream módszer, amely a Forrás.

Meg tudjuk mondani a kódot, hogyan kell feldolgozni az ilyen típusú testet az Akka segítségével Mosogató, amely példánkban egyszerűen beírja a OutputStream.

5.4. Időkorlátok

Kérés készítésekor beállíthatunk egy meghatározott időkorlátot is, így a kérés megszakad, ha nem kapjuk meg időben a teljes választ.

Ez különösen hasznos szolgáltatás, ha azt látjuk, hogy egy általunk lekérdezett szolgáltatás különösen lassú, és nyitott kapcsolatok halmozását okozhatja, amely a válaszra vár.

A hangolási paraméterek segítségével globális időkorlátot állíthatunk be minden kérésünkre. Kérésspecifikus időtúllépéshez hozzáadhatunk egy kérést a setRequestTimeout:

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

Még mindig egy esetet kell kezelni: Lehet, hogy minden adatot megkaptunk, de a mi Fogyasztó nagyon lassú lehet a feldolgozása. Ez akkor fordulhat elő, ha sok adatgyűjtés, adatbázis-hívás stb.

Alacsony teljesítményű rendszerekben egyszerűen hagyhatjuk a kódot futtatni, amíg be nem fejeződik. Előfordulhat azonban, hogy meg akarjuk szakítani a régóta futó tevékenységeket.

Ennek eléréséhez be kell csomagolnunk néhány kódot határidős kezelése.

Szimuláljunk egy nagyon hosszú folyamatot a kódunkban:

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

Ez egy rendben 10 másodperc múlva, de nem akarunk ennyit várni.

Ehelyett a időtúllépés burkolót, arra utasítjuk a kódunkat, hogy legfeljebb 1 másodpercig várjon:

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

Most a jövőnk mindkét irányban eredményt ad: a számítási eredményt, ha a Fogyasztó időben elkészült, vagy a határidős időtúllépés.

5.5. Kivételek kezelése

Az előző példában létrehoztunk egy olyan függvényt, amely vagy eredményt ad, vagy kudarcot vall egy kivétellel. Tehát most mindkét forgatókönyvet kezelnünk kell.

A sikerrel és a kudarccal is kezelhetjük a handleAsync módszer.

Tegyük fel, hogy vissza akarjuk adni az eredményt, ha megvan, vagy naplózni a hibát, és visszaadni a kivételt további kezeléshez:

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

A kódnak most vissza kell adnia a CompletionStage tartalmazó TimeoutException dobott.

Egyszerűen felhívhatjuk a assertEquals a visszatérő kivétel osztályán:

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

A teszt futtatásakor naplózza a kapott kivételt is:

[hiba] c.HomeControllerTest - Kivétel dobva java.util.concurrent.TimeoutException: Időtúllépés 1 másodperc múlva ...

6. Kérjen szűrőket

Előfordul, hogy valamilyen logikát kell futtatnunk, mielőtt a kérés elindulna.

Meg tudnánk manipulálni a WSRequest objektum egyszer inicializálva van, de elegánsabb technika a WSRequestFilter.

Az inicializálás során, az aktiválási módszer meghívása előtt beállítható egy szűrő, amely csatolva van a kérelem logikájához.

Megadhatjuk saját szűrőnket a WSRequestFilter felületet, vagy adhatunk hozzá egy készenlétet.

Gyakori forgatókönyv a naplózás, hogy néz ki a kérelem, mielőtt végrehajtaná.

Ebben az esetben csak be kell állítanunk a AhcCurlRequestLogger:

ws.url (url) ... .setRequestFilter (új AhcCurlRequestLogger ()) ... .get ();

A kapott naplónak van egy becsavar-szerű formátum:

[info] p.l.w.a.AhcCurlRequestLogger - curl \ --verbose \ --request GET \ --header 'kulcs: érték' \ '// localhost: 19001'

Beállíthatjuk a kívánt naplószintet a sajátunk megváltoztatásával logback.xml konfiguráció.

7. Válaszok gyorsítótárazás

WSClient támogatja a válaszok gyorsítótárát is.

Ez a szolgáltatás különösen akkor hasznos, ha ugyanazt a kérést többször is elindítják, és nincs szükségünk minden alkalommal a legfrissebb adatokra.

Akkor is segít, ha a hívott szolgáltatás átmenetileg leáll.

7.1. Adja hozzá a gyorsítótár függőségeit

A gyorsítótár konfigurálásához először hozzá kell adnunk a függőséget a build.sbt:

libraryDependencies + = ehcache

Ez az Ehcache-t állítja be gyorsítótárazásként.

Ha nem akarjuk kifejezetten az Ehcache-t, használhatunk bármilyen más JSR-107 gyorsítótár-megvalósítást.

7.2. Erő gyorsítótárazás Heurisztikus

Alapértelmezés szerint a Play WS nem tárolja a HTTP-válaszokat gyorsítótárba, ha a szerver nem ad vissza gyorsítótár-konfigurációt.

Ennek kijátszására kényszeríthetjük a heurisztikus gyorsítótárat, ha beállítást adunk a sajátunkhoz application.conf:

play.ws.cache.heuristics.enabled = true

Ez úgy konfigurálja a rendszert, hogy eldöntse, mikor érdemes gyorsítótáraznia a HTTP-választ, függetlenül a távoli szolgáltatás meghirdetett gyorsítótárától.

8. További hangolás

Külső szolgáltatás igénylése bizonyos ügyfélkonfigurációt igényelhet. Lehet, hogy kezelnünk kell az átirányításokat, egy lassú szervert vagy valamilyen szűrést, a user-agent fejlécétől függően.

Ennek megoldására hangolhatjuk WS-ügyfelünket, a tulajdonságaik használatával application.conf:

play.ws.followRedirects = false play.ws.useragent = MyPlayApplication play.ws.compressionEnabled = true # ideje várni a kapcsolat létrejöttére play.ws.timeout.connection = 30 # ideje várni az adatokra a kapcsolat létrejötte után nyitott play.ws.timeout.idle = 30 # maximális rendelkezésre álló idő a play.ws.timeout.request = 300 kérés teljesítéséhez

Lehetőség van az alapul szolgáló elemek konfigurálására is AsyncHttpClient közvetlenül.

A rendelkezésre álló tulajdonságok teljes listája a AhcConfig.

9. Következtetés

Ebben a cikkben feltártuk a Play WS könyvtárat és annak főbb jellemzőit. Konfiguráltuk a projektünket, megtanultuk, hogyan indítsuk el a gyakori kéréseket, és hogyan dolgozzuk fel válaszaikat mind szinkron, mind aszinkron módon.

Nagy adatletöltésekkel dolgoztunk, és láttuk, hogyan lehet rövidíteni a hosszú ideje futó tevékenységeket.

Végül megvizsgáltuk a gyorsítótárat, hogy javítsuk a teljesítményt, és hogy miként hangoljuk be az ügyfelet.

Mint mindig, az oktatóanyag forráskódja is elérhető a GitHubon.

Java alsó

Most jelentettem be az újat Tanulj tavaszt tanfolyam, amelynek középpontjában az 5. tavasz és a tavaszi bakancs 2 alapjai állnak:

>> ELLENŐRIZZE A FOLYAMATOT