כתיבת מסנני שער ענן אביב מותאמים אישית

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

במדריך זה נלמד כיצד לכתוב פילטרים מותאמים אישית של Spring Cloud Gateway.

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

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

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

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

2. הגדרת פרויקט

נתחיל בהגדרת יישום בסיסי בו נשתמש כשער ה- API שלנו.

2.1. תצורת Maven

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

   org.springframework.cloud תלות באביב-ענן Hoxton.SR4 יבוא פום 

כעת אנו יכולים להוסיף את ספריות ענן האביב שלנו מבלי לציין את הגרסה בפועל בה אנו משתמשים:

 org.springframework.cloud spring-cloud-starter-gateway 

הגרסה האחרונה של Spring Cloud Release Train ניתן למצוא באמצעות מנוע החיפוש Maven Central. כמובן, עלינו תמיד לבדוק שהגרסה תואמת את גרסת Spring Boot בה אנו משתמשים בתיעוד Spring Cloud.

2.2. תצורת שער ה- API

אנו נניח שיש יישום שני הפועל באופן מקומי בנמל 8081, שחושף משאב (למען הפשטות, פשוט פשוט חוּט) כאשר מכה /מַשׁאָב.

עם זאת בחשבון, נגדיר את השער שלנו לבקשות פרוקסי לשירות זה. בקצרה, כאשר אנו שולחים בקשה לשער עם /שֵׁרוּת קידומת בנתיב ה- URI, נעביר את השיחה לשירות זה.

אז כשאנחנו מתקשרים / שירות / משאב בשער שלנו, עלינו לקבל את חוּט תְגוּבָה.

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

spring: cloud: gateway: routes: - id: service_route uri: // localhost: 8081 predicates: - Path = / service / ** filters: - RewritePath = / service (? /?. ​​*), $ \ {segment}

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

רישום: רמה: org.springframework.cloud.gateway: DEBUG reactor.netty.http.client: DEBUG

3. יצירת מסננים גלובליים

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

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

ראשית, נראה כיצד נוכל לבצע את ההיגיון לפני שליחת בקשת ה- proxy (המכונה גם פילטר "קדם")

3.1. כתיבת לוגיקה מסננת "קדם" עולמית

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

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

מחלקה ציבורית @Component LoggingGlobalPreFilter מיישם את GlobalFilter {logger logger logger = LoggerFactory.getLogger (LoggingGlobalPreFilter.class); @ מסנן מונו ציבורי ציבורי (חילופי ServerWebExchange, רשת GatewayFilterChain) {logger.info ("בוצע פילטר טרום גלובלי"); החזרת chain.filter (החלפה); }}

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

בואו נגדיר מסנן "פוסט", שיכול להיות קצת יותר מסובך אם איננו מכירים את מודל התכנות התגובתי ו- API של Webflux של Spring.

3.2. כתיבת לוגיקה מסננת "פוסט" עולמית

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

לדוגמא, אנו יכולים להגדיר את המסנן "פוסט" שלנו בכיתת תצורה:

@Configuration בכיתה ציבורית LoggingGlobalFiltersConfigurations {final Logger logger = LoggerFactory.getLogger (LoggingGlobalFiltersConfigurations.class); @Bean GlobalFilter הציבורי postGlobalFilter () {return (exchange, chain) -> {return chain.filter (exchange). ואז (Mono.fromRunnable (() -> {logger.info ("הוצאת המסנן הגלובלי")})) ); }; }}

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

בואו ננסה את זה עכשיו על ידי התקשרות ל / שירות / משאב כתובת URL בשירות השער שלנו ובדיקת מסוף היומן:

DEBUG --- oscghRoutePredicateHandlerMapping: התאמת מסלול: service_route DEBUG --- oscghRoutePredicateHandlerMapping: מיפוי [החלפה: GET // localhost / service / resource] למסלול {id = 'service_route', uri = // localhost: 8081, order = 0, פרדיקט = נתיבים: [/ service / **], התאמת קו נטוי נגרר: true, gatewayFilters = [[[RewritePath /service(?/?.*) = '$ {segment}'], סדר = 1]]} INFO --- cbscfglobal.LoggingGlobalPreFilter: סינון טרום גלובלי מבוצע DEBUG --- r.netty.http.client.HttpClientConnect: [id: 0x58f7e075, L: /127.0.0.1: 57215 - R: localhost / 127.0.0.1: 8081 ] המטפל מוחל: {uri = // localhost: 8081 / resource, method = GET} DEBUG --- rnhttp.client.HttpClientOperations: [id: 0x58f7e075, L: /127.0.0.1: 57215 - R: localhost / 127.0.0.1:8081] תגובה שהתקבלה (קריאה אוטומטית: שקר): [Content-Type = text / html; charset = UTF-8, Content-Length = 16] INFO --- cfgLoggingGlobalFiltersConfigurations: Global Post Filter filter DEBUG - - rnhttp.client.HttpClientOperations: [id: 0x58f7e075, L: / 127 .0.0.1: 57215 - R: localhost / 127.0.0.1: 8081] קיבלה מנות HTTP אחרונות

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

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

המחלקה הציבורית של @Component FirstPreLastPostGlobalFilter מיישמת את GlobalFilter, הוזמן {final Logger logger = LoggerFactory.getLogger (FirstPreLastPostGlobalFilter.class); @ מסנן מונו ציבורי ציבורי (חילופי ServerWebExchange, רשת GatewayFilterChain) {logger.info ("מסנן טרום גלובלי ראשון"); return chain.filter (exchange). ואז (Mono.fromRunnable (() -> {logger.info ("המסנן הגלובלי של ההודעה האחרונה")})); } @Override ציבורי int getOrder () {להחזיר -1; }}

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

בשל אופי שרשרת המסננים, פילטר בעל עדיפות נמוכה יותר (סדר נמוך בשרשרת) יבצע את ההיגיון ה"קדם "שלו בשלב מוקדם יותר, אך יישום ה"פוסט" שלו יופעל מאוחר יותר:

4. יצירה GatewayFilterס

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

4.1. הגדרת ה- GatewayFilterFactory

על מנת ליישם א GatewayFilterנצטרך ליישם את GatewayFilterFactory מִמְשָׁק. Spring Cloud Gateway מספק גם שיעור מופשט כדי לפשט את התהליך, את תקציר GatewayFilterFactory מעמד:

מחלקה ציבורית @Component LoggingGatewayFilterFactory מרחיב AbstractGatewayFilterFactory {לוגר לוגר לוגר = LoggerFactory.getLogger (LoggingGatewayFilterFactory.class); LoggingGatewayFilterFactory ציבורי () {super (Config.class); } @Override Public GatewayFilter להחיל (config config) {// ...} מחלקה סטטית ציבורית Config {// ...}}

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

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

מחלקה סטטית ציבורית Config {private String baseMessage; טרום לוגר בוליאני פרטי; פוסט בוליאני פרטי לוגר; // קבלנים, גטרים וקובעים ...}

במילים פשוטות, שדות אלה הם:

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

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

@Override ציבור GatewayFilter להחיל (Config config) {return (exchange, chain) -> {// Pre-processing if (config.isPreLogger ()) {logger.info ("Pre GatewayFilter logging:" + config.getBaseMessage ()) ; } return chain.filter (exchange) .then (Mono.fromRunnable (() -> {// Post-processing if (config.isPostLogger ()) {logger.info ("Post GatewayFilter logging:" + config.getBaseMessage () );}})); }; }

4.2. רישום GatewayFilter עם נכסים

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

... מסננים: - RewritePath = / service (? /?. ​​*), $ \ {segment} - שם: logs args: baseMessage: ההודעה המותאמת אישית שלי preLogger: נכון postLogger: נכון

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

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

מסננים: - RewritePath = / service (? /?. ​​*), $ \ {segment} - Logging = ההודעה המותאמת אישית שלי, נכון, נכון

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

@Override רשימת משתמשים shortcutFieldOrder () {return Arrays.asList ("baseMessage", "preLogger", "postLogger"); }

4.3. הזמנת GatewayFilter

אם אנו רוצים להגדיר את המיקום של המסנן בשרשרת המסננים, אנו יכולים לאחזר OrderedGatewayFilter למשל מ ה AbstractGatewayFilterFactory # החל שיטה במקום ביטוי למבדה רגיל:

@Override ציבור GatewayFilter להחיל (Config config) {להחזיר OrderedGatewayFilter חדש ((חילופי, שרשרת) -> {// ...}, 1); }

4.4. רישום GatewayFilter מבחינה תכנותית

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

נתיבי RouteLocator ציבוריים @Bean (Builder RouteLocatorBuilder, LoggingGatewayFilterFactory loggingFactory) {return builder.routes () .route ("service_route_java_config", r -> r.path ("/ service / **") .filters (f -> f.rewritePath ("/service(?/?.*)", "$ \ {segment}") .filter (loggingFactory.apply (Config חדש ("ההודעה המותאמת אישית שלי", נכון, נכון))) .uri ("/ / localhost: 8081 ")) .build (); }

5. תרחישים מתקדמים

עד כה, כל מה שעשינו הוא רישום הודעה בשלבים שונים של תהליך השער.

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

לאחר מכן נראה דוגמאות לתרחישים שונים אלה.

5.1. בדיקה ושינוי הבקשה

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

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

  1. אם נקבל את קבל שפה כותרת, אנחנו רוצים לשמור את זה
  2. אחרת, השתמש ב- אזור ערך פרמטר שאילתה
  3. אם גם זה אינו קיים, השתמש באזור ברירת המחדל
  4. לבסוף, אנו רוצים להסיר את אזור param שאילתה

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

בואו להגדיר את מסנן השער שלנו כמסנן "מראש" ואז:

(exchange, chain) -> {if (exchange.getRequest () .getHeaders () .getAcceptLanguage () .isEmpty ()) {// אכלס את כותרת Accept-Language ...} // הסר את פרמיית השאילתה ... החזרת chain.filter (החלפה); };

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

מחרוזת queryParamLocale = exchange.getRequest () .getQueryParams () .getFirst ("locale"); Locale requestLocale = Optional.ofNullable (queryParamLocale) .map (l -> Locale.forLanguageTag (l)) .orElse (config.getDefaultLocale ());

כעת סיקרנו את שתי הנקודות הבאות של ההתנהגות. אך עדיין לא שינינו את הבקשה. לזה, נצטרך להשתמש ב- לְהִשְׁתַנוֹת יכולת.

עם זאת, המסגרת תהיה יצירת מְעַצֵב של הישות, תוך שמירה על האובייקט המקורי ללא שינוי.

שינוי הכותרות הוא פשוט מכיוון שנוכל להשיג התייחסות ל- HttpHeaders אובייקט מפה:

exchange.getRequest () .mutate () .headers (h -> h.setAcceptLanguageAsLocales (Collections.singletonList (requestLocale)))

אך מצד שני, שינוי ה- URI אינו משימה של מה בכך.

נצטרך להשיג חדש Exchange Server Exchange מופע מהמקור לְהַחלִיף אובייקט, שינוי המקור ServerHttpRequest למשל:

ServerWebExchange modifiedExchange = exchange.mutate () // כאן נשנה את הבקשה המקורית: .request (originalRequest -> originalRequest) .build (); return chain.filter (modifiedExchange);

עכשיו הגיע הזמן לעדכן את ה- URI של הבקשה המקורית על ידי הסרת params השאילתה:

originalRequest -> originalRequest.uri (UriComponentsBuilder.fromUri (exchange.getRequest () .getURI ()) .replaceQueryParams (חדש LinkedMultiValueMap ()) .build () .toUri ())

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

5.2. שינוי התגובה

בהמשך לאותו תרחיש מקרה, נגדיר כעת מסנן "פוסט". השירות הדמיוני שלנו נהג לאחזר כותרת מותאמת אישית כדי לציין את השפה שבחרה לבסוף במקום להשתמש בקונבנציונאלי תוכן-שפה כּוֹתֶרֶת.

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

(exchange, chain) -> {return chain.filter (exchange) .then (Mono.fromRunnable (() -> {ServerHttpResponse response = exchange.getResponse (); Optional.ofNullable (exchange.getRequest () .getQueryParams (). getFirst ("locale")) .ifPresent (qp -> {String responseContentLanguage = response.getHeaders () .getContentLanguage () .getLanguage (); response.getHeaders () .add ("Bael-Custom-Language-Header", responseContentLanguage) );});})); }

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

זו דוגמה טובה לחשיבות סדר המסננים בשרשרת; אם נגדיר את הביצוע של המסנן הזה אחרי זה שיצרנו בסעיף הקודם, אז לְהַחלִיף האובייקט כאן יכיל התייחסות ל- ServerHttpRequest שלעולם לא יהיה שום פרמטר שאילתה.

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

5.3. בקשות בשרשרת לשירותים אחרים

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

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

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

במסנן שלנו נתחיל בהגשת הבקשה לשירות השפות:

(exchange, chain) -> {return WebClient.create (). get () .uri (config.getLanguageEndpoint ()). exchange () // ...}

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

השלב הבא יהיה לחלץ את השפה - מגוף התגובה או מהתצורה אם התגובה לא הצליחה - ולנתח אותה:

// ... .flatMap (תגובה -> {return (response.statusCode () .is2xxSuccessful ())? response.bodyToMono (String.class): Mono.just (config.getDefaultLanguage ());}). מפה ( LanguageRange :: לנתח) // ...

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

.map (range -> {exchange.getRequest () .mutate (). headers (h -> h.setAcceptLanguage (range)) .build (); exchange exchange;}). flatMap (chain :: filter);

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

6. מסקנה

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

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