From 7ee5019f7ea84cbea6166c6e81451cca96c7d95b Mon Sep 17 00:00:00 2001 From: Ger Roza Date: Fri, 22 Nov 2019 23:42:56 -0300 Subject: [PATCH] [BAEL-3313] spring-cloud/spring-cloud-gateway | Writing custom Spring Cloud Gateway Filters (#8182) * updated dependency management in spring-cloud-gateway pom.xml * * renamed org package to com * renamed spring.cloud package to springcloudgateway * deleted SpringContextIntegrationTest, as per BAEL-14304 * updated spring Junit test to jupiter * separated introduction-application properties from application.yml, fixing launch error due to file not found exception * Added Service to use as Proxied Service * Added global filters and debug logs for article * fixed error in properties source, plus added GatewayFilter Factories * implemented Modify Request example * implemented Modify Response example * implemented Chain Request example * Added Tests: * Live Test for gateway * Integration tests for services Fixed small issues * renamed tests that were not following BDD naming --- spring-cloud/spring-cloud-gateway/pom.xml | 63 ++++++++----- .../SecondServiceApplication.java | 15 ++++ .../web/SecondServiceRestController.java | 18 ++++ .../baeldung/service/ServiceApplication.java | 15 ++++ .../service/web/ServiceRestController.java | 22 +++++ .../spring/cloud/GatewayApplication.java | 13 --- .../CustomFiltersGatewayApplication.java | 15 ++++ .../customfilters/config/WebClientConfig.java | 16 ++++ .../ChainRequestGatewayFilterFactory.java | 90 +++++++++++++++++++ .../LoggingGatewayFilterFactory.java | 85 ++++++++++++++++++ .../ModifyRequestGatewayFilterFactory.java | 78 ++++++++++++++++ .../ModifyResponseGatewayFilterFactory.java | 48 ++++++++++ .../LoggingGlobalFiltersConfigurations.java | 39 ++++++++ .../global/LoggingGlobalPreFilter.java | 28 ++++++ .../routes/ServiceRouteConfiguration.java | 28 ++++++ .../IntroductionGatewayApplication.java | 15 ++++ .../src/main/resources/application.yml | 20 +---- ...ustomfilters-global-application.properties | 19 ++++ .../introduction-application.properties | 7 ++ .../secondservice-application.properties | 1 + .../resources/service-application.properties | 1 + .../SecondServiceIntegrationTest.java | 26 ++++++ .../secondservice/SpringContextTest.java | 12 +++ .../service/ServiceIntegrationTest.java | 29 ++++++ .../baeldung/service/SpringContextTest.java | 12 +++ .../customfilters/CustomFiltersLiveTest.java | 90 +++++++++++++++++++ .../utils/LoggerListAppender.java | 25 ++++++ .../introduction/SpringContextTest.java | 15 ++++ .../SpringContextIntegrationTest.java | 17 ---- .../java/org/baeldung/SpringContextTest.java | 17 ---- .../src/test/resources/logback-test.xml | 9 ++ 31 files changed, 805 insertions(+), 83 deletions(-) create mode 100644 spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/secondservice/SecondServiceApplication.java create mode 100644 spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/secondservice/web/SecondServiceRestController.java create mode 100644 spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/service/ServiceApplication.java create mode 100644 spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/service/web/ServiceRestController.java delete mode 100644 spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/spring/cloud/GatewayApplication.java create mode 100644 spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/CustomFiltersGatewayApplication.java create mode 100644 spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/config/WebClientConfig.java create mode 100644 spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/factories/ChainRequestGatewayFilterFactory.java create mode 100644 spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/factories/LoggingGatewayFilterFactory.java create mode 100644 spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/factories/ModifyRequestGatewayFilterFactory.java create mode 100644 spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/factories/ModifyResponseGatewayFilterFactory.java create mode 100644 spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/global/LoggingGlobalFiltersConfigurations.java create mode 100644 spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/global/LoggingGlobalPreFilter.java create mode 100644 spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/routes/ServiceRouteConfiguration.java create mode 100644 spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/introduction/IntroductionGatewayApplication.java create mode 100644 spring-cloud/spring-cloud-gateway/src/main/resources/customfilters-global-application.properties create mode 100644 spring-cloud/spring-cloud-gateway/src/main/resources/introduction-application.properties create mode 100644 spring-cloud/spring-cloud-gateway/src/main/resources/secondservice-application.properties create mode 100644 spring-cloud/spring-cloud-gateway/src/main/resources/service-application.properties create mode 100644 spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/secondservice/SecondServiceIntegrationTest.java create mode 100644 spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/secondservice/SpringContextTest.java create mode 100644 spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/service/ServiceIntegrationTest.java create mode 100644 spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/service/SpringContextTest.java create mode 100644 spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/customfilters/CustomFiltersLiveTest.java create mode 100644 spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/customfilters/utils/LoggerListAppender.java create mode 100644 spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/introduction/SpringContextTest.java delete mode 100644 spring-cloud/spring-cloud-gateway/src/test/java/org/baeldung/SpringContextIntegrationTest.java delete mode 100644 spring-cloud/spring-cloud-gateway/src/test/java/org/baeldung/SpringContextTest.java create mode 100644 spring-cloud/spring-cloud-gateway/src/test/resources/logback-test.xml diff --git a/spring-cloud/spring-cloud-gateway/pom.xml b/spring-cloud/spring-cloud-gateway/pom.xml index c297d90896..61f0267ba0 100644 --- a/spring-cloud/spring-cloud-gateway/pom.xml +++ b/spring-cloud/spring-cloud-gateway/pom.xml @@ -1,5 +1,6 @@ - 4.0.0 spring-cloud-gateway @@ -17,8 +18,25 @@ org.springframework.cloud - spring-cloud-gateway - ${cloud.version} + spring-cloud-dependencies + ${spring-cloud-dependencies.version} + pom + import + + + + + org.junit + junit-bom + 5.5.2 + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} pom import @@ -28,20 +46,7 @@ org.springframework.cloud - spring-cloud-starter - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-test - test + spring-cloud-starter-gateway @@ -54,13 +59,31 @@ validation-api - io.projectreactor.ipc - reactor-netty + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-maven-plugin + + + + - 2.0.1.RELEASE + Greenwich.SR3 + + + 2.1.9.RELEASE 6.0.2.Final diff --git a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/secondservice/SecondServiceApplication.java b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/secondservice/SecondServiceApplication.java new file mode 100644 index 0000000000..69be1be9ca --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/secondservice/SecondServiceApplication.java @@ -0,0 +1,15 @@ +package com.baeldung.secondservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.PropertySource; + +@SpringBootApplication +@PropertySource("classpath:secondservice-application.properties") +public class SecondServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(SecondServiceApplication.class, args); + } + +} diff --git a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/secondservice/web/SecondServiceRestController.java b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/secondservice/web/SecondServiceRestController.java new file mode 100644 index 0000000000..f047b123ef --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/secondservice/web/SecondServiceRestController.java @@ -0,0 +1,18 @@ +package com.baeldung.secondservice.web; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import reactor.core.publisher.Mono; + +@RestController +public class SecondServiceRestController { + + @GetMapping("/resource/language") + public Mono> getResource() { + return Mono.just(ResponseEntity.ok() + .body("es")); + + } +} diff --git a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/service/ServiceApplication.java b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/service/ServiceApplication.java new file mode 100644 index 0000000000..9853b78088 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/service/ServiceApplication.java @@ -0,0 +1,15 @@ +package com.baeldung.service; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.PropertySource; + +@SpringBootApplication +@PropertySource("classpath:service-application.properties") +public class ServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(ServiceApplication.class, args); + } + +} diff --git a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/service/web/ServiceRestController.java b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/service/web/ServiceRestController.java new file mode 100644 index 0000000000..12f7151e59 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/service/web/ServiceRestController.java @@ -0,0 +1,22 @@ +package com.baeldung.service.web; + +import java.util.Locale; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import reactor.core.publisher.Mono; + +@RestController +public class ServiceRestController { + + @GetMapping("/resource") + public Mono> getResource() { + return Mono.just(ResponseEntity.ok() + .header(HttpHeaders.CONTENT_LANGUAGE, Locale.ENGLISH.getLanguage()) + .body("Service Resource")); + + } +} diff --git a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/spring/cloud/GatewayApplication.java b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/spring/cloud/GatewayApplication.java deleted file mode 100644 index ba384749df..0000000000 --- a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/spring/cloud/GatewayApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.baeldung.spring.cloud; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class GatewayApplication { - - public static void main(String[] args) { - SpringApplication.run(GatewayApplication.class, args); - } - -} \ No newline at end of file diff --git a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/CustomFiltersGatewayApplication.java b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/CustomFiltersGatewayApplication.java new file mode 100644 index 0000000000..a9f18e71fd --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/CustomFiltersGatewayApplication.java @@ -0,0 +1,15 @@ +package com.baeldung.springcloudgateway.customfilters; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.PropertySource; + +@SpringBootApplication +@PropertySource("classpath:customfilters-global-application.properties") +public class CustomFiltersGatewayApplication { + + public static void main(String[] args) { + SpringApplication.run(CustomFiltersGatewayApplication.class, args); + } + +} \ No newline at end of file diff --git a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/config/WebClientConfig.java b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/config/WebClientConfig.java new file mode 100644 index 0000000000..8a7771f0e3 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/config/WebClientConfig.java @@ -0,0 +1,16 @@ +package com.baeldung.springcloudgateway.customfilters.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + WebClient client() { + return WebClient.builder() + .build(); + } + +} diff --git a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/factories/ChainRequestGatewayFilterFactory.java b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/factories/ChainRequestGatewayFilterFactory.java new file mode 100644 index 0000000000..f7e754fd70 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/factories/ChainRequestGatewayFilterFactory.java @@ -0,0 +1,90 @@ +package com.baeldung.springcloudgateway.customfilters.filters.factories; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale.LanguageRange; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +import reactor.core.publisher.Mono; + +@Component +public class ChainRequestGatewayFilterFactory extends AbstractGatewayFilterFactory { + + final Logger logger = LoggerFactory.getLogger(ChainRequestGatewayFilterFactory.class); + + private final WebClient client; + + public ChainRequestGatewayFilterFactory(WebClient client) { + super(Config.class); + this.client = client; + } + + @Override + public List shortcutFieldOrder() { + return Arrays.asList("endpoint", "defaultLanguage"); + } + + @Override + public GatewayFilter apply(Config config) { + return (exchange, chain) -> { + return client.get() + .uri(config.getEndpoint()) + .exchange() + .flatMap(response -> { + return (response.statusCode() + .is2xxSuccessful()) ? response.bodyToMono(String.class) : Mono.just(config.getDefaultLanguage()); + }) + .map(LanguageRange::parse) + .map(range -> { + exchange.getRequest() + .mutate() + .headers(h -> h.setAcceptLanguage(range)) + .build(); + + String allOutgoingRequestLanguages = exchange.getRequest() + .getHeaders() + .getAcceptLanguage() + .stream() + .map(r -> r.getRange()) + .collect(Collectors.joining(",")); + + logger.info("Chain Request output - Request contains Accept-Language header: " + allOutgoingRequestLanguages); + + return exchange; + }) + .flatMap(chain::filter); + + }; + } + + public static class Config { + private String endpoint; + private String defaultLanguage; + + public Config() { + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public String getDefaultLanguage() { + return defaultLanguage; + } + + public void setDefaultLanguage(String defaultLanguage) { + this.defaultLanguage = defaultLanguage; + } + } +} diff --git a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/factories/LoggingGatewayFilterFactory.java b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/factories/LoggingGatewayFilterFactory.java new file mode 100644 index 0000000000..db73ba99c0 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/factories/LoggingGatewayFilterFactory.java @@ -0,0 +1,85 @@ +package com.baeldung.springcloudgateway.customfilters.filters.factories; + +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.OrderedGatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.stereotype.Component; + +import reactor.core.publisher.Mono; + +@Component +public class LoggingGatewayFilterFactory extends AbstractGatewayFilterFactory { + + final Logger logger = LoggerFactory.getLogger(LoggingGatewayFilterFactory.class); + + public static final String BASE_MSG = "baseMessage"; + public static final String PRE_LOGGER = "preLogger"; + public static final String POST_LOGGER = "postLogger"; + + public LoggingGatewayFilterFactory() { + super(Config.class); + } + + @Override + public List shortcutFieldOrder() { + return Arrays.asList(BASE_MSG, PRE_LOGGER, POST_LOGGER); + } + + @Override + public GatewayFilter apply(Config config) { + return new OrderedGatewayFilter((exchange, chain) -> { + if (config.isPreLogger()) + logger.info("Pre GatewayFilter logging: " + config.getBaseMessage()); + return chain.filter(exchange) + .then(Mono.fromRunnable(() -> { + if (config.isPostLogger()) + logger.info("Post GatewayFilter logging: " + config.getBaseMessage()); + })); + }, -2); + } + + public static class Config { + private String baseMessage; + private boolean preLogger; + private boolean postLogger; + + public Config() { + }; + + public Config(String baseMessage, boolean preLogger, boolean postLogger) { + super(); + this.baseMessage = baseMessage; + this.preLogger = preLogger; + this.postLogger = postLogger; + } + + public String getBaseMessage() { + return this.baseMessage; + } + + public boolean isPreLogger() { + return preLogger; + } + + public boolean isPostLogger() { + return postLogger; + } + + public void setBaseMessage(String baseMessage) { + this.baseMessage = baseMessage; + } + + public void setPreLogger(boolean preLogger) { + this.preLogger = preLogger; + } + + public void setPostLogger(boolean postLogger) { + this.postLogger = postLogger; + } + } +} diff --git a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/factories/ModifyRequestGatewayFilterFactory.java b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/factories/ModifyRequestGatewayFilterFactory.java new file mode 100644 index 0000000000..0f039bb41d --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/factories/ModifyRequestGatewayFilterFactory.java @@ -0,0 +1,78 @@ +package com.baeldung.springcloudgateway.customfilters.filters.factories; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.stereotype.Component; + +@Component +public class ModifyRequestGatewayFilterFactory extends AbstractGatewayFilterFactory { + + final Logger logger = LoggerFactory.getLogger(ModifyRequestGatewayFilterFactory.class); + + public ModifyRequestGatewayFilterFactory() { + super(Config.class); + } + + @Override + public List shortcutFieldOrder() { + return Arrays.asList("defaultLocale"); + } + + @Override + public GatewayFilter apply(Config config) { + return (exchange, chain) -> { + if (exchange.getRequest() + .getHeaders() + .getAcceptLanguage() + .isEmpty()) { + + String queryParamLocale = exchange.getRequest() + .getQueryParams() + .getFirst("locale"); + + Locale requestLocale = Optional.ofNullable(queryParamLocale) + .map(l -> Locale.forLanguageTag(l)) + .orElse(config.getDefaultLocale()); + + exchange.getRequest() + .mutate() + .headers(h -> h.setAcceptLanguageAsLocales(Collections.singletonList(requestLocale))) + .build(); + } + + String allOutgoingRequestLanguages = exchange.getRequest() + .getHeaders() + .getAcceptLanguage() + .stream() + .map(range -> range.getRange()) + .collect(Collectors.joining(",")); + + logger.info("Modify Request output - Request contains Accept-Language header: " + allOutgoingRequestLanguages); + return chain.filter(exchange); + }; + } + + public static class Config { + private Locale defaultLocale; + + public Config() { + } + + public Locale getDefaultLocale() { + return defaultLocale; + } + + public void setDefaultLocale(String defaultLocale) { + this.defaultLocale = Locale.forLanguageTag(defaultLocale); + }; + } +} diff --git a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/factories/ModifyResponseGatewayFilterFactory.java b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/factories/ModifyResponseGatewayFilterFactory.java new file mode 100644 index 0000000000..55b39fce29 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/factories/ModifyResponseGatewayFilterFactory.java @@ -0,0 +1,48 @@ +package com.baeldung.springcloudgateway.customfilters.filters.factories; + +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.stereotype.Component; + +import reactor.core.publisher.Mono; + +@Component +public class ModifyResponseGatewayFilterFactory extends AbstractGatewayFilterFactory { + + final Logger logger = LoggerFactory.getLogger(ModifyResponseGatewayFilterFactory.class); + + public ModifyResponseGatewayFilterFactory() { + super(Config.class); + } + + @Override + public GatewayFilter apply(Config config) { + return (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); + logger.info("Added custom header to Response"); + }); + })); + }; + } + + public static class Config { + } +} diff --git a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/global/LoggingGlobalFiltersConfigurations.java b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/global/LoggingGlobalFiltersConfigurations.java new file mode 100644 index 0000000000..cf2ff3af16 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/global/LoggingGlobalFiltersConfigurations.java @@ -0,0 +1,39 @@ +package com.baeldung.springcloudgateway.customfilters.filters.global; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +import reactor.core.publisher.Mono; + +@Configuration +public class 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"); + })); + }; + } + + @Bean + @Order(-1) + public GlobalFilter FirstPreLastPostGlobalFilter() { + return (exchange, chain) -> { + logger.info("First Pre Global Filter"); + return chain.filter(exchange) + .then(Mono.fromRunnable(() -> { + logger.info("Last Post Global Filter"); + })); + }; + } + +} diff --git a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/global/LoggingGlobalPreFilter.java b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/global/LoggingGlobalPreFilter.java new file mode 100644 index 0000000000..d91075e4b6 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/filters/global/LoggingGlobalPreFilter.java @@ -0,0 +1,28 @@ +package com.baeldung.springcloudgateway.customfilters.filters.global; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; + +import reactor.core.publisher.Mono; + +@Component +public class LoggingGlobalPreFilter implements GlobalFilter, Ordered { + + final Logger logger = LoggerFactory.getLogger(LoggingGlobalPreFilter.class); + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + logger.info("Global Pre Filter executed"); + return chain.filter(exchange); + } + + @Override + public int getOrder() { + return 0; + } +} diff --git a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/routes/ServiceRouteConfiguration.java b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/routes/ServiceRouteConfiguration.java new file mode 100644 index 0000000000..b4f6eda374 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/customfilters/routes/ServiceRouteConfiguration.java @@ -0,0 +1,28 @@ +package com.baeldung.springcloudgateway.customfilters.routes; + +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.context.annotation.Bean; + +import com.baeldung.springcloudgateway.customfilters.filters.factories.LoggingGatewayFilterFactory; +import com.baeldung.springcloudgateway.customfilters.filters.factories.LoggingGatewayFilterFactory.Config; + +/** + * Note: We want to keep this as an example of configuring a Route with a custom filter + * + * This corresponds with the properties configuration we have + */ +// @Configuration +public class ServiceRouteConfiguration { + + @Bean + public 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("http://localhost:8081")) + .build(); + } +} diff --git a/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/introduction/IntroductionGatewayApplication.java b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/introduction/IntroductionGatewayApplication.java new file mode 100644 index 0000000000..d276597a6b --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/introduction/IntroductionGatewayApplication.java @@ -0,0 +1,15 @@ +package com.baeldung.springcloudgateway.introduction; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.PropertySource; + +@SpringBootApplication +@PropertySource("classpath:introduction-application.properties") +public class IntroductionGatewayApplication { + + public static void main(String[] args) { + SpringApplication.run(IntroductionGatewayApplication.class, args); + } + +} \ No newline at end of file diff --git a/spring-cloud/spring-cloud-gateway/src/main/resources/application.yml b/spring-cloud/spring-cloud-gateway/src/main/resources/application.yml index 2450638e46..a33bca2055 100644 --- a/spring-cloud/spring-cloud-gateway/src/main/resources/application.yml +++ b/spring-cloud/spring-cloud-gateway/src/main/resources/application.yml @@ -1,16 +1,4 @@ -server: - port: 80 -spring: - cloud: - gateway: - routes: - - id: baeldung_route - uri: http://www.baeldung.com - predicates: - - Path=/baeldung - -management: - endpoints: - web: - exposure: - include: "*" +logging: + level: + org.springframework.cloud.gateway: DEBUG + reactor.netty.http.client: DEBUG diff --git a/spring-cloud/spring-cloud-gateway/src/main/resources/customfilters-global-application.properties b/spring-cloud/spring-cloud-gateway/src/main/resources/customfilters-global-application.properties new file mode 100644 index 0000000000..116bc706cb --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/main/resources/customfilters-global-application.properties @@ -0,0 +1,19 @@ +spring.cloud.gateway.routes[0].id=service_route +spring.cloud.gateway.routes[0].uri=http://localhost:8081 +spring.cloud.gateway.routes[0].predicates[0]=Path=/service/** +spring.cloud.gateway.routes[0].filters[0]=RewritePath=/service(?/?.*), $\{segment} +spring.cloud.gateway.routes[0].filters[1]=Logging=My Custom Message, true, true +# Or, as an alternative: +#spring.cloud.gateway.routes[0].filters[1].name=Logging +#spring.cloud.gateway.routes[0].filters[1].args[baseMessage]=My Custom Message +#spring.cloud.gateway.routes[0].filters[1].args[preLogger]=true +#spring.cloud.gateway.routes[0].filters[1].args[postLogger]=true + +spring.cloud.gateway.routes[0].filters[2]=ModifyRequest=en +spring.cloud.gateway.routes[0].filters[3]=ModifyResponse +spring.cloud.gateway.routes[0].filters[4]=ChainRequest=http://localhost:8082/resource/language, fr + +management.endpoints.web.exposure.include=* + +server.port=80 + diff --git a/spring-cloud/spring-cloud-gateway/src/main/resources/introduction-application.properties b/spring-cloud/spring-cloud-gateway/src/main/resources/introduction-application.properties new file mode 100644 index 0000000000..d7a6c4e072 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/main/resources/introduction-application.properties @@ -0,0 +1,7 @@ +spring.cloud.gateway.routes[0].id=baeldung_route +spring.cloud.gateway.routes[0].uri=http://www.baeldung.com +spring.cloud.gateway.routes[0].predicates[0]=Path=/baeldung + +management.endpoints.web.exposure.include=* + +server.port=80 diff --git a/spring-cloud/spring-cloud-gateway/src/main/resources/secondservice-application.properties b/spring-cloud/spring-cloud-gateway/src/main/resources/secondservice-application.properties new file mode 100644 index 0000000000..3cf12afeb9 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/main/resources/secondservice-application.properties @@ -0,0 +1 @@ +server.port=8082 diff --git a/spring-cloud/spring-cloud-gateway/src/main/resources/service-application.properties b/spring-cloud/spring-cloud-gateway/src/main/resources/service-application.properties new file mode 100644 index 0000000000..4d360de145 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/main/resources/service-application.properties @@ -0,0 +1 @@ +server.port=8081 diff --git a/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/secondservice/SecondServiceIntegrationTest.java b/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/secondservice/SecondServiceIntegrationTest.java new file mode 100644 index 0000000000..9a1e0b0712 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/secondservice/SecondServiceIntegrationTest.java @@ -0,0 +1,26 @@ +package com.baeldung.secondservice; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.test.web.reactive.server.WebTestClient; + +import com.baeldung.secondservice.web.SecondServiceRestController; + +@WebFluxTest(SecondServiceRestController.class) +public class SecondServiceIntegrationTest { + + @Autowired + private WebTestClient webClient; + + @Test + public void whenResourceLanguageEndpointCalled_thenRetrievesSpanishLanguageString() throws Exception { + this.webClient.get() + .uri("/resource/language") + .exchange() + .expectStatus() + .isOk() + .expectBody(String.class) + .isEqualTo("es"); + } +} diff --git a/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/secondservice/SpringContextTest.java b/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/secondservice/SpringContextTest.java new file mode 100644 index 0000000000..127ef7fe32 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/secondservice/SpringContextTest.java @@ -0,0 +1,12 @@ +package com.baeldung.secondservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(classes = SecondServiceApplication.class) +public class SpringContextTest { + + @Test + public void whenSpringContextIsBootstrapped_thenNoExceptions() { + } +} diff --git a/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/service/ServiceIntegrationTest.java b/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/service/ServiceIntegrationTest.java new file mode 100644 index 0000000000..cb65ac3a50 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/service/ServiceIntegrationTest.java @@ -0,0 +1,29 @@ +package com.baeldung.service; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.http.HttpHeaders; +import org.springframework.test.web.reactive.server.WebTestClient; + +import com.baeldung.service.web.ServiceRestController; + +@WebFluxTest(ServiceRestController.class) +public class ServiceIntegrationTest { + + @Autowired + private WebTestClient webClient; + + @Test + public void whenResourceEndpointCalled_thenRetrievesResourceStringWithContentLanguageHeader() throws Exception { + this.webClient.get() + .uri("/resource") + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .valueEquals(HttpHeaders.CONTENT_LANGUAGE, "en") + .expectBody(String.class) + .isEqualTo("Service Resource"); + } +} diff --git a/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/service/SpringContextTest.java b/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/service/SpringContextTest.java new file mode 100644 index 0000000000..28216dca86 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/service/SpringContextTest.java @@ -0,0 +1,12 @@ +package com.baeldung.service; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(classes = ServiceApplication.class) +public class SpringContextTest { + + @Test + public void whenSpringContextIsBootstrapped_thenNoExceptions() { + } +} diff --git a/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/customfilters/CustomFiltersLiveTest.java b/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/customfilters/CustomFiltersLiveTest.java new file mode 100644 index 0000000000..40275bd206 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/customfilters/CustomFiltersLiveTest.java @@ -0,0 +1,90 @@ +package com.baeldung.springcloudgateway.customfilters; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; + +import com.baeldung.springcloudgateway.customfilters.utils.LoggerListAppender; + +import ch.qos.logback.classic.spi.ILoggingEvent; + +/** + * This test requires: + * * the service in com.baeldung.service running + * * the 'second service' in com.baeldung.secondservice running + * + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +public class CustomFiltersLiveTest { + + @LocalServerPort + String port; + + private WebTestClient client; + + @BeforeEach + public void clearLogList() { + LoggerListAppender.clearEventList(); + client = WebTestClient.bindToServer() + .baseUrl("http://localhost:" + port) + .build(); + } + + @Test + public void whenCallServiceThroughGateway_thenAllConfiguredFiltersGetExecuted() { + ResponseSpec response = client.get() + .uri("/service/resource") + .exchange(); + + response.expectStatus() + .isOk() + .expectBody(String.class) + .isEqualTo("Service Resource"); + + assertThat(LoggerListAppender.getEvents()) + // Global Pre Filter + .haveAtLeastOne(eventContains("Global Pre Filter executed")) + // Global Post Filter + .haveAtLeastOne(eventContains("Global Post Filter executed")) + // Global Pre and Post Filter + .haveAtLeastOne(eventContains("First Pre Global Filter")) + .haveAtLeastOne(eventContains("Last Post Global Filter")) + // Logging Filter Factory + .haveAtLeastOne(eventContains("Pre GatewayFilter logging: My Custom Message")) + .haveAtLeastOne(eventContains("Post GatewayFilter logging: My Custom Message")) + // Modify Request + .haveAtLeastOne(eventContains("Modify Request output - Request contains Accept-Language header:")) + // Modify Response + .areNot(eventContains("Added custom header to Response")) + // Chain Request + .haveAtLeastOne(eventContains("Chain Request output - Request contains Accept-Language header:")); + } + + @Test + public void givenRequestWithLocaleQueryParam_whenCallServiceThroughGateway_thenAllConfiguredFiltersGetExecuted() { + ResponseSpec response = client.get() + .uri("/service/resource?locale=en") + .exchange(); + + response.expectStatus() + .isOk() + .expectBody(String.class) + .isEqualTo("Service Resource"); + + assertThat(LoggerListAppender.getEvents()) + // Modify Response + .haveAtLeastOne(eventContains("Added custom header to Response")); + } + + private Condition eventContains(String substring) { + return new Condition(entry -> (substring == null || (entry.getFormattedMessage() != null && entry.getFormattedMessage() + .contains(substring))), String.format("entry with message '%s'", substring)); + } +} diff --git a/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/customfilters/utils/LoggerListAppender.java b/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/customfilters/utils/LoggerListAppender.java new file mode 100644 index 0000000000..b6337dabb6 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/customfilters/utils/LoggerListAppender.java @@ -0,0 +1,25 @@ +package com.baeldung.springcloudgateway.customfilters.utils; + +import java.util.ArrayList; +import java.util.List; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; + +public class LoggerListAppender extends AppenderBase { + + static private List events = new ArrayList<>(); + + @Override + protected void append(ILoggingEvent eventObject) { + events.add(eventObject); + } + + public static List getEvents() { + return events; + } + + public static void clearEventList() { + events.clear(); + } +} \ No newline at end of file diff --git a/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/introduction/SpringContextTest.java b/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/introduction/SpringContextTest.java new file mode 100644 index 0000000000..1550265f22 --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/introduction/SpringContextTest.java @@ -0,0 +1,15 @@ +package com.baeldung.springcloudgateway.introduction; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import com.baeldung.springcloudgateway.introduction.IntroductionGatewayApplication; + + +@SpringBootTest(classes = IntroductionGatewayApplication.class) +public class SpringContextTest { + + @Test + public void whenSpringContextIsBootstrapped_thenNoExceptions() { + } +} diff --git a/spring-cloud/spring-cloud-gateway/src/test/java/org/baeldung/SpringContextIntegrationTest.java b/spring-cloud/spring-cloud-gateway/src/test/java/org/baeldung/SpringContextIntegrationTest.java deleted file mode 100644 index f2addf5c1a..0000000000 --- a/spring-cloud/spring-cloud-gateway/src/test/java/org/baeldung/SpringContextIntegrationTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.baeldung; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; - -import com.baeldung.spring.cloud.GatewayApplication; - -@RunWith(SpringRunner.class) -@SpringBootTest(classes = GatewayApplication.class) -public class SpringContextIntegrationTest { - - @Test - public void whenSpringContextIsBootstrapped_thenNoExceptions() { - } -} diff --git a/spring-cloud/spring-cloud-gateway/src/test/java/org/baeldung/SpringContextTest.java b/spring-cloud/spring-cloud-gateway/src/test/java/org/baeldung/SpringContextTest.java deleted file mode 100644 index b7e2acf7a8..0000000000 --- a/spring-cloud/spring-cloud-gateway/src/test/java/org/baeldung/SpringContextTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.baeldung; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; - -import com.baeldung.spring.cloud.GatewayApplication; - -@RunWith(SpringRunner.class) -@SpringBootTest(classes = GatewayApplication.class) -public class SpringContextTest { - - @Test - public void whenSpringContextIsBootstrapped_thenNoExceptions() { - } -} diff --git a/spring-cloud/spring-cloud-gateway/src/test/resources/logback-test.xml b/spring-cloud/spring-cloud-gateway/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..8febdc8b1a --- /dev/null +++ b/spring-cloud/spring-cloud-gateway/src/test/resources/logback-test.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file