From 22bcf3759b417f0ed88181a7163de5a7a2ea5927 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Tue, 13 May 2025 17:02:35 +0200 Subject: [PATCH 1/5] 5.0.x (#1490) * Init 5.0.x. Signed-off-by: Olga Maciaszek-Sharma * Adjust to changes in FW. Signed-off-by: Olga Maciaszek-Sharma * Fix LoadBalancerWebClientHttpServiceGroupConfigurer implementation. Refactor. Signed-off-by: Olga Maciaszek-Sharma --------- Signed-off-by: Olga Maciaszek-Sharma --- docs/pom.xml | 2 +- pom.xml | 4 ++-- spring-cloud-commons-dependencies/pom.xml | 4 ++-- spring-cloud-commons/pom.xml | 2 +- .../cloud/client/loadbalancer/RequestData.java | 7 ++++--- .../CompatibilityVerifierProperties.java | 5 +++-- .../configuration/SpringBootVersionVerifier.java | 10 +++++----- ...eLoadBalancerExchangeFilterFunctionTests.java | 5 +++-- .../configuration/SpringBootDependencyTests.java | 15 ++++++++------- spring-cloud-context-integration-tests/pom.xml | 2 +- .../pom.xml | 2 +- spring-cloud-context/pom.xml | 2 +- spring-cloud-loadbalancer/pom.xml | 2 +- .../core/ServiceInstanceListSupplierBuilder.java | 9 +++++---- .../XForwardedHeadersTransformerTests.java | 10 +++++----- ...lthCheckServiceInstanceListSupplierTests.java | 6 +++--- ...cerServiceInstanceCookieTransformerTests.java | 8 ++++---- ...ySessionServiceInstanceListSupplierTests.java | 16 +++++++++------- .../ServiceInstanceListSuppliersTestUtils.java | 10 ++++++---- .../core/XForwardedHeadersTransformerTests.java | 10 +++++----- ...icrometerStatsLoadBalancerLifecycleTests.java | 14 ++++++++------ spring-cloud-starter-bootstrap/pom.xml | 2 +- spring-cloud-starter-loadbalancer/pom.xml | 2 +- spring-cloud-starter/pom.xml | 2 +- spring-cloud-test-support/pom.xml | 2 +- 25 files changed, 82 insertions(+), 71 deletions(-) diff --git a/docs/pom.xml b/docs/pom.xml index 333bf8e9..cf7b1cb4 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -8,7 +8,7 @@ org.springframework.cloud spring-cloud-commons-parent - 4.3.0-SNAPSHOT + 5.0.0-SNAPSHOT jar Spring Cloud Commons Docs diff --git a/pom.xml b/pom.xml index 770a92a7..f16fdfd6 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.springframework.cloud spring-cloud-commons-parent - 4.3.0-SNAPSHOT + 5.0.0-SNAPSHOT pom Spring Cloud Commons Parent Spring Cloud Commons Parent @@ -13,7 +13,7 @@ org.springframework.cloud spring-cloud-build - 4.3.0-SNAPSHOT + 5.0.0-SNAPSHOT diff --git a/spring-cloud-commons-dependencies/pom.xml b/spring-cloud-commons-dependencies/pom.xml index d0b105b9..766bf89a 100644 --- a/spring-cloud-commons-dependencies/pom.xml +++ b/spring-cloud-commons-dependencies/pom.xml @@ -6,11 +6,11 @@ spring-cloud-dependencies-parent org.springframework.cloud - 4.3.0-SNAPSHOT + 5.0.0-SNAPSHOT spring-cloud-commons-dependencies - 4.3.0-SNAPSHOT + 5.0.0-SNAPSHOT pom spring-cloud-commons-dependencies Spring Cloud Commons Dependencies diff --git a/spring-cloud-commons/pom.xml b/spring-cloud-commons/pom.xml index 05a6dffe..f316a3e6 100644 --- a/spring-cloud-commons/pom.xml +++ b/spring-cloud-commons/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-commons-parent - 4.3.0-SNAPSHOT + 5.0.0-SNAPSHOT .. spring-cloud-commons diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/RequestData.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/RequestData.java index 8c320190..2bd21ab6 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/RequestData.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/RequestData.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-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. @@ -29,6 +29,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.client.ClientRequest; @@ -80,7 +81,7 @@ public class RequestData { } private static MultiValueMap buildCookies(MultiValueMap cookies) { - HttpHeaders newCookies = new HttpHeaders(); + MultiValueMap newCookies = new LinkedMultiValueMap<>(); if (cookies != null) { cookies.forEach((key, value) -> value .forEach(cookie -> newCookies.put(cookie.getName(), Collections.singletonList(cookie.getValue())))); @@ -89,7 +90,7 @@ public class RequestData { } private static MultiValueMap buildCookiesFromHeaders(HttpHeaders headers) { - HttpHeaders newCookies = new HttpHeaders(); + MultiValueMap newCookies = new LinkedMultiValueMap<>(); if (headers == null) { return newCookies; } diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/configuration/CompatibilityVerifierProperties.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/configuration/CompatibilityVerifierProperties.java index a5663ab7..27cb50da 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/configuration/CompatibilityVerifierProperties.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/configuration/CompatibilityVerifierProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-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. @@ -22,6 +22,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; /** * @author Marcin Grzejszczak + * @author Olga Maciaszek-Sharma */ @ConfigurationProperties("spring.cloud.compatibility-verifier") public class CompatibilityVerifierProperties { @@ -36,7 +37,7 @@ public class CompatibilityVerifierProperties { * the patch version if you don't want to specify a concrete value. Example: * {@code 3.5.x} */ - private List compatibleBootVersions = List.of("3.5.x"); + private List compatibleBootVersions = List.of("4.0.x"); public boolean isEnabled() { return this.enabled; diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/configuration/SpringBootVersionVerifier.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/configuration/SpringBootVersionVerifier.java index 880f49f9..f1e6cb69 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/configuration/SpringBootVersionVerifier.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/configuration/SpringBootVersionVerifier.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-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. @@ -27,7 +27,7 @@ import org.springframework.boot.SpringBootVersion; import org.springframework.util.StringUtils; /** - * Verifies if Spring Boot has proper version. + * Verifies if Spring Boot has the proper version. */ class SpringBootVersionVerifier implements CompatibilityVerifier { @@ -35,7 +35,7 @@ class SpringBootVersionVerifier implements CompatibilityVerifier { final Map ACCEPTED_VERSIONS = new HashMap<>() { { - this.put("3.5", is3_5()); + this.put("4.0", is4_0()); } }; @@ -70,12 +70,12 @@ class SpringBootVersionVerifier implements CompatibilityVerifier { return SpringBootVersion.getVersion(); } - CompatibilityPredicate is3_5() { + CompatibilityPredicate is4_0() { return new CompatibilityPredicate() { @Override public String toString() { - return "Predicate for Boot 3.5"; + return "Predicate for Boot 4.0"; } @Override diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunctionTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunctionTests.java index 29bef5bc..5c84e5c4 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunctionTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/RetryableLoadBalancerExchangeFilterFunctionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-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. @@ -31,6 +31,7 @@ import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ExchangeFunction; @@ -75,7 +76,7 @@ class RetryableLoadBalancerExchangeFilterFunctionTests { when(factory.getInstance("test")).thenReturn(new TestReactiveLoadBalancer()); when(factory.getProperties(any())).thenReturn(properties); when(clientRequest.headers()).thenReturn(new HttpHeaders()); - when(clientRequest.cookies()).thenReturn(new HttpHeaders()); + when(clientRequest.cookies()).thenReturn(new LinkedMultiValueMap<>()); } diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/configuration/SpringBootDependencyTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/configuration/SpringBootDependencyTests.java index 825ddc6a..0bc8910a 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/configuration/SpringBootDependencyTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/configuration/SpringBootDependencyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-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. @@ -27,6 +27,7 @@ import static org.assertj.core.api.BDDAssertions.then; /** * @author Marcin Grzejszczak + * @author Olga Maciaszek-Sharma */ public class SpringBootDependencyTests { @@ -176,8 +177,8 @@ public class SpringBootDependencyTests { @Test public void should_match_against_current_manifest() { try { - verifyCurrentVersionFromManifest("3.5"); - verifyCurrentVersionFromManifest("3.5.x"); + verifyCurrentVersionFromManifest("4.0"); + verifyCurrentVersionFromManifest("4.0.x"); } catch (AssertionError e) { // if (e.getMessage() != null && e.getMessage().contains("3.3.")) { @@ -204,7 +205,7 @@ public class SpringBootDependencyTests { @Test public void should_match_against_current_predicate() { - List acceptedVersions = Collections.singletonList("3.0"); + List acceptedVersions = Collections.singletonList("4.0"); SpringBootVersionVerifier versionVerifier = new SpringBootVersionVerifier(acceptedVersions) { @Override String getVersionFromManifest() { @@ -212,7 +213,7 @@ public class SpringBootDependencyTests { } }; versionVerifier.ACCEPTED_VERSIONS.clear(); - versionVerifier.ACCEPTED_VERSIONS.put("3.0", versionVerifier.is3_5()); + versionVerifier.ACCEPTED_VERSIONS.put("4.0", versionVerifier.is4_0()); VerificationResult verificationResult = versionVerifier.verify(); @@ -222,7 +223,7 @@ public class SpringBootDependencyTests { @Test public void should_match_against_current_predicate_with_version_ending_with_x() { - List acceptedVersions = Collections.singletonList("3.0.x"); + List acceptedVersions = Collections.singletonList("4.0.x"); SpringBootVersionVerifier versionVerifier = new SpringBootVersionVerifier(acceptedVersions) { @Override String getVersionFromManifest() { @@ -230,7 +231,7 @@ public class SpringBootDependencyTests { } }; versionVerifier.ACCEPTED_VERSIONS.clear(); - versionVerifier.ACCEPTED_VERSIONS.put("3.0", versionVerifier.is3_5()); + versionVerifier.ACCEPTED_VERSIONS.put("4.0", versionVerifier.is4_0()); VerificationResult verificationResult = versionVerifier.verify(); diff --git a/spring-cloud-context-integration-tests/pom.xml b/spring-cloud-context-integration-tests/pom.xml index b316f1b2..80b02ae3 100644 --- a/spring-cloud-context-integration-tests/pom.xml +++ b/spring-cloud-context-integration-tests/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-commons-parent - 4.3.0-SNAPSHOT + 5.0.0-SNAPSHOT .. spring-cloud-context-integration-tests diff --git a/spring-cloud-context-webflux-integration-tests/pom.xml b/spring-cloud-context-webflux-integration-tests/pom.xml index 3a181468..e5ab3be9 100644 --- a/spring-cloud-context-webflux-integration-tests/pom.xml +++ b/spring-cloud-context-webflux-integration-tests/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-commons-parent - 4.3.0-SNAPSHOT + 5.0.0-SNAPSHOT .. spring-cloud-context-webflux-integration-tests diff --git a/spring-cloud-context/pom.xml b/spring-cloud-context/pom.xml index 02277300..2fd63822 100644 --- a/spring-cloud-context/pom.xml +++ b/spring-cloud-context/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-commons-parent - 4.3.0-SNAPSHOT + 5.0.0-SNAPSHOT .. spring-cloud-context diff --git a/spring-cloud-loadbalancer/pom.xml b/spring-cloud-loadbalancer/pom.xml index d05d4b4a..7a093345 100644 --- a/spring-cloud-loadbalancer/pom.xml +++ b/spring-cloud-loadbalancer/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-commons-parent - 4.3.0-SNAPSHOT + 5.0.0-SNAPSHOT .. spring-cloud-loadbalancer diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java index 19a3c72c..efe1cdab 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSupplierBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * 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. @@ -380,9 +380,10 @@ public final class ServiceInstanceListSupplierBuilder { return new HealthCheckServiceInstanceListSupplier(delegate, loadBalancerClientFactory, (serviceInstance, healthCheckPath) -> webClient.get() .uri(UriComponentsBuilder.fromUriString(getUri(serviceInstance, healthCheckPath)).build().toUri()) - .exchange() - .flatMap(clientResponse -> clientResponse.releaseBody() - .thenReturn(HttpStatus.OK.equals(clientResponse.statusCode())))); + .retrieve() + .toBodilessEntity() + .map(response -> HttpStatus.OK.equals(response.getStatusCode())) + .onErrorReturn(false)); } private ServiceInstanceListSupplier blockingHealthCheckServiceInstanceListSupplier(RestTemplate restTemplate, diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/blocking/XForwardedHeadersTransformerTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/blocking/XForwardedHeadersTransformerTests.java index 6899772b..183cd7d0 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/blocking/XForwardedHeadersTransformerTests.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/blocking/XForwardedHeadersTransformerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-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. @@ -65,9 +65,9 @@ class XForwardedHeadersTransformerTests { HttpRequest newRequest = transformer.transformRequest(request, serviceInstance); - assertThat(newRequest.getHeaders()).containsKey("X-Forwarded-Host"); + assertThat(newRequest.getHeaders().containsHeader("X-Forwarded-Host")).isTrue(); assertThat(newRequest.getHeaders().getFirst("X-Forwarded-Host")).isEqualTo("google.com"); - assertThat(newRequest.getHeaders()).containsKey("X-Forwarded-Proto"); + assertThat(newRequest.getHeaders().containsHeader("X-Forwarded-Proto")).isTrue(); assertThat(newRequest.getHeaders().getFirst("X-Forwarded-Proto")).isEqualTo("https"); } @@ -78,8 +78,8 @@ class XForwardedHeadersTransformerTests { XForwardedHeadersTransformer transformer = new XForwardedHeadersTransformer(loadBalancerClientFactory); HttpRequest newRequest = transformer.transformRequest(request, serviceInstance); - assertThat(newRequest.getHeaders()).doesNotContainKey("X-Forwarded-Host"); - assertThat(newRequest.getHeaders()).doesNotContainKey("X-Forwarded-Proto"); + assertThat(newRequest.getHeaders().containsHeader("X-Forwarded-Host")).isFalse(); + assertThat(newRequest.getHeaders().containsHeader("X-Forwarded-Proto")).isFalse(); } } diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplierTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplierTests.java index 1507fbf1..f5889529 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplierTests.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/HealthCheckServiceInstanceListSupplierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-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. @@ -703,7 +703,7 @@ class HealthCheckServiceInstanceListSupplierTests { } @Test - void shouldCheckUseProvidedPortForHealthCheckRequest() { + void shouldCheckUserProvidedPortForHealthCheckRequest() { Throwable exception = catchThrowable(() -> { String serviceId = "ignored-service"; properties.getHealthCheck().setPort(8888); @@ -718,7 +718,7 @@ class HealthCheckServiceInstanceListSupplierTests { listSupplier.isAlive(serviceInstance).block(); }); - assertThat(exception).hasMessageContaining("Connection refused: /127.0.0.1:888"); + assertThat(exception).hasMessageContaining("Connection refused: /127.0.0.1:8888"); } private static Stream healthCheckFunctions() { diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/LoadBalancerServiceInstanceCookieTransformerTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/LoadBalancerServiceInstanceCookieTransformerTests.java index a7d8a376..a10f922d 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/LoadBalancerServiceInstanceCookieTransformerTests.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/LoadBalancerServiceInstanceCookieTransformerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-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. @@ -74,7 +74,7 @@ class LoadBalancerServiceInstanceCookieTransformerTests { void shouldReturnPassedRequestWhenNoServiceInstance() { HttpRequest newRequest = transformer.transformRequest(request, null); - assertThat(newRequest.getHeaders()).doesNotContainKey(HttpHeaders.COOKIE); + assertThat(newRequest.getHeaders().containsHeader(HttpHeaders.COOKIE)).isFalse(); } @Test @@ -82,7 +82,7 @@ class LoadBalancerServiceInstanceCookieTransformerTests { properties.getStickySession().setInstanceIdCookieName(null); HttpRequest newRequest = transformer.transformRequest(request, serviceInstance); - assertThat(newRequest.getHeaders()).doesNotContainKey(HttpHeaders.COOKIE); + assertThat(newRequest.getHeaders().containsHeader(HttpHeaders.COOKIE)).isFalse(); } @Test @@ -90,7 +90,7 @@ class LoadBalancerServiceInstanceCookieTransformerTests { properties.getStickySession().setInstanceIdCookieName(""); HttpRequest newRequest = transformer.transformRequest(request, serviceInstance); - assertThat(newRequest.getHeaders()).doesNotContainKey(HttpHeaders.COOKIE); + assertThat(newRequest.getHeaders().containsHeader(HttpHeaders.COOKIE)).isFalse(); } diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplierTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplierTests.java index ad2aee6e..21500ca7 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplierTests.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/RequestBasedStickySessionServiceInstanceListSupplierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-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. @@ -17,6 +17,7 @@ package org.springframework.cloud.loadbalancer.core; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -31,7 +32,8 @@ import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; import org.springframework.cloud.client.loadbalancer.Request; import org.springframework.cloud.client.loadbalancer.RequestData; import org.springframework.cloud.client.loadbalancer.RequestDataContext; -import org.springframework.http.HttpHeaders; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.client.ClientRequest; import static org.assertj.core.api.Assertions.assertThat; @@ -72,8 +74,8 @@ class RequestBasedStickySessionServiceInstanceListSupplierTests { @Test void shouldReturnInstanceBasedOnCookieFromClientRequest() { - HttpHeaders headers = new HttpHeaders(); - headers.add(properties.getStickySession().getInstanceIdCookieName(), "test-1"); + MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.put(properties.getStickySession().getInstanceIdCookieName(), Collections.singletonList("test-1")); when(clientRequest.cookies()).thenReturn(headers); Request request = new DefaultRequest<>( new RequestDataContext(new RequestData(clientRequest))); @@ -86,8 +88,8 @@ class RequestBasedStickySessionServiceInstanceListSupplierTests { @Test void shouldReturnAllDelegateInstancesIfInstanceBasedOnCookieFromClientRequestNotFound() { - HttpHeaders headers = new HttpHeaders(); - headers.add(properties.getStickySession().getInstanceIdCookieName(), "test-4"); + MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.put(properties.getStickySession().getInstanceIdCookieName(), Collections.singletonList("test-4")); when(clientRequest.cookies()).thenReturn(headers); Request request = new DefaultRequest<>( new RequestDataContext(new RequestData(clientRequest))); @@ -99,7 +101,7 @@ class RequestBasedStickySessionServiceInstanceListSupplierTests { @Test void shouldReturnAllInstancesFromDelegateIfClientRequestHasNoCookie() { - when(clientRequest.cookies()).thenReturn(new HttpHeaders()); + when(clientRequest.cookies()).thenReturn(new LinkedMultiValueMap<>()); Request request = new DefaultRequest<>( new RequestDataContext(new RequestData(clientRequest))); diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSuppliersTestUtils.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSuppliersTestUtils.java index 8dceeee7..068ab9f1 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSuppliersTestUtils.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/ServiceInstanceListSuppliersTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * 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. @@ -26,6 +26,7 @@ import org.springframework.http.HttpStatus; import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientRequestException; import org.springframework.web.util.UriComponentsBuilder; import static org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplierBuilder.getUri; @@ -46,9 +47,10 @@ final class ServiceInstanceListSuppliersTestUtils { static BiFunction> healthCheckFunction(WebClient webClient) { return (serviceInstance, healthCheckPath) -> webClient.get() .uri(UriComponentsBuilder.fromUriString(getUri(serviceInstance, healthCheckPath)).build().toUri()) - .exchange() - .flatMap(clientResponse -> clientResponse.releaseBody() - .thenReturn(HttpStatus.OK.equals(clientResponse.statusCode()))); + .retrieve() + .toBodilessEntity() + .map(response -> HttpStatus.OK.equals(response.getStatusCode())) + .onErrorReturn(throwable -> !(throwable instanceof WebClientRequestException), false); } static BiFunction> healthCheckFunction(RestTemplate restTemplate) { diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/XForwardedHeadersTransformerTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/XForwardedHeadersTransformerTests.java index 3c6b500c..948e44f0 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/XForwardedHeadersTransformerTests.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/core/XForwardedHeadersTransformerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-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. @@ -66,9 +66,9 @@ class XForwardedHeadersTransformerTests { ClientRequest newRequest = transformer.transformRequest(request, serviceInstance); - assertThat(newRequest.headers()).containsKey("X-Forwarded-Host"); + assertThat(newRequest.headers().containsHeader("X-Forwarded-Host")).isTrue(); assertThat(newRequest.headers().getFirst("X-Forwarded-Host")).isEqualTo("spring.io"); - assertThat(newRequest.headers()).containsKey("X-Forwarded-Proto"); + assertThat(newRequest.headers().containsHeader("X-Forwarded-Proto")).isTrue(); assertThat(newRequest.headers().getFirst("X-Forwarded-Proto")).isEqualTo("https"); } @@ -79,8 +79,8 @@ class XForwardedHeadersTransformerTests { ClientRequest newRequest = transformer.transformRequest(request, serviceInstance); - assertThat(newRequest.headers()).doesNotContainKey("X-Forwarded-Host"); - assertThat(newRequest.headers()).doesNotContainKey("X-Forwarded-Proto"); + assertThat(newRequest.headers().containsHeader("X-Forwarded-Host")).isFalse(); + assertThat(newRequest.headers().containsHeader("X-Forwarded-Proto")).isFalse(); } } diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/stats/MicrometerStatsLoadBalancerLifecycleTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/stats/MicrometerStatsLoadBalancerLifecycleTests.java index c5d7bc5c..e6b14386 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/stats/MicrometerStatsLoadBalancerLifecycleTests.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/stats/MicrometerStatsLoadBalancerLifecycleTests.java @@ -44,6 +44,7 @@ import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalanc import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMapAdapter; import static org.assertj.core.api.Assertions.assertThat; @@ -71,7 +72,7 @@ class MicrometerStatsLoadBalancerLifecycleTests { @Test void shouldRecordSuccessfulTimedRequest() { RequestData requestData = new RequestData(HttpMethod.GET, URI.create("http://test.org/test"), new HttpHeaders(), - new HttpHeaders(), new HashMap<>()); + new LinkedMultiValueMap<>(), new HashMap<>()); Request lbRequest = new DefaultRequest<>(new RequestDataContext(requestData)); Response lbResponse = new DefaultResponse( new DefaultServiceInstance("test-1", "test", "test.org", 8080, false, new HashMap<>())); @@ -93,6 +94,7 @@ class MicrometerStatsLoadBalancerLifecycleTests { Tag.of("serviceInstance.port", "8080"), Tag.of("status", "200"), Tag.of("uri", "/test")); } + @SuppressWarnings("unchecked") @Test void shouldNotAddPathValueWhenDisabled() { ReactiveLoadBalancer.Factory factory = mock(ReactiveLoadBalancer.Factory.class); @@ -102,7 +104,7 @@ class MicrometerStatsLoadBalancerLifecycleTests { MicrometerStatsLoadBalancerLifecycle statsLifecycle = new MicrometerStatsLoadBalancerLifecycle(meterRegistry, factory); RequestData requestData = new RequestData(HttpMethod.GET, URI.create("http://test.org/test"), new HttpHeaders(), - new HttpHeaders(), new HashMap<>()); + new LinkedMultiValueMap<>(), new HashMap<>()); Request lbRequest = new DefaultRequest<>(new RequestDataContext(requestData)); Response lbResponse = new DefaultResponse( new DefaultServiceInstance("test-1", "test", "test.org", 8080, false, new HashMap<>())); @@ -126,7 +128,7 @@ class MicrometerStatsLoadBalancerLifecycleTests { String uriTemplate = "/test/{pathParam}/test"; attributes.put(attributeName, uriTemplate); RequestData requestData = new RequestData(HttpMethod.GET, URI.create("http://test.org/test/123/test"), - new HttpHeaders(), new HttpHeaders(), attributes); + new HttpHeaders(), new LinkedMultiValueMap<>(), attributes); Request lbRequest = new DefaultRequest<>(new RequestDataContext(requestData)); Response lbResponse = new DefaultResponse( new DefaultServiceInstance("test-1", "test", "test.org", 8080, false, new HashMap<>())); @@ -152,7 +154,7 @@ class MicrometerStatsLoadBalancerLifecycleTests { @Test void shouldRecordFailedTimedRequest() { RequestData requestData = new RequestData(HttpMethod.GET, URI.create("http://test.org/test"), new HttpHeaders(), - new HttpHeaders(), new HashMap<>()); + new LinkedMultiValueMap<>(), new HashMap<>()); Request lbRequest = new DefaultRequest<>(new RequestDataContext(requestData)); Response lbResponse = new DefaultResponse( new DefaultServiceInstance("test-1", "test", "test.org", 8080, false, new HashMap<>())); @@ -175,7 +177,7 @@ class MicrometerStatsLoadBalancerLifecycleTests { @Test void shouldNotRecordDiscardedRequest() { RequestData requestData = new RequestData(HttpMethod.GET, URI.create("http://test.org/test"), new HttpHeaders(), - new HttpHeaders(), new HashMap<>()); + new LinkedMultiValueMap<>(), new HashMap<>()); Request lbRequest = new DefaultRequest<>(new RequestDataContext(requestData)); Response lbResponse = new EmptyResponse(); statsLifecycle.onStartRequest(lbRequest, lbResponse); @@ -247,7 +249,7 @@ class MicrometerStatsLoadBalancerLifecycleTests { @Test void shouldHandleNullLoadBalancerResponse() { RequestData requestData = new RequestData(HttpMethod.GET, URI.create("http://test.org/test"), new HttpHeaders(), - new HttpHeaders(), new HashMap<>()); + new LinkedMultiValueMap<>(), new HashMap<>()); Request lbRequest = new DefaultRequest<>(new RequestDataContext(requestData)); assertThatCode(() -> { statsLifecycle.onStartRequest(lbRequest, null); diff --git a/spring-cloud-starter-bootstrap/pom.xml b/spring-cloud-starter-bootstrap/pom.xml index 3749612b..89ceeb8a 100644 --- a/spring-cloud-starter-bootstrap/pom.xml +++ b/spring-cloud-starter-bootstrap/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-commons-parent - 4.3.0-SNAPSHOT + 5.0.0-SNAPSHOT .. jar diff --git a/spring-cloud-starter-loadbalancer/pom.xml b/spring-cloud-starter-loadbalancer/pom.xml index 6caac92c..22b291de 100644 --- a/spring-cloud-starter-loadbalancer/pom.xml +++ b/spring-cloud-starter-loadbalancer/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-commons-parent - 4.3.0-SNAPSHOT + 5.0.0-SNAPSHOT .. 4.0.0 diff --git a/spring-cloud-starter/pom.xml b/spring-cloud-starter/pom.xml index 220b42ca..69236610 100644 --- a/spring-cloud-starter/pom.xml +++ b/spring-cloud-starter/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-commons-parent - 4.3.0-SNAPSHOT + 5.0.0-SNAPSHOT spring-cloud-starter spring-cloud-starter diff --git a/spring-cloud-test-support/pom.xml b/spring-cloud-test-support/pom.xml index 5bfaef1f..840733a1 100644 --- a/spring-cloud-test-support/pom.xml +++ b/spring-cloud-test-support/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-commons-parent - 4.3.0-SNAPSHOT + 5.0.0-SNAPSHOT .. spring-cloud-test-support From a2b379752a2d1e9ee425cb6bb52f1a8c56a338b3 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Thu, 15 May 2025 15:58:53 +0200 Subject: [PATCH 2/5] Add LoadBalancer integration for Spring Interface Clients AutoConfiguration (#1491) Add LoadBalancer integration for Spring Interface Clients AutoConfiguration. --------- Signed-off-by: Olga Maciaszek-Sharma --- .../spring-cloud-commons/loadbalancer.adoc | 34 +++- docs/modules/ROOT/partials/_configprops.adoc | 2 +- .../LoadBalancerAutoConfiguration.java | 12 +- .../loadbalancer/LoadBalancerProperties.java | 5 +- ...rRestClientHttpServiceGroupConfigurer.java | 95 +++++++++++ .../loadbalancer/LoadBalancerUriTools.java | 32 +++- ...cerBeanPostProcessorAutoConfiguration.java | 13 +- ...erWebClientHttpServiceGroupConfigurer.java | 96 +++++++++++ ...actLoadBalancerAutoConfigurationTests.java | 17 +- ...ClientHttpServiceGroupConfigurerTests.java | 149 +++++++++++++++++ .../LoadBalancerUriToolsTests.java | 31 +++- ...ClientHttpServiceGroupConfigurerTests.java | 150 ++++++++++++++++++ ...dBalancerClientAutoConfigurationTests.java | 24 ++- 13 files changed, 649 insertions(+), 11 deletions(-) create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurer.java create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurer.java create mode 100644 spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurerTests.java create mode 100644 spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurerTests.java diff --git a/docs/modules/ROOT/pages/spring-cloud-commons/loadbalancer.adoc b/docs/modules/ROOT/pages/spring-cloud-commons/loadbalancer.adoc index 6468a80f..d98704d7 100644 --- a/docs/modules/ROOT/pages/spring-cloud-commons/loadbalancer.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-commons/loadbalancer.adoc @@ -549,8 +549,40 @@ NOTE: For the properties where maps where already used, where you can specify a NOTE: Starting with `4.1.0`, we have introduced the `callGetWithRequestOnDelegates` flag in `LoadBalancerProperties`. If this flag is set to `true`, `ServiceInstanceListSupplier#get(Request request)` method will be implemented to call `delegate.get(request)` in classes assignable from `DelegatingServiceInstanceListSupplier` that don't already implement that method, with the exclusion of `CachingServiceInstanceListSupplier` and `HealthCheckServiceInstanceListSupplier`, which should be placed in the instance supplier hierarchy directly after the supplier performing instance retrieval over the network, before any request-based filtering is done. It is set to `true` by default. -[[-aot-and-native-image-support]] +[[aot-and-native-image-support]] == AOT and Native Image Support Since `4.0.0`, Spring Cloud LoadBalancer supports Spring AOT transformations and native images. However, to use this feature, you need to explicitly define your `LoadBalancerClient` service IDs. You can do so by using the `value` or `name` attributes of the `@LoadBalancerClient` annotation or as values of the `spring.cloud.loadbalancer.eager-load.clients` property. +== LoadBalancer Integration for Spring Interface Clients AutoConfiguration + +Since `5.0.0`, Spring Cloud LoadBalancer supports https://docs.spring.io/spring-framework/reference/7.0-SNAPSHOT/integration/rest-clients.html#rest-http-interface[Spring Interface Clients] AutoConfiguration through the `LoadBalancerRestClientHttpServiceGroupConfigurer` and `LoadBalancerWebClientHttpServiceGroupConfigurer`. + +For each Interface Client group, if the group `baseUrl` (defined under the +`spring.http.client.service.group.[groupName].base-url` property) is `null`, a `serviceId`-based URL for load-balancing is set up as the `baseUrl`, with `serviceId` resolved from the Interface Client `groupName`. + +If the group `baseUrl` is `null` or it is already a `serviceId`-based URL, a +`DeferringLoadBalancerInterceptor` instance is picked from the application context for blocking scenarios, and a `DeferringLoadBalancerExchangeFilterFunction` instance for reactive scenarios, and is added to the group's `RestClient.Builder` or `WebClient.Builder` if available, allowing for the requests to be load-balanced. + +For example, in an app with the following Interface Clients configuration: + +[source,java,indent=0] +---- +@SpringBootApplication +@ImportHttpServices(group = "verificationClient", types = {VerificationService.class, + PersonService.class}) +public class HttpVerificationClientApplication { + + public static void main(String[] args) { + SpringApplication.run(HttpVerificationClientApplication.class, args); + } +} +---- + +If the `spring.http.client.service.group.verificationClient.base-url` property is not set, it will be automatically set to `http://verificationClient`. The default scheme (`http`) is used initially; however, if a secure `ServiceInstance` is selected through load-balancing, it will be changed to `https`. + +If the `spring.http.client.service.group.verificationClient.base-url` property is set to a URL that uses the `groupName` / `serviceId` as the host (for example, `https://verificationClient/path`), it will be left unchanged. In both of these cases, either a `DeferringLoadBalancerInterceptor` or `DeferringLoadBalancerExchangeFilterFunction` will be added to the group's client builder, enabling the requests to be load-balanced. + +If the `spring.http.client.service.group.verificationClient.base-url` property is set to a URL that does not have `verificationClient` as the host (for example, `http://someOtherHost/path`), no load-balancer integration will be applied. + +TIP: To use a different default scheme instead of `http` (for example, `ws`, which would be converted to `wss` if a secure `ServiceInstance` is selected), set the `spring.http.client.service.group.verificationClient.base-url` property with the desired scheme. diff --git a/docs/modules/ROOT/partials/_configprops.adoc b/docs/modules/ROOT/partials/_configprops.adoc index e2abc13a..4837be4e 100644 --- a/docs/modules/ROOT/partials/_configprops.adoc +++ b/docs/modules/ROOT/partials/_configprops.adoc @@ -1,7 +1,7 @@ |=== |Name | Default | Description -|spring.cloud.compatibility-verifier.compatible-boot-versions | `+++3.5.x+++` | Default accepted versions for the Spring Boot dependency. You can set {@code x} for the patch version if you don't want to specify a concrete value. Example: {@code 3.5.x} +|spring.cloud.compatibility-verifier.compatible-boot-versions | `+++4.0.x+++` | Default accepted versions for the Spring Boot dependency. You can set {@code x} for the patch version if you don't want to specify a concrete value. Example: {@code 3.5.x} |spring.cloud.compatibility-verifier.enabled | `+++false+++` | Enables creation of Spring Cloud compatibility verification. |spring.cloud.config.allow-override | `+++true+++` | Flag to indicate that {@link #isOverrideSystemProperties() systemPropertiesOverride} can be used. Set to false to prevent users from changing the default accidentally. Default true. |spring.cloud.config.initialize-on-context-refresh | `+++false+++` | Flag to initialize bootstrap configuration on context refresh event. Default false. diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java index 6f7ed529..a682c4b0 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-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. @@ -30,6 +30,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.http.client.service.HttpClientServiceProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer; @@ -110,6 +111,15 @@ public class LoadBalancerAutoConfiguration { return new LoadBalancerRestTemplateBuilderBeanPostProcessor<>(loadBalancerInterceptorProvider, context); } + @Bean + @ConditionalOnBean({ HttpClientServiceProperties.class, ReactiveLoadBalancer.Factory.class }) + @ConditionalOnMissingBean(LoadBalancerRestClientHttpServiceGroupConfigurer.class) + LoadBalancerRestClientHttpServiceGroupConfigurer loadBalancerRestClientHttpServiceGroupConfigurer( + ObjectProvider loadBalancerInterceptorProvider, + HttpClientServiceProperties properties) { + return new LoadBalancerRestClientHttpServiceGroupConfigurer(loadBalancerInterceptorProvider, properties); + } + } @AutoConfiguration diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerProperties.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerProperties.java index 49a0d80a..4057de50 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerProperties.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-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. @@ -145,8 +145,7 @@ public class LoadBalancerProperties { this.hintHeaderName = hintHeaderName; } - // TODO: fix spelling in a major release - public void setxForwarded(XForwarded xForwarded) { + public void setXForwarded(XForwarded xForwarded) { this.xForwarded = xForwarded; } diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurer.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurer.java new file mode 100644 index 00000000..672c16bd --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurer.java @@ -0,0 +1,95 @@ +/* + * Copyright 2012-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.client.loadbalancer; + +import java.net.URI; + +import org.jspecify.annotations.NonNull; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.http.client.service.HttpClientServiceProperties; +import org.springframework.util.function.SingletonSupplier; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer; + +import static org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools.constructInterfaceClientsBaseUrl; +import static org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools.isServiceIdUrl; + +/** + * Load-balancer-specific {@link RestClientHttpServiceGroupConfigurer} implementation. If + * the group {@code baseUrl} is {@code null}, sets up a {@code baseUrl} with LoadBalancer + * {@code serviceId} -resolved from Interface Client {@code groupName} set as + * {@code host}. If the group {@code baseUrl} is {@code null} or + * {@link LoadBalancerUriTools#isServiceIdUrl(String, String)}, a + * {@link DeferringLoadBalancerInterceptor} instance picked from application context is + * added to the group's {@link RestClient.Builder} if available, allowing for the requests + * to be load-balanced. + * + * @author Olga Maciaszek-Sharma + * @since 5.0.0 + * @see RestClientHttpServiceGroupConfigurer + * @see HttpClientServiceProperties + */ +public class LoadBalancerRestClientHttpServiceGroupConfigurer implements RestClientHttpServiceGroupConfigurer { + + // Make sure Boot's configurers run before + private static final int ORDER = 10; + + private final SingletonSupplier loadBalancerInterceptorSupplier; + + private final HttpClientServiceProperties clientServiceProperties; + + public LoadBalancerRestClientHttpServiceGroupConfigurer( + ObjectProvider loadBalancerInterceptorProvider, + HttpClientServiceProperties clientServiceProperties) { + this.loadBalancerInterceptorSupplier = SingletonSupplier + .ofNullable(loadBalancerInterceptorProvider::getIfAvailable); + this.clientServiceProperties = clientServiceProperties; + } + + @Override + public void configureGroups(@NonNull Groups groups) { + DeferringLoadBalancerInterceptor loadBalancerInterceptor = loadBalancerInterceptorSupplier.get(); + if (loadBalancerInterceptor == null) { + throw new IllegalStateException( + DeferringLoadBalancerInterceptor.class.getSimpleName() + " bean not available."); + } + groups.configureClient((group, builder) -> { + String groupName = group.name(); + HttpClientServiceProperties.Group groupProperties = clientServiceProperties.getGroup().get(groupName); + if (groupProperties == null || groupProperties.getBaseUrl() == null) { + URI baseUrl = constructBaseUrl(groupName); + builder.baseUrl(baseUrl); + builder.requestInterceptor(loadBalancerInterceptor); + } + else if (isServiceIdUrl(groupProperties.getBaseUrl(), groupName)) { + builder.requestInterceptor(loadBalancerInterceptor); + } + }); + + } + + @Override + public int getOrder() { + return ORDER; + } + + private URI constructBaseUrl(String groupName) { + return constructInterfaceClientsBaseUrl(groupName); + } + +} diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriTools.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriTools.java index 7715875d..ace12bd1 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriTools.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriTools.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-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. @@ -17,11 +17,15 @@ package org.springframework.cloud.client.loadbalancer; import java.net.URI; +import java.net.URISyntaxException; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import org.springframework.cloud.client.ServiceInstance; import org.springframework.web.util.UriComponentsBuilder; @@ -35,6 +39,8 @@ public final class LoadBalancerUriTools { throw new IllegalStateException("Can't instantiate a utility class"); } + private static final Log LOG = LogFactory.getLog(LoadBalancerUriTools.class); + private static final String PERCENTAGE_SIGN = "%"; private static final String DEFAULT_SCHEME = "http"; @@ -115,4 +121,28 @@ public final class LoadBalancerUriTools { return originalOrDefault; } + public static URI constructInterfaceClientsBaseUrl(String groupName) { + return UriComponentsBuilder.newInstance().scheme(DEFAULT_SCHEME).host(groupName).encode().build().toUri(); + } + + public static boolean isServiceIdUrl(String baseUrlString, String serviceId) { + if (serviceId == null) { + return false; + } + if (baseUrlString == null) { + return false; + } + URI baseUrl; + try { + baseUrl = new URI(baseUrlString); + } + catch (URISyntaxException e) { + if (LOG.isErrorEnabled()) { + LOG.error("Incorrect baseUrl String syntax", e); + } + return false; + } + return serviceId.equals(baseUrl.getHost()); + } + } diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerBeanPostProcessorAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerBeanPostProcessorAutoConfiguration.java index f755f95d..58913842 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerBeanPostProcessorAutoConfiguration.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerBeanPostProcessorAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-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. @@ -21,6 +21,8 @@ import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.http.client.reactive.service.ReactiveHttpClientServiceProperties; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.context.ApplicationContext; @@ -66,6 +68,15 @@ public class LoadBalancerBeanPostProcessorAutoConfiguration { return new DeferringLoadBalancerExchangeFilterFunction<>(exchangeFilterFunctionProvider); } + @Bean + @ConditionalOnBean({ ReactiveHttpClientServiceProperties.class, ReactiveLoadBalancer.Factory.class }) + @ConditionalOnMissingBean(LoadBalancerWebClientHttpServiceGroupConfigurer.class) + LoadBalancerWebClientHttpServiceGroupConfigurer loadBalancerWebClientHttpServiceGroupConfigurer( + ObjectProvider> deferringExchangeFilterFunction, + ReactiveHttpClientServiceProperties properties) { + return new LoadBalancerWebClientHttpServiceGroupConfigurer(deferringExchangeFilterFunction, properties); + } + } static final class OnAnyLoadBalancerImplementationPresentCondition extends AnyNestedCondition { diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurer.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurer.java new file mode 100644 index 00000000..cdd51ae2 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurer.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-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.client.loadbalancer.reactive; + +import java.net.URI; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.http.client.reactive.service.ReactiveHttpClientServiceProperties; +import org.springframework.boot.autoconfigure.http.client.service.HttpClientServiceProperties; +import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools; +import org.springframework.util.function.SingletonSupplier; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.support.WebClientHttpServiceGroupConfigurer; + +import static org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools.constructInterfaceClientsBaseUrl; +import static org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools.isServiceIdUrl; + +/** + * Load-balancer-specific {@link WebClientHttpServiceGroupConfigurer} implementation. If + * the group {@code baseUrl} is {@code null}, sets up a {@code baseUrl} with LoadBalancer + * {@code serviceId} -resolved from Interface Client {@code groupName} set as + * {@code host}. If the group {@code baseUrl} is {@code null} or + * {@link LoadBalancerUriTools#isServiceIdUrl(String, String)}, a + * {@link DeferringLoadBalancerExchangeFilterFunction} instance picked from application + * context is added to the group's {@link WebClient.Builder} if available, allowing for + * the requests to be load-balanced. + * + * @author Olga Maciaszek-Sharma + * @since 5.0.0 + * @see WebClient.Builder + * @see HttpClientServiceProperties + */ +public class LoadBalancerWebClientHttpServiceGroupConfigurer implements WebClientHttpServiceGroupConfigurer { + + // Make sure Boot's configurers run before + private static final int ORDER = 10; + + private final SingletonSupplier> loadBalancerFilterFunctionSupplier; + + private final ReactiveHttpClientServiceProperties clientServiceProperties; + + public LoadBalancerWebClientHttpServiceGroupConfigurer( + ObjectProvider> exchangeFilterFunctionProvider, + ReactiveHttpClientServiceProperties clientServiceProperties) { + this.loadBalancerFilterFunctionSupplier = SingletonSupplier + .ofNullable(exchangeFilterFunctionProvider::getIfAvailable); + this.clientServiceProperties = clientServiceProperties; + } + + @Override + public void configureGroups(Groups groups) { + DeferringLoadBalancerExchangeFilterFunction loadBalancerFilterFunction = loadBalancerFilterFunctionSupplier + .get(); + if (loadBalancerFilterFunction == null) { + throw new IllegalStateException( + DeferringLoadBalancerExchangeFilterFunction.class.getSimpleName() + " bean not available."); + } + groups.configureClient((group, builder) -> { + String groupName = group.name(); + ReactiveHttpClientServiceProperties.Group groupProperties = clientServiceProperties.getGroup() + .get(groupName); + if (groupProperties == null || groupProperties.getBaseUrl() == null) { + URI baseUrl = constructBaseUrl(groupName); + builder.baseUrl(String.valueOf(baseUrl)); + builder.filter(loadBalancerFilterFunction); + } + else if (isServiceIdUrl(groupProperties.getBaseUrl(), groupName)) { + builder.filter(loadBalancerFilterFunction); + } + }); + } + + @Override + public int getOrder() { + return ORDER; + } + + private URI constructBaseUrl(String groupName) { + return constructInterfaceClientsBaseUrl(groupName); + } + +} diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java index 2a387edd..abbee725 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/AbstractLoadBalancerAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-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. @@ -25,9 +25,11 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.http.client.service.HttpClientServiceProperties; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerWebClientHttpServiceGroupConfigurer; import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -154,6 +156,14 @@ public abstract class AbstractLoadBalancerAutoConfigurationTests { }); } + @Test + void loadBalancerRestClientHttpServiceGroupConfigurerPresent() { + applicationContextRunner.withUserConfiguration(OneRestClientBuilder.class).run(context -> { + assertThat(context.getBeansOfType(LoadBalancerRestClientHttpServiceGroupConfigurer.class)).hasSize(1); + assertThat(context.getBeansOfType(LoadBalancerWebClientHttpServiceGroupConfigurer.class)).hasSize(0); + }); + } + protected abstract void assertLoadBalanced(RestClient.Builder restClientBuilder); protected abstract void assertLoadBalanced(RestTemplate restTemplate); @@ -180,6 +190,11 @@ public abstract class AbstractLoadBalancerAutoConfigurationTests { return RestClient.builder(); } + @Bean + HttpClientServiceProperties httpClientServiceProperties() { + return new HttpClientServiceProperties(); + } + } @Configuration(proxyBeanMethods = false) diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurerTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurerTests.java new file mode 100644 index 00000000..8d6ba31a --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurerTests.java @@ -0,0 +1,149 @@ +/* + * Copyright 2012-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.client.loadbalancer; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.http.client.service.HttpClientServiceProperties; +import org.springframework.boot.autoconfigure.http.client.service.HttpClientServiceProperties.Group; +import org.springframework.web.client.RestClient; +import org.springframework.web.service.invoker.HttpServiceProxyFactory; +import org.springframework.web.service.registry.HttpServiceGroup; +import org.springframework.web.service.registry.HttpServiceGroupConfigurer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link LoadBalancerRestClientHttpServiceGroupConfigurer} + * + * @author Olga Maciaszek-Sharma + */ +@SuppressWarnings({ "removal" }) +class LoadBalancerRestClientHttpServiceGroupConfigurerTests { + + private static final String GROUP_NAME = "testService"; + + private HttpClientServiceProperties clientServiceProperties; + + private ObjectProvider interceptorProvider; + + @BeforeEach + void setup() { + DeferringLoadBalancerInterceptor interceptor = mock(DeferringLoadBalancerInterceptor.class); + interceptorProvider = new SimpleObjectProvider<>(interceptor); + clientServiceProperties = new HttpClientServiceProperties(); + } + + @Test + void shouldAddInterceptorWhenBaseUrlIsNotSet() { + LoadBalancerRestClientHttpServiceGroupConfigurer configurer = new LoadBalancerRestClientHttpServiceGroupConfigurer( + interceptorProvider, clientServiceProperties); + TestGroups groups = new TestGroups(); + + configurer.configureGroups(groups); + + groups.builder.requestInterceptors(interceptors -> { + assertThat(interceptors).hasSize(1); + assertThat(interceptors.get(0).getClass()).isEqualTo(DeferringLoadBalancerInterceptor.class); + }); + } + + @Test + void shouldAddInterceptorWhenBaseUrlIsServiceIdUrl() { + Group group = new Group(); + group.setBaseUrl("https://" + GROUP_NAME + "/path"); + clientServiceProperties.getGroup().put(GROUP_NAME, group); + LoadBalancerRestClientHttpServiceGroupConfigurer configurer = new LoadBalancerRestClientHttpServiceGroupConfigurer( + interceptorProvider, clientServiceProperties); + TestGroups groups = new TestGroups(); + + configurer.configureGroups(groups); + + groups.builder.requestInterceptors(interceptors -> { + assertThat(interceptors).hasSize(1); + assertThat(interceptors.get(0).getClass()).isEqualTo(DeferringLoadBalancerInterceptor.class); + }); + } + + @Test + void shouldNotAddInterceptorWhenBaseUrlIsNotServiceIdUrl() { + Group group = new Group(); + group.setBaseUrl("https://some-other-service/path"); + clientServiceProperties.getGroup().put(GROUP_NAME, group); + LoadBalancerRestClientHttpServiceGroupConfigurer configurer = new LoadBalancerRestClientHttpServiceGroupConfigurer( + interceptorProvider, clientServiceProperties); + TestGroups groups = new TestGroups(); + + configurer.configureGroups(groups); + + groups.builder.requestInterceptors(interceptors -> assertThat(interceptors).hasSize(0)); + } + + private static class TestGroups implements HttpServiceGroupConfigurer.Groups { + + RestClient.Builder builder = RestClient.builder(); + + @Override + public HttpServiceGroupConfigurer.Groups filterByName(String... groupNames) { + throw new UnsupportedOperationException("Please, implement me."); + } + + @Override + public HttpServiceGroupConfigurer.Groups filter(Predicate predicate) { + throw new UnsupportedOperationException("Please, implement me."); + } + + @Override + public void configureClient(Consumer clientConfigurer) { + + } + + @Override + public void configureClient(BiConsumer clientConfigurer) { + clientConfigurer.accept(new TestGroup(GROUP_NAME, HttpServiceGroup.ClientType.REST_CLIENT, new HashSet<>()), + builder); + } + + @Override + public void configureProxyFactory( + BiConsumer proxyFactoryConfigurer) { + + } + + @Override + public void configure(BiConsumer clientConfigurer, + BiConsumer proxyFactoryConfigurer) { + + } + + } + + private record TestGroup(String name, ClientType clientType, + Set> httpServiceTypes) implements HttpServiceGroup { + + } + +} diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriToolsTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriToolsTests.java index 9a70c6c1..c700cfcc 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriToolsTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriToolsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-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. @@ -19,13 +19,19 @@ package org.springframework.cloud.client.loadbalancer; import java.net.URI; import java.util.LinkedHashMap; import java.util.Map; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.web.util.InvalidUrlException; import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for {@link LoadBalancerUriTools}. @@ -183,6 +189,29 @@ class LoadBalancerUriToolsTests { assertThat(reconstructed.getPort()).isEqualTo(serviceInstance.getPort()); } + @ParameterizedTest(name = "{index} => url={0}, serviceId={1}, expected={2}") + @MethodSource("provideUrlAndServiceIdForIsServiceIdUrl") + void verifyServiceIdUrl(String url, String serviceId, boolean expected) { + assertThat(LoadBalancerUriTools.isServiceIdUrl(url, serviceId)).isEqualTo(expected); + } + + @Test + void verifyServiceIdIncorrectUrl() { + assertThatExceptionOfType(InvalidUrlException.class).isThrownBy(() -> { + URI baseUrl = UriComponentsBuilder.fromUriString("https://:testService/xxx").build().toUri(); + + LoadBalancerUriTools.isServiceIdUrl(String.valueOf(baseUrl), null); + }); + + } + + private static Stream provideUrlAndServiceIdForIsServiceIdUrl() { + return Stream.of(org.junit.jupiter.params.provider.Arguments.of("https://testService/xxx", "testService", true), + org.junit.jupiter.params.provider.Arguments.of("https://test/xxx", "testService", false), + org.junit.jupiter.params.provider.Arguments.of("https://testService/xxx", null, false), + org.junit.jupiter.params.provider.Arguments.of(null, "testService", false)); + } + } class TestServiceInstance implements ServiceInstance { diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurerTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurerTests.java new file mode 100644 index 00000000..5916a7bd --- /dev/null +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurerTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2012-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.client.loadbalancer.reactive; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.http.client.reactive.service.ReactiveHttpClientServiceProperties; +import org.springframework.cloud.client.loadbalancer.SimpleObjectProvider; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.service.invoker.HttpServiceProxyFactory; +import org.springframework.web.service.registry.HttpServiceGroup; +import org.springframework.web.service.registry.HttpServiceGroupConfigurer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link LoadBalancerWebClientHttpServiceGroupConfigurer} + * + * @author Olga Maciaszek-Sharma + */ +@SuppressWarnings({ "unchecked", "removal" }) +class LoadBalancerWebClientHttpServiceGroupConfigurerTests { + + private static final String GROUP_NAME = "testService"; + + private ReactiveHttpClientServiceProperties clientServiceProperties; + + private ObjectProvider> exchangeFilterFunctionProvider; + + @BeforeEach + void setup() { + DeferringLoadBalancerExchangeFilterFunction exchangeFilterFunction = mock( + DeferringLoadBalancerExchangeFilterFunction.class); + exchangeFilterFunctionProvider = new SimpleObjectProvider<>(exchangeFilterFunction); + clientServiceProperties = new ReactiveHttpClientServiceProperties(); + } + + @Test + void shouldAddInterceptorWhenBaseUrlIsNotSet() { + LoadBalancerWebClientHttpServiceGroupConfigurer configurer = new LoadBalancerWebClientHttpServiceGroupConfigurer( + exchangeFilterFunctionProvider, clientServiceProperties); + TestGroups groups = new TestGroups(); + + configurer.configureGroups(groups); + + groups.builder.filters(filterFunctions -> { + assertThat(filterFunctions).hasSize(1); + assertThat(filterFunctions.get(0).getClass()).isEqualTo(DeferringLoadBalancerExchangeFilterFunction.class); + }); + } + + @Test + void shouldAddInterceptorWhenBaseUrlIsServiceIdUrl() { + ReactiveHttpClientServiceProperties.Group group = new ReactiveHttpClientServiceProperties.Group(); + group.setBaseUrl("https://" + GROUP_NAME + "/path"); + clientServiceProperties.getGroup().put(GROUP_NAME, group); + LoadBalancerWebClientHttpServiceGroupConfigurer configurer = new LoadBalancerWebClientHttpServiceGroupConfigurer( + exchangeFilterFunctionProvider, clientServiceProperties); + TestGroups groups = new TestGroups(); + + configurer.configureGroups(groups); + + groups.builder.filters(filterFunctions -> { + assertThat(filterFunctions).hasSize(1); + assertThat(filterFunctions.get(0).getClass()).isEqualTo(DeferringLoadBalancerExchangeFilterFunction.class); + }); + } + + @Test + void shouldNotAddInterceptorWhenBaseUrlIsNotServiceIdUrl() { + ReactiveHttpClientServiceProperties.Group group = new ReactiveHttpClientServiceProperties.Group(); + group.setBaseUrl("https://some-other-service/path"); + clientServiceProperties.getGroup().put(GROUP_NAME, group); + LoadBalancerWebClientHttpServiceGroupConfigurer configurer = new LoadBalancerWebClientHttpServiceGroupConfigurer( + exchangeFilterFunctionProvider, clientServiceProperties); + TestGroups groups = new TestGroups(); + + configurer.configureGroups(groups); + + groups.builder.filters(filterFunctions -> assertThat(filterFunctions).hasSize(0)); + } + + private static class TestGroups implements HttpServiceGroupConfigurer.Groups { + + WebClient.Builder builder = WebClient.builder(); + + @Override + public HttpServiceGroupConfigurer.Groups filterByName(String... groupNames) { + throw new UnsupportedOperationException("Please, implement me."); + } + + @Override + public HttpServiceGroupConfigurer.Groups filter(Predicate predicate) { + throw new UnsupportedOperationException("Please, implement me."); + } + + @Override + public void configureClient(Consumer clientConfigurer) { + + } + + @Override + public void configureClient(BiConsumer clientConfigurer) { + clientConfigurer.accept(new TestGroup(GROUP_NAME, HttpServiceGroup.ClientType.WEB_CLIENT, new HashSet<>()), + builder); + } + + @Override + public void configureProxyFactory( + BiConsumer proxyFactoryConfigurer) { + + } + + @Override + public void configure(BiConsumer clientConfigurer, + BiConsumer proxyFactoryConfigurer) { + + } + + } + + private record TestGroup(String name, ClientType clientType, + Set> httpServiceTypes) implements HttpServiceGroup { + + } + +} diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerClientAutoConfigurationTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerClientAutoConfigurationTests.java index 0d0f3c6c..1b5e8686 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerClientAutoConfigurationTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/ReactorLoadBalancerClientAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-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. @@ -23,19 +23,24 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.http.client.reactive.service.ReactiveHttpClientServiceProperties; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties; import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; +import org.springframework.cloud.client.loadbalancer.LoadBalancerRestClientHttpServiceGroupConfigurer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.web.reactive.function.client.WebClient; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.BDDAssertions.then; import static org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerTestUtils.assertLoadBalanced; import static org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerTestUtils.getFilters; @@ -150,6 +155,18 @@ public class ReactorLoadBalancerClientAutoConfigurationTests { then(clientProperties.getHealthCheck().getInterval()).isEqualTo(Duration.ofSeconds(30)); } + @Test + void loadBalancerRestClientHttpServiceGroupConfigurerPresent() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorLoadBalancerClientAutoConfiguration.class, + LoadBalancerBeanPostProcessorAutoConfiguration.class)) + .withUserConfiguration(OneWebClientBuilder.class) + .run(context -> { + assertThat(context.getBeansOfType(LoadBalancerWebClientHttpServiceGroupConfigurer.class)).hasSize(1); + assertThat(context.getBeansOfType(LoadBalancerRestClientHttpServiceGroupConfigurer.class)).hasSize(0); + }); + } + private ConfigurableApplicationContext init(Class config) { return LoadBalancerTestUtils.init(config, ReactorLoadBalancerClientAutoConfiguration.class, LoadBalancerBeanPostProcessorAutoConfiguration.class); @@ -211,6 +228,11 @@ public class ReactorLoadBalancerClientAutoConfigurationTests { return new TestService(loadBalancedWebClientBuilder()); } + @Bean + ReactiveHttpClientServiceProperties reactiveHttpClientServiceProperties() { + return new ReactiveHttpClientServiceProperties(); + } + private static final class TestService { public final WebClient webClient; From 165876830701b3258b3d33f4cff26cedc97f54fd Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Mon, 19 May 2025 17:53:07 +0200 Subject: [PATCH 3/5] Use `lb` scheme to find the baseUrls for load-balancing. (#1492) Signed-off-by: Olga Maciaszek-Sharma --- .../spring-cloud-commons/loadbalancer.adoc | 15 +++++---- ...rRestClientHttpServiceGroupConfigurer.java | 11 +++++-- .../loadbalancer/LoadBalancerUriTools.java | 33 +++---------------- ...erWebClientHttpServiceGroupConfigurer.java | 14 ++++++-- ...ClientHttpServiceGroupConfigurerTests.java | 8 ++--- .../LoadBalancerUriToolsTests.java | 29 ---------------- ...ClientHttpServiceGroupConfigurerTests.java | 8 ++--- 7 files changed, 40 insertions(+), 78 deletions(-) diff --git a/docs/modules/ROOT/pages/spring-cloud-commons/loadbalancer.adoc b/docs/modules/ROOT/pages/spring-cloud-commons/loadbalancer.adoc index d98704d7..56926206 100644 --- a/docs/modules/ROOT/pages/spring-cloud-commons/loadbalancer.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-commons/loadbalancer.adoc @@ -204,7 +204,7 @@ WARNING: If using any of the Service Discovery-backed suppliers, adding this hea from the Service Registry. TIP: The `HealthCheckServiceInstanceListSupplier` relies on having updated instances provided by a delegate flux. In the rare cases when you want to use a delegate that does not refresh the instances, even though the list of instances may change (such as the `DiscoveryClientServiceInstanceListSupplier` provided by us), you can set `spring.cloud.loadbalancer.health-check.refetch-instances` to `true` to have the instance list refreshed by the `HealthCheckServiceInstanceListSupplier`. You can then also adjust the refretch intervals by modifying the value of `spring.cloud.loadbalancer.health-check.refetch-instances-interval` and opt to disable the additional healthcheck repetitions by setting `spring.cloud.loadbalancer.health-check.repeat-health-check` to `false` as every instances refetch - will also trigger a healthcheck. +will also trigger a healthcheck. `HealthCheckServiceInstanceListSupplier` uses properties prefixed with `spring.cloud.loadbalancer.health-check`. You can set the `initialDelay` and `interval` @@ -561,7 +561,7 @@ Since `5.0.0`, Spring Cloud LoadBalancer supports https://docs.spring.io/spring- For each Interface Client group, if the group `baseUrl` (defined under the `spring.http.client.service.group.[groupName].base-url` property) is `null`, a `serviceId`-based URL for load-balancing is set up as the `baseUrl`, with `serviceId` resolved from the Interface Client `groupName`. -If the group `baseUrl` is `null` or it is already a `serviceId`-based URL, a +If the group `baseUrl` is `null` or its scheme is set to `lb`, a `DeferringLoadBalancerInterceptor` instance is picked from the application context for blocking scenarios, and a `DeferringLoadBalancerExchangeFilterFunction` instance for reactive scenarios, and is added to the group's `RestClient.Builder` or `WebClient.Builder` if available, allowing for the requests to be load-balanced. For example, in an app with the following Interface Clients configuration: @@ -579,10 +579,11 @@ public class HttpVerificationClientApplication { } ---- -If the `spring.http.client.service.group.verificationClient.base-url` property is not set, it will be automatically set to `http://verificationClient`. The default scheme (`http`) is used initially; however, if a secure `ServiceInstance` is selected through load-balancing, it will be changed to `https`. +If the `spring.http.client.service.group.verificationClient.base-url` property is not set, it will be automatically set to `http://verificationClient`. +The default scheme (`http`) is used initially; however, if a secure `ServiceInstance` is selected through load-balancing, it will be changed to `https`. -If the `spring.http.client.service.group.verificationClient.base-url` property is set to a URL that uses the `groupName` / `serviceId` as the host (for example, `https://verificationClient/path`), it will be left unchanged. In both of these cases, either a `DeferringLoadBalancerInterceptor` or `DeferringLoadBalancerExchangeFilterFunction` will be added to the group's client builder, enabling the requests to be load-balanced. +If the `spring.http.client.service.group.verificationClient.base-url` property is set to a URL that has the `lb` scheme, (for example, `lb://verificationClient/path`), it will be used with `http` being initially set as the default scheme. +If a secure `ServiceInstance` is selected through load-balancing, the scheme will be changed to `https`. +In both of these cases, either a `DeferringLoadBalancerInterceptor` or `DeferringLoadBalancerExchangeFilterFunction` will be added to the group's client builder, enabling the requests to be load-balanced. -If the `spring.http.client.service.group.verificationClient.base-url` property is set to a URL that does not have `verificationClient` as the host (for example, `http://someOtherHost/path`), no load-balancer integration will be applied. - -TIP: To use a different default scheme instead of `http` (for example, `ws`, which would be converted to `wss` if a secure `ServiceInstance` is selected), set the `spring.http.client.service.group.verificationClient.base-url` property with the desired scheme. +If the `spring.http.client.service.group.verificationClient.base-url` property is set to a URL that does not have the scheme set to `lb`, (for example, `lb://verificationClient/path`), no load-balancer integration will be applied. diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurer.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurer.java index 672c16bd..350bcca9 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurer.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurer.java @@ -25,9 +25,10 @@ import org.springframework.boot.autoconfigure.http.client.service.HttpClientServ import org.springframework.util.function.SingletonSupplier; import org.springframework.web.client.RestClient; import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer; +import org.springframework.web.util.UriComponentsBuilder; +import static org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools.DEFAULT_SCHEME; import static org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools.constructInterfaceClientsBaseUrl; -import static org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools.isServiceIdUrl; /** * Load-balancer-specific {@link RestClientHttpServiceGroupConfigurer} implementation. If @@ -71,12 +72,16 @@ public class LoadBalancerRestClientHttpServiceGroupConfigurer implements RestCli groups.configureClient((group, builder) -> { String groupName = group.name(); HttpClientServiceProperties.Group groupProperties = clientServiceProperties.getGroup().get(groupName); - if (groupProperties == null || groupProperties.getBaseUrl() == null) { + String baseUrlString = groupProperties == null ? null : groupProperties.getBaseUrl(); + URI existingBaseUrl = baseUrlString == null ? null : URI.create(baseUrlString); + if (existingBaseUrl == null) { URI baseUrl = constructBaseUrl(groupName); builder.baseUrl(baseUrl); builder.requestInterceptor(loadBalancerInterceptor); } - else if (isServiceIdUrl(groupProperties.getBaseUrl(), groupName)) { + else if ("lb".equalsIgnoreCase(existingBaseUrl.getScheme())) { + URI baseUrl = UriComponentsBuilder.fromUri(existingBaseUrl).scheme(DEFAULT_SCHEME).build().toUri(); + builder.baseUrl(baseUrl); builder.requestInterceptor(loadBalancerInterceptor); } }); diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriTools.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriTools.java index ace12bd1..d25cff78 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriTools.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriTools.java @@ -17,15 +17,11 @@ package org.springframework.cloud.client.loadbalancer; import java.net.URI; -import java.net.URISyntaxException; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - import org.springframework.cloud.client.ServiceInstance; import org.springframework.web.util.UriComponentsBuilder; @@ -39,16 +35,17 @@ public final class LoadBalancerUriTools { throw new IllegalStateException("Can't instantiate a utility class"); } - private static final Log LOG = LogFactory.getLog(LoadBalancerUriTools.class); - private static final String PERCENTAGE_SIGN = "%"; - private static final String DEFAULT_SCHEME = "http"; - private static final String DEFAULT_SECURE_SCHEME = "https"; private static final Map INSECURE_SCHEME_MAPPINGS; + /** + * Default scheme. + */ + public static final String DEFAULT_SCHEME = "http"; + static { INSECURE_SCHEME_MAPPINGS = new HashMap<>(); INSECURE_SCHEME_MAPPINGS.put(DEFAULT_SCHEME, DEFAULT_SECURE_SCHEME); @@ -125,24 +122,4 @@ public final class LoadBalancerUriTools { return UriComponentsBuilder.newInstance().scheme(DEFAULT_SCHEME).host(groupName).encode().build().toUri(); } - public static boolean isServiceIdUrl(String baseUrlString, String serviceId) { - if (serviceId == null) { - return false; - } - if (baseUrlString == null) { - return false; - } - URI baseUrl; - try { - baseUrl = new URI(baseUrlString); - } - catch (URISyntaxException e) { - if (LOG.isErrorEnabled()) { - LOG.error("Incorrect baseUrl String syntax", e); - } - return false; - } - return serviceId.equals(baseUrl.getHost()); - } - } diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurer.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurer.java index cdd51ae2..0d42aef8 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurer.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurer.java @@ -25,9 +25,10 @@ import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools; import org.springframework.util.function.SingletonSupplier; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.support.WebClientHttpServiceGroupConfigurer; +import org.springframework.web.util.UriComponentsBuilder; +import static org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools.DEFAULT_SCHEME; import static org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools.constructInterfaceClientsBaseUrl; -import static org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools.isServiceIdUrl; /** * Load-balancer-specific {@link WebClientHttpServiceGroupConfigurer} implementation. If @@ -73,12 +74,19 @@ public class LoadBalancerWebClientHttpServiceGroupConfigurer implements WebClien String groupName = group.name(); ReactiveHttpClientServiceProperties.Group groupProperties = clientServiceProperties.getGroup() .get(groupName); - if (groupProperties == null || groupProperties.getBaseUrl() == null) { + String baseUrlString = groupProperties == null ? null : groupProperties.getBaseUrl(); + URI existingBaseUrl = baseUrlString == null ? null : URI.create(baseUrlString); + if (existingBaseUrl == null) { URI baseUrl = constructBaseUrl(groupName); builder.baseUrl(String.valueOf(baseUrl)); builder.filter(loadBalancerFilterFunction); } - else if (isServiceIdUrl(groupProperties.getBaseUrl(), groupName)) { + else if ("lb".equalsIgnoreCase(existingBaseUrl.getScheme())) { + String baseUrl = UriComponentsBuilder.fromUri(existingBaseUrl) + .scheme(DEFAULT_SCHEME) + .build() + .toUriString(); + builder.baseUrl(baseUrl); builder.filter(loadBalancerFilterFunction); } }); diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurerTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurerTests.java index 8d6ba31a..35c0c3c9 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurerTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurerTests.java @@ -72,9 +72,9 @@ class LoadBalancerRestClientHttpServiceGroupConfigurerTests { } @Test - void shouldAddInterceptorWhenBaseUrlIsServiceIdUrl() { + void shouldAddInterceptorWhenBaseUrlHasLbScheme() { Group group = new Group(); - group.setBaseUrl("https://" + GROUP_NAME + "/path"); + group.setBaseUrl("lb://" + GROUP_NAME + "/path"); clientServiceProperties.getGroup().put(GROUP_NAME, group); LoadBalancerRestClientHttpServiceGroupConfigurer configurer = new LoadBalancerRestClientHttpServiceGroupConfigurer( interceptorProvider, clientServiceProperties); @@ -89,9 +89,9 @@ class LoadBalancerRestClientHttpServiceGroupConfigurerTests { } @Test - void shouldNotAddInterceptorWhenBaseUrlIsNotServiceIdUrl() { + void shouldNotAddInterceptorWhenBaseDoesNotHaveLbScheme() { Group group = new Group(); - group.setBaseUrl("https://some-other-service/path"); + group.setBaseUrl("http://" + GROUP_NAME + "/path"); clientServiceProperties.getGroup().put(GROUP_NAME, group); LoadBalancerRestClientHttpServiceGroupConfigurer configurer = new LoadBalancerRestClientHttpServiceGroupConfigurer( interceptorProvider, clientServiceProperties); diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriToolsTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriToolsTests.java index c700cfcc..be40f95a 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriToolsTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriToolsTests.java @@ -19,19 +19,13 @@ package org.springframework.cloud.client.loadbalancer; import java.net.URI; import java.util.LinkedHashMap; import java.util.Map; -import java.util.stream.Stream; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import org.springframework.cloud.client.ServiceInstance; -import org.springframework.web.util.InvalidUrlException; import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for {@link LoadBalancerUriTools}. @@ -189,29 +183,6 @@ class LoadBalancerUriToolsTests { assertThat(reconstructed.getPort()).isEqualTo(serviceInstance.getPort()); } - @ParameterizedTest(name = "{index} => url={0}, serviceId={1}, expected={2}") - @MethodSource("provideUrlAndServiceIdForIsServiceIdUrl") - void verifyServiceIdUrl(String url, String serviceId, boolean expected) { - assertThat(LoadBalancerUriTools.isServiceIdUrl(url, serviceId)).isEqualTo(expected); - } - - @Test - void verifyServiceIdIncorrectUrl() { - assertThatExceptionOfType(InvalidUrlException.class).isThrownBy(() -> { - URI baseUrl = UriComponentsBuilder.fromUriString("https://:testService/xxx").build().toUri(); - - LoadBalancerUriTools.isServiceIdUrl(String.valueOf(baseUrl), null); - }); - - } - - private static Stream provideUrlAndServiceIdForIsServiceIdUrl() { - return Stream.of(org.junit.jupiter.params.provider.Arguments.of("https://testService/xxx", "testService", true), - org.junit.jupiter.params.provider.Arguments.of("https://test/xxx", "testService", false), - org.junit.jupiter.params.provider.Arguments.of("https://testService/xxx", null, false), - org.junit.jupiter.params.provider.Arguments.of(null, "testService", false)); - } - } class TestServiceInstance implements ServiceInstance { diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurerTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurerTests.java index 5916a7bd..fdff5a92 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurerTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurerTests.java @@ -73,9 +73,9 @@ class LoadBalancerWebClientHttpServiceGroupConfigurerTests { } @Test - void shouldAddInterceptorWhenBaseUrlIsServiceIdUrl() { + void shouldAddInterceptorWhenBaseUrlHasLbScheme() { ReactiveHttpClientServiceProperties.Group group = new ReactiveHttpClientServiceProperties.Group(); - group.setBaseUrl("https://" + GROUP_NAME + "/path"); + group.setBaseUrl("lb://" + GROUP_NAME + "/path"); clientServiceProperties.getGroup().put(GROUP_NAME, group); LoadBalancerWebClientHttpServiceGroupConfigurer configurer = new LoadBalancerWebClientHttpServiceGroupConfigurer( exchangeFilterFunctionProvider, clientServiceProperties); @@ -90,9 +90,9 @@ class LoadBalancerWebClientHttpServiceGroupConfigurerTests { } @Test - void shouldNotAddInterceptorWhenBaseUrlIsNotServiceIdUrl() { + void shouldNotAddInterceptorWhenBaseDoesNotHaveLbScheme() { ReactiveHttpClientServiceProperties.Group group = new ReactiveHttpClientServiceProperties.Group(); - group.setBaseUrl("https://some-other-service/path"); + group.setBaseUrl("https://" + GROUP_NAME + "/path"); clientServiceProperties.getGroup().put(GROUP_NAME, group); LoadBalancerWebClientHttpServiceGroupConfigurer configurer = new LoadBalancerWebClientHttpServiceGroupConfigurer( exchangeFilterFunctionProvider, clientServiceProperties); From e764074482391c4a7f49c236cfd4e5c532a9bc88 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Tue, 20 May 2025 17:29:28 +0200 Subject: [PATCH 4/5] Adjust to changes in FW. Fix javadocs. Signed-off-by: Olga Maciaszek-Sharma --- ...rRestClientHttpServiceGroupConfigurer.java | 21 +++++++++---------- ...erWebClientHttpServiceGroupConfigurer.java | 20 ++++++++---------- ...ClientHttpServiceGroupConfigurerTests.java | 20 ++++++------------ ...ClientHttpServiceGroupConfigurerTests.java | 21 +++++++------------ 4 files changed, 32 insertions(+), 50 deletions(-) diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurer.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurer.java index 350bcca9..bcedc405 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurer.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurer.java @@ -34,11 +34,10 @@ import static org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools * Load-balancer-specific {@link RestClientHttpServiceGroupConfigurer} implementation. If * the group {@code baseUrl} is {@code null}, sets up a {@code baseUrl} with LoadBalancer * {@code serviceId} -resolved from Interface Client {@code groupName} set as - * {@code host}. If the group {@code baseUrl} is {@code null} or - * {@link LoadBalancerUriTools#isServiceIdUrl(String, String)}, a - * {@link DeferringLoadBalancerInterceptor} instance picked from application context is - * added to the group's {@link RestClient.Builder} if available, allowing for the requests - * to be load-balanced. + * {@code host}. If the group {@code baseUrl} is {@code null} or has {@code lb} set as its + * scheme, a {@link DeferringLoadBalancerInterceptor} instance picked from application + * context is added to the group's {@link RestClient.Builder} if available, allowing for + * the requests to be load-balanced. * * @author Olga Maciaszek-Sharma * @since 5.0.0 @@ -69,23 +68,23 @@ public class LoadBalancerRestClientHttpServiceGroupConfigurer implements RestCli throw new IllegalStateException( DeferringLoadBalancerInterceptor.class.getSimpleName() + " bean not available."); } - groups.configureClient((group, builder) -> { + + groups.forEachGroup((group, clientBuilder, factoryBuilder) -> { String groupName = group.name(); HttpClientServiceProperties.Group groupProperties = clientServiceProperties.getGroup().get(groupName); String baseUrlString = groupProperties == null ? null : groupProperties.getBaseUrl(); URI existingBaseUrl = baseUrlString == null ? null : URI.create(baseUrlString); if (existingBaseUrl == null) { URI baseUrl = constructBaseUrl(groupName); - builder.baseUrl(baseUrl); - builder.requestInterceptor(loadBalancerInterceptor); + clientBuilder.baseUrl(baseUrl); + clientBuilder.requestInterceptor(loadBalancerInterceptor); } else if ("lb".equalsIgnoreCase(existingBaseUrl.getScheme())) { URI baseUrl = UriComponentsBuilder.fromUri(existingBaseUrl).scheme(DEFAULT_SCHEME).build().toUri(); - builder.baseUrl(baseUrl); - builder.requestInterceptor(loadBalancerInterceptor); + clientBuilder.baseUrl(baseUrl); + clientBuilder.requestInterceptor(loadBalancerInterceptor); } }); - } @Override diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurer.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurer.java index 0d42aef8..bba085d9 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurer.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurer.java @@ -21,7 +21,6 @@ import java.net.URI; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.http.client.reactive.service.ReactiveHttpClientServiceProperties; import org.springframework.boot.autoconfigure.http.client.service.HttpClientServiceProperties; -import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools; import org.springframework.util.function.SingletonSupplier; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.support.WebClientHttpServiceGroupConfigurer; @@ -34,11 +33,10 @@ import static org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools * Load-balancer-specific {@link WebClientHttpServiceGroupConfigurer} implementation. If * the group {@code baseUrl} is {@code null}, sets up a {@code baseUrl} with LoadBalancer * {@code serviceId} -resolved from Interface Client {@code groupName} set as - * {@code host}. If the group {@code baseUrl} is {@code null} or - * {@link LoadBalancerUriTools#isServiceIdUrl(String, String)}, a - * {@link DeferringLoadBalancerExchangeFilterFunction} instance picked from application - * context is added to the group's {@link WebClient.Builder} if available, allowing for - * the requests to be load-balanced. + * {@code host}. If the group {@code baseUrl} is {@code null} or has {@code lb} set as its + * scheme, a {@link DeferringLoadBalancerExchangeFilterFunction} instance picked from + * application context is added to the group's {@link WebClient.Builder} if available, + * allowing for the requests to be load-balanced. * * @author Olga Maciaszek-Sharma * @since 5.0.0 @@ -70,7 +68,7 @@ public class LoadBalancerWebClientHttpServiceGroupConfigurer implements WebClien throw new IllegalStateException( DeferringLoadBalancerExchangeFilterFunction.class.getSimpleName() + " bean not available."); } - groups.configureClient((group, builder) -> { + groups.forEachGroup((group, clientBuilder, factoryBuilder) -> { String groupName = group.name(); ReactiveHttpClientServiceProperties.Group groupProperties = clientServiceProperties.getGroup() .get(groupName); @@ -78,16 +76,16 @@ public class LoadBalancerWebClientHttpServiceGroupConfigurer implements WebClien URI existingBaseUrl = baseUrlString == null ? null : URI.create(baseUrlString); if (existingBaseUrl == null) { URI baseUrl = constructBaseUrl(groupName); - builder.baseUrl(String.valueOf(baseUrl)); - builder.filter(loadBalancerFilterFunction); + clientBuilder.baseUrl(String.valueOf(baseUrl)); + clientBuilder.filter(loadBalancerFilterFunction); } else if ("lb".equalsIgnoreCase(existingBaseUrl.getScheme())) { String baseUrl = UriComponentsBuilder.fromUri(existingBaseUrl) .scheme(DEFAULT_SCHEME) .build() .toUriString(); - builder.baseUrl(baseUrl); - builder.filter(loadBalancerFilterFunction); + clientBuilder.baseUrl(baseUrl); + clientBuilder.filter(loadBalancerFilterFunction); } }); } diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurerTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurerTests.java index 35c0c3c9..a3c7a3b1 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurerTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurerTests.java @@ -18,8 +18,6 @@ package org.springframework.cloud.client.loadbalancer; import java.util.HashSet; import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.Consumer; import java.util.function.Predicate; import org.junit.jupiter.api.BeforeEach; @@ -117,26 +115,20 @@ class LoadBalancerRestClientHttpServiceGroupConfigurerTests { } @Override - public void configureClient(Consumer clientConfigurer) { + public void forEachClient(HttpServiceGroupConfigurer.ForClient configurer) { } @Override - public void configureClient(BiConsumer clientConfigurer) { - clientConfigurer.accept(new TestGroup(GROUP_NAME, HttpServiceGroup.ClientType.REST_CLIENT, new HashSet<>()), - builder); - } - - @Override - public void configureProxyFactory( - BiConsumer proxyFactoryConfigurer) { + public void forEachProxyFactory(HttpServiceGroupConfigurer.ForProxyFactory configurer) { } @Override - public void configure(BiConsumer clientConfigurer, - BiConsumer proxyFactoryConfigurer) { - + public void forEachGroup(HttpServiceGroupConfigurer.ForGroup groupConfigurer) { + groupConfigurer.configureGroup( + new TestGroup(GROUP_NAME, HttpServiceGroup.ClientType.REST_CLIENT, new HashSet<>()), builder, + HttpServiceProxyFactory.builder()); } } diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurerTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurerTests.java index fdff5a92..673ef8f9 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurerTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurerTests.java @@ -18,8 +18,6 @@ package org.springframework.cloud.client.loadbalancer.reactive; import java.util.HashSet; import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.Consumer; import java.util.function.Predicate; import org.junit.jupiter.api.BeforeEach; @@ -107,6 +105,7 @@ class LoadBalancerWebClientHttpServiceGroupConfigurerTests { WebClient.Builder builder = WebClient.builder(); + // @Override public HttpServiceGroupConfigurer.Groups filterByName(String... groupNames) { throw new UnsupportedOperationException("Please, implement me."); @@ -118,26 +117,20 @@ class LoadBalancerWebClientHttpServiceGroupConfigurerTests { } @Override - public void configureClient(Consumer clientConfigurer) { + public void forEachClient(HttpServiceGroupConfigurer.ForClient configurer) { } @Override - public void configureClient(BiConsumer clientConfigurer) { - clientConfigurer.accept(new TestGroup(GROUP_NAME, HttpServiceGroup.ClientType.WEB_CLIENT, new HashSet<>()), - builder); - } - - @Override - public void configureProxyFactory( - BiConsumer proxyFactoryConfigurer) { + public void forEachProxyFactory(HttpServiceGroupConfigurer.ForProxyFactory configurer) { } @Override - public void configure(BiConsumer clientConfigurer, - BiConsumer proxyFactoryConfigurer) { - + public void forEachGroup(HttpServiceGroupConfigurer.ForGroup groupConfigurer) { + groupConfigurer.configureGroup( + new TestGroup(GROUP_NAME, HttpServiceGroup.ClientType.WEB_CLIENT, new HashSet<>()), builder, + HttpServiceProxyFactory.builder()); } } From 1f78b895a1e754162eec5b0172fb722f1f338b5a Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Tue, 3 Jun 2025 16:42:43 +0200 Subject: [PATCH 5/5] Adjust tests to changes in Framework. Signed-off-by: Olga Maciaszek-Sharma --- ...BalancerRestClientHttpServiceGroupConfigurerTests.java | 8 ++++---- ...dBalancerWebClientHttpServiceGroupConfigurerTests.java | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurerTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurerTests.java index a3c7a3b1..affe5889 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurerTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurerTests.java @@ -115,18 +115,18 @@ class LoadBalancerRestClientHttpServiceGroupConfigurerTests { } @Override - public void forEachClient(HttpServiceGroupConfigurer.ForClient configurer) { + public void forEachClient(HttpServiceGroupConfigurer.ClientCallback configurer) { } @Override - public void forEachProxyFactory(HttpServiceGroupConfigurer.ForProxyFactory configurer) { + public void forEachProxyFactory(HttpServiceGroupConfigurer.ProxyFactoryCallback configurer) { } @Override - public void forEachGroup(HttpServiceGroupConfigurer.ForGroup groupConfigurer) { - groupConfigurer.configureGroup( + public void forEachGroup(HttpServiceGroupConfigurer.GroupCallback groupConfigurer) { + groupConfigurer.withGroup( new TestGroup(GROUP_NAME, HttpServiceGroup.ClientType.REST_CLIENT, new HashSet<>()), builder, HttpServiceProxyFactory.builder()); } diff --git a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurerTests.java b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurerTests.java index 673ef8f9..34e3e402 100644 --- a/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurerTests.java +++ b/spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurerTests.java @@ -117,18 +117,18 @@ class LoadBalancerWebClientHttpServiceGroupConfigurerTests { } @Override - public void forEachClient(HttpServiceGroupConfigurer.ForClient configurer) { + public void forEachClient(HttpServiceGroupConfigurer.ClientCallback configurer) { } @Override - public void forEachProxyFactory(HttpServiceGroupConfigurer.ForProxyFactory configurer) { + public void forEachProxyFactory(HttpServiceGroupConfigurer.ProxyFactoryCallback configurer) { } @Override - public void forEachGroup(HttpServiceGroupConfigurer.ForGroup groupConfigurer) { - groupConfigurer.configureGroup( + public void forEachGroup(HttpServiceGroupConfigurer.GroupCallback groupConfigurer) { + groupConfigurer.withGroup( new TestGroup(GROUP_NAME, HttpServiceGroup.ClientType.WEB_CLIENT, new HashSet<>()), builder, HttpServiceProxyFactory.builder()); }