Skrivning af tilpassede Spring Cloud Gateway-filtre

1. Oversigt

I denne vejledning lærer vi, hvordan man skriver tilpassede Spring Cloud Gateway-filtre.

Vi introducerede denne ramme i vores tidligere indlæg, Exploring the New Spring Cloud Gateway, hvor vi kiggede på mange indbyggede filtre.

Ved denne lejlighed går vi dybere ind, vi skriver tilpassede filtre for at få mest muligt ud af vores API Gateway.

Først ser vi, hvordan vi kan oprette globale filtre, der påvirker hver enkelt anmodning, der håndteres af gatewayen. Derefter skriver vi gatewayfilterfabrikker, der kan anvendes granulært på bestemte ruter og anmodninger.

Endelig arbejder vi på mere avancerede scenarier, hvor vi lærer at ændre anmodningen eller svaret, og endda hvordan vi sammenkæder anmodningen med opkald til andre tjenester på en reaktiv måde.

2. Opsætning af projekt

Vi starter med at oprette en grundlæggende applikation, som vi bruger som vores API-gateway.

2.1. Maven-konfiguration

Når du arbejder med Spring Cloud-biblioteker, er det altid et godt valg at oprette en afhængighedsstyringskonfiguration til at håndtere afhængighederne for os:

   org.springframework.cloud spring-cloud-afhængigheder Hoxton.SR4 pom import 

Nu kan vi tilføje vores Spring Cloud-biblioteker uden at specificere den aktuelle version, vi bruger:

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

Den seneste Spring Cloud Release Train-version kan findes ved hjælp af Maven Central-søgemaskinen. Selvfølgelig skal vi altid kontrollere, at versionen er kompatibel med Spring Boot-versionen, vi bruger i Spring Cloud-dokumentationen.

2.2. API Gateway-konfiguration

Vi antager, at der er en anden applikation, der kører lokalt i havn 8081, der udsætter en ressource (for enkelhedens skyld bare en simpel Snor) når du rammer / ressource.

Med dette i tankerne konfigurerer vi vores gateway til proxyanmodninger til denne service. I en nøddeskal, når vi sender en anmodning til gatewayen med en /service præfikset i URI-stien, videresender vi opkaldet til denne tjeneste.

Så når vi ringer / service / ressource i vores port skulle vi modtage Snor respons.

For at opnå dette konfigurerer vi denne rute ved hjælp af applikationsegenskaber:

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

Og for at kunne spore gateway-processen korrekt, aktiverer vi også nogle logfiler:

logning: niveau: org.springframework.cloud.gateway: DEBUG reactor.netty.http.client: DEBUG

3. Oprettelse af globale filtre

Når gatewayhåndtereren bestemmer, at en anmodning matcher en rute, sender rammen anmodningen gennem en filterkæde. Disse filtre udfører muligvis logik, inden anmodningen sendes eller derefter.

I dette afsnit starter vi med at skrive enkle globale filtre. Det betyder, at det vil påvirke hver eneste anmodning.

Først ser vi, hvordan vi kan udføre logikken, inden proxyforespørgslen sendes (også kendt som et "præ" -filter)

3.1. Skrivning af global "præ" filterlogik

Som vi sagde, opretter vi enkle filtre på dette tidspunkt, da hovedmålet her kun er at se, at filteret faktisk bliver udført på det rigtige tidspunkt; bare at logge en simpel besked gør tricket.

Alt hvad vi skal gøre for at oprette et tilpasset globalt filter er at implementere Spring Cloud Gateway GlobalFilter interface, og tilføj det til konteksten som en bønne:

@Komponent offentlig klasse LoggingGlobalPreFilter implementerer GlobalFilter {final Logger logger = LoggerFactory.getLogger (LoggingGlobalPreFilter.class); @Override offentligt monofilter (ServerWebExchange-udveksling, GatewayFilterChain-kæde) {logger.info ("Global Pre Filter udført"); return chain.filter (udveksling); }}

Vi kan let se, hvad der foregår her; Når dette filter er påkaldt, logger vi en besked og fortsætter med udførelsen af ​​filterkæden.

