Skriver tilpassede Spring Cloud Gateway-filtre

1. Oversikt

I denne opplæringen lærer vi hvordan vi skriver tilpassede Spring Cloud Gateway-filtre.

Vi introduserte dette rammeverket i vårt forrige innlegg, Exploring the New Spring Cloud Gateway, hvor vi så på mange innebygde filtre.

Ved denne anledningen går vi dypere, vi skriver tilpassede filtre for å få mest mulig ut av API Gateway.

Først ser vi hvordan vi kan lage globale filtre som vil påvirke hver eneste forespørsel som håndteres av gatewayen. Deretter skriver vi gatewayfilterfabrikker som kan brukes granulært på bestemte ruter og forespørsler.

Til slutt vil vi jobbe med mer avanserte scenarier, lære hvordan du endrer forespørselen eller svaret, og til og med hvordan vi kan kjede forespørselen med samtaler til andre tjenester på en reaktiv måte.

2. Prosjektoppsett

Vi begynner med å sette opp en grunnleggende applikasjon som vi bruker som API-gateway.

2.1. Maven-konfigurasjon

Når du arbeider med Spring Cloud-biblioteker, er det alltid et godt valg å sette opp en avhengighetsadministrasjonskonfigurasjon for å håndtere avhengighetene for oss:

   org.springframework.cloud vår-sky-avhengigheter Hoxton.SR4 pom import 

Nå kan vi legge til våre Spring Cloud-biblioteker uten å spesifisere den faktiske versjonen vi bruker:

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

Den siste versjonen av Spring Cloud Release Train finner du ved hjelp av Maven Central-søkemotoren. Selvfølgelig bør vi alltid sjekke at versjonen er kompatibel med Spring Boot-versjonen vi bruker i Spring Cloud-dokumentasjonen.

2.2. API Gateway Configuration

Vi antar at det er et annet program som kjører lokalt i havn 8081, som avslører en ressurs (for enkelhets skyld, bare en enkel String) når du treffer /ressurs.

Med dette i bakhodet konfigurerer vi gatewayen vår til proxy-forespørsler til denne tjenesten. I et nøtteskall, når vi sender en forespørsel til porten med en /service prefikset i URI-banen, videresender vi samtalen til denne tjenesten.

Så når vi ringer / tjeneste / ressurs i vår port, bør vi motta String respons.

For å oppnå dette konfigurerer vi denne ruten ved hjelp av applikasjonsegenskaper:

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

Og i tillegg, for å kunne spore gateway-prosessen riktig, vil vi også aktivere noen logger:

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

3. Opprette globale filtre

Når gatewaybehandleren bestemmer at en forespørsel samsvarer med en rute, sender rammeverket forespørselen gjennom en filterkjede. Disse filtrene kan utføre logikk før forespørselen sendes, eller etterpå.

I denne delen starter vi med å skrive enkle globale filtre. Det betyr at det vil påvirke hver eneste forespørsel.

Først ser vi hvordan vi kan utføre logikken før fullmaktsforespørselen sendes (også kjent som et "pre" -filter)

3.1. Skriver global "pre" filterlogikk

Som vi sa, vil vi lage enkle filtre på dette punktet, siden hovedmålet her bare er å se at filteret faktisk blir utført i riktig øyeblikk; bare å logge en enkel melding vil gjøre susen.

Alt vi trenger å gjøre for å lage et tilpasset globalt filter er å implementere Spring Cloud Gateway GlobalFilter grensesnitt, og legg det til konteksten som en bønne:

@Komponent offentlig klasse LoggingGlobalPreFilter implementerer GlobalFilter {final Logger logger = LoggerFactory.getLogger (LoggingGlobalPreFilter.class); @Override public Mono filter (ServerWebExchange exchange, GatewayFilterChain chain) {logger.info ("Global Pre Filter executed"); retur chain.filter (bytte); }}

Vi kan lett se hva som skjer her; Når dette filteret er påkalt, logger vi en melding og fortsetter med kjøringen av filterkjeden.

La oss nå definere et "post" -filter, som kan være litt vanskeligere hvis vi ikke er kjent med den reaktive programmeringsmodellen og Spring Webflux API.

3.2. Skriver global "Post" filterlogikk

En annen ting å merke seg om det globale filteret vi nettopp har definert, er at GlobalFilter grensesnitt definerer bare en metode. Dermed kan det uttrykkes som et lambdauttrykk, slik at vi enkelt kan definere filtre.

For eksempel kan vi definere "post" -filteret vårt i en konfigurasjonsklasse:

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

Enkelt sagt, her kjører vi en ny Mono forekomst etter at kjeden fullførte kjøringen.

