Merge branch '5.0.x'

This commit is contained in:
spencergibb
2025-06-05 12:24:54 -04:00
38 changed files with 678 additions and 85 deletions

View File

@@ -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`
@@ -549,8 +549,41 @@ 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 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:
[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 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 the scheme set to `lb`, (for example, `lb://verificationClient/path`), no load-balancer integration will be applied.

View File

@@ -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.

View File

@@ -8,7 +8,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-parent</artifactId>
<version>4.3.1-SNAPSHOT</version>
<version>5.0.0-SNAPSHOT</version>
</parent>
<packaging>jar</packaging>
<name>Spring Cloud Commons Docs</name>

View File

@@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-parent</artifactId>
<version>4.3.1-SNAPSHOT</version>
<version>5.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Spring Cloud Commons Parent</name>
<description>Spring Cloud Commons Parent</description>
@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-build</artifactId>
<version>4.3.1-SNAPSHOT</version>
<version>5.0.0-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<scm>

View File

@@ -6,11 +6,11 @@
<parent>
<artifactId>spring-cloud-dependencies-parent</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>4.3.1-SNAPSHOT</version>
<version>5.0.0-SNAPSHOT</version>
<relativePath/>
</parent>
<artifactId>spring-cloud-commons-dependencies</artifactId>
<version>4.3.1-SNAPSHOT</version>
<version>5.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>spring-cloud-commons-dependencies</name>
<description>Spring Cloud Commons Dependencies</description>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-parent</artifactId>
<version>4.3.1-SNAPSHOT</version>
<version>5.0.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-commons</artifactId>

View File

@@ -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<DeferringLoadBalancerInterceptor> loadBalancerInterceptorProvider,
HttpClientServiceProperties properties) {
return new LoadBalancerRestClientHttpServiceGroupConfigurer(loadBalancerInterceptorProvider, properties);
}
}
@AutoConfiguration

View File

@@ -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;
}

View File

