Aangepaste Spring Cloud Gateway-filters schrijven

1. Overzicht

In deze zelfstudie leren we hoe u aangepaste Spring Cloud Gateway-filters schrijft.

We hebben dit raamwerk geïntroduceerd in ons vorige bericht, Exploring the New Spring Cloud Gateway, waar we veel ingebouwde filters hebben bekeken.

Bij deze gelegenheid gaan we dieper in, we zullen aangepaste filters schrijven om het meeste uit onze API Gateway te halen.

Eerst zullen we zien hoe we globale filters kunnen maken die van invloed zijn op elk verzoek dat door de gateway wordt afgehandeld. Vervolgens zullen we gateway-filterfabrieken schrijven, die gedetailleerd kunnen worden toegepast op bepaalde routes en verzoeken.

Ten slotte werken we aan meer geavanceerde scenario's, leren we hoe we het verzoek of het antwoord kunnen wijzigen en zelfs hoe we het verzoek reactief kunnen koppelen aan oproepen naar andere services.

2. Projectconfiguratie

We beginnen met het opzetten van een basistoepassing die we gaan gebruiken als onze API-gateway.

2.1. Maven-configuratie

Wanneer u met Spring Cloud-bibliotheken werkt, is het altijd een goede keuze om een ​​configuratie voor afhankelijkheidsbeheer in te stellen om de afhankelijkheden voor ons af te handelen:

   org.springframework.cloud spring-cloud-afhankelijkheden Hoxton.SR4 pom import 

Nu kunnen we onze Spring Cloud-bibliotheken toevoegen zonder de daadwerkelijke versie die we gebruiken op te geven:

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

De nieuwste Spring Cloud Release Train-versie kan worden gevonden met behulp van de Maven Central-zoekmachine. Natuurlijk moeten we altijd controleren of de versie compatibel is met de Spring Boot-versie die we gebruiken in de Spring Cloud-documentatie.

2.2. API Gateway-configuratie

We gaan ervan uit dat er een tweede applicatie lokaal in de poort draait 8081, die een bron blootlegt (voor de eenvoud, gewoon een simple Draad) bij het slaan / resource.

Met dit in gedachten zullen we onze gateway configureren voor proxyverzoeken voor deze service. Kortom, wanneer we een verzoek naar de gateway sturen met een /onderhoud prefix in het URI-pad, sturen we de oproep door naar deze service.

Dus als we bellen / service / resource in onze gateway zouden we de Draad reactie.

Om dit te bereiken, zullen we deze route configureren met applicatie-eigenschappen:

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

En bovendien, om het gateway-proces correct te kunnen traceren, zullen we ook enkele logboeken inschakelen:

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

3. Globale filters maken

Zodra de gateway-handler bepaalt dat een verzoek overeenkomt met een route, stuurt het raamwerk het verzoek door een filterketen. Deze filters kunnen logica uitvoeren voordat het verzoek wordt verzonden, of daarna.

In deze sectie beginnen we met het schrijven van eenvoudige globale filters. Dat betekent dat het van invloed is op elk afzonderlijk verzoek.

Eerst zullen we zien hoe we de logica kunnen uitvoeren voordat het proxyverzoek wordt verzonden (ook wel bekend als een 'pre'-filter)

3.1. Globale "Pre" -filterlogica schrijven

Zoals we al zeiden, zullen we op dit punt eenvoudige filters maken, aangezien het belangrijkste doel hier alleen is om te zien dat het filter daadwerkelijk op het juiste moment wordt uitgevoerd; het loggen van een eenvoudig bericht is voldoende.

Het enige wat we hoeven te doen om een ​​aangepast globaal filter te maken, is de Spring Cloud Gateway te implementeren GlobalFilter interface, en voeg het toe aan de context als een boon:

@Component openbare klasse LoggingGlobalPreFilter implementeert GlobalFilter {laatste Logger-logger = LoggerFactory.getLogger (LoggingGlobalPreFilter.class); @Override openbaar Mono-filter (ServerWebExchange-uitwisseling, GatewayFilterChain-keten) {logger.info ("Globaal voorfilter uitgevoerd"); return chain.filter (uitwisseling); }}

We kunnen gemakkelijk zien wat er hier aan de hand is; zodra dit filter is aangeroepen, zullen we een bericht registreren en doorgaan met de uitvoering van de filterketen.

