diff --git a/spring-cloud-gateway-integration-tests/httpclient/pom.xml b/spring-cloud-gateway-integration-tests/httpclient/pom.xml new file mode 100644 index 00000000..54472f7b --- /dev/null +++ b/spring-cloud-gateway-integration-tests/httpclient/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + httpclient + jar + + Spring Cloud Gateway HttpClient Integration Test + Spring Cloud Gateway HttpClient Integration Test + + + + + + org.springframework.cloud + spring-cloud-gateway-integration-tests + 4.3.0-SNAPSHOT + .. + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-webflux + + + + org.springframework.cloud + spring-cloud-starter-gateway-server-webmvc + + + + org.springframework.retry + spring-retry + + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + + org.apache.httpcomponents.client5 + httpclient5 + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.assertj + assertj-core + test + + + + + + + maven-deploy-plugin + + true + + + + + diff --git a/spring-cloud-gateway-integration-tests/httpclient/src/main/java/org/springframework/cloud/gateway/tests/httpclient/HttpClientApplication.java b/spring-cloud-gateway-integration-tests/httpclient/src/main/java/org/springframework/cloud/gateway/tests/httpclient/HttpClientApplication.java new file mode 100644 index 00000000..6e198797 --- /dev/null +++ b/spring-cloud-gateway-integration-tests/httpclient/src/main/java/org/springframework/cloud/gateway/tests/httpclient/HttpClientApplication.java @@ -0,0 +1,121 @@ +/* + * Copyright 2013-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.tests.httpclient; + +import java.time.Duration; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hc.core5.util.Timeout; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpComponentsClientHttpRequestFactoryBuilder; +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient; +import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.ServerResponse; + +import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.prefixPath; +import static org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerFilterFunctions.lb; +import static org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions.retry; +import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route; +import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http; + +/** + * @author jiangyuan + */ +@SpringBootConfiguration +@EnableAutoConfiguration +@LoadBalancerClient(name = "myservice", configuration = MyServiceConf.class) +public class HttpClientApplication { + + public static void main(String[] args) { + SpringApplication.run(HttpClientApplication.class, args); + } + + @Bean + public HttpComponentsClientHttpRequestFactoryBuilder httpComponentsClientHttpRequestFactoryBuilder() { + return ClientHttpRequestFactoryBuilder.httpComponents() + .withConnectionManagerCustomizer(builder -> builder.setMaxConnTotal(2).setMaxConnPerRoute(2)) + .withDefaultRequestConfigCustomizer( + c -> c.setConnectionRequestTimeout(Timeout.of(Duration.ofMillis(3000)))); + } + + @Bean + public RouterFunction gatewayRouterFunctionsRetry() { + return route("test-retry").GET("/retry", http()) + .filter(lb("myservice")) + .filter(prefixPath("/do")) + .filter(retry(3)) + .build(); + } + + @RestController + protected static class RetryController { + + Log log = LogFactory.getLog(getClass()); + + ConcurrentHashMap map = new ConcurrentHashMap<>(); + + @GetMapping("/do/retry") + public ResponseEntity retry(@RequestParam("key") String key, + @RequestParam(name = "count", defaultValue = "3") int count, + @RequestParam(name = "failStatus", required = false) Integer failStatus) { + AtomicInteger num = map.computeIfAbsent(key, s -> new AtomicInteger()); + int i = num.incrementAndGet(); + log.warn("Retry count: " + i); + String body = String.valueOf(i); + if (i < count) { + HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; + if (failStatus != null) { + httpStatus = HttpStatus.resolve(failStatus); + } + return ResponseEntity.status(httpStatus).header("X-Retry-Count", body).body("temporarily broken"); + } + return ResponseEntity.status(HttpStatus.OK).header("X-Retry-Count", body).body(body); + } + + } + +} + +class MyServiceConf { + + @Value("${local.server.port}") + private int port = 0; + + @Bean + public ServiceInstanceListSupplier staticServiceInstanceListSupplier() { + return ServiceInstanceListSuppliers.from("myservice", + new DefaultServiceInstance("myservice-1", "myservice", "localhost", port, false)); + } + +} diff --git a/spring-cloud-gateway-integration-tests/httpclient/src/main/resources/application.yml b/spring-cloud-gateway-integration-tests/httpclient/src/main/resources/application.yml new file mode 100644 index 00000000..106caafa --- /dev/null +++ b/spring-cloud-gateway-integration-tests/httpclient/src/main/resources/application.yml @@ -0,0 +1,3 @@ +logging: + level: + org.springframework.cloud.gateway: TRACE diff --git a/spring-cloud-gateway-integration-tests/httpclient/src/test/java/org/springframework/cloud/gateway/tests/httpclient/HttpClientApplicationTests.java b/spring-cloud-gateway-integration-tests/httpclient/src/test/java/org/springframework/cloud/gateway/tests/httpclient/HttpClientApplicationTests.java new file mode 100644 index 00000000..13269d41 --- /dev/null +++ b/spring-cloud-gateway-integration-tests/httpclient/src/test/java/org/springframework/cloud/gateway/tests/httpclient/HttpClientApplicationTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.tests.httpclient; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * @author jiangyuan + */ +@SpringBootTest(classes = HttpClientApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext +public class HttpClientApplicationTests { + + @LocalServerPort + private int port; + + @Test + public void retryWorks() { + WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port).build(); + client.get().uri("/retry?key=get").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("3"); + } + +} diff --git a/spring-cloud-gateway-integration-tests/pom.xml b/spring-cloud-gateway-integration-tests/pom.xml index 8fc43a9b..cab51af8 100644 --- a/spring-cloud-gateway-integration-tests/pom.xml +++ b/spring-cloud-gateway-integration-tests/pom.xml @@ -24,6 +24,7 @@ grpc http2 mvc-failure-analyzer + httpclient diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctionTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctionTests.java index be2bfcba..edae11e4 100644 --- a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctionTests.java +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/RetryFilterFunctionTests.java @@ -16,41 +16,30 @@ package org.springframework.cloud.gateway.server.mvc.filter; -import java.time.Duration; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.apache.hc.core5.util.Timeout; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties; -import org.springframework.cloud.gateway.server.mvc.handler.ProxyExchange; -import org.springframework.cloud.gateway.server.mvc.handler.ProxyExchangeHandlerFunction; -import org.springframework.cloud.gateway.server.mvc.handler.RestClientProxyExchange; import org.springframework.cloud.gateway.server.mvc.test.HttpbinTestcontainers; import org.springframework.cloud.gateway.server.mvc.test.LocalServerPortUriResolver; import org.springframework.cloud.gateway.server.mvc.test.TestLoadBalancerConfig; import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient; -import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; -import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.core.log.LogMessage; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.test.context.ContextConfiguration; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; @@ -58,13 +47,11 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.client.RestClient; import org.springframework.web.servlet.function.RouterFunction; import org.springframework.web.servlet.function.ServerResponse; import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.adaptCachedBody; import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.prefixPath; -import static org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions.setPath; import static org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions.retry; import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route; import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http; @@ -106,17 +93,6 @@ public class RetryFilterFunctionTests { .isEqualTo("3"); } - @Test - public void retryWorksWithHttpComponentsClient() { - restClient.get() - .uri("/retrywithhttpcomponentsclient?key=retryWorksWithHttpComponentsClient") - .exchange() - .expectStatus() - .isOk() - .expectBody(String.class) - .isEqualTo("3"); - } - @SpringBootConfiguration @EnableAutoConfiguration @LoadBalancerClient(name = "httpbin", configuration = TestLoadBalancerConfig.Httpbin.class) @@ -134,41 +110,6 @@ public class RetryFilterFunctionTests { // @formatter:on } - @Bean - public RouterFunction gatewayRouterFunctionsRetryWithHttpComponentsClient( - GatewayMvcProperties properties, - ObjectProvider requestHttpHeadersFilters, - ObjectProvider responseHttpHeadersFilters, - ApplicationContext applicationContext) { - - // build httpComponents client factory - ClientHttpRequestFactory clientHttpRequestFactory = ClientHttpRequestFactoryBuilder.httpComponents() - .withConnectionManagerCustomizer(builder -> builder.setMaxConnTotal(2).setMaxConnPerRoute(2)) - .withDefaultRequestConfigCustomizer( - c -> c.setConnectionRequestTimeout(Timeout.of(Duration.ofMillis(3000)))) - .build(); - - // build proxyExchange use httpComponents - RestClient.Builder restClientBuilder = RestClient.builder(); - restClientBuilder.requestFactory(clientHttpRequestFactory); - ProxyExchange proxyExchange = new RestClientProxyExchange(restClientBuilder.build(), properties); - - // build handler function use httpComponents - ProxyExchangeHandlerFunction function = new ProxyExchangeHandlerFunction(proxyExchange, - requestHttpHeadersFilters, responseHttpHeadersFilters); - function.onApplicationEvent(new ContextRefreshedEvent(applicationContext)); - - // @formatter:off - return route("testretrywithhttpcomponentsclient") - .GET("/retrywithhttpcomponentsclient", function) - .before(new LocalServerPortUriResolver()) - .filter(retry(3)) - .filter(setPath("/retry")) - .filter(prefixPath("/do")) - .build(); - // @formatter:on - } - @Bean public RouterFunction gatewayRouterFunctionsRetryBody() { // @formatter:off diff --git a/spring-cloud-gateway-server-mvc/src/test/resources/application.yml b/spring-cloud-gateway-server-mvc/src/test/resources/application.yml index 60537dfc..9bc1759e 100644 --- a/spring-cloud-gateway-server-mvc/src/test/resources/application.yml +++ b/spring-cloud-gateway-server-mvc/src/test/resources/application.yml @@ -6,7 +6,4 @@ logging: org.springframework.retry: TRACE spring: mvc: - log-request-details: true - http: - client: - factory: REACTOR \ No newline at end of file + log-request-details: true \ No newline at end of file