Add LoadBalancer integration for Spring Interface Clients AutoConfiguration (#1491)
Add LoadBalancer integration for Spring Interface Clients AutoConfiguration. --------- Signed-off-by: Olga Maciaszek-Sharma <olga.maciaszek-sharma@broadcom.com>
This commit is contained in:
committed by
GitHub
parent
22bcf3759b
commit
a2b379752a
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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.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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<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.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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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<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 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> {
|
||||
|
||||
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 configureClient(Consumer<RestClient.Builder> clientConfigurer) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureClient(BiConsumer<HttpServiceGroup, RestClient.Builder> clientConfigurer) {
|
||||
clientConfigurer.accept(new TestGroup(GROUP_NAME, HttpServiceGroup.ClientType.REST_CLIENT, new HashSet<>()),
|
||||
builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureProxyFactory(
|
||||
BiConsumer<HttpServiceGroup, HttpServiceProxyFactory.Builder> proxyFactoryConfigurer) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(BiConsumer<HttpServiceGroup, RestClient.Builder> clientConfigurer,
|
||||
BiConsumer<HttpServiceGroup, HttpServiceProxyFactory.Builder> proxyFactoryConfigurer) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private record TestGroup(String name, ClientType clientType,
|
||||
Set<Class<?>> httpServiceTypes) implements HttpServiceGroup {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Arguments> 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 {
|
||||
|
||||
@@ -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<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 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> {
|
||||
|
||||
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 configureClient(Consumer<WebClient.Builder> clientConfigurer) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureClient(BiConsumer<HttpServiceGroup, WebClient.Builder> clientConfigurer) {
|
||||
clientConfigurer.accept(new TestGroup(GROUP_NAME, HttpServiceGroup.ClientType.WEB_CLIENT, new HashSet<>()),
|
||||
builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureProxyFactory(
|
||||
BiConsumer<HttpServiceGroup, HttpServiceProxyFactory.Builder> proxyFactoryConfigurer) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(BiConsumer<HttpServiceGroup, WebClient.Builder> clientConfigurer,
|
||||
BiConsumer<HttpServiceGroup, HttpServiceProxyFactory.Builder> proxyFactoryConfigurer) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private record TestGroup(String name, ClientType clientType,
|
||||
Set<Class<?>> httpServiceTypes) implements HttpServiceGroup {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user