Skip to content

Commit 0ff70af

Browse files
committed
Add ServiceAddressResolver class to support LoadBalanced RestTemplate direct access to services via IP address and port (#1601)
1 parent a0c5768 commit 0ff70af

File tree

8 files changed

+202
-28
lines changed

8 files changed

+202
-28
lines changed

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,14 @@ static class LoadBalancerInterceptorConfig {
118118

119119
@Bean
120120
public LoadBalancerInterceptor loadBalancerInterceptor(LoadBalancerClient loadBalancerClient,
121-
LoadBalancerRequestFactory requestFactory) {
122-
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
121+
LoadBalancerRequestFactory requestFactory, ServiceAddressResolver serviceAddressResolver) {
122+
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory, serviceAddressResolver);
123+
}
124+
125+
@Bean
126+
@ConditionalOnMissingBean
127+
public ServiceAddressResolver serviceAddressResolver() {
128+
return new ServiceAddressResolver();
123129
}
124130

125131
@Bean
@@ -181,9 +187,15 @@ public static class RetryInterceptorAutoConfiguration {
181187
@ConditionalOnMissingBean
182188
public RetryLoadBalancerInterceptor loadBalancerInterceptor(LoadBalancerClient loadBalancerClient,
183189
LoadBalancerRequestFactory requestFactory, LoadBalancedRetryFactory loadBalancedRetryFactory,
184-
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory) {
190+
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory, ServiceAddressResolver serviceAddressResolver) {
185191
return new RetryLoadBalancerInterceptor(loadBalancerClient, requestFactory, loadBalancedRetryFactory,
186-
loadBalancerFactory);
192+
loadBalancerFactory, serviceAddressResolver);
193+
}
194+
195+
@Bean
196+
@ConditionalOnMissingBean
197+
public ServiceAddressResolver serviceAddressResolver() {
198+
return new ServiceAddressResolver();
187199
}
188200

189201
@Bean

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,25 @@ public class LoadBalancerInterceptor implements BlockingLoadBalancerInterceptor
3535
private final LoadBalancerClient loadBalancer;
3636

3737
private final LoadBalancerRequestFactory requestFactory;
38+
private final ServiceAddressResolver serviceAddressResolver;
3839

39-
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
40+
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory,
41+
ServiceAddressResolver serviceAddressResolver) {
4042
this.loadBalancer = loadBalancer;
4143
this.requestFactory = requestFactory;
44+
this.serviceAddressResolver = serviceAddressResolver;
4245
}
4346

44-
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
47+
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, ServiceAddressResolver serviceAddressResolver) {
4548
// for backwards compatibility
46-
this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
49+
this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer), serviceAddressResolver);
4750
}
4851

4952
@Override
5053
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
5154
throws IOException {
5255
URI originalUri = request.getURI();
53-
String serviceName = originalUri.getHost();
56+
final String serviceName = serviceAddressResolver.getServiceId(originalUri);
5457
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
5558
return loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
5659
}

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,24 @@ public class RetryLoadBalancerInterceptor implements BlockingLoadBalancerInterce
5656

5757
private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory;
5858

59+
private final ServiceAddressResolver serviceAddressResolver;
60+
5961
public RetryLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory,
60-
LoadBalancedRetryFactory lbRetryFactory,
61-
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory) {
62+
LoadBalancedRetryFactory lbRetryFactory,
63+
ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory,
64+
ServiceAddressResolver serviceAddressResolver) {
6265
this.loadBalancer = loadBalancer;
6366
this.requestFactory = requestFactory;
6467
this.lbRetryFactory = lbRetryFactory;
6568
this.loadBalancerFactory = loadBalancerFactory;
69+
this.serviceAddressResolver = serviceAddressResolver;
6670
}
6771