La oss prøve det nå ved å ringe / tjeneste / ressurs URL i vår gateway-tjeneste, og sjekke ut loggkonsollen:

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 = Baner: [/ service / **], samsvar med etterfølgende skråstrek: true, gatewayFilters = [[[RewritePath /service(?/?.*) = '$ {segment}'], rekkefølge = 1]]} INFO --- cbscfglobal.LoggingGlobalPreFilter: Global Pre Filter utført DEBUG --- r.netty.http.client.HttpClientConnect: [id: 0x58f7e075, L: /127.0.0.1: 57215 - R: localhost / 127.0.0.1: 8081 ] Handler blir brukt: {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] Mottatt respons (automatisk lesing: usann): [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] Mottatt siste HTTP-pakke

Som vi kan se, blir filtre effektivt utført før og etter at gatewayen videresender forespørselen til tjenesten.

Naturligvis kan vi kombinere "pre" og "post" logikk i ett filter:

@Komponent offentlig klasse FirstPreLastPostGlobalFilter implementerer GlobalFilter, bestilt {final Logger logger = LoggerFactory.getLogger (FirstPreLastPostGlobalFilter.class); @Override offentlig monofilter (ServerWebExchange-utveksling, GatewayFilterChain-kjede) {logger.info ("First Pre Global Filter"); return chain.filter (exchange) .then (Mono.fromRunnable (() -> {logger.info ("Last Post Global Filter");})); } @ Override public int getOrder () {retur -1; }}

Merk at vi også kan implementere Bestilt grensesnitt hvis vi bryr oss om plassering av filteret i kjeden.

På grunn av filterkjedens natur vil et filter med lavere forrang (en lavere orden i kjeden) utføre sin "pre" -logikk i et tidligere stadium, men implementeringen av "post" vil bli påkalt senere:

4. Å skape GatewayFilters

Globale filtre er ganske nyttige, men vi trenger ofte å utføre finkornede tilpassede Gateway-filteroperasjoner som bare gjelder noen ruter.

4.1. Definere GatewayFilterFactory

For å implementere en GatewayFilter, må vi implementere GatewayFilterFactory grensesnitt. Spring Cloud Gateway gir også en abstrakt klasse for å forenkle prosessen AbstractGatewayFilterFactory klasse:

@Component public class LoggingGatewayFilterFactory utvider 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 definert den grunnleggende strukturen til vår GatewayFilterFactory. Vi bruker en Konfig klasse for å tilpasse filteret vårt når vi initialiserer det.

I dette tilfellet kan vi for eksempel definere tre grunnleggende felt i vår konfigurasjon:

offentlig statisk klasse Config {private String baseMessage; privat boolsk preLogger; privat boolsk postLogger; // konstruksjoner, settere og settere ...}

Enkelt sagt, disse feltene er:

  1. en tilpasset melding som vil bli inkludert i loggoppføringen
  2. et flagg som indikerer om filteret skal logges før du videresender forespørselen
  3. et flagg som indikerer om filteret skal logges etter mottak av svaret fra den nærliggende tjenesten

Og nå kan vi bruke disse konfigurasjonene til å hente en GatewayFilter forekomst, som igjen kan representeres med en lambda-funksjon:

@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 logging:" + config.getBaseMessage () );}})); }; }

4.2. Registrering av GatewayFilter med Egenskaper

Vi kan nå enkelt registrere filteret vårt til ruten vi definerte tidligere i applikasjonsegenskapene:

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

Vi må bare angi konfigurasjonsargumentene. Et viktig poeng her er at vi trenger en ikke-argument-konstruktør og settere konfigurert i vår LoggingGatewayFilterFactory.Config klasse for at denne tilnærmingen skal fungere skikkelig.

Hvis vi i stedet vil konfigurere filteret ved hjelp av den kompakte notasjonen, kan vi gjøre:

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

Vi må tilpasse fabrikken litt mer. Kort sagt, vi må overstyre shortcutFieldOrder metode, for å indikere rekkefølgen og hvor mange argumenter snarveiegenskapen vil bruke:

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

4.3. Bestiller GatewayFilter

Hvis vi vil konfigurere posisjonen til filteret i filterkjeden, kan vi hente en OrderedGatewayFilter forekomst fra AbstractGatewayFilterFactory # gjelder metode i stedet for et vanlig lambdauttrykk:

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

4.4. Registrering av GatewayFilter Programmatisk

Videre kan vi også registrere filteret vårt programmatisk. La oss omdefinere ruten vi har brukt, denne gangen ved å sette opp 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 egendefinerte melding", sant, sant))) .uri ("/ / localhost: 8081 ")) .build (); }

5. Avanserte scenarier

Så langt er alt vi har gjort å logge en melding på forskjellige stadier av gateway-prosessen.

Vanligvis trenger vi filtrene våre for å gi mer avansert funksjonalitet. For eksempel kan det hende vi må sjekke eller manipulere forespørselen vi mottok, endre svaret vi henter, eller til og med kjede den reaktive strømmen med samtaler til andre forskjellige tjenester.

Deretter ser vi eksempler på disse forskjellige scenariene.

5.1. Kontrollere og endre forespørselen

La oss forestille oss et hypotetisk scenario. Tjenesten vår pleide å tjene innholdet basert på en lokal spørringsparameter. Deretter endret vi API for å bruke Godta-språk header i stedet, men noen klienter bruker fortsatt spørringsparameteren.

Dermed vil vi konfigurere gatewayen til å normalisere seg etter denne logikken:

  1. hvis vi mottar Godta-språk header, vi vil beholde det
  2. ellers bruker du lokal spørringsparameterverdi
  3. Hvis det ikke er til stede heller, bruk et standard språk
  4. til slutt vil vi fjerne lokal spørring param

Merk: For å holde ting enkelt her, fokuserer vi bare på filterlogikken; for å se på hele implementeringen finner vi en lenke til kodebasen på slutten av opplæringen.

La oss konfigurere gateway-filteret vårt som et "pre" -filter og deretter:

(utveksling, kjede) -> {hvis (exchange.getRequest () .getHeaders () .getAcceptLanguage () .isEmpty ()) {// fylle ut akseptert språk-header ...} // fjern spørringsparameteren ... retur chain.filter (bytte); };

Her tar vi vare på det første aspektet av logikken. Vi kan se at inspeksjon av ServerHttpRequest objektet er veldig enkelt. På dette tidspunktet fikk vi bare tilgang til overskriftene, men som vi får se neste, kan vi få andre attributter like enkelt:

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

Nå har vi dekket de to neste punktene i oppførselen. Men vi har ikke endret forespørselen ennå. For dette, vi må bruke mutere evne.

Med dette vil rammeverket skape en Dekoratør av enheten, og holder det opprinnelige objektet uendret.

Det er enkelt å endre topptekstene fordi vi kan få en referanse til HttpHeaders kartobjekt:

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

Men på den annen side er det ikke en triviell oppgave å endre URI.

Vi må skaffe oss en ny ServerWebExchange instans fra originalen Utveksling objekt, modifisering av originalen ServerHttpRequest forekomst:

ServerWebExchange modifiedExchange = exchange.mutate () // Her vil vi endre den opprinnelige forespørselen: .request (originalRequest -> originalRequest) .build (); retur chain.filter (modifiedExchange);

Nå er det på tide å oppdatere den opprinnelige URI-forespørselen ved å fjerne søkeparametrene:

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

Der vi går, kan vi prøve det nå. I kodebasen la vi til loggoppføringer før vi ringte til neste kjedefilter for å se nøyaktig hva som blir sendt i forespørselen.

5.2. Endring av svaret

Fortsatt med samme saksscenario, vil vi definere et "post" -filter nå. Vår imaginære tjeneste brukte for å hente et tilpasset overskrift for å indikere språket det endelig valgte i stedet for å bruke det konvensjonelle Innholdsspråk Overskrift.

Derfor vil vi at vårt nye filter skal legge til denne svaroverskriften, men bare hvis forespørselen inneholder lokal header vi introduserte i forrige avsnitt.

(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 enkelt få en referanse til responsobjektet, og vi trenger ikke lage en kopi av det for å endre det, som med forespørselen.

Dette er et godt eksempel på viktigheten av rekkefølgen på filtrene i kjeden; Hvis vi konfigurerer kjøringen av dette filteret etter det vi opprettet i forrige avsnitt, så Utveksling objektet her vil inneholde en referanse til a ServerHttpRequest som aldri vil ha noen spørreparametere.

Det betyr ikke engang at dette utløses effektivt etter utførelsen av alle "pre" -filtrene, fordi vi fremdeles har en referanse til den opprinnelige forespørselen, takket være mutere logikk.

5.3. Kjedeforespørsler til andre tjenester

Det neste trinnet i vårt hypotetiske scenario er å stole på en tredje tjeneste for å indikere hvilken Godta-språk header vi skal bruke.

Dermed oppretter vi et nytt filter som ringer til denne tjenesten, og bruker svarteksten som forespørselstittel for proxy-service-API.

I et reaktivt miljø betyr dette å koble forespørsler for å unngå å blokkere utførelsen av asynkronisering.

I filteret vårt begynner vi med å gjøre forespørselen til språktjenesten:

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

Legg merke til at vi returnerer denne flytende operasjonen, for som vi sa, vi vil kjede utgangen av samtalen med vår nærliggende forespørsel.

Det neste trinnet vil være å trekke ut språket - enten fra svarteksten eller fra konfigurasjonen hvis svaret ikke lyktes - og analysere det:

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

Til slutt setter vi inn LanguageRange verdi som forespørselstittel som vi gjorde før, og fortsett filterkjeden:

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

Det er det, nå vil interaksjonen utføres på en ikke-blokkerende måte.

6. Konklusjon

Nå som vi har lært hvordan vi skriver tilpassede Spring Cloud Gateway-filtre og har sett hvordan vi kan manipulere forespørselen og svarenhetene, er vi klare til å få mest mulig ut av dette rammeverket.

Som alltid kan alle de komplette eksemplene finnes over på GitHub. Husk at for å teste det, må vi kjøre integrasjon og live tester gjennom Maven.