Add LoadBalancerWebClientHttpServiceGroupConfigurer. Refactor. Add javadocs.

Signed-off-by: Olga Maciaszek-Sharma <olga.maciaszek-sharma@broadcom.com>
This commit is contained in:
Olga Maciaszek-Sharma
2025-05-08 15:56:01 +02:00
parent b6523b8a77
commit 8327b53479
5 changed files with 171 additions and 28 deletions

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.

View File

@@ -17,7 +17,6 @@
package org.springframework.cloud.client.loadbalancer;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
@@ -27,17 +26,33 @@ import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.http.client.service.HttpClientServiceProperties;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.http.client.ClientHttpRequestInterceptor;
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.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 customisers run before
private static final int ORDER = 10;
private static final Log LOG = LogFactory.getLog(LoadBalancerRestClientHttpServiceGroupConfigurer.class);
private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerClientFactory;
@@ -60,7 +75,8 @@ public class LoadBalancerRestClientHttpServiceGroupConfigurer implements RestCli
public void configureGroups(@NonNull Groups<RestClient.Builder> groups) {
DeferringLoadBalancerInterceptor loadBalancerInterceptor = loadBalancerInterceptorSupplier.get();
if (loadBalancerInterceptor == null) {
throw new IllegalStateException(ClientHttpRequestInterceptor.class.getSimpleName() + " not available.");
throw new IllegalStateException(
DeferringLoadBalancerInterceptor.class.getSimpleName() + " instance not available.");
}
groups.configureClient((group, builder) -> {
String groupName = group.name();
@@ -77,31 +93,15 @@ public class LoadBalancerRestClientHttpServiceGroupConfigurer implements RestCli
}
private boolean isServiceIdUrl(String baseUrlString, String groupName) {
if (baseUrlString == null) {
return false;
}
URI baseUrl;
try {
baseUrl = new URI(baseUrlString);
}
catch (URISyntaxException e) {
if (LOG.isErrorEnabled()) {
LOG.error("Incorrect baseUrlString syntax", e);
}
return false;
}
return groupName.equals(baseUrl.getHost());
@Override
public int getOrder() {
return ORDER;
}
private URI constructBaseUrl(String groupName) {
LoadBalancerProperties loadBalancerProperties = loadBalancerClientFactory.getProperties(groupName);
return UriComponentsBuilder.newInstance()
.scheme(loadBalancerProperties.getInterfaceClients().getDefaultScheme())
.host(groupName)
.encode()
.build()
.toUri();
return constructInterfaceClientsBaseUrl(groupName,
loadBalancerProperties.getInterfaceClients().getDefaultScheme());
}
}

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.
@@ -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,25 @@ public final class LoadBalancerUriTools {
return originalOrDefault;
}
public static URI constructInterfaceClientsBaseUrl(String groupName, String defaultScheme) {
return UriComponentsBuilder.newInstance().scheme(defaultScheme).host(groupName).encode().build().toUri();
}
public static boolean isServiceIdUrl(String baseUrlString, String serviceId) {
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());
}
}

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,9 @@ 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.service.HttpClientServiceProperties;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.context.ApplicationContext;
@@ -66,6 +69,17 @@ public class LoadBalancerBeanPostProcessorAutoConfiguration {
return new DeferringLoadBalancerExchangeFilterFunction<>(exchangeFilterFunctionProvider);
}
@Bean
@ConditionalOnBean({ HttpClientServiceProperties.class, ReactiveLoadBalancer.Factory.class })
@ConditionalOnMissingBean(LoadBalancerWebClientHttpServiceGroupConfigurer.class)
LoadBalancerWebClientHttpServiceGroupConfigurer loadBalancerRestClientHttpServiceGroupConfigurer(
ObjectProvider<DeferringLoadBalancerExchangeFilterFunction<LoadBalancedExchangeFilterFunction>> deferringExchangeFilterFunction,
HttpClientServiceProperties properties,
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory) {
return new LoadBalancerWebClientHttpServiceGroupConfigurer(deferringExchangeFilterFunction, properties,
loadBalancerFactory);
}
}
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.service.HttpClientServiceProperties;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
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 customisers run before
private static final int ORDER = 10;
private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerClientFactory;
private final SingletonSupplier<DeferringLoadBalancerExchangeFilterFunction<LoadBalancedExchangeFilterFunction>> loadBalancerFilterFunctionSupplier;
private final HttpClientServiceProperties clientServiceProperties;
public LoadBalancerWebClientHttpServiceGroupConfigurer(
ObjectProvider<DeferringLoadBalancerExchangeFilterFunction<LoadBalancedExchangeFilterFunction>> exchangeFilterFunctionProvider,
HttpClientServiceProperties clientServiceProperties,
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerClientFactory) {
this.loadBalancerFilterFunctionSupplier = SingletonSupplier
.ofNullable(exchangeFilterFunctionProvider.getIfAvailable());
this.clientServiceProperties = clientServiceProperties;
this.loadBalancerClientFactory = loadBalancerClientFactory;
}
@Override
public void configureGroups(Groups<WebClient.Builder> groups) {
DeferringLoadBalancerExchangeFilterFunction<LoadBalancedExchangeFilterFunction> loadBalancerFilterFunction = loadBalancerFilterFunctionSupplier
.get();
if (loadBalancerFilterFunction == null) {
throw new IllegalStateException(
DeferringLoadBalancerExchangeFilterFunction.class.getSimpleName() + " instance 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(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) {
LoadBalancerProperties loadBalancerProperties = loadBalancerClientFactory.getProperties(groupName);
return constructInterfaceClientsBaseUrl(groupName,
loadBalancerProperties.getInterfaceClients().getDefaultScheme());
}
}