תכנות HTTP אסינכרוני עם Play Framework

ג'אווה טופ

רק הכרזתי על החדש למד אביב קורס, המתמקד ביסודות האביב 5 ומגף האביב 2:

>> בדוק את הקורס

1. סקירה כללית

לעתים קרובות שירותי האינטרנט שלנו צריכים להשתמש בשירותי אינטרנט אחרים על מנת לבצע את עבודתם. זה יכול להיות קשה להגיש בקשות משתמשים תוך שמירה על זמן תגובה נמוך. שירות חיצוני איטי יכול להגדיל את זמן התגובה שלנו ולגרום למערכת שלנו לערום בקשות, תוך שימוש במשאבים רבים יותר. זה המקום שבו גישה שאינה חוסמת יכולה לעזור מאוד

במדריך זה נפעיל בקשות אסינכרוניות מרובות לשירות מיישום Play Framework. על ידי מינוף יכולת ה- HTTP הלא חוסמת של Java, נוכל לשאול בצורה חלקה על משאבים חיצוניים מבלי להשפיע על ההיגיון העיקרי שלנו.

בדוגמה שלנו נחקור את ספריית Play WebService.

2. ספריית Play WebService (WS)

WS היא ספרייה עוצמתית המספקת שיחות HTTP אסינכרוניות באמצעות Java פעולה.

באמצעות ספרייה זו, הקוד שלנו שולח בקשות אלה וממשיך ללא חסימה. כדי לעבד את תוצאת הבקשה, אנו מספקים פונקציה צורכת, כלומר יישום של ה- צרכן מִמְשָׁק.

דפוס זה חולק כמה קווי דמיון עם יישום ה- Callbacks של JavaScript, הבטחות, וה אסינכרון / להמתין תבנית.

בואו נבנה פשוט צרכן שמתעד חלק מנתוני התגובה:

ws.url (url) .thenAccept (r -> log.debug ("Thread #" + Thread.currentThread (). getId () + "בקשה מלאה: קוד תגובה =" + r.getStatus () + "| תגובה: "+ r.getBody () +" | זמן נוכחי: "+ System.currentTimeMillis ()))

שֶׁלָנוּ צרכן הוא רק כניסה לדוגמא זו. הצרכן יכול לעשות כל מה שאנחנו צריכים לעשות עם התוצאה, עם זאת, כמו לאחסן את התוצאה במסד נתונים.

אם נסתכל עמוק יותר על יישום הספרייה, נוכל לראות ש- WS עוטף ומגדיר תצורה של Java AsyncHttpClient, שהוא חלק מה- JDK הסטנדרטי ואינו תלוי ב- Play.

3. הכינו פרויקט לדוגמא

כדי להתנסות במסגרת, בואו ליצור כמה בדיקות יחידה להפעלת בקשות. ניצור יישום אינטרנט שלד כדי לענות עליהן ונשתמש במסגרת WS לביצוע בקשות HTTP.

3.1. יישום האינטרנט שלד

קודם כל, אנו יוצרים את הפרויקט הראשוני באמצעות ה- sbt חדש פקודה:

sbt playframework חדש / play-java-seed.g8

בתיקיה החדשה, אנו אז לערוך את build.sbt הקובץ והוסף את התלות בספריית WS:

libraryDependences + = javaWs

כעת נוכל להפעיל את השרת באמצעות ה- ריצת sbt פקודה:

$ sbt run ... --- (הפעלת היישום, טעינה אוטומטית מופעלת) --- [מידע] pcsAkkaHttpServer - האזנה ל- HTTP ב / 0: 0: 0: 0: 0: 0: 0: 0: 9000

לאחר שהיישום התחיל, נוכל לבדוק שהכל בסדר על ידי גלישה // localhost: 9000, שיפתח את דף הפתיחה של Play.

3.2. סביבת הבדיקה

כדי לבדוק את היישום שלנו, נשתמש בכיתת הבדיקה היחידה HomeControllerTest.

ראשית, עלינו להאריך WithServer שיספק את מחזור חיי השרת:

מחלקה ציבורית HomeControllerTest מתרחבת עם שרת { 

תודה להורה שלה, מחלקה זו מפעילה כעת את שרת האינטרנט השלד שלנו במצב בדיקה וביציאה אקראית, לפני הפעלת המבחנים. ה WithServer class גם עוצר את היישום לאחר סיום הבדיקה.

לאחר מכן, עלינו לספק יישום להפעלה.

אנחנו יכולים ליצור את זה עם רעיוןשל GuiceApplicationBuilder:

@Override מוגן יישום supplyApplication () {להחזיר GuiceApplicationBuilder חדש (). Build (); } 

ולבסוף, הגדרנו את כתובת השרת לשימוש בבדיקות שלנו, באמצעות מספר היציאה שמספק שרת הבדיקה:

@Override @ לפני התקנת החלל הציבורי () {OptionalInt optHttpsPort = testServer.getRunningHttpsPort (); אם (optHttpsPort.isPresent ()) {port = optHttpsPort.getAsInt (); url = "// localhost:" + port; } אחר {port = testServer.getRunningHttpPort () .getAsInt (); url = "// localhost:" + port; }}

עכשיו אנחנו מוכנים לכתוב מבחנים. מסגרת הבדיקה המקיפה מאפשרת לנו להתרכז בקידוד בקשות הבדיקה שלנו.

4. הכן WSRequest

בואו נראה כיצד נוכל להפעיל סוגים בסיסיים של בקשות, כגון GET או POST, ובקשות מרובות חלקים להעלאת קבצים.

4.1. אתחל את בקשת WSRequest לְהִתְנַגֵד

קודם כל, עלינו להשיג א WSClient למשל כדי להגדיר ולאתחל את הבקשות שלנו.

ביישום אמיתי, אנו יכולים להשיג לקוח, מוגדר אוטומטית עם הגדרות ברירת מחדל, באמצעות הזרקת תלות:

@Autowired WSClient ws;

בשיעור המבחן שלנו אנו משתמשים WSTestClient, זמין ממסגרת Play Test:

WSClient ws = play.test.WSTestClient.newClient (יציאה);

ברגע שיש לנו את הלקוח שלנו, אנו יכולים לאתחל את בקשת WSRequest חפץ על ידי קריאה ל url שיטה:

ws.url (url)

ה url השיטה עושה מספיק כדי לאפשר לנו לפטר בקשה. עם זאת, אנו יכולים להתאים את זה עוד יותר על ידי הוספת כמה הגדרות מותאמות אישית:

ws.url (url) .addHeader ("מפתח", "ערך") .addQueryParameter ("num", "" + num);

כפי שאנו רואים, די קל להוסיף כותרות ופרמטרים של שאילתה.

לאחר שתגדיר את בקשתנו באופן מלא, אנו יכולים להתקשר לשיטה ליזום אותה.

4.2. בקשת GET כללית

כדי להפעיל בקשת GET עלינו להתקשר ל לקבל שיטה על שלנו בקשת WSRequest לְהִתְנַגֵד:

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

מכיוון שמדובר בקוד שאינו חוסם, הוא מתחיל את הבקשה ואז ממשיך בביצוע בשורה הבאה של הפונקציה שלנו.

החפץ חזר על ידי לקבל הוא שלב השלמה למשל, שהוא חלק מה- העתיד ממשק API.

לאחר סיום שיחת ה- HTTP, שלב זה מבצע מספר הוראות בלבד. זה עוטף את התגובה בא WSResponse לְהִתְנַגֵד.

בדרך כלל תוצאה זו תועבר לשלב הבא בשרשרת ההוצאה לפועל. בדוגמה זו לא סיפקנו שום פונקציה צורכת ולכן התוצאה אבדה.

מסיבה זו, בקשה זו היא מסוג "אש-ושכח".

4.3. הגש טופס

הגשת טופס אינה שונה מאוד מה- לקבל דוגמא.

כדי להפעיל את הבקשה אנחנו פשוט קוראים הודעה שיטה:

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

בתרחיש זה, עלינו להעביר גוף כפרמטר. זה יכול להיות מחרוזת פשוטה כמו קובץ, מסמך json או xml, א BodyWritable או א מָקוֹר.

4.4. הגש נתונים מרובי חלקים / טפסים

טופס מרובה חלקים מחייב אותנו לשלוח גם שדות קלט וגם נתונים מקובץ או זרם מצורף.

כדי ליישם זאת במסגרת, אנו משתמשים ב- הודעה שיטה עם מָקוֹר.

בתוך המקור נוכל לעטוף את כל סוגי הנתונים השונים הדרושים לטופס שלנו:

קובץ מקור = FileIO.fromPath (Paths.get ("hello.txt")); FilePart file = FilePart חדש ("fileParam", "myfile.txt", "text / plain", file); DataPart data = DataPart חדש ("מפתח", "ערך"); ws.url (url) ... .post (Source.from (Arrays.asList (קובץ, נתונים)));

אף על פי שגישה זו מוסיפה תצורה נוספת, היא עדיין דומה מאוד לסוגי הבקשות האחרים.

5. עבד את תגובת Async

עד לנקודה זו, הפעלנו רק בקשות אש ושכחה, ​​כאשר הקוד שלנו לא עושה שום דבר עם נתוני התגובה.

בואו נבדוק כעת שתי טכניקות לעיבוד תגובה אסינכרונית.

אנחנו יכולים לחסום את החוט הראשי, מחכים ל- עתיד, או לצרוך בצורה אסינכרונית עם צרכן.

5.1. תגובה על התהליך על ידי חסימה עם העתיד

גם כאשר אנו משתמשים במסגרת אסינכרונית, אנו עשויים לבחור לחסום את ביצוע הקוד שלנו ולהמתין לתגובה.

משתמש ב העתיד API, אנחנו רק צריכים כמה שינויים בקוד שלנו כדי ליישם את התרחיש הזה:

תגובת WSResponse = ws.url (url) .get () .toCompletableFuture () .get ();

זה יכול להיות שימושי, למשל, כדי לספק עקביות חזקה בנתונים שלא נוכל להשיג בדרכים אחרות.

5.2. עיבוד תגובה בצורה אסינכרונית

כדי לעבד תגובה אסינכרונית מבלי לחסום, אנו מספקים א צרכן אוֹ פוּנקצִיָה שמנוהל על ידי המסגרת האסינכרונית כאשר התגובה זמינה.

לדוגמה, בואו נוסיף a צרכן לדוגמא הקודמת שלנו כדי לתעד את התגובה:

ws.url (url) .addHeader ("key", "value") .addQueryParameter ("num", "" + 1). get () .thenAccept (r -> log.debug ("Thread #" + Thread). currentThread (). getId () + "בקשה מלאה: קוד תגובה =" + r.getStatus () + "| תגובה:" + r.getBody () + "| זמן נוכחי:" + System.currentTimeMillis ()));