Laten we nu een "post" -filter definiëren, dat wat lastiger kan zijn als we niet vertrouwd zijn met het Reactive programmeermodel en de Spring Webflux API.

3.2. Globale "Post" -filterlogica schrijven

Een ander ding om op te merken over het globale filter dat we zojuist hebben gedefinieerd, is dat de GlobalFilter interface definieert slechts één methode. Het kan dus worden uitgedrukt als een lambda-uitdrukking, waardoor we filters gemakkelijk kunnen definiëren.

We kunnen bijvoorbeeld ons 'post'-filter definiëren in een configuratieklasse:

@Configuration openbare klasse LoggingGlobalFiltersConfigurations {laatste Logger-logger = LoggerFactory.getLogger (LoggingGlobalFiltersConfigurations.class); @Bean public GlobalFilter postGlobalFilter () {return (exchange, chain) -> {return chain.filter (exchange) .dan (Mono.fromRunnable (() -> {logger.info ("Global Post Filter uitgevoerd");}) ); }; }}

Simpel gezegd, hier hebben we een nieuwe Mono instantie nadat de keten zijn uitvoering heeft voltooid.

Laten we het nu uitproberen door de / service / resource URL in onze gateway-service en het uitchecken van de logconsole:

DEBUG --- oscghRoutePredicateHandlerMapping: Matched route: service_route DEBUG --- oscghRoutePredicateHandlerMapping: Mapping [Exchange: GET // localhost / service / resource] naar route {id = 'service_route', uri = // localhost: 8081, order = 0, predikaat = paden: [/ service / **], volg slash: true, gatewayFilters = [[[RewritePath /service(?/?.*) = '$ {segment}'], order = 1]]} INFO --- cbscfglobal.LoggingGlobalPreFilter: Globaal voorfilter uitgevoerd DEBUG --- r.netty.http.client.HttpClientConnect: [id: 0x58f7e075, L: /127.0.0.1: 57215 - R: localhost / 127.0.0.1: 8081 ] Handler wordt toegepast: {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] Antwoord ontvangen (auto-read: false): [Content-Type = text / html; charset = UTF-8, Content-Length = 16] INFO --- cfgLoggingGlobalFiltersConfigurations: Global Post Filter uitgevoerd DEBUG - - rnhttp.client.HttpClientOperations: [id: 0x58f7e075, L: / 127 .0.0.1: 57215 - R: localhost / 127.0.0.1: 8081] Laatste HTTP-pakket ontvangen

Zoals we kunnen zien, worden de filters effectief uitgevoerd voordat en nadat de gateway het verzoek doorstuurt naar de service.

Uiteraard kunnen we "pre" en "post" -logica combineren in één filter:

@Component openbare klasse FirstPreLastPostGlobalFilter implementeert GlobalFilter, besteld {laatste Logger-logger = LoggerFactory.getLogger (FirstPreLastPostGlobalFilter.class); @Override openbaar Mono-filter (ServerWebExchange-uitwisseling, GatewayFilterChain-keten) {logger.info ("First Pre Global Filter"); return chain.filter (exchange) .then (Mono.fromRunnable (() -> {logger.info ("Last Post Global Filter");})); } @Override public int getOrder () {return -1; }}

Merk op dat we ook het Besteld interface als we geven om de plaatsing van het filter in de keten.

Vanwege de aard van de filterketen, zal een filter met een lagere prioriteit (een lagere volgorde in de keten) zijn 'pre'-logica in een eerdere fase uitvoeren, maar de' post'-implementatie wordt later aangeroepen:

4. Creëren GatewayFilters

Globale filters zijn best handig, maar we moeten vaak fijnmazige aangepaste Gateway-filterbewerkingen uitvoeren die slechts op enkele routes van toepassing zijn.

4.1. Het definiëren van de GatewayFilterFactory

Om een GatewayFilter, zullen we het GatewayFilterFactory koppel. Spring Cloud Gateway biedt ook een abstracte klasse om het proces te vereenvoudigen, de SamenvattingGatewayFilterFactory klasse:

@Component openbare klasse LoggingGatewayFilterFactory breidt AbstractGatewayFilterFactory uit {laatste Logger-logger = LoggerFactory.getLogger (LoggingGatewayFilterFactory.class); openbare LoggingGatewayFilterFactory () {super (Config.class); } @Override public GatewayFilter apply (Config config) {// ...} public static class Config {// ...}}

Hier hebben we de basisstructuur van onze gedefinieerd GatewayFilterFactory. We gebruiken een Config class om ons filter aan te passen wanneer we het initialiseren.

In dit geval kunnen we bijvoorbeeld drie basisvelden definiëren in onze configuratie:

openbare statische klasse Config {privé String baseMessage; privé booleaanse preLogger; privé booleaanse postLogger; // constructeurs, getters en setters ...}

Simpel gezegd, deze velden zijn:

  1. een aangepast bericht dat wordt opgenomen in het logboekitem
  2. een vlag die aangeeft of het filter moet loggen voordat het verzoek wordt doorgestuurd
  3. een vlag die aangeeft of het filter moet loggen na ontvangst van het antwoord van de proxy-service

En nu kunnen we deze configuraties gebruiken om een GatewayFilter instantie, die weer kan worden weergegeven met een lambda-functie:

@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. Het registreren van het GatewayFilter met Eigenschappen

We kunnen ons filter nu eenvoudig registreren op de route die we eerder hebben gedefinieerd in de applicatie-eigenschappen:

... filters: - RewritePath = / service (? /?. ​​*), $ \ {segment} - naam: Logging args: baseMessage: Mijn aangepast bericht preLogger: true postLogger: true

We hoeven alleen de configuratie-argumenten aan te geven. Een belangrijk punt hier is dat we een constructor zonder argument en setters nodig hebben die zijn geconfigureerd in onze LoggingGatewayFilterFactory.Config klasse om deze aanpak correct te laten werken.

Als we het filter in plaats daarvan willen configureren met de compacte notatie, kunnen we het volgende doen:

filters: - RewritePath = / service (? /?. ​​*), $ \ {segment} - Logging = Mijn aangepast bericht, waar, waar

We zullen onze fabriek een beetje meer moeten aanpassen. Kortom, we moeten het shortcutFieldOrder methode, om de volgorde aan te geven en hoeveel argumenten de eigenschap van de snelkoppeling zal gebruiken:

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

4.3. Bestellen van het GatewayFilter

Als we de positie van het filter in de filterketen willen configureren, kunnen we een BesteldGatewayFilter voorbeeld van de AbstractGatewayFilterFactory # toepassen methode in plaats van een gewone lambda-uitdrukking:

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

4.4. Het registreren van het GatewayFilter Programmatisch

Bovendien kunnen we ons filter ook programmatisch registreren. Laten we de route die we hebben gebruikt opnieuw definiëren, deze keer door een RouteLocator Boon:

@Bean openbare RouteLocator-routes (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 ("My Custom Message", true, true)))) .uri ("/ / localhost: 8081 ")) .build (); }

