Skip to content

Commit 5b5bf58

Browse files
Allow building @LoadBalanced RestClient in component constructor. (#1339)
1 parent b5abbf6 commit 5b5bf58

10 files changed

+240
-46
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.client.loadbalancer;
18+
19+
import org.springframework.http.client.ClientHttpRequestInterceptor;
20+
21+
/**
22+
* A marker interface for {@link ClientHttpRequestInterceptor} instances used for
23+
* load-balancing.
24+
*
25+
* @author Olga Maciaszek-Sharma
26+
* @since 4.1.2
27+
*/
28+
public interface BlockingLoadBalancerInterceptor extends ClientHttpRequestInterceptor {
29+
30+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.client.loadbalancer;
18+
19+
import java.io.IOException;
20+
21+
import org.springframework.beans.factory.ObjectProvider;
22+
import org.springframework.http.HttpRequest;
23+
import org.springframework.http.client.ClientHttpRequestExecution;
24+
import org.springframework.http.client.ClientHttpRequestInterceptor;
25+
import org.springframework.http.client.ClientHttpResponse;
26+
27+
/**
28+
* An {@link ClientHttpRequestInterceptor} implementation that uses {@link ObjectProvider}
29+
* to resolve appropriate {@link BlockingLoadBalancerInterceptor} delegate when the
30+
* {@link ClientHttpRequestInterceptor#intercept(HttpRequest, byte[], ClientHttpRequestExecution)}
31+
* method is first called.
32+
*
33+
* @author Olga Maciaszek-Sharma
34+
* @since 4.1.2
35+
*/
36+
public class DeferringLoadBalancerInterceptor implements ClientHttpRequestInterceptor {
37+
38+
private final ObjectProvider<BlockingLoadBalancerInterceptor> loadBalancerInterceptorProvider;
39+
40+
private BlockingLoadBalancerInterceptor delegate;
41+
42+
public DeferringLoadBalancerInterceptor(
43+
ObjectProvider<BlockingLoadBalancerInterceptor> loadBalancerInterceptorProvider) {
44+
this.loadBalancerInterceptorProvider = loadBalancerInterceptorProvider;
45+
}
46+
47+
@Override
48+
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
49+
throws IOException {
50+
tryResolveDelegate();
51+
return delegate.intercept(request, body, execution);
52+
}
53+
54+
private void tryResolveDelegate() {
55+
if (delegate == null) {
56+
delegate = loadBalancerInterceptorProvider.getIfAvailable();
57+
if (delegate == null) {
58+
throw new IllegalStateException("LoadBalancer interceptor not available.");
59+
}
60+
}
61+
}
62+
63+
// Visible for tests
64+
ObjectProvider<BlockingLoadBalancerInterceptor> getLoadBalancerInterceptorProvider() {
65+
return loadBalancerInterceptorProvider;
66+
}
67+
68+
}

spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfiguration.java

Lines changed: 23 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@
3333
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3434
import org.springframework.cloud.client.ServiceInstance;
3535
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
36+
import org.springframework.context.ApplicationContext;
3637
import org.springframework.context.annotation.Bean;
3738
import org.springframework.context.annotation.Conditional;
3839
import org.springframework.context.annotation.Configuration;
3940
import org.springframework.http.client.ClientHttpRequestInterceptor;
4041
import org.springframework.retry.support.RetryTemplate;
41-
import org.springframework.web.client.RestClient;
4242
import org.springframework.web.client.RestTemplate;
4343

4444
/**
@@ -60,10 +60,6 @@ public class LoadBalancerAutoConfiguration {
6060
@Autowired(required = false)
6161
private List<RestTemplate> restTemplates = Collections.emptyList();
6262

63-
@LoadBalanced
64-
@Autowired(required = false)
65-
private List<RestClient.Builder> restClientBuilders = Collections.emptyList();
66-
6763
@Autowired(required = false)
6864
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
6965

@@ -79,26 +75,33 @@ public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
7975
});
8076
}
8177

82-
@Bean
83-
public SmartInitializingSingleton loadBalancedRestClientBuilderInitializer(
84-
ObjectProvider<List<RestClientBuilderCustomizer>> restClientBuilderCustomizers) {
85-
return () -> restClientBuilderCustomizers.ifAvailable(customizers -> {
86-
for (RestClient.Builder restClientBuilder : restClientBuilders) {
87-
for (RestClientBuilderCustomizer customizer : customizers) {
88-
customizer.customize(restClientBuilder);
89-
}
90-
91-
}
92-
});
93-
}
94-
9578
@Bean
9679
@ConditionalOnMissingBean
9780
public LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {
9881
return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
9982
}
10083

101-
@Configuration(proxyBeanMethods = false)
84+
@AutoConfiguration
85+
static class DeferringLoadBalancerInterceptorConfig {
86+
87+
@Bean
88+
@ConditionalOnMissingBean
89+
public DeferringLoadBalancerInterceptor deferringLoadBalancerInterceptor(
90+
ObjectProvider<BlockingLoadBalancerInterceptor> loadBalancerInterceptorObjectProvider) {
91+
return new DeferringLoadBalancerInterceptor(loadBalancerInterceptorObjectProvider);
92+
}
93+
94+
@Bean
95+
@ConditionalOnBean(DeferringLoadBalancerInterceptor.class)
96+
@ConditionalOnMissingBean
97+
LoadBalancerRestClientBuilderBeanPostProcessor lbRestClientPostProcessor(
98+
DeferringLoadBalancerInterceptor loadBalancerInterceptor, ApplicationContext context) {
99+
return new LoadBalancerRestClientBuilderBeanPostProcessor(loadBalancerInterceptor, context);
100+
}
101+
102+
}
103+
104+
@AutoConfiguration
102105
@Conditional(RetryMissingOrDisabledCondition.class)
103106
static class LoadBalancerInterceptorConfig {
104107

@@ -118,13 +121,6 @@ public RestTemplateCustomizer restTemplateCustomizer(LoadBalancerInterceptor loa
118121
};
119122
}
120123

121-
@Bean
122-
@ConditionalOnMissingBean
123-
public RestClientBuilderCustomizer restClientBuilderCustomizer(
124-
LoadBalancerInterceptor loadBalancerInterceptor) {
125-
return restClientBuilder -> restClientBuilder.requestInterceptor(loadBalancerInterceptor);
126-
}
127-
128124
}
129125

130126
private static class RetryMissingOrDisabledCondition extends AnyNestedCondition {
@@ -148,7 +144,7 @@ static class RetryDisabled {
148144
/**
149145
* Auto configuration for retry mechanism.
150146
*/
151-
@Configuration(proxyBeanMethods = false)
147+
@AutoConfiguration
152148
@ConditionalOnClass(RetryTemplate.class)
153149
public static class RetryAutoConfiguration {
154150

@@ -189,13 +185,6 @@ public RestTemplateCustomizer restTemplateCustomizer(RetryLoadBalancerIntercepto
189185
};
190186
}
191187

192-
@Bean
193-
@ConditionalOnMissingBean
194-
public RestClientBuilderCustomizer restClientBuilderCustomizer(
195-
RetryLoadBalancerInterceptor loadBalancerInterceptor) {
196-
return restClientBuilder -> restClientBuilder.requestInterceptor(loadBalancerInterceptor);
197-
}
198-
199188
}
200189

201190
}

spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerInterceptor.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
import org.springframework.http.HttpRequest;
2323
import org.springframework.http.client.ClientHttpRequestExecution;
24-
import org.springframework.http.client.ClientHttpRequestInterceptor;
2524
import org.springframework.http.client.ClientHttpResponse;
2625
import org.springframework.util.Assert;
2726

@@ -31,7 +30,7 @@
3130
* @author Ryan Baxter
3231
* @author William Tran
3332
*/
34-
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
33+
public class LoadBalancerInterceptor implements BlockingLoadBalancerInterceptor {
3534

3635
private final LoadBalancerClient loadBalancer;
3736

spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerRestClientBuilderBeanPostProcessor.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@
2828
*
2929
* @author Olga Maciaszek-Sharma
3030
* @since 4.1.0
31-
* @deprecated to be removed in the next release.
3231
*/
33-
@Deprecated(forRemoval = true)
3432
public class LoadBalancerRestClientBuilderBeanPostProcessor implements BeanPostProcessor {
3533

3634
private final ClientHttpRequestInterceptor loadBalancerInterceptor;

spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/RestClientBuilderCustomizer.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
*
2626
* @author Olga Maciaszek-Sharma
2727
* @since 4.1.1
28+
* @deprecated to be removed in the next major release.
2829
*/
30+
@Deprecated(forRemoval = true)
2931
public interface RestClientBuilderCustomizer {
3032

3133
void customize(RestClient.Builder restClientBuilder);

spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/RetryLoadBalancerInterceptor.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,7 +28,6 @@
2828
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
2929
import org.springframework.http.HttpRequest;
3030
import org.springframework.http.client.ClientHttpRequestExecution;
31-
import org.springframework.http.client.ClientHttpRequestInterceptor;
3231
import org.springframework.http.client.ClientHttpResponse;
3332
import org.springframework.retry.RetryListener;
3433
import org.springframework.retry.backoff.BackOffPolicy;
@@ -45,7 +44,7 @@
4544
* @author Olga Maciaszek-Sharma
4645
*/
4746
@SuppressWarnings({ "unchecked", "rawtypes" })
48-
public class RetryLoadBalancerInterceptor implements ClientHttpRequestInterceptor {
47+
public class RetryLoadBalancerInterceptor implements BlockingLoadBalancerInterceptor {
4948

5049
private static final Log LOG = LogFactory.getLog(RetryLoadBalancerInterceptor.class);
5150

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.client.loadbalancer;
18+
19+
import java.io.IOException;
20+
import java.net.URI;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
import org.springframework.beans.factory.annotation.Autowired;
25+
import org.springframework.boot.SpringBootConfiguration;
26+
import org.springframework.boot.test.context.SpringBootTest;
27+
import org.springframework.cloud.client.ServiceInstance;
28+
import org.springframework.context.ApplicationContext;
29+
import org.springframework.context.annotation.Bean;
30+
import org.springframework.web.client.RestClient;
31+
32+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
33+
34+
/**
35+
* Integration tests for load-balanced {@link RestClient}.
36+
*
37+
* @author Olga Maciaszek-Sharma
38+
*/
39+
@SpringBootTest(
40+
classes = { LoadBalancedRestClientIntegrationTests.TestConfig.class, LoadBalancerAutoConfiguration.class },
41+
properties = "spring.cloud.loadbalancer.retry.enabled=false")
42+
public class LoadBalancedRestClientIntegrationTests {
43+
44+
private final RestClient client;
45+
46+
@Autowired
47+
ApplicationContext context;
48+
49+
public LoadBalancedRestClientIntegrationTests(@Autowired RestClient.Builder clientBuilder) {
50+
this.client = clientBuilder.build();
51+
}
52+
53+
@Test
54+
void shouldBuildLoadBalancedRestClientInConstructor() {
55+
// Interceptors are not visible in RestClient
56+
assertThatThrownBy(() -> client.get().uri("http://test-service").retrieve())
57+
.hasMessage("LoadBalancerInterceptor invoked.");
58+
}
59+
60+
@SpringBootConfiguration
61+
static class TestConfig {
62+
63+
@LoadBalanced
64+
@Bean
65+
RestClient.Builder restClientBuilder() {
66+
return RestClient.builder();
67+
}
68+
69+
@Bean
70+
LoadBalancerClient testLoadBalancerClient() {
71+
return new LoadBalancerClient() {
72+
@Override
73+
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
74+
throw new UnsupportedOperationException("LoadBalancerInterceptor invoked.");
75+
}
76+
77+
@Override
78+
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request)
79+
throws IOException {
80+
throw new UnsupportedOperationException("LoadBalancerInterceptor invoked.");
81+
}
82+
83+
@Override
84+
public URI reconstructURI(ServiceInstance instance, URI original) {
85+
throw new UnsupportedOperationException("LoadBalancerInterceptor invoked.");
86+
}
87+
88+
@Override
89+
public ServiceInstance choose(String serviceId) {
90+
throw new UnsupportedOperationException("LoadBalancerInterceptor invoked.");
91+
}
92+
93+
@Override
94+
public <T> ServiceInstance choose(String serviceId, Request<T> request) {
95+
throw new UnsupportedOperationException("LoadBalancerInterceptor invoked.");
96+
}
97+
};
98+
}
99+
100+
}
101+
102+
}

spring-cloud-commons/src/test/java/org/springframework/cloud/client/loadbalancer/LoadBalancerAutoConfigurationTests.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -45,7 +45,10 @@ protected void assertLoadBalanced(RestTemplate restTemplate) {
4545
protected void assertLoadBalanced(RestClient.Builder restClientBuilder) {
4646
restClientBuilder.requestInterceptors(interceptors -> {
4747
assertThat(interceptors).hasSize(1);
48-
assertThat(interceptors.get(0)).isInstanceOf(LoadBalancerInterceptor.class);
48+
assertThat(interceptors.get(0)).isInstanceOf(DeferringLoadBalancerInterceptor.class);
49+
DeferringLoadBalancerInterceptor interceptor = (DeferringLoadBalancerInterceptor) interceptors.get(0);
50+
assertThat(interceptor.getLoadBalancerInterceptorProvider().getObject())
51+
.isInstanceOf(LoadBalancerInterceptor.class);
4952
});
5053
}
5154

0 commit comments

Comments
 (0)