6872
@Override
6973
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
7074
final ClientHttpRequestExecution execution) throws IOException {
7175
final URI originalUri = request.getURI();
72-
final String serviceName = originalUri.getHost();
76+
final String serviceName = serviceAddressResolver.getServiceId(originalUri);
7377
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
7478
final LoadBalancedRetryPolicy retryPolicy = lbRetryFactory.createRetryPolicy(serviceName, loadBalancer);
7579
RetryTemplate template = createRetryTemplate(serviceName, request, retryPolicy);
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package org.springframework.cloud.client.loadbalancer;
2+
3+
import org.springframework.cloud.client.DefaultServiceInstance;
4+
import org.springframework.cloud.client.ServiceInstance;
5+
6+
import java.net.URI;
7+
8+
import org.springframework.cloud.client.loadbalancer.DefaultRequestContext;
9+
import org.springframework.cloud.client.loadbalancer.Request;
10+
import org.springframework.cloud.client.loadbalancer.RequestData;
11+
12+
/**
13+
* ServiceAddressResolver is responsible for resolving service addresses to ServiceInstance objects.
14+
*
15+
* <p>This class provides functionality to determine if a URI represents a direct address call
16+
* and to convert such addresses into ServiceInstance representations that can be used by
17+
* load balancer implementations.</p>
18+
*
19+
* @author xzxiaoshan
20+
* @since 2025-11-20 13:24:08
21+
*/
22+
public class ServiceAddressResolver {
23+
24+
/**
25+
* Determines whether the given URI represents a direct address call.
26+
*
27+
* <p>A URI is considered a direct address if:
28+
* <ul>
29+
* <li>A port is explicitly specified (not microservice service name calls,
30+
* as ports are obtained from instances in the registry)</li>
31+
* <li>The host contains a colon, which may indicate IPv6</li>
32+
* <li>The host contains a dot, which indicates an IPv4 address or domain name</li>
33+
* <li>The host is "localhost"</li>
34+
* </ul>
35+
* </p>
36+
*
37+
* @param uri the URI to check
38+
* @return true if the URI represents a direct address call, false otherwise
39+
*/
40+
public boolean isDirectAddress(URI uri) {
41+
String host = uri.getHost();
42+
int port = uri.getPort();
43+
// 1. Port specified definitely not a microservice service name call,
44+
// as ports are obtained from instances in the registry
45+
// 2. Host containing colons may be IPv6, containing dots are IPv4 addresses or domain names
46+
return port >= 0 || host.contains(".") || host.contains(":") || host.equalsIgnoreCase("localhost");
47+
}
48+
49+
/**
50+
* Determines whether the given request represents a direct address call.
51+
*
52+
* <p>This method extracts the URI from the request and delegates to
53+
* {@link #isDirectAddress(URI)} for the actual check.</p>
54+
*
55+
* @param request the load balancer request to check
56+
* @return true if the request represents a direct address call, false otherwise
57+
* @throws IllegalArgumentException if unable to extract URI from request context
58+
*/
59+
public boolean isDirectAddress(Request<?> request) {
60+
URI uri = this.getUriFromRequest(request);
61+
return this.isDirectAddress(uri);
62+
}
63+
64+
/**
65+
* Gets the service ID from the given URI.
66+
*
67+
* <p>If the URI represents a direct address with an explicitly specified port,
68+
* the service ID will be constructed as "host:port". Otherwise, just the host
69+
* will be returned as the service ID.</p>
70+
*
71+
* @param uri the URI to extract service ID from
72+
* @return the service ID derived from the URI
73+
*/
74+
public String getServiceId(URI uri) {
75+
String host = uri.getHost();
76+
int port = uri.getPort();
77+
if (this.isDirectAddress(uri) && port >= 0) {
78+
return host + ":" + port;
79+
} else {
80+
return host;
81+
}
82+
}
83+
84+
/**
85+
* Extracts the URI from the given request.
86+
*
87+
* <p>This protected method handles different context types that might contain
88+
* URI information and extracts the URI accordingly.</p>
89+
*
90+
* @param request the load balancer request containing context information
91+
* @return the URI extracted from the request
92+
* @throws IllegalArgumentException if unable to extract URI from request context
93+
*/
94+
protected URI getUriFromRequest(Request<?> request) {
95+
// Extract the URI from the request context
96+
Object context = request.getContext();
97+
URI uri = null;
98+
99+
// Handle different context types that might contain URI information
100+
if (context instanceof DefaultRequestContext) {
101+
Object clientRequest = ((DefaultRequestContext) context).getClientRequest();
102+
if (clientRequest instanceof RequestData) {
103+
uri = ((RequestData) clientRequest).getUrl();
104+
}
105+
} else if (context instanceof RequestData) {
106+
uri = ((RequestData) context).getUrl();
107+
}
108+
109+
// If we couldn't extract URI from context, return null or throw exception
110+
if (uri == null) {
111+
throw new IllegalArgumentException("Unable to extract URI from request context");
112+
}
113+
114+
return uri;
115+
}
116+
117+
/**
118+
* Converts a direct address to a ServiceInstance.
119+
*
120+
* <p>This method extracts the URI from the request context and creates a
121+
* ServiceInstance representation that can be used by load balancer implementations.</p>
122+
*
123+
* @param request the load balancer request containing context information
124+
* @return a ServiceInstance representation of the direct address
125+
* @throws IllegalArgumentException if unable to extract URI from request context
126+
*/
127+
public ServiceInstance createDirectServiceInstance(Request<?> request) {
128+
URI uri = this.getUriFromRequest(request);
129+
130+
String serviceId = this.getServiceId(uri);
131+
String host = uri.getHost();
132+
int port = uri.getPort();
133+
boolean secure = "https".equalsIgnoreCase(uri.getScheme()) ||
134+
"wss".equalsIgnoreCase(uri.getScheme());
135+
136+
return new DefaultServiceInstance(null, serviceId, host, port, secure);
137+
}
138+
}

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public void interceptDisableRetry() throws Throwable {
113113
.thenThrow(new IOException());
114114
properties.getRetry().setEnabled(false);
115115
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
116-
loadBalancedRetryFactory, lbFactory);
116+
loadBalancedRetryFactory, lbFactory, new ServiceAddressResolver());
117117
byte[] body = new byte[] {};
118118
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
119119