5. Geavanceerde scenario's

Tot nu toe hebben we alleen een bericht geregistreerd in verschillende stadia van het gateway-proces.

Meestal hebben we onze filters nodig om meer geavanceerde functionaliteit te bieden. We moeten bijvoorbeeld het verzoek dat we hebben ontvangen controleren of manipuleren, het antwoord dat we ophalen aanpassen of zelfs de reactieve stroom koppelen aan oproepen naar andere verschillende services.

Vervolgens zullen we voorbeelden van deze verschillende scenario's zien.

5.1. Het verzoek controleren en wijzigen

Laten we ons een hypothetisch scenario voorstellen. Onze service diende om de inhoud ervan te dienen op basis van een locale queryparameter. Vervolgens hebben we de API gewijzigd om de Accepteer-taal header, maar sommige clients gebruiken nog steeds de queryparameter.

Daarom willen we de gateway configureren om te normaliseren volgens deze logica:

  1. als we de Accepteer-taal header, dat willen we behouden
  2. gebruik anders de locale queryparameterwaarde
  3. als dat ook niet aanwezig is, gebruik dan een standaard landinstelling
  4. Ten slotte willen we de locale queryparameter

Opmerking: om het hier eenvoudig te houden, zullen we ons alleen concentreren op de filterlogica; om de hele implementatie te bekijken, vinden we aan het einde van de tutorial een link naar de codebase.

Laten we ons gateway-filter configureren als een 'voor'-filter en dan:

(exchange, chain) -> {if (exchange.getRequest () .getHeaders () .getAcceptLanguage () .isEmpty ()) {// vul de Accept-Language header ...} // verwijder de queryparameter ... return chain.filter (uitwisseling); };

Hier zorgen we voor het eerste aspect van de logica. We kunnen zien dat het inspecteren van de ServerHttpRequest object is heel eenvoudig. Op dit punt hebben we alleen toegang gekregen tot de headers, maar zoals we hierna zullen zien, kunnen we net zo gemakkelijk andere attributen verkrijgen:

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

Nu hebben we de volgende twee punten van het gedrag behandeld. Maar we hebben het verzoek nog niet gewijzigd. Voor deze, we zullen gebruik moeten maken van de muteren vermogen.

Hiermee creëert het framework een Decorateur van de entiteit, waarbij het oorspronkelijke object ongewijzigd blijft.

Het wijzigen van de headers is eenvoudig omdat we een verwijzing naar het HttpHeaders kaartobject:

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

Maar aan de andere kant is het wijzigen van de URI geen triviale taak.

We zullen een nieuwe moeten aanschaffen ServerWebExchange instantie van het origineel uitwisseling object, waarbij het origineel wordt gewijzigd ServerHttpRequest voorbeeld:

ServerWebExchange modifiedExchange = exchange.mutate () // Hier zullen we het oorspronkelijke verzoek wijzigen: .request (originalRequest -> originalRequest) .build (); return chain.filter (modifiedExchange);

Nu is het tijd om de oorspronkelijke aanvraag-URI bij te werken door de queryparams te verwijderen:

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

Daar gaan we, we kunnen het nu uitproberen. In de codebase hebben we logboekvermeldingen toegevoegd voordat we het volgende kettingfilter aanroepen om precies te zien wat er in het verzoek wordt verzonden.

5.2. Het antwoord wijzigen

Als we doorgaan met hetzelfde casusscenario, zullen we nu een 'post'-filter definiëren. Onze denkbeeldige service gebruikte om een ​​aangepaste koptekst op te halen om de taal aan te geven die het uiteindelijk koos in plaats van de conventionele te gebruiken Inhoud-taal koptekst.

Daarom willen we dat ons nieuwe filter deze antwoordheader toevoegt, maar alleen als het verzoek de locale koptekst die we in de vorige sectie hebben geïntroduceerd.

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

We kunnen gemakkelijk een verwijzing naar het responsobject verkrijgen en we hoeven er geen kopie van te maken om het te wijzigen, zoals bij het verzoek.

Dit is een goed voorbeeld van het belang van de volgorde van de filters in de keten; als we de uitvoering van dit filter configureren na het filter dat we in de vorige sectie hebben gemaakt, dan is het uitwisseling object bevat hier een verwijzing naar een ServerHttpRequest die nooit een queryparameter zal hebben.

Het maakt niet eens uit dat dit effectief wordt geactiveerd na het uitvoeren van alle 'voor'-filters, omdat we nog steeds een verwijzing hebben naar het oorspronkelijke verzoek, dankzij de muteren logica.

5.3. Verzoeken koppelen aan andere services

De volgende stap in ons hypothetische scenario is het vertrouwen op een derde service om aan te geven welke Accepteer-taal header die we zouden moeten gebruiken.

We maken dus een nieuw filter dat deze service aanroept en de antwoordtekst gebruikt als de verzoekheader voor de proxy-service-API.

In een reactieve omgeving betekent dit het koppelen van verzoeken om te voorkomen dat de asynchrone uitvoering wordt geblokkeerd.

In ons filter beginnen we met het verzoek aan de taaldienst:

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

Merk op dat we deze vloeiende operatie retourneren, omdat, zoals we al zeiden, we de uitvoer van de oproep zullen koppelen aan ons proxy-verzoek.

De volgende stap is om de taal te extraheren - ofwel uit de antwoordtekst of uit de configuratie als het antwoord niet succesvol was - en deze te ontleden:

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

Ten slotte stellen we de Taalbereik waarde als de verzoekheader zoals we eerder deden, en ga door met de filterketen:

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

Dat is alles, nu wordt de interactie op een niet-blokkerende manier uitgevoerd.

6. Conclusie

Nu we hebben geleerd hoe we aangepaste Spring Cloud Gateway-filters kunnen schrijven en hebben gezien hoe we de aanvraag- en reactie-entiteiten kunnen manipuleren, zijn we klaar om het meeste uit dit raamwerk te halen.

Zoals altijd zijn alle complete voorbeelden te vinden op GitHub. Onthoud dat om het te testen, we integratie- en live-tests moeten uitvoeren via Maven.