Aszinkron HTTP programozás a Play keretrendszerrel
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 FOLYAMATOT1. Á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
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ó