Allow building @LoadBalanced RestClient in component constructor. (#1339)
This commit is contained in:
committed by
GitHub
parent
b5abbf6b55
commit
5b5bf5863d
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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 org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
|
||||
/**
|
||||
* A marker interface for {@link ClientHttpRequestInterceptor} instances used for
|
||||
* load-balancing.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 4.1.2
|
||||
*/
|
||||
public interface BlockingLoadBalancerInterceptor extends ClientHttpRequestInterceptor {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.io.IOException;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
|
||||
/**
|
||||
* An {@link ClientHttpRequestInterceptor} implementation that uses {@link ObjectProvider}
|
||||
* to resolve appropriate {@link BlockingLoadBalancerInterceptor} delegate when the
|
||||
* {@link ClientHttpRequestInterceptor#intercept(HttpRequest, byte[], ClientHttpRequestExecution)}
|
||||
* method is first called.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 4.1.2
|
||||
*/
|
||||
public class DeferringLoadBalancerInterceptor implements ClientHttpRequestInterceptor {
|
||||
|
||||
private final ObjectProvider<BlockingLoadBalancerInterceptor> loadBalancerInterceptorProvider;
|
||||
|
||||
private BlockingLoadBalancerInterceptor delegate;
|
||||
|
||||
public DeferringLoadBalancerInterceptor(
|
||||
ObjectProvider<BlockingLoadBalancerInterceptor> loadBalancerInterceptorProvider) {
|
||||
this.loadBalancerInterceptorProvider = loadBalancerInterceptorProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
|
||||
throws IOException {
|
||||
tryResolveDelegate();
|
||||
return delegate.intercept(request, body, execution);
|
||||
}
|
||||
|
||||
private void tryResolveDelegate() {
|
||||
if (delegate == null) {
|
||||
delegate = loadBalancerInterceptorProvider.getIfAvailable();
|
||||
if (delegate == null) {
|
||||
throw new IllegalStateException("LoadBalancer interceptor not available.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Visible for tests
|
||||
ObjectProvider<BlockingLoadBalancerInterceptor> getLoadBalancerInterceptorProvider() {
|
||||
return loadBalancerInterceptorProvider;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -33,12 +33,12 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
@@ -60,10 +60,6 @@ public class LoadBalancerAutoConfiguration {
|
||||
@Autowired(required = false)
|
||||
private List<RestTemplate> restTemplates = Collections.emptyList();
|
||||
|
||||
@LoadBalanced
|
||||
@Autowired(required = false)
|
||||
private List<RestClient.Builder> restClientBuilders = Collections.emptyList();
|
||||
|
||||
@Autowired(required = false)
|
||||
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
|
||||
|
||||
@@ -79,26 +75,33 @@ public class LoadBalancerAutoConfiguration {
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SmartInitializingSingleton loadBalancedRestClientBuilderInitializer(
|
||||
ObjectProvider<List<RestClientBuilderCustomizer>> restClientBuilderCustomizers) {
|
||||
return () -> restClientBuilderCustomizers.ifAvailable(customizers -> {
|
||||
for (RestClient.Builder restClientBuilder : restClientBuilders) {
|
||||
for (RestClientBuilderCustomizer customizer : customizers) {
|
||||
customizer.customize(restClientBuilder);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {
|
||||
return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@AutoConfiguration
|
||||
static class DeferringLoadBalancerInterceptorConfig {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public DeferringLoadBalancerInterceptor deferringLoadBalancerInterceptor(
|
||||
ObjectProvider<BlockingLoadBalancerInterceptor> loadBalancerInterceptorObjectProvider) {
|
||||
return new DeferringLoadBalancerInterceptor(loadBalancerInterceptorObjectProvider);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(DeferringLoadBalancerInterceptor.class)
|
||||
@ConditionalOnMissingBean
|
||||
LoadBalancerRestClientBuilderBeanPostProcessor lbRestClientPostProcessor(
|
||||
DeferringLoadBalancerInterceptor loadBalancerInterceptor, ApplicationContext context) {
|
||||
return new LoadBalancerRestClientBuilderBeanPostProcessor(loadBalancerInterceptor, context);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@AutoConfiguration
|
||||
@Conditional(RetryMissingOrDisabledCondition.class)
|
||||
static class LoadBalancerInterceptorConfig {
|
||||
|
||||
@@ -118,13 +121,6 @@ public class LoadBalancerAutoConfiguration {
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public RestClientBuilderCustomizer restClientBuilderCustomizer(
|
||||
LoadBalancerInterceptor loadBalancerInterceptor) {
|
||||
return restClientBuilder -> restClientBuilder.requestInterceptor(loadBalancerInterceptor);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class RetryMissingOrDisabledCondition extends AnyNestedCondition {
|
||||
@@ -148,7 +144,7 @@ public class LoadBalancerAutoConfiguration {
|
||||
/**
|
||||
* Auto configuration for retry mechanism.
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(RetryTemplate.class)
|
||||
public static class RetryAutoConfiguration {
|
||||
|
||||
@@ -189,13 +185,6 @@ public class LoadBalancerAutoConfiguration {
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public RestClientBuilderCustomizer restClientBuilderCustomizer(
|
||||
RetryLoadBalancerInterceptor loadBalancerInterceptor) {
|
||||
return restClientBuilder -> restClientBuilder.requestInterceptor(loadBalancerInterceptor);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import java.net.URI;
|
||||
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -31,7 +30,7 @@ import org.springframework.util.Assert;
|
||||
* @author Ryan Baxter
|
||||
* @author William Tran
|
||||
*/
|
||||
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
|
||||
public class LoadBalancerInterceptor implements BlockingLoadBalancerInterceptor {
|
||||
|
||||
private final LoadBalancerClient loadBalancer;
|
||||
|
||||
|
||||
@@ -28,9 +28,7 @@ import org.springframework.web.client.RestClient;
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 4.1.0
|
||||
* @deprecated to be removed in the next release.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public class LoadBalancerRestClientBuilderBeanPostProcessor implements BeanPostProcessor {
|
||||
|
||||
private final ClientHttpRequestInterceptor loadBalancerInterceptor;
|
||||
|
||||
@@ -25,7 +25,9 @@ import org.springframework.web.client.RestClient;
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 4.1.1
|
||||
* @deprecated to be removed in the next major release.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public interface RestClientBuilderCustomizer {
|
||||
|
||||
void customize(RestClient.Builder restClientBuilder);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -28,7 +28,6 @@ import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.retry.RetryListener;
|
||||
import org.springframework.retry.backoff.BackOffPolicy;
|
||||
@@ -45,7 +44,7 @@ import org.springframework.util.StreamUtils;
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public class RetryLoadBalancerInterceptor implements ClientHttpRequestInterceptor {
|
||||
public class RetryLoadBalancerInterceptor implements BlockingLoadBalancerInterceptor {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(RetryLoadBalancerInterceptor.class);
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Integration tests for load-balanced {@link RestClient}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@SpringBootTest(
|
||||
classes = { LoadBalancedRestClientIntegrationTests.TestConfig.class, LoadBalancerAutoConfiguration.class },
|
||||
properties = "spring.cloud.loadbalancer.retry.enabled=false")
|
||||
public class LoadBalancedRestClientIntegrationTests {
|
||||
|
||||
private final RestClient client;
|
||||
|
||||
@Autowired
|
||||
ApplicationContext context;
|
||||
|
||||
public LoadBalancedRestClientIntegrationTests(@Autowired RestClient.Builder clientBuilder) {
|
||||
this.client = clientBuilder.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBuildLoadBalancedRestClientInConstructor() {
|
||||
// Interceptors are not visible in RestClient
|
||||
assertThatThrownBy(() -> client.get().uri("http://test-service").retrieve())
|
||||
.hasMessage("LoadBalancerInterceptor invoked.");
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
static class TestConfig {
|
||||
|
||||
@LoadBalanced
|
||||
@Bean
|
||||
RestClient.Builder restClientBuilder() {
|
||||
return RestClient.builder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
LoadBalancerClient testLoadBalancerClient() {
|
||||
return new LoadBalancerClient() {
|
||||
@Override
|
||||
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
|
||||
throw new UnsupportedOperationException("LoadBalancerInterceptor invoked.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request)
|
||||
throws IOException {
|
||||
throw new UnsupportedOperationException("LoadBalancerInterceptor invoked.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI reconstructURI(ServiceInstance instance, URI original) {
|
||||
throw new UnsupportedOperationException("LoadBalancerInterceptor invoked.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServiceInstance choose(String serviceId) {
|
||||
throw new UnsupportedOperationException("LoadBalancerInterceptor invoked.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ServiceInstance choose(String serviceId, Request<T> request) {
|
||||
throw new UnsupportedOperationException("LoadBalancerInterceptor invoked.");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -45,7 +45,10 @@ public class LoadBalancerAutoConfigurationTests extends AbstractLoadBalancerAuto
|
||||
protected void assertLoadBalanced(RestClient.Builder restClientBuilder) {
|
||||
restClientBuilder.requestInterceptors(interceptors -> {
|
||||
assertThat(interceptors).hasSize(1);
|
||||
assertThat(interceptors.get(0)).isInstanceOf(LoadBalancerInterceptor.class);
|
||||
assertThat(interceptors.get(0)).isInstanceOf(DeferringLoadBalancerInterceptor.class);
|
||||
DeferringLoadBalancerInterceptor interceptor = (DeferringLoadBalancerInterceptor) interceptors.get(0);
|
||||
assertThat(interceptor.getLoadBalancerInterceptorProvider().getObject())
|
||||
.isInstanceOf(LoadBalancerInterceptor.class);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -46,7 +46,7 @@ public class RetryLoadBalancerAutoConfigurationTests extends AbstractLoadBalance
|
||||
protected void assertLoadBalanced(RestClient.Builder restClientBuilder) {
|
||||
restClientBuilder.requestInterceptors(interceptors -> {
|
||||
assertThat(interceptors).hasSize(1);
|
||||
assertThat(interceptors.get(0)).isInstanceOf(RetryLoadBalancerInterceptor.class);
|
||||
assertThat(interceptors.get(0)).isInstanceOf(DeferringLoadBalancerInterceptor.class);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -72,7 +72,11 @@ public class RetryLoadBalancerAutoConfigurationTests extends AbstractLoadBalance
|
||||
|
||||
restClientBuilder.requestInterceptors(interceptors -> {
|
||||
assertThat(interceptors).hasSize(1);
|
||||
assertThat(interceptors.get(0)).isInstanceOf(LoadBalancerInterceptor.class);
|
||||
assertThat(interceptors.get(0)).isInstanceOf(DeferringLoadBalancerInterceptor.class);
|
||||
DeferringLoadBalancerInterceptor interceptor = (DeferringLoadBalancerInterceptor) interceptors
|
||||
.get(0);
|
||||
assertThat(interceptor.getLoadBalancerInterceptorProvider().getObject())
|
||||
.isInstanceOf(LoadBalancerInterceptor.class);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user