Pisanje prilagođenih filtara Spring Cloud Gateway

1. Pregled

U ovom uputstvu naučit ćemo kako pisati prilagođene filtre Spring Cloud Gateway.

Ovaj smo okvir predstavili u našem prethodnom postu, Istražujući New Spring Cloud Gateway, gdje smo pogledali mnoge ugrađene filtre.

Ovom prilikom ćemo ući dublje, napisat ćemo prilagođene filtre kako bismo na najbolji način iskoristili naš API pristupnik.

Prvo ćemo vidjeti kako možemo stvoriti globalne filtre koji će utjecati na svaki pojedini zahtjev koji obrađuje pristupnik. Zatim ćemo napisati tvornice filtara pristupnika, koje se mogu detaljno primijeniti na određene rute i zahtjeve.

Napokon, poradit ćemo na naprednijim scenarijima, naučiti kako izmijeniti zahtjev ili odgovor, pa čak i kako povezati zahtjev pozivima s drugim službama, na reaktivan način.

2. Postavljanje projekta

Počet ćemo s postavljanjem osnovne aplikacije koju ćemo koristiti kao naš API pristupnik.

2.1. Maven konfiguracija

Kada radite s knjižnicama Spring Cloud, uvijek je dobar izbor postaviti konfiguraciju upravljanja ovisnostima koja će za nas rješavati ovisnosti:

   org.springframework.cloud proljeće-oblak-ovisnosti Hoxton.SR4 pom uvoz 

Sada možemo dodati naše knjižnice Spring Cloud bez navođenja stvarne verzije koju koristimo:

 org.springframework.cloud proljeće-oblak-starter-vrata 

Najnoviju verziju Spring Cloud Release Train možete pronaći pomoću pretraživača Maven Central. Naravno, uvijek bismo trebali provjeriti je li verzija kompatibilna s verzijom Spring Boot koju koristimo u dokumentaciji Spring Cloud.

2.2. Konfiguracija API pristupnika

Pretpostavit ćemo da postoji druga aplikacija koja se lokalno izvodi u luci 8081, koji izlaže resurs (radi jednostavnosti, samo jednostavan Niz) prilikom udaranja /resurs.

Imajući to na umu, konfigurirat ćemo naš pristupnik za proxy zahtjeve za ovu uslugu. Ukratko, kada na gateway pošaljemo zahtjev s /servis prefiksa u URI stazi, poziv ćemo proslijediti ovoj službi.

Dakle, kad nazovemo / usluga / resurs u našem gatewayu trebali bismo primiti Niz odgovor.

Da bismo to postigli, konfigurirat ćemo ovu rutu pomoću svojstva primjene:

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

Osim toga, da bismo mogli pravilno pratiti postupak pristupnika, omogućit ćemo i neke zapisnike:

zapisivanje: razina: org.springframework.cloud.gateway: DEBUG reactor.netty.http.client: DEBUG

3. Izrada globalnih filtara

Jednom kada obrađivač pristupnika utvrdi da se zahtjev podudara s rutom, okvir prosljeđuje zahtjev kroz lanac filtra. Ovi filtri mogu izvršavati logiku prije slanja zahtjeva ili kasnije.

U ovom ćemo odjeljku započeti pisanjem jednostavnih globalnih filtara. To znači da će utjecati na svaki pojedinačni zahtjev.

Prvo ćemo vidjeti kako možemo izvršiti logiku prije slanja zahtjeva za proxy (poznat i kao "pred" filtar)

3.1. Pisanje globalne logike filtra "pre"

Kao što smo rekli, u ovom ćemo trenutku stvoriti jednostavne filtre, jer je glavni cilj ovdje samo vidjeti da li se filtar zapravo izvršava u točnom trenutku; samo prijavljivanje jednostavne poruke učinit će trik.

Sve što moramo učiniti za stvaranje prilagođenog globalnog filtra je implementacija Spring Cloud Gatewaya GlobalFilter sučelje i dodajte ga u kontekst kao grah:

@Component javna klasa LoggingGlobalPreFilter implementira GlobalFilter {final Logger logger = LoggerFactory.getLogger (LoggingGlobalPreFilter.class); @Override javni mono filtar (razmjena ServerWebExchange, lanac GatewayFilterChain) {logger.info ("Izvršen globalni filtar pre"); povratni lanac.filter (razmjena); }}

Lako možemo vidjeti što se ovdje događa; nakon što se ovaj filtar pozove, zabilježit ćemo poruku i nastaviti s izvršavanjem lanca filtra.

Ajmo sada definirati filtar "post", koji može biti malo zamršeniji ako nismo upoznati s reaktivnim modelom programiranja i Spring Webflux API-jem.

