From 8327b534790e65c1190e6b3345f12e36fe6d8357 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Thu, 8 May 2025 15:56:01 +0200 Subject: [PATCH] Add LoadBalancerWebClientHttpServiceGroupConfigurer. Refactor. Add javadocs. Signed-off-by: Olga Maciaszek-Sharma --- .../LoadBalancerAutoConfiguration.java | 2 +- ...rRestClientHttpServiceGroupConfigurer.java | 50 ++++----- .../loadbalancer/LoadBalancerUriTools.java | 29 ++++- ...cerBeanPostProcessorAutoConfiguration.java | 16 ++- ...erWebClientHttpServiceGroupConfigurer.java | 102 ++++++++++++++++++ 5 files changed, 171 insertions(+), 28 deletions(-) create mode 100644 spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurer.java diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java index 68f78637..35ea42c4 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurer.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurer.java index 81120375..bc5f0cbe 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurer.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientHttpServiceGroupConfigurer.java @@ -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 loadBalancerClientFactory; @@ -60,7 +75,8 @@ public class LoadBalancerRestClientHttpServiceGroupConfigurer implements RestCli public void configureGroups(@NonNull Groups 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()); } } diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriTools.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriTools.java index 7715875d..22a48f48 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriTools.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUriTools.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,11 +17,15 @@ package org.springframework.cloud.client.loadbalancer; import java.net.URI; +import java.net.URISyntaxException; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import org.springframework.cloud.client.ServiceInstance; import org.springframework.web.util.UriComponentsBuilder; @@ -35,6 +39,8 @@ public final class LoadBalancerUriTools { throw new IllegalStateException("Can't instantiate a utility class"); } + private static final Log LOG = LogFactory.getLog(LoadBalancerUriTools.class); + private static final String PERCENTAGE_SIGN = "%"; private static final String DEFAULT_SCHEME = "http"; @@ -115,4 +121,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()); + } + } diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerBeanPostProcessorAutoConfiguration.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerBeanPostProcessorAutoConfiguration.java index f755f95d..2ed36074 100644 --- a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerBeanPostProcessorAutoConfiguration.java +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerBeanPostProcessorAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,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> deferringExchangeFilterFunction, + HttpClientServiceProperties properties, + ReactiveLoadBalancer.Factory loadBalancerFactory) { + return new LoadBalancerWebClientHttpServiceGroupConfigurer(deferringExchangeFilterFunction, properties, + loadBalancerFactory); + } + } static final class OnAnyLoadBalancerImplementationPresentCondition extends AnyNestedCondition { diff --git a/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurer.java b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurer.java new file mode 100644 index 00000000..d7f24075 --- /dev/null +++ b/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/LoadBalancerWebClientHttpServiceGroupConfigurer.java @@ -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 loadBalancerClientFactory; + + private final SingletonSupplier> loadBalancerFilterFunctionSupplier; + + private final HttpClientServiceProperties clientServiceProperties; + + public LoadBalancerWebClientHttpServiceGroupConfigurer( + ObjectProvider> exchangeFilterFunctionProvider, + HttpClientServiceProperties clientServiceProperties, + ReactiveLoadBalancer.Factory loadBalancerClientFactory) { + this.loadBalancerFilterFunctionSupplier = SingletonSupplier + .ofNullable(exchangeFilterFunctionProvider.getIfAvailable()); + this.clientServiceProperties = clientServiceProperties; + this.loadBalancerClientFactory = loadBalancerClientFactory; + } + + @Override + public void configureGroups(Groups groups) { + DeferringLoadBalancerExchangeFilterFunction 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()); + } + +}