Lad os nu definere et "post" -filter, som kan være lidt vanskeligere, hvis vi ikke er bekendt med den reaktive programmeringsmodel og Spring Webflux API.

3.2. Skrivning af global "Post" -filterlogik

En anden ting at bemærke ved det globale filter, vi lige har defineret, er, at GlobalFilter interface definerer kun en metode. Således kan det udtrykkes som et lambda-udtryk, så vi nemt kan definere filtre.

For eksempel kan vi definere vores "post" -filter i en konfigurationsklasse:

@Configuration offentlig klasse 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");}) ); }; }}

Kort sagt, her kører vi en ny Mono eksempel efter at kæden havde afsluttet sin udførelse

Lad os prøve det nu ved at ringe til / service / ressource URL i vores gateway-tjeneste og tjek logkonsollen:

DEBUG --- oscghRoutePredicateHandlerMapping: Matchet rute: service_route DEBUG --- oscghRoutePredicateHandlerMapping: Mapping [Exchange: GET // localhost / service / resource] to Route {id = 'service_route', uri = // localhost: 8081, order = 0, predikat = Stier: [/ service / **], match efterfølgende skråstreg: sand, gatewayFilters = [[[RewritePath /service(?/?.*) = '$ {segment}'], rækkefølge = 1]]} INFO --- cbscfglobal.LoggingGlobalPreFilter: Globalt forfilter udført DEBUG --- r.netty.http.client.HttpClientConnect: [id: 0x58f7e075, L: /127.0.0.1: 57215 - R: localhost / 127.0.0.1: 8081 ] Handler anvendes: {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] Modtaget svar (automatisk læsning: falsk): [Content-Type = text / html; charset = UTF-8, Content-Length = 16] INFO --- cfgLoggingGlobalFiltersConfigurations: Global Post Filter executed DEBUG - - rnhttp.client.HttpClientOperations: [id: 0x58f7e075, L: / 127 .0.0.1: 57215 - R: localhost / 127.0.0.1: 8081] Modtaget sidste HTTP-pakke

Som vi kan se, udføres filtre effektivt før og efter gatewayen videresender anmodningen til tjenesten.

Naturligvis kan vi kombinere "præ" og "post" logik i et enkelt filter:

@Komponent offentlig klasse FirstPreLastPostGlobalFilter implementerer GlobalFilter, bestilt {final Logger logger = LoggerFactory.getLogger (FirstPreLastPostGlobalFilter.class); @ Override offentligt monofilter (ServerWebExchange-udveksling, GatewayFilterChain-kæde) {logger.info ("First Pre Global Filter"); return chain.filter (exchange) .then (Mono.fromRunnable (() -> {logger.info ("Sidste indlæg globalt filter")})); } @ Override public int getOrder () {return -1; }}

Bemærk, at vi også kan implementere Bestilt interface, hvis vi er interesserede i placeringen af ​​filteret i kæden.

På grund af filterkæden vil et filter med lavere forrang (en lavere orden i kæden) udføre sin "præ" -logik i et tidligere stadium, men det er "post" -implementering vil blive påberåbt senere:

4. Oprettelse GatewayFilters

Globale filtre er ret nyttige, men vi har ofte brug for at udføre finkornede brugerdefinerede Gateway-filterhandlinger, der kun gælder for nogle ruter.

4.1. Definition af GatewayFilterFactory

For at gennemføre en GatewayFilter, bliver vi nødt til at implementere GatewayFilterFactory interface. Spring Cloud Gateway giver også en abstrakt klasse for at forenkle processen AbstraktGatewayFilterFactory klasse:

@Komponent offentlig klasse LoggingGatewayFilterFactory udvider AbstractGatewayFilterFactory {final Logger logger = LoggerFactory.getLogger (LoggingGatewayFilterFactory.class); offentlig LoggingGatewayFilterFactory () {super (Config.class); } @Override public GatewayFilter apply (Config config) {// ...} public static class Config {// ...}}

Her har vi defineret vores grundlæggende struktur GatewayFilterFactory. Vi bruger en Konfig klasse for at tilpasse vores filter, når vi initialiserer det.

I dette tilfælde kan vi for eksempel definere tre grundlæggende felter i vores konfiguration:

offentlig statisk klasse Config {private String baseMessage; privat boolsk preLogger; privat boolsk postLogger; // entreprenører, getters og settere ...}

Kort sagt, disse felter er:

  1. en brugerdefineret besked, der vil blive inkluderet i logposten
  2. et flag, der angiver, om filteret skal logge, inden anmodningen videresendes
  3. et flag, der angiver, om filteret skal logge efter modtagelse af svaret fra den proxy-tjeneste

Og nu kan vi bruge disse konfigurationer til at hente en GatewayFilter eksempel, som igen kan repræsenteres med en lambda-funktion:

@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). derefter (Mono.fromRunnable (() -> {// Efterbehandling hvis (config.isPostLogger ()) {logger.info ("Post GatewayFilter logging:" + config.getBaseMessage () );}})); }; }

4.2. Registrering af GatewayFilter med egenskaber

Vi kan nu let registrere vores filter til den rute, vi tidligere definerede i applikationsegenskaberne:

... filtre: - RewritePath = / service (? /?. ​​*), $ \ {segment} - navn: Logging args: baseMessage: My Custom Message preLogger: true postLogger: true

Vi skal blot angive konfigurationsargumenterne. Et vigtigt punkt her er, at vi har brug for en no-argument konstruktør og settere konfigureret i vores LoggingGatewayFilterFactory.Config klasse for at denne tilgang fungerer korrekt.

Hvis vi i stedet ønsker at konfigurere filteret ved hjælp af den kompakte notation, kan vi gøre:

filtre: - RewritePath = / service (? /?. ​​*), $ \ {segment} - Logging = Min brugerdefinerede besked, sand, sand

Vi bliver nødt til at tilpasse vores fabrik lidt mere. Kort sagt, vi er nødt til at tilsidesætte shortcutFieldOrder metode til at angive rækkefølgen og hvor mange argumenter genvejsegenskaben vil bruge:

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

4.3. Bestilling af GatewayFilter

Hvis vi vil konfigurere placeringen af ​​filteret i filterkæden, kan vi hente en OrderedGatewayFilter eksempel fra AbstractGatewayFilterFactory # gælder metode i stedet for et almindeligt lambda-udtryk:

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

4.4. Registrering af GatewayFilter Programmatisk

Desuden kan vi også registrere vores filter programmatisk. Lad os omdefinere den rute, vi har brugt, denne gang ved at oprette en RouteLocator bønne:

@Bean offentlige RouteLocator-ruter (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 (ny Config ("Min brugerdefinerede besked", sandt, sandt))) .uri ("/ / localhost: 8081 ")) .build (); }

5. Avancerede scenarier

Indtil videre er alt, hvad vi har gjort, at logge en besked på forskellige stadier af gateway-processen.

Normalt har vi brug for vores filtre for at give mere avanceret funktionalitet. For eksempel er vi muligvis nødt til at kontrollere eller manipulere den anmodning, vi modtog, ændre det svar, vi henter, eller endda kæde den reaktive strøm med opkald til andre forskellige tjenester.

Dernæst ser vi eksempler på disse forskellige scenarier.

5.1. Kontrol og ændring af anmodningen

Lad os forestille os et hypotetisk scenario. Vores service bruges til at tjene sit indhold baseret på en landestandard forespørgselsparameter. Derefter ændrede vi API'en til at bruge Accept-sprog header i stedet, men nogle klienter bruger stadig forespørgselsparameteren.

Således vil vi konfigurere gatewayen til at normalisere efter denne logik:

  1. hvis vi modtager Accept-sprog header, vi vil beholde det
  2. ellers skal du bruge landestandard forespørgselsparameterværdi
  3. hvis det heller ikke er til stede, skal du bruge et standardland
  4. endelig vil vi fjerne landestandard forespørgsel param

Bemærk: For at holde tingene enkle her fokuserer vi kun på filterlogikken; for at se på hele implementeringen finder vi et link til kodebasen i slutningen af ​​vejledningen.

Lad os konfigurere vores gateway-filter som et "pre" -filter og derefter:

(udveksling, kæde) -> {hvis (exchange.getRequest () .getHeaders () .getAcceptLanguage () .isEmpty ()) {// udfyld Accept-Language header ...} // fjern forespørgselens param ... return chain.filter (udveksling); };

Her tager vi os af det første aspekt af logikken. Vi kan se, at inspektion af ServerHttpRequest objektet er virkelig simpelt. På dette tidspunkt har vi kun fået adgang til dets overskrifter, men som vi ser næste, kan vi lige så let få andre attributter:

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

Nu har vi dækket de næste to punkter i adfærden. Men vi har endnu ikke ændret anmodningen. For det, vi bliver nødt til at gøre brug af mutere evne.

Med dette vil rammen skabe en Dekoratør af enheden og opretholder det oprindelige objekt uændret.

Det er enkelt at ændre overskrifterne, fordi vi kan få en henvisning til HttpHeaders kortobjekt:

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

Men på den anden side er ændring af URI ikke en triviel opgave.

Vi bliver nødt til at skaffe et nyt ServerWebExchange instans fra originalen udveksling objekt, ændre originalen ServerHttpRequest eksempel:

ServerWebExchange modifiedExchange = exchange.mutate () // Her ændrer vi den oprindelige anmodning: .request (originalRequest -> originalRequest) .build (); return chain.filter (modifiedExchange);

Nu er det tid til at opdatere den oprindelige anmodning URI ved at fjerne forespørgselsparametrene:

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

Der går vi, vi kan prøve det nu. I kodebasen tilføjede vi logposter, inden vi ringede til det næste kædefilter for at se nøjagtigt, hvad der sendes i anmodningen.

5.2. Ændring af svaret

Når vi fortsætter med det samme sagscenarie, definerer vi et "post" -filter nu. Vores imaginære tjeneste bruges til at hente en brugerdefineret header for at angive det sprog, den endelig valgte i stedet for at bruge det konventionelle Indholdssprog header.

Derfor ønsker vi, at vores nye filter tilføjer dette svartekst, men kun hvis anmodningen indeholder landestandard header, vi introducerede i det foregående afsnit.

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

Vi kan nemt få en henvisning til svarobjektet, og vi behøver ikke oprette en kopi af det for at ændre det, som med anmodningen.

Dette er et godt eksempel på vigtigheden af ​​rækkefølgen af ​​filtrene i kæden; hvis vi konfigurerer udførelsen af ​​dette filter efter det, vi oprettede i det foregående afsnit, så udveksling objekt her vil indeholde en henvisning til a ServerHttpRequest der aldrig vil have nogen forespørgsel param.

Det betyder ikke engang, at dette effektivt udløses efter udførelsen af ​​alle "pre" filtre, fordi vi stadig har en henvisning til den oprindelige anmodning takket være mutere logik.

5.3. Kædeanmodninger til andre tjenester

Det næste trin i vores hypotetiske scenario er at stole på en tredje tjeneste for at angive hvilken Accept-sprog header, vi skal bruge.

Således opretter vi et nyt filter, der foretager et opkald til denne tjeneste, og bruger dens responstekst som anmodningsoverskrift for den proxy-service-API.

I et reaktivt miljø betyder dette sammenkædningsanmodninger for at undgå at blokere udførelsen af ​​async.

I vores filter starter vi med at stille anmodningen til sprogtjenesten:

(udveksling, kæde) -> {returner WebClient.create (). get () .uri (config.getLanguageEndpoint ()) .exchange () // ...}

Bemærk, at vi returnerer denne flydende operation, fordi vi, som vi sagde, kæder output af opkaldet med vores nærliggende anmodning.

Det næste trin vil være at udtrække sproget - enten fra svarteksten eller fra konfigurationen, hvis svaret ikke lykkedes - og analysere det:

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

Endelig indstiller vi LanguageRange værdi som anmodningshoved som vi gjorde før, og fortsæt filterkæden:

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

Det er det, nu vil interaktionen udføres på en ikke-blokerende måde.

6. Konklusion

Nu hvor vi har lært at skrive tilpassede Spring Cloud Gateway-filtre og set hvordan man manipulerer anmodnings- og svarenhederne, er vi klar til at få mest muligt ud af denne ramme.

Som altid kan alle de komplette eksempler findes over på GitHub. Husk venligst at for at teste det, er vi nødt til at køre integration og live tests gennem Maven.


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