@@ -0,0 +1,99 @@
/*
* 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 org.springframework.web.util.UriComponentsBuilder;
import static org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools.DEFAULT_SCHEME;
import static org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools.constructInterfaceClientsBaseUrl;
/**
* 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 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
* @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<DeferringLoadBalancerInterceptor> loadBalancerInterceptorSupplier;
private final HttpClientServiceProperties clientServiceProperties;
public LoadBalancerRestClientHttpServiceGroupConfigurer(
ObjectProvider<DeferringLoadBalancerInterceptor> loadBalancerInterceptorProvider,
HttpClientServiceProperties clientServiceProperties) {
this.loadBalancerInterceptorSupplier = SingletonSupplier
.ofNullable(loadBalancerInterceptorProvider::getIfAvailable);
this.clientServiceProperties = clientServiceProperties;
}
@Override
public void configureGroups(@NonNull Groups<RestClient.Builder> groups) {
DeferringLoadBalancerInterceptor loadBalancerInterceptor = loadBalancerInterceptorSupplier.get();
if (loadBalancerInterceptor == null) {
throw new IllegalStateException(
DeferringLoadBalancerInterceptor.class.getSimpleName() + " bean not available.");
}
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);
clientBuilder.baseUrl(baseUrl);
clientBuilder.requestInterceptor(loadBalancerInterceptor);
}
else if ("lb".equalsIgnoreCase(existingBaseUrl.getScheme())) {
URI baseUrl = UriComponentsBuilder.fromUri(existingBaseUrl).scheme(DEFAULT_SCHEME).build().toUri();
clientBuilder.baseUrl(baseUrl);
clientBuilder.requestInterceptor(loadBalancerInterceptor);
}
});
}
@Override
public int getOrder() {
return ORDER;
}
private URI constructBaseUrl(String groupName) {
return constructInterfaceClientsBaseUrl(groupName);
}
}

View File

@@ -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.
@@ -37,12 +37,15 @@ public final class LoadBalancerUriTools {
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<String, String> 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);
@@ -115,4 +118,8 @@ public final class LoadBalancerUriTools {
return originalOrDefault;
}
public static URI constructInterfaceClientsBaseUrl(String groupName) {
return UriComponentsBuilder.newInstance().scheme(DEFAULT_SCHEME).host(groupName).encode().build().toUri();
}
}

View File

@@ -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<String, String> buildCookies(MultiValueMap<String, HttpCookie> cookies) {
HttpHeaders newCookies = new HttpHeaders();
MultiValueMap<String, String> 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<String, String> buildCookiesFromHeaders(HttpHeaders headers) {
HttpHeaders newCookies = new HttpHeaders();
MultiValueMap<String, String> newCookies = new LinkedMultiValueMap<>();
if (headers == null) {
return newCookies;
}

View File

@@ -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<DeferringLoadBalancerExchangeFilterFunction<LoadBalancedExchangeFilterFunction>> deferringExchangeFilterFunction,
ReactiveHttpClientServiceProperties properties) {
return new LoadBalancerWebClientHttpServiceGroupConfigurer(deferringExchangeFilterFunction, properties);
}
}
static final class OnAnyLoadBalancerImplementationPresentCondition extends AnyNestedCondition {

View File

@@ -0,0 +1,102 @@
/*
* 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.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;
/**
* 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 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
* @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<DeferringLoadBalancerExchangeFilterFunction<LoadBalancedExchangeFilterFunction>> loadBalancerFilterFunctionSupplier;
private final ReactiveHttpClientServiceProperties clientServiceProperties;
public LoadBalancerWebClientHttpServiceGroupConfigurer(
ObjectProvider<DeferringLoadBalancerExchangeFilterFunction<LoadBalancedExchangeFilterFunction>> exchangeFilterFunctionProvider,
ReactiveHttpClientServiceProperties clientServiceProperties) {
this.loadBalancerFilterFunctionSupplier = SingletonSupplier
.ofNullable(exchangeFilterFunctionProvider::getIfAvailable);
this.clientServiceProperties = clientServiceProperties;
}
@Override
public void configureGroups(Groups<WebClient.Builder> groups) {
DeferringLoadBalancerExchangeFilterFunction<LoadBalancedExchangeFilterFunction> loadBalancerFilterFunction = loadBalancerFilterFunctionSupplier
.get();
if (loadBalancerFilterFunction == null) {
throw new IllegalStateException(
DeferringLoadBalancerExchangeFilterFunction.class.getSimpleName() + " bean not available.");
}
groups.forEachGroup((group, clientBuilder, factoryBuilder) -> {
String groupName = group.name();
ReactiveHttpClientServiceProperties.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);
clientBuilder.baseUrl(String.valueOf(baseUrl));
clientBuilder.filter(loadBalancerFilterFunction);
}
else if ("lb".equalsIgnoreCase(existingBaseUrl.getScheme())) {
String baseUrl = UriComponentsBuilder.fromUri(existingBaseUrl)
.scheme(DEFAULT_SCHEME)
.build()
.toUriString();
clientBuilder.baseUrl(baseUrl);
clientBuilder.filter(loadBalancerFilterFunction);
}
});
}
@Override
public int getOrder() {
return ORDER;
}
private URI constructBaseUrl(String groupName) {
return constructInterfaceClientsBaseUrl(groupName);
}
}

View File

@@ -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<String> compatibleBootVersions = List.of("3.5.x");
private List<String> compatibleBootVersions = List.of("4.0.x");
public boolean isEnabled() {
return this.enabled;

View File

@@ -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<String, CompatibilityPredicate> 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

View File

@@ -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)

View File

@@ -0,0 +1,141 @@
/*
* 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.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<DeferringLoadBalancerInterceptor> 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 shouldAddInterceptorWhenBaseUrlHasLbScheme() {
Group group = new Group();
group.setBaseUrl("lb://" + 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 shouldNotAddInterceptorWhenBaseDoesNotHaveLbScheme() {
Group group = new Group();
group.setBaseUrl("http://" + 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(0));
}
private static class TestGroups implements HttpServiceGroupConfigurer.Groups<RestClient.Builder> {
RestClient.Builder builder = RestClient.builder();
@Override
public HttpServiceGroupConfigurer.Groups<RestClient.Builder> filterByName(String... groupNames) {
throw new UnsupportedOperationException("Please, implement me.");
}
@Override
public HttpServiceGroupConfigurer.Groups<RestClient.Builder> filter(Predicate<HttpServiceGroup> predicate) {
throw new UnsupportedOperationException("Please, implement me.");
}
@Override
public void forEachClient(HttpServiceGroupConfigurer.ClientCallback<RestClient.Builder> configurer) {
}
@Override
public void forEachProxyFactory(HttpServiceGroupConfigurer.ProxyFactoryCallback configurer) {
}
@Override
public void forEachGroup(HttpServiceGroupConfigurer.GroupCallback<RestClient.Builder> groupConfigurer) {
groupConfigurer.withGroup(
new TestGroup(GROUP_NAME, HttpServiceGroup.ClientType.REST_CLIENT, new HashSet<>()), builder,
HttpServiceProxyFactory.builder());
}
}
private record TestGroup(String name, ClientType clientType,
Set<Class<?>> httpServiceTypes) implements HttpServiceGroup {
}
}

View File

@@ -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.

View File

@@ -0,0 +1,143 @@
/*
* 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.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<DeferringLoadBalancerExchangeFilterFunction<LoadBalancedExchangeFilterFunction>> exchangeFilterFunctionProvider;
@BeforeEach
void setup() {
DeferringLoadBalancerExchangeFilterFunction<LoadBalancedExchangeFilterFunction> 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 shouldAddInterceptorWhenBaseUrlHasLbScheme() {
ReactiveHttpClientServiceProperties.Group group = new ReactiveHttpClientServiceProperties.Group();
group.setBaseUrl("lb://" + 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 shouldNotAddInterceptorWhenBaseDoesNotHaveLbScheme() {
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(0));
}
private static class TestGroups implements HttpServiceGroupConfigurer.Groups<WebClient.Builder> {
WebClient.Builder builder = WebClient.builder();
//
@Override
public HttpServiceGroupConfigurer.Groups<WebClient.Builder> filterByName(String... groupNames) {
throw new UnsupportedOperationException("Please, implement me.");
}
@Override
public HttpServiceGroupConfigurer.Groups<WebClient.Builder> filter(Predicate<HttpServiceGroup> predicate) {
throw new UnsupportedOperationException("Please, implement me.");
}
@Override
public void forEachClient(HttpServiceGroupConfigurer.ClientCallback<WebClient.Builder> configurer) {
}
@Override
public void forEachProxyFactory(HttpServiceGroupConfigurer.ProxyFactoryCallback configurer) {
}
@Override
public void forEachGroup(HttpServiceGroupConfigurer.GroupCallback<WebClient.Builder> groupConfigurer) {
groupConfigurer.withGroup(
new TestGroup(GROUP_NAME, HttpServiceGroup.ClientType.WEB_CLIENT, new HashSet<>()), builder,
HttpServiceProxyFactory.builder());
}
}
private record TestGroup(String name, ClientType clientType,
Set<Class<?>> httpServiceTypes) implements HttpServiceGroup {
}
}

View File

@@ -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;

View File

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

View File

@@ -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<String> acceptedVersions = Collections.singletonList("3.0");
List<String> 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<String> acceptedVersions = Collections.singletonList("3.0.x");
List<String> 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();

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-parent</artifactId>
<version>4.3.1-SNAPSHOT</version>
<version>5.0.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-context-integration-tests</artifactId>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-parent</artifactId>
<version>4.3.1-SNAPSHOT</version>
<version>5.0.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-context-webflux-integration-tests</artifactId>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-parent</artifactId>
<version>4.3.1-SNAPSHOT</version>
<version>5.0.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-context</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-parent</artifactId>
<version>4.3.1-SNAPSHOT</version>
<version>5.0.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-loadbalancer</artifactId>

View File

@@ -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,

View File

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

View File

@@ -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<Arguments> healthCheckFunctions() {

View File

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

View File

@@ -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<String, String> headers = new LinkedMultiValueMap<>();
headers.put(properties.getStickySession().getInstanceIdCookieName(), Collections.singletonList("test-1"));
when(clientRequest.cookies()).thenReturn(headers);
Request<RequestDataContext> 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<String, String> headers = new LinkedMultiValueMap<>();
headers.put(properties.getStickySession().getInstanceIdCookieName(), Collections.singletonList("test-4"));
when(clientRequest.cookies()).thenReturn(headers);
Request<RequestDataContext> 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<RequestDataContext> request = new DefaultRequest<>(
new RequestDataContext(new RequestData(clientRequest)));

View File

@@ -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<ServiceInstance, String, Mono<Boolean>> 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<ServiceInstance, String, Mono<Boolean>> healthCheckFunction(RestTemplate restTemplate) {

View File

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

View File

@@ -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<Object> lbRequest = new DefaultRequest<>(new RequestDataContext(requestData));
Response<ServiceInstance> 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<ServiceInstance> 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<Object> lbRequest = new DefaultRequest<>(new RequestDataContext(requestData));
Response<ServiceInstance> 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<Object> lbRequest = new DefaultRequest<>(new RequestDataContext(requestData));
Response<ServiceInstance> 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<Object> lbRequest = new DefaultRequest<>(new RequestDataContext(requestData));
Response<ServiceInstance> 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<Object> lbRequest = new DefaultRequest<>(new RequestDataContext(requestData));
Response<ServiceInstance> 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<Object> lbRequest = new DefaultRequest<>(new RequestDataContext(requestData));
assertThatCode(() -> {
statsLifecycle.onStartRequest(lbRequest, null);

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-parent</artifactId>
<version>4.3.1-SNAPSHOT</version>
<version>5.0.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<packaging>jar</packaging>

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-parent</artifactId>
<version>4.3.1-SNAPSHOT</version>
<version>5.0.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-parent</artifactId>
<version>4.3.1-SNAPSHOT</version>
<version>5.0.0-SNAPSHOT</version>
</parent>
<artifactId>spring-cloud-starter</artifactId>
<name>spring-cloud-starter</name>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons-parent</artifactId>
<version>4.3.1-SNAPSHOT</version>
<version>5.0.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-test-support</artifactId>