@@ -129,7 +129,7 @@ public void interceptInvalidHost() throws Throwable {
129129
when(request.getURI()).thenReturn(new URI("http://foo_underscore"));
130130
properties.getRetry().setEnabled(true);
131131
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
132-
loadBalancedRetryFactory, lbFactory);
132+
loadBalancedRetryFactory, lbFactory, new ServiceAddressResolver());
133133
byte[] body = new byte[] {};
134134
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
135135
assertThatIllegalStateException().isThrownBy(() -> interceptor.intercept(request, body, execution));
@@ -147,7 +147,7 @@ public void interceptNeverRetry() throws Throwable {
147147
when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
148148
properties.getRetry().setEnabled(true);
149149
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
150-
loadBalancedRetryFactory, lbFactory);
150+
loadBalancedRetryFactory, lbFactory, new ServiceAddressResolver());
151151
byte[] body = new byte[] {};
152152
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
153153
interceptor.intercept(request, body, execution);
@@ -168,7 +168,7 @@ public void interceptSuccess() throws Throwable {
168168
when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
169169
properties.getRetry().setEnabled(true);
170170
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
171-
new MyLoadBalancedRetryFactory(policy), lbFactory);
171+
new MyLoadBalancedRetryFactory(policy), lbFactory, new ServiceAddressResolver());
172172
byte[] body = new byte[] {};
173173
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
174174
ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
@@ -195,7 +195,7 @@ public void interceptRetryOnStatusCode() throws Throwable {
195195
.thenReturn(clientHttpResponseOk);
196196
properties.getRetry().setEnabled(true);
197197
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
198-
new MyLoadBalancedRetryFactory(policy), lbFactory);
198+
new MyLoadBalancedRetryFactory(policy), lbFactory, new ServiceAddressResolver());
199199
byte[] body = new byte[] {};
200200
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
201201
ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
@@ -229,7 +229,7 @@ public void interceptRetryFailOnStatusCode() throws Throwable {
229229
byte[] body = new byte[] {};
230230
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
231231
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
232-
new MyLoadBalancedRetryFactory(policy), lbFactory);
232+
new MyLoadBalancedRetryFactory(policy), lbFactory, new ServiceAddressResolver());
233233
ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
234234