3.2. Pisanje logike globalnog filtra "Post"

Još jedna stvar koju treba primijetiti kod globalnog filtra koji smo upravo definirali jest da GlobalFilter sučelje definira samo jednu metodu. Dakle, može se izraziti kao lambda izraz, što nam omogućuje prikladno definiranje filtara.

Na primjer, možemo definirati naš filtar "post" u klasi konfiguracije:

@Configuration javna klasa LoggingGlobalFiltersConfigurations {final Logger logger = LoggerFactory.getLogger (LoggingGlobalFiltersConfigurations.class); @Bean public GlobalFilter postGlobalFilter () {return (razmjena, lanac) -> {return chain.filter (exchange). Then (Mono.fromRunnable (() -> {logger.info ("Izvršen globalni filtar pošte");}) ); }; }}

Jednostavno rečeno, ovdje radimo novi Mono primjer nakon što je lanac dovršio svoje izvršenje.

Isprobajmo sada pozivom na / usluga / resurs URL u našoj usluzi gatewaya i provjeravanje konzole dnevnika:

DEBUG --- oscghRoutePredicateHandlerMapping: Ruta se podudara: service_route DEBUG --- oscghRoutePredicateHandlerMapping: Mapiranje [Exchange: GET // localhost / service / resource] do rute {id = 'service_route', uri = // localhost: 8081, order = 0, predikat = Putovi: [/ service / **], kosa crta podudaranja: true, gatewayFilters = [[[RewritePath /service(?/?.*) = '$ {segment}'], poredak = 1]]} INFO --- cbscfglobal.LoggingGlobalPreFilter: Izvršen je globalni filtar PRETRAGA --- r.netty.http.client.HttpClientConnect: [id: 0x58f7e075, L: /127.0.0.1: 57215 - R: localhost / 127.0.0.1: 8081 ] Primjenjuje se rukovatelj: {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] Primljeni odgovor (automatsko čitanje: netačno): [Content-Type = text / html; charset = UTF-8, Content-Length = 16] INFO --- cfgLoggingGlobalFiltersConfigurations: Izvršen globalni filtar pošte DEBUG - - rnhttp.client.HttpClientOperations: [id: 0x58f7e075, L: / 127 .0.0.1: 57215 - R: localhost / 127.0.0.1: 8081] Primljeni zadnji HTTP paket

Kao što vidimo, filtri se učinkovito izvršavaju prije i nakon što gateway prosljeđuje zahtjev na uslugu.

Naravno, možemo kombinirati logiku "pre" i "post" u jednom filtru:

@Component javna klasa FirstPreLastPostGlobalFilter implementira GlobalFilter, poredani {final Logger logger = LoggerFactory.getLogger (FirstPreLastPostGlobalFilter.class); @Override javni mono filtar (razmjena ServerWebExchange, lanac GatewayFilterChain) {logger.info ("Prvi pred globalni filtar"); povrat lanca.filter (razmjena). then (Mono.fromRunnable (() -> {logger.info ("Global Post Filter");})); } @Override public int getOrder () {return -1; }}

Napomena: također možemo implementirati Naređeno sučelje ako nam je stalo do postavljanja filtra u lanac.

Zbog prirode lanca filtara, filtar s nižim prioritetom (niži redoslijed u lancu) izvršit će svoju "pre" logiku u ranijoj fazi, ali njegova "post" implementacija pozvat će se kasnije:

4. Stvaranje GatewayFilters

Globalni filtri su vrlo korisni, ali često moramo izvršiti sitnozrnate prilagođene operacije filtriranja pristupnika koje se primjenjuju samo na neke rute.

4.1. Definiranje GatewayFilterFactory

U svrhu provedbe a GatewayFilter, morat ćemo implementirati GatewayFilterFactory sučelje. Spring Cloud Gateway također nudi apstraktnu nastavu za pojednostavljivanje postupka, SažetakGatewayFilterFactory razred:

@Component javna klasa LoggingGatewayFilterFactory proširuje AbstractGatewayFilterFactory {final Logger logger = LoggerFactory.getLogger (LoggingGatewayFilterFactory.class); javni LoggingGatewayFilterFactory () {super (Config.class); } @Override public GatewayFilter apply (Config config) {// ...} public static class Config {// ...}}

Ovdje smo definirali osnovnu strukturu našeg GatewayFilterFactory. Koristit ćemo a Config klase za prilagodbu našeg filtra kad ga inicijaliziramo.

U ovom slučaju, na primjer, u našoj konfiguraciji možemo definirati tri osnovna polja:

javna statička klasa Config {private String baseMessage; privatni logički preLogger; privatni logički postLogger; // izvođači, geteri i postavljači ...}

Jednostavno rečeno, ova polja su:

  1. prilagođena poruka koja će biti uključena u zapisnik
  2. zastava koja označava treba li se filtar prijaviti prije prosljeđivanja zahtjeva
  3. zastava koja označava da li bi se filtar trebao prijaviti nakon primanja odgovora proksiirane službe

A sada možemo koristiti ove konfiguracije za dohvaćanje a GatewayFilter instancu, koja se opet može predstaviti lambda funkcijom:

@Override public GatewayFilter apply (Config config) {return (exchange, chain) -> {// Prethodna obrada if (config.isPreLogger ()) {logger.info ("Pre GatewayFilter logging:" + config.getBaseMessage ()) ; } return chain.filter (exchange). then (Mono.fromRunnable (() -> {// Post-processing if (config.isPostLogger ()) {logger.info ("Log GatewayFilter logging:" + config.getBaseMessage ()) );}})); }; }

4.2. Registriranje GatewayFilter sa Svojstvima

Sada možemo lako registrirati naš filtar na rutu koju smo prethodno definirali u svojstvima aplikacije:

... filtri: - RewritePath = / service (? /?. ​​*), $ \ {segment} - name: Argumenti za evidentiranje: baseMessage: Moja prilagođena poruka preLogger: true postLogger: true

Jednostavno moramo navesti argumente konfiguracije. Ovdje je važna stvar da nam trebaju konstruktor bez argumenata i postavljači konfigurirani u našem LoggingGatewayFilterFactory.Config razreda kako bi ovaj pristup ispravno radio.

Ako umjesto toga želimo konfigurirati filtar pomoću kompaktnog zapisa, možemo učiniti:

filtri: - RewritePath = / service (? /?. ​​*), $ \ {segment} - Logging = Moja prilagođena poruka, true, true

Morat ćemo još malo doraditi našu tvornicu. Ukratko, moramo nadjačati shortcutFieldOrder metoda, da naznači redoslijed i koliko će argumenata koristiti svojstvo prečaca:

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

4.3. Naručivanje GatewayFilter

Ako želimo konfigurirati položaj filtra u lancu filtara, možemo dohvatiti NaručeniGatewayFilter primjer od SažetakGatewayFilterFactory # primijeniti metoda umjesto običnog lambda izraza:

@Override public GatewayFilter apply (Config config) {return new OrderedGatewayFilter ((exchange, chain) -> {// ...}, 1); }

4.4. Registriranje GatewayFilter Programski

Nadalje, svoj filtar možemo registrirati i programski. Idemo redefinirati rutu koju smo koristili, ovaj put postavljanjem a RouteLocator grah:

@Bean javne rute 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 (nova konfiguracija ("Moja prilagođena poruka", true, true)))) .uri ("/ / localhost: 8081 ")) .build (); }

5. Napredni scenariji

Do sada smo sve što smo radili bilježili poruke u različitim fazama postupka pristupnika.

Naši filtri obično su nam potrebni za pružanje naprednijih funkcija. Na primjer, možda ćemo trebati provjeriti ili manipulirati zahtjevom koji smo primili, izmijeniti odgovor koji dohvaćamo ili čak povezati reaktivni tok pozivima na druge različite usluge.

Dalje ćemo vidjeti primjere ovih različitih scenarija.

5.1. Provjera i izmjena zahtjeva

Zamislimo hipotetski scenarij. Naša usluga koristila je svoj sadržaj na temelju a lokalitet parametar upita. Zatim smo promijenili API da koristi Prihvati-jezik umjesto toga zaglavlje, ali neki klijenti i dalje koriste parametar upita.

Stoga želimo konfigurirati pristupnik da se normalizira slijedeći ovu logiku:

  1. ako primimo Prihvati-jezik zaglavlje, želimo to zadržati
  2. u suprotnom koristite lokalitet vrijednost parametra upita
  3. ako ni to nije prisutno, upotrijebite zadani jezik
  4. konačno, želimo ukloniti lokalitet parametar upita

Napomena: Kako bismo ovdje pojednostavili stvari, usredotočit ćemo se samo na logiku filtra; da bismo pogledali cijelu implementaciju, naći ćemo vezu do baze kodova na kraju vodiča.

Konfigurirajmo naš gateway filtar kao "pre" filter:

(razmjena, lanac) -> {if (exchange.getRequest () .getHeaders () .getAcceptLanguage () .isEmpty ()) {// popunite zaglavlje Accept-Language ...} // uklonite parametar upita ... povratni lanac.filter (razmjena); };

Ovdje se brinemo o prvom aspektu logike. Možemo vidjeti da je pregledanje ServerHttpRequest objekt je stvarno jednostavan. U ovom trenutku pristupili smo samo njegovim zaglavljima, ali kao što ćemo vidjeti dalje, jednako lako možemo dobiti i druge atribute:

Niz queryParamLocale = exchange.getRequest () .getQueryParams () .getFirst ("locale"); Locale requestLocale = Izborno.ofNullable (queryParamLocale) .map (l -> Locale.forLanguageTag (l)) .orElse (config.getDefaultLocale ());

Sad smo obradili sljedeće dvije točke ponašanja. Ali zahtjev još nismo izmijenili. Za ovo, morat ćemo se poslužiti mutirati sposobnost.

Ovim će okvir stvoriti Dekorater entiteta, zadržavajući izvorni objekt nepromijenjenim.

Izmjena zaglavlja je jednostavna jer možemo dobiti referencu na HttpHeaders objekt na karti:

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

No, s druge strane, izmjena URI-a nije trivijalni zadatak.

Morat ćemo nabaviti novu ServerWebExchange primjer iz izvornika razmjena objekt, mijenjajući izvornik ServerHttpRequest primjer:

ServerWebExchange modifiedExchange = exchange.mutate () // Ovdje ćemo izmijeniti izvorni zahtjev: .request (originalRequest -> originalRequest) .build (); povratni lanac.filter (modifiedExchange);

Sada je vrijeme da ažurirate izvorni URI zahtjeva uklanjanjem parametara upita:

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

Eto, možemo to sada isprobati. U bazu koda dodali smo zapise dnevnika prije pozivanja sljedećeg lančanog filtra kako bismo vidjeli točno što se šalje u zahtjevu.

5.2. Izmjena odgovora

Nastavljajući s istim scenarijem slučaja, sada ćemo definirati filtar "post". Naša zamišljena usluga koristila je dohvaćanje prilagođenog zaglavlja da bi naznačila jezik koji je konačno odabrala, umjesto da koristi uobičajeni Sadržaj-jezik Zaglavlje.

Stoga želimo da naš novi filtar doda ovo zaglavlje odgovora, ali samo ako zahtjev sadrži lokalitet zaglavlje koje smo uveli u prethodnom odjeljku.

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

Referencu na objekt odgovora možemo dobiti lako i ne trebamo ga stvoriti da bismo ga izmijenili, kao u zahtjevu.

Ovo je dobar primjer važnosti redoslijeda filtara u lancu; ako konfiguriramo izvršavanje ovog filtra nakon onog koji smo stvorili u prethodnom odjeljku, tada razmjena ovdje će sadržavati referencu na ServerHttpRequest koji nikada neće imati parametar upita.

Nije čak ni važno što se to učinkovito pokreće nakon izvršavanja svih "pre" filtara, jer još uvijek imamo referencu na izvorni zahtjev, zahvaljujući mutirati logika.

5.3. Povezivanje zahtjeva za ostale usluge

Sljedeći se korak u našem hipotetičkom scenariju oslanja na treću uslugu koja će naznačiti koju Prihvati-jezik zaglavlje koje bismo trebali koristiti.

Stoga ćemo stvoriti novi filtar koji upućuje poziv ovoj usluzi i koristi njezino tijelo odgovora kao zaglavlje zahtjeva za API proksirane usluge.

U reaktivnom okruženju to znači ulančavanje zahtjeva kako bi se izbjeglo blokiranje izvršavanja asinkronizacije.

U našem filtru započet ćemo s podnošenjem zahtjeva jezičnoj službi:

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

Primijetite da vraćamo ovu tečnu operaciju, jer ćemo, kao što smo rekli, izlaz poziva povezati našim proksiranim zahtjevom.

Sljedeći korak bit će izdvajanje jezika - ili iz tijela odgovora ili iz konfiguracije ako odgovor nije bio uspješan - i njegovo raščlanjivanje:

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

Napokon ćemo postaviti Opseg jezika vrijednost kao zaglavlje zahtjeva kao što smo to činili prije, i nastavite lanac filtra:

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

To je to, sada će se interakcija provoditi na neblokirajući način.

6. Zaključak

Sad kad smo naučili kako pisati prilagođene filtre Spring Cloud Gateway i vidjeli kako manipulirati entitetima zahtjeva i odgovora, spremni smo maksimalno iskoristiti ovaj okvir.

Kao i uvijek, svi cjeloviti primjeri mogu se naći u preko na GitHubu. Imajte na umu da da bismo ga testirali, moramo pokrenuti integraciju i testove uživo putem Mavena.


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