Písanie vlastných filtrov brány Spring Cloud

1. Prehľad

V tomto tutoriále sa naučíme, ako napísať vlastné filtre Spring Cloud Gateway.

Tento rámec sme predstavili v našom predchádzajúcom príspevku, Exploring the New Spring Cloud Gateway, kde sme sa pozreli na veľa zabudovaných filtrov.

Pri tejto príležitosti pôjdeme hlbšie, napíšeme vlastné filtre, aby sme čo najlepšie využili našu bránu API.

Najskôr uvidíme, ako môžeme vytvoriť globálne filtre, ktoré ovplyvnia každú jednu požiadavku vybavenú bránou. Potom napíšeme továrne na filtre brány, ktoré je možné podrobne aplikovať na konkrétne trasy a požiadavky.

Na záver budeme pracovať na pokročilejších scenároch, naučíme sa, ako upraviť požiadavku alebo odpoveď, a dokonca, ako reaktívne spojiť požiadavku s hovormi do iných služieb.

2. Nastavenie projektu

Začneme nastavením základnej aplikácie, ktorú budeme používať ako našu bránu API.

2.1. Konfigurácia Maven

Pri práci s knižnicami Spring Cloud je vždy dobrou voľbou nastavenie konfigurácie správy závislostí, ktorá bude závislosti zvládať za nás:

   org.springframework.cloud závislosť jar-cloud-závislosť Hoxton.SR4 pom import 

Teraz môžeme pridať naše knižnice Spring Cloud bez zadania skutočnej verzie, ktorú používame:

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

Najnovšiu verziu Spring Cloud Release Train nájdete pomocou vyhľadávacieho nástroja Maven Central. Samozrejme by sme mali vždy skontrolovať, či je verzia kompatibilná s verziou Spring Boot, ktorú používame v dokumentácii Spring Cloud.

2.2. Konfigurácia brány API

Budeme predpokladať, že v prístave je lokálne spustená druhá aplikácia 8081, ktorý vystavuje zdroj (pre jednoduchosť iba jednoduchý String) pri údere / zdroj.

Z tohto dôvodu nakonfigurujeme našu bránu na požiadavky servera proxy na túto službu. Stručne povedané, keď pošleme požiadavku na bránu s / služba prefix v ceste URI, presmerujeme hovor na túto službu.

Takže, keď voláme / služba / zdroj v našej bráne by sme mali dostať String odpoveď.

Aby sme to dosiahli, nakonfigurujeme túto trasu pomocou vlastnosti aplikácie:

pružina: cloud: brána: trasy: - id: smer_služby uri: // localhost: 8081 predikáty: - cesta = / služba / ** filtre: - RewritePath = / služba (? /?. ​​*), $ \ {segment}

A navyše, aby sme mohli správne sledovať proces brány, povolíme aj niektoré protokoly:

logovanie: level: org.springframework.cloud.gateway: DEBUG reactor.netty.http.client: DEBUG

3. Vytváranie globálnych filtrov

Keď obslužná rutina brány určí, že požiadavka sa zhoduje s trasou, rámec odovzdá požiadavku cez reťazec filtra. Tieto filtre môžu vykonávať logiku pred odoslaním požiadavky alebo neskôr.

V tejto časti začneme písaním jednoduchých globálnych filtrov. To znamená, že to ovplyvní každú jednu požiadavku.

Najskôr uvidíme, ako môžeme vykonať logiku pred odoslaním žiadosti o proxy server (známy tiež ako „pred“ filter)

3.1. Písanie globálnej logiky filtra „pred“

Ako sme už povedali, v tomto okamihu vytvoríme jednoduché filtre, pretože hlavným cieľom je iba zistiť, či sa filter skutočne vykonáva v správnom okamihu; obyčajné prihlásenie jednoduchej správy bude stačiť.

Všetko, čo musíme urobiť, aby sme vytvorili vlastný globálny filter, je implementácia Spring Cloud Gateway GlobalFilter rozhranie a pridajte ho do kontextu ako fazuľa:

@Component public class LoggingGlobalPreFilter implementuje GlobalFilter {final Logger logger = LoggerFactory.getLogger (LoggingGlobalPreFilter.class); @ Verejný verejný Mono filter (výmena ServerWebExchange, reťazec GatewayFilterChain) {logger.info ("Globálny predbežný filter vykonaný"); spätný reťaz.filtre (výmena); }}

Ľahko vidíme, čo sa tu deje; akonáhle sa tento filter vyvolá, zaznamenáme správu a pokračujeme v realizácii reťazca filtra.

Poďme si teraz definovať „post“ filter, ktorý môže byť o niečo zložitejší, pokiaľ nie sme oboznámení s reaktívnym programovacím modelom a API Spring Webflux.

3.2. Písanie globálnej logiky filtra „zverejnenia“

Jedna ďalšia vec, ktorú si musíme všimnúť na globálnom filtri, ktorý sme práve definovali, je GlobalFilter rozhranie definuje iba jednu metódu. Môže byť teda vyjadrený ako výraz lambda, čo nám umožňuje pohodlne definovať filtre.

Napríklad môžeme definovať náš „post“ filter v konfiguračnej triede:

@Configuration verejná trieda LoggingGlobalFiltersConfigurations {final Logger logger = LoggerFactory.getLogger (LoggingGlobalFiltersConfigurations.class); @Bean public GlobalFilter postGlobalFilter () {return (exchange, chain) -> {return chain.filter (exchange) .then (Mono.fromRunnable (() -> {logger.info ("Global Post Filter executed");}) ); }; }}

Jednoducho povedané, tu prevádzkujeme nový Mono napríklad po tom, čo reťazec dokončil svoje vykonávanie.

Vyskúšajme to teraz zavolaním na / služba / zdroj URL v našej službe brány a kontrola konzoly denníka:

DEBUG --- oscghRoutePredicateHandlerMapping: Route matched: service_route DEBUG --- oscghRoutePredicateHandlerMapping: Mapping [Exchange: GET // localhost / service / resource] to Route {id = 'service_route', uri = // localhost: 8081, order = 0, predikát = Cesty: [/ service / **], koncová lomka: true, gatewayFilters = [[[[RewritePath /service(?/?.*) = '$ {segment}'], objednávka = 1]]} INFO --- cbscfglobal.LoggingGlobalPreFilter: Bol vykonaný globálny predbežný filter DEBUG --- r.netty.http.client.HttpClientConnect: [id: 0x58f7e075, L: /127.0.0.1: 57215 - R: localhost / 127.0.0.1: 8081 ] Používa sa obslužný program: {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] Prijatá odpoveď (automatické čítanie: nepravda): [Content-Type = text / html; charset = UTF-8, Content-Length = 16] INFO --- cfgLoggingGlobalFiltersConfigurations: Globální filtr příspěvků proveden DEBUG - - rnhttp.client.HttpClientOperations: [id: 0x58f7e075, L: / 127 .0.0.1: 57215 - R: localhost / 127.0.0.1: 8081] Bol prijatý posledný paket HTTP

Ako vidíme, filtre sa efektívne vykonajú pred a po tom, čo brána postúpi požiadavku službe.

Logiku „pre“ a „post“ prirodzene môžeme kombinovať do jedného filtra:

@Component public class FirstPreLastPostGlobalFilter implementuje GlobalFilter, objednané {final Logger logger = LoggerFactory.getLogger (FirstPreLastPostGlobalFilter.class); @ Verejný verejný Mono filter (výmena ServerWebExchange, reťazec GatewayFilterChain) {logger.info ("Prvý predbežný globálny filter"); return chain.filter (exchange) .then (Mono.fromRunnable (() -> {logger.info ("Last Post Global Filter");})); } @Override public int getOrder () {návrat -1; }}

Upozorňujeme, že môžeme implementovať aj Objednané rozhranie, ak nám záleží na umiestnení filtra v reťazci.

Kvôli povahe reťazca filtrov vykoná filter s nižšou prednosťou (nižšie poradie v reťazci) svoju „pred“ logiku v skoršej fáze, ale jeho „post“ implementácia sa vyvolá neskôr:

4. Tvorenie GatewayFilters

Globálne filtre sú celkom užitočné, ale často musíme vykonať podrobnejšie vlastné filtračné operácie brány, ktoré sa vzťahujú iba na niektoré trasy.

4.1. Definovanie GatewayFilterFactory

Za účelom implementácie a GatewayFilter, budeme musieť implementovať GatewayFilterFactory rozhranie. Spring Cloud Gateway tiež poskytuje abstraktnú triedu na zjednodušenie procesu, AbstractGatewayFilterFactory trieda:

@Component public class LoggingGatewayFilterFactory rozširuje AbstractGatewayFilterFactory {final Logger logger = LoggerFactory.getLogger (LoggingGatewayFilterFactory.class); public LoggingGatewayFilterFactory () {super (Config.class); } @Override public GatewayFilter apply (Config config) {// ...} verejná statická trieda Config {// ...}}

Tu sme definovali základnú štruktúru našej GatewayFilterFactory. Použijeme a Konfig triedy na prispôsobenie nášho filtra, keď ho inicializujeme.

V tomto prípade môžeme napríklad v našej konfigurácii definovať tri základné polia:

verejná statická trieda Config {private String baseMessage; súkromný boolean preLogger; súkromný boolean postLogger; // dodávatelia, zakladatelia a zakladatelia ...}

Jednoducho povedané, tieto polia sú:

  1. vlastná správa, ktorá bude zahrnutá v položke protokolu
  2. príznak označujúci, či sa má filter prihlásiť pred odoslaním žiadosti
  3. príznak označujúci, či sa má filter prihlásiť po prijatí odpovede od proxy služby

A teraz môžeme tieto konfigurácie použiť na získanie a GatewayFilter inštanciu, ktorú je možné znova znázorniť funkciou lambda:

@Override public GatewayFilter apply (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 loging:" + config.getBaseMessage () );}})); }; }

4.2. Registruje sa GatewayFilter s vlastnosťami

Teraz môžeme ľahko zaregistrovať náš filter na trasu, ktorú sme definovali predtým vo vlastnostiach aplikácie:

... filtre: - RewritePath = / service (? /?. ​​*), $ \ {segment} - názov: protokolovacie args: baseMessage: moja vlastná správa preLogger: true postLogger: true

Musíme jednoducho uviesť konfiguračné argumenty. Dôležitým bodom je, že potrebujeme konštruktor a argumenty konfigurácie bez argumentov v našom LoggingGatewayFilterFactory.Config triedy, aby tento prístup správne fungoval.

Ak chceme filter nakonfigurovať pomocou kompaktnej notácie, môžeme urobiť:

filtre: - RewritePath = / služba (? /?. ​​*), $ \ {segment} - protokolovanie = moja vlastná správa, pravda, pravda

Budeme musieť našu továreň trochu vylepšiť. Stručne povedané, musíme prekonať shortcutFieldOrder metóda, ktorá označuje poradie a koľko argumentov použije vlastnosť skratky:

@Override public List shortcutFieldOrder () {return Arrays.asList ("baseMessage", "preLogger", "postLogger"); }

4.3. Objednávka GatewayFilter

Ak chceme nakonfigurovať pozíciu filtra v reťazci filtra, môžeme načítať znak OrderedGatewayFilter inštancia z Použije sa AbstractGatewayFilterFactory # metóda namiesto obyčajného výrazu lambda:

@Override public GatewayFilter apply (Konfigurácia konfigurácie) {return new OrderedGatewayFilter ((exchange, chain) -> {// ...}, 1); }

4.4. Registruje sa GatewayFilter Programovo

Ďalej môžeme programovo registrovať aj náš filter. Poďme predefinovať trasu, ktorú sme používali, tentokrát nastavením a RouteLocator fazuľa:

@Bean verejné trasy RouteLocator (RouteLocatorBuilder builder, LoggingGatewayFilterFactory loggingFactory) {return builder.routes () .route ("service_route_java_config", r -> r.path ("/ service / **") .filters (f -> f.rewritePath ("/service(?/?.*)", "$ \ {segment}") .filter (loggingFactory.apply (new Config ("Moja vlastná správa", true, true)))) )uri ("/ / localhost: 8081 ")) .build (); }

5. Pokročilé scenáre

Zatiaľ sme robili iba to, že sme protokolovali správu v rôznych fázach procesu brány.

Zvyčajne potrebujeme, aby naše filtre poskytovali pokročilejšie funkcie. Možno budeme musieť napríklad skontrolovať alebo manipulovať s prijatou požiadavkou, upraviť odpoveď, ktorú získavame, alebo dokonca reaktívny prúd zreťaziť pomocou hovorov s inými rôznymi službami.

Ďalej uvidíme príklady týchto rôznych scenárov.

5.1. Kontrola a úprava žiadosti

Poďme si predstaviť hypotetický scenár. Naša služba slúžila na poskytovanie jej obsahu na základe a miestne nastavenie parameter dopytu. Potom sme zmenili API na použitie Prijať jazyk hlavička, ale niektorí klienti stále používajú parameter dotazu.

Preto chceme nakonfigurovať bránu tak, aby sa normalizovala podľa tejto logiky:

  1. ak dostaneme Prijať jazyk hlavičku, to si chceme nechať
  2. inak použite miestne nastavenie hodnota parametra dotazu
  3. ak nie je k dispozícii, použite predvolené miestne nastavenie
  4. nakoniec chceme odstrániť miestne nastavenie parameter dopytu

Poznámka: Aby sme to zjednodušili, zameriame sa iba na logiku filtra; aby sme sa pozreli na celú implementáciu, nájdeme na konci tutoriálu odkaz na databázu kódov.

Nakonfigurujme náš filter brány ako „predbežný“ filter a potom:

(exchange, chain) -> {if (exchange.getRequest () .getHeaders () .getAcceptLanguage () .isEmpty ()) {// vyplniť hlavičku Accept-Language ...} // odstrániť parameter dotazu ... spätný reťaz.filtre (výmena); };

Tu sa staráme o prvý aspekt logiky. Vidíme, že inšpekcia ServerHttpRequest objekt je naozaj jednoduchý. V tomto okamihu sme pristupovali iba k jeho hlavičkám, ale ako uvidíme ďalej, ďalšie atribúty môžeme získať rovnako ľahko:

Reťazec queryParamLocale = exchange.getRequest () .getQueryParams () .getFirst ("miestne nastavenie"); Locale requestLocale = Optional.ofNullable (queryParamLocale) .map (l -> Locale.forLanguageTag (l)) .orElse (config.getDefaultLocale ());

Teraz sme sa zaoberali ďalšími dvoma bodmi správania. Ale zatiaľ sme požiadavku nezmenili. Pre to, budeme musieť využiť mutovať spôsobilosť.

Týmto bude rámec vytvárať a Maliarka subjektu, pričom pôvodný objekt zostane nezmenený.

Úprava hlavičiek je jednoduchá, pretože môžeme získať odkaz na HttpHeaders mapový objekt:

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

Na druhej strane však úprava URI nie je triviálna úloha.

Budeme musieť zohnať nový ServerWebExchange inštancia z originálu výmena objekt, úprava pôvodného ServerHttpRequest inštancia:

ServerWebExchange modifiedExchange = exchange.mutate () // Tu upravíme pôvodnú požiadavku: .request (originalRequest -> originalRequest) .build (); návrat chain.filter (modifiedExchange);

Teraz je čas aktualizovať pôvodný identifikátor URI požiadavky odstránením parametrov dotazu:

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

Ideme na to, môžeme si to teraz vyskúšať. V kódovej základni sme pred zavolaním nasledujúceho filtra reťazca pridali položky protokolu, aby sme videli, čo sa v žiadosti presne odosiela.

5.2. Úprava odpovede

Pri pokračovaní rovnakého scenára teraz definujeme „post“ filter. Naša imaginárna služba používala na získanie vlastnej hlavičky na označenie jazyka, ktorý si nakoniec vybrala, namiesto použitia konvenčnej Jazyk obsahu hlavička.

Preto chceme, aby náš nový filter pridal túto hlavičku odpovede, ale iba ak žiadosť obsahuje miestne nastavenie hlavičku, ktorú sme predstavili v predchádzajúcej časti.

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

Odkaz na objekt odpovede môžeme získať ľahko a na jeho úpravu nemusíme vytvárať jeho kópie, ako je to v prípade požiadavky.

Toto je dobrý príklad dôležitosti poradia filtrov v reťazci; ak nakonfigurujeme vykonávanie tohto filtra po tom, ktorý sme vytvorili v predchádzajúcej časti, potom výmena objekt tu bude obsahovať odkaz na a ServerHttpRequest že nikdy nebude mať žiadny parameter dopytu.

Nezáleží ani na tom, že sa to efektívne spustí po vykonaní všetkých „pred“ filtrov, pretože stále máme odkaz na pôvodnú požiadavku, vďaka mutovať logika.

5.3. Požiadavky na reťazenie k iným službám

Ďalším krokom v našom hypotetickom scenári je spoliehanie sa na tretiu službu, ktorá naznačuje Prijať jazyk hlavičku, ktorú by sme mali použiť.

Vytvoríme teda nový filter, ktorý zavolá túto službu, a použije jej telo odpovede ako hlavičku požiadavky pre API služby proxy.

V reaktívnom prostredí to znamená reťazenie požiadaviek, aby sa zabránilo blokovaniu vykonania asynchronizácie.

V našom filtri začneme zadaním požiadavky jazykovej službe:

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

Všimnite si, že vraciame túto plynulú operáciu, pretože, ako sme povedali, spojíme výstup hovoru s našou splnomocnenou požiadavkou.

Ďalším krokom bude extrakcia jazyka - buď z tela odpovede, alebo z konfigurácie, ak odpoveď nebola úspešná - a analyzovať ho:

// ... .flatMap (response -> {return (response.statusCode () .is2xxSuccessful ())? response.bodyToMono (String.class): Mono.just (config.getDefaultLanguage ());}). map ( LanguageRange :: parse) // ...

Nakoniec nastavíme Rozsah jazykov hodnotu ako hlavičku požiadavky, ako sme to robili predtým, a pokračujte v reťazci filtra:

.map (rozsah -> {exchange.getRequest () .mutate () .headers (h -> h.setAcceptLanguage (rozsah)) .build (); návratová výmena;}). flatMap (chain :: filter);

To je ono, teraz bude interakcia prebiehať neblokujúcim spôsobom.

6. Záver

Teraz, keď sme sa naučili, ako písať vlastné filtre Spring Cloud Gateway, a videli sme, ako manipulovať s entitami požiadaviek a odpovedí, sme pripravení využiť tento rámec naplno.

Všetky úplné príklady ako vždy nájdete na serveri GitHub. Pamätajte, že aby sme to mohli otestovať, musíme spustiť integráciu a živé testy cez Maven.