235235
verify(client, times(1)).execute(eq("foo"), eq(serviceInstance),
@@ -261,7 +261,7 @@ public void interceptRetry() throws Throwable {
261261
when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
262262
properties.getRetry().setEnabled(true);
263263
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
264-
new MyLoadBalancedRetryFactory(policy, backOffPolicy), lbFactory);
264+
new MyLoadBalancedRetryFactory(policy, backOffPolicy), lbFactory, new ServiceAddressResolver());
265265
byte[] body = new byte[] {};
266266
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
267267
ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
@@ -287,7 +287,7 @@ public void interceptFailedRetry() throws Exception {
287287
when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
288288
properties.getRetry().setEnabled(true);
289289
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
290-
new MyLoadBalancedRetryFactory(policy), lbFactory);
290+
new MyLoadBalancedRetryFactory(policy), lbFactory, new ServiceAddressResolver());
291291
byte[] body = new byte[] {};
292292
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
293293
assertThatIOException().isThrownBy(() -> interceptor.intercept(request, body, execution));
@@ -317,7 +317,7 @@ public void retryListenerTest() throws Throwable {
317317
when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
318318
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
319319
new MyLoadBalancedRetryFactory(policy, backOffPolicy, new RetryListener[] { retryListener }),
320-
lbFactory);
320+
lbFactory, new ServiceAddressResolver());
321321
byte[] body = new byte[] {};
322322
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
323323
ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
@@ -345,7 +345,7 @@ public void retryWithDefaultConstructorTest() throws Throwable {
345345
when(lbRequestFactory.createRequest(any(), any(), any())).thenReturn(mock(LoadBalancerRequest.class));
346346
when(policy.retryableException(any())).thenReturn(true);
347347
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
348-
new MyLoadBalancedRetryFactory(policy, backOffPolicy), lbFactory);
348+
new MyLoadBalancedRetryFactory(policy, backOffPolicy), lbFactory, new ServiceAddressResolver());
349349
byte[] body = new byte[] {};
350350
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
351351
ClientHttpResponse rsp = interceptor.intercept(request, body, execution);
@@ -370,7 +370,7 @@ public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback
370370
};
371371
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
372372
new MyLoadBalancedRetryFactory(policy, backOffPolicy, new RetryListener[] { myRetryListener }),
373-
lbFactory);
373+
lbFactory, new ServiceAddressResolver());
374374
ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class);
375375
assertThatExceptionOfType(TerminatedRetryException.class)
376376
.isThrownBy(() -> interceptor.intercept(request, new byte[] {}, execution));
@@ -386,7 +386,7 @@ public void shouldNotDuplicateLifecycleCalls() throws IOException, URISyntaxExce
386386
when(request.getURI()).thenReturn(new URI("http://test"));
387387
TestLoadBalancerClient client = new TestLoadBalancerClient();
388388
RetryLoadBalancerInterceptor interceptor = new RetryLoadBalancerInterceptor(client, lbRequestFactory,
389-
loadBalancedRetryFactory, lbFactory);
389+
loadBalancedRetryFactory, lbFactory, new ServiceAddressResolver());
390390

391391
interceptor.intercept(request, new byte[] {}, mock(ClientHttpRequestExecution.class));
392392

spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/blocking/client/BlockingLoadBalancerClient.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.net.URI;
2121
import java.util.Set;
2222

23+
import org.springframework.cloud.client.loadbalancer.ServiceAddressResolver;
2324
import reactor.core.publisher.Mono;
2425

2526
import org.springframework.cloud.client.ServiceInstance;
@@ -59,9 +60,12 @@
5960
public class BlockingLoadBalancerClient implements LoadBalancerClient {
6061

6162
private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerClientFactory;
63+
private final ServiceAddressResolver serviceAddressResolver;
6264

63-
public BlockingLoadBalancerClient(ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerClientFactory) {
65+
public BlockingLoadBalancerClient(ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerClientFactory,
66+
ServiceAddressResolver serviceAddressResolver) {
6467
this.loadBalancerClientFactory = loadBalancerClientFactory;
68+
this.serviceAddressResolver = serviceAddressResolver;
6569
}
6670

6771
@Override
@@ -156,6 +160,10 @@ public ServiceInstance choose(String serviceId) {
156160

157161
@Override
158162
public <T> ServiceInstance choose(String serviceId, Request<T> request) {
163+
// If it is a direct connection address
164+
if(serviceAddressResolver.isDirectAddress(request)) {
165+
return serviceAddressResolver.createDirectServiceInstance(request);
166+
}
159167
ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory.getInstance(serviceId);
160168
if (loadBalancer == null) {
161169
return null;

spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/BlockingLoadBalancerClientAutoConfiguration.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.boot.context.properties.EnableConfigurationProperties;
2626
import org.springframework.cloud.client.ServiceInstance;
2727
import org.springframework.cloud.client.loadbalancer.BlockingRestClassesPresentCondition;
28+
import org.springframework.cloud.client.loadbalancer.ServiceAddressResolver;
2829
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
2930
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
3031
import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties;
@@ -58,8 +59,9 @@ public class BlockingLoadBalancerClientAutoConfiguration {
5859
@Bean
5960
@ConditionalOnBean(LoadBalancerClientFactory.class)
6061
@ConditionalOnMissingBean
61-
public LoadBalancerClient blockingLoadBalancerClient(LoadBalancerClientFactory loadBalancerClientFactory) {
62-
return new BlockingLoadBalancerClient(loadBalancerClientFactory);
62+
public LoadBalancerClient blockingLoadBalancerClient(LoadBalancerClientFactory loadBalancerClientFactory,
63+
ServiceAddressResolver serviceAddressResolver) {
64+
return new BlockingLoadBalancerClient(loadBalancerClientFactory, serviceAddressResolver);
6365
}
6466

6567
@Bean

0 commit comments

Comments
 (0)