לאחר מכן אנו רואים את התגובה ביומנים:

[debug] c.HomeControllerTest - חוט מס '30 בקשה מלאה: קוד תגובה = 200 | תגובה: {"תוצאה": "בסדר", "Params": {"num": ["1"]}, "כותרות": {"accept": ["* / *"], "host": [" localhost: 19001 "]," key ": [" value "]," user-agent ": [" AHC / 2.1 "]}} | זמן נוכחי: 1579303109613

ראוי לציין שהשתמשנו ואז קבל, הדורש א צרכן פונקציה מכיוון שאיננו צריכים להחזיר דבר לאחר הכניסה.

כשאנחנו רוצים שהשלב הנוכחי יחזיר משהו, כדי שנוכל להשתמש בו בשלב הבא, אנחנו צריכים לאחר מכן החל במקום זאת, שלוקח א פוּנקצִיָה.

אלה משתמשים במוסכמים של ממשקי Java הפונקציונליים הסטנדרטיים.

5.3. גוף תגובה גדול

הקוד שיישמנו עד כה הוא פיתרון טוב לתגובות קטנות ולרוב מקרי השימוש. עם זאת, אם נצטרך לעבד כמה מאות מגה בייט של נתונים, נצטרך אסטרטגיה טובה יותר.

עלינו לציין: בקש שיטות כמו לקבל ו הודעה טען את כל התגובה בזיכרון.

כדי למנוע אפשרות אפשרית OutOfMemoryErrorנוכל להשתמש בזרמי Akka כדי לעבד את התגובה מבלי לתת לה למלא את הזיכרון שלנו.

לדוגמה, אנו יכולים לכתוב את גופו בקובץ:

ws.url (url) .stream () .thenAccept (תגובה -> {נסה {OutputStream outputStream = Files.newOutputStream (נתיב); כיור outputWriter = Sink.foreach (בתים -> outputStream.write (bytes.toArray ())); response.getBodyAsSource (). runWith (outputWriter, materializer); } לתפוס (IOException e) {log.error ("אירעה שגיאה בפתיחת זרם הפלט", e); }});

ה זרם שיטה מחזירה א שלב השלמה איפה ה WSResponse יש getBodyAsStream שיטה המספקת א מָקוֹר.

אנו יכולים לומר לקוד כיצד לעבד גוף מסוג זה באמצעות Akka's כִּיוֹר, שבדוגמה שלנו פשוט יכתבו את כל הנתונים שעוברים ב- OutputStream.

5.4. פסק זמן

בעת בניית בקשה, אנו יכולים גם להגדיר פסק זמן ספציפי, כך שהבקשה תופרע אם לא נקבל את המענה המלא בזמן.

זו תכונה שימושית במיוחד כשאנחנו רואים ששירות שאנו שואלים אליו הוא איטי במיוחד ועלול לגרום לערימה של חיבורים פתוחים תקועים ומחכים לתגובה.

אנו יכולים לקבוע פסק זמן גלובלי לכל בקשותינו באמצעות פרמטרים לכוונון. עבור פסק זמן ספציפי לבקשה, אנו יכולים להוסיף לבקשה באמצעות setRequestTimeout:

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

יש עדיין מקרה אחד לטפל בו: אולי קיבלנו את כל הנתונים, אבל את זה צרכן עשוי להיות מאוד איטי בעיבוד זה. זה עלול לקרות אם יש המון נתונים, שיחות בסיס נתונים וכו '.

במערכות תפוקה נמוכה אנו יכולים פשוט לתת לקוד לפעול עד לסיומו. עם זאת, ייתכן שנרצה לבטל פעילויות ארוכות טווח.

כדי להשיג זאת, עלינו לעטוף את הקוד שלנו בכמה עתידיים טיפול.

בואו לדמות קוד ארוך מאוד בקוד שלנו:

ws.url (url) .get () .thenApply (תוצאה -> {נסה {Thread.sleep (10000L); החזר Results.ok ();} לתפוס (InterruptedException e) {return Results.status (SERVICE_UNAVAILABLE);}} );

זה יחזיר בסדר תגובה אחרי 10 שניות, אבל אנחנו לא רוצים לחכות כל כך הרבה זמן.

במקום זאת, עם פסק זמן עטיפה, אנו מורים לקוד שלנו להמתין לא יותר משנייה אחת:

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

כעת עתידנו יחזיר תוצאה בכל מקרה: תוצאת החישוב אם צרכן סיים בזמן, או היוצא מן הכלל בגלל עתידיים פסק זמן.

5.5. טיפול בחריגים

בדוגמה הקודמת, יצרנו פונקציה שמחזירה תוצאה או נכשלת בחריג. אז עכשיו אנחנו צריכים לטפל בשני התרחישים.

אנו יכולים להתמודד עם תרחישי הצלחה וכישלון באמצעות ה- handleAsync שיטה.

בואו נגיד שאנחנו רוצים להחזיר את התוצאה, אם יש לנו אותה, או נרשום את השגיאה ונחזיר את החריג להמשך הטיפול:

CompletionStage res = f.handleAsync ((תוצאה, ה) -> {אם (e! = Null) {log.error ("חריג נזרק", e); להחזיר e.getCause ();} אחר {תוצאה להחזיר;}} ); 

הקוד אמור להחזיר כעת שלב השלמה המכיל את פסק זמן פסק זמן נזרק.

אנחנו יכולים לאמת את זה פשוט על ידי התקשרות ל- assertEquals על הכיתה של האובייקט החריג שהוחזר:

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

בעת הפעלת הבדיקה, זה יירשם גם את החריג שקיבלנו:

[שגיאה] c.HomeControllerTest - חריג שנזרק java.util.concurrent.TimeoutException: פסק זמן לאחר שנייה אחת ...

6. בקש מסננים

לפעמים, אנחנו צריכים להפעיל קצת היגיון לפני שמופעלת בקשה.

נוכל לתמרן את בקשת WSRequest אובייקט לאחר איתחול, אך טכניקה אלגנטית יותר היא הגדרת a WSRequestFilter.

ניתן להגדיר פילטר במהלך האתחול, לפני שהוא קורא לשיטת ההפעלה, והוא מצורף לוגיקת הבקשה.

אנו יכולים להגדיר את המסנן שלנו על ידי יישום ה- WSRequestFilter ממשק, או שנוכל להוסיף אחד מוכן.

תרחיש נפוץ הוא רישום כיצד נראית הבקשה לפני ביצועה.

במקרה זה, אנחנו רק צריכים להגדיר את AhcCurlRequestLogger:

ws.url (url) ... .setRequestFilter (AhcCurlRequestLogger חדש ()) ... .get ();

ביומן שהתקבל יש סִלְסוּלפורמט כמו:

[מידע] p.l.w.a.AhcCurlRequestLogger - curl \ --verbose \ --request GET \ --header 'key: value' \ '// localhost: 19001'

אנו יכולים לקבוע את רמת היומן הרצויה, על ידי שינוי רמת logback.xml תְצוּרָה.

7. תגובות מטמון

WSClient תומך גם במטמון התגובות.

תכונה זו שימושית במיוחד כאשר אותה בקשה מופעלת מספר פעמים ואיננו זקוקים לנתונים הטריים ביותר בכל פעם.

זה עוזר גם כאשר השירות שאליו אנו מתקשרים אינו פעיל באופן זמני.

7.1. הוסף תלות במטמון

כדי לקבוע את תצורת המטמון עלינו להוסיף תחילה את התלות שלנו build.sbt:

libraryDependences + = ehcache

זה מגדיר את Ehcache כשכבת המטמון שלנו.

אם איננו רוצים Ehcache באופן ספציפי, אנו יכולים להשתמש בכל יישום מטמון JSR-107 אחר.

7.2. היוריסטיקה של מטמון כוח

כברירת מחדל, Play WS לא ישמור בתגובה תגובות HTTP אם השרת לא מחזיר תצורת מטמון כלשהי.

כדי לעקוף זאת, אנו יכולים לכפות את המטמון ההיסטורי על ידי הוספת הגדרה ל application.conf:

play.ws.cache.heuristics.enabled = נכון

פעולה זו תגדיר את המערכת כך שתחליט מתי כדאי לשמור מטמון תגובה HTTP, ללא קשר למטמון שמפורסם של השירות המרוחק.

8. כוונון נוסף

הגשת בקשות לשירות חיצוני עשויה לדרוש תצורת לקוח מסוימת. ייתכן שיהיה עלינו לטפל בהפניות מחדש, בשרת איטי או בסינון כלשהו, ​​תלוי בכותרת המשתמש-סוכן.

כדי לטפל בכך, אנו יכולים לכוון את לקוח ה- WS שלנו באמצעות מאפיינים שלנו application.conf:

play.ws.followRedirects = שקר play.ws.useragent = MyPlayApplication play.ws.compressionEnabled = זמן אמיתי לחכות לקמת החיבור play.ws.timeout.connection = 30 # זמן לחכות לנתונים לאחר חיבור פתוח play.ws.timeout.idle = 30 זמן מקסימאלי זמין להשלמת הבקשה play.ws.timeout.request = 300

אפשר גם להגדיר את התשתית הבסיסית AsyncHttpClient באופן ישיר.

ניתן לבדוק את רשימת המאפיינים הזמינים בקוד המקור של AhcConfig.

9. מסקנה

במאמר זה בחנו את ספריית Play WS ואת התכונות העיקריות שלה. הגדרנו את הפרויקט שלנו, למדנו כיצד לפטר בקשות נפוצות ולעבד את התגובה שלהן, באופן סינכרוני ואסינכרוני.

עבדנו עם הורדות נתונים גדולות וראינו כיצד לקצר פעילויות ארוכות טווח.

לבסוף בדקנו מטמון לשיפור הביצועים וכיצד לכוון את הלקוח.

כמו תמיד, קוד המקור של הדרכה זו זמין ב- GitHub.

תחתית Java

רק הכרזתי על החדש למד אביב קורס, המתמקד ביסודות האביב 5 ומגף האביב 2:

>> בדוק את הקורס

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