Skip to content

Commit

Permalink
Support @LoadBalanced RestTemplateBuilder (#1403)
Browse files Browse the repository at this point in the history
  • Loading branch information
OlgaMaciaszek authored Oct 15, 2024
1 parent 23739d6 commit 2cf7717
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,89 @@ IMPORTANT: Notice the use of the `@Primary` annotation on the plain `RestTemplat

TIP: If you see errors such as `java.lang.IllegalArgumentException: Can not set org.springframework.web.client.RestTemplate field com.my.app.Foo.restTemplate to com.sun.proxy.$Proxy89`, try injecting `RestOperations` or setting `spring.aop.proxyTargetClass=true`.

[[rest-template-builder-loadbalancer-client]]
== Using `@LoadBalanced RestTemplateBuilder` to create a LoadBalancer Client

You can also configure a `RestTemplate` to use a Load-Balancer client by annotating a
`RestTemplateBuilder` bean with `@LoadBalanced`:

[source,java,indent=0]
----
import org.springframework.boot.web.client.RestTemplateBuilder;@Configuration
public class MyConfiguration {
@Bean
@LoadBalanced
RestTemplateBuilder loadBalancedRestTemplateBuilder() {
return new RestTemplateBuilder();
}
}
public class MyClass {
private final RestTemplate restTemplate;
MyClass(@LoadBalanced RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder.build();
}
public String getStores() {
return restTemplate.getForObject("http://stores/stores", String.class);
}
}
----

The URI needs to use a virtual host name (that is, a service name, not a host name).
The `BlockingLoadBalancerClient` is used to create a full physical address.

IMPORTANT: To use it, add xref:spring-cloud-commons/loadbalancer.adoc#spring-cloud-loadbalancer-starter[Spring Cloud LoadBalancer starter] to your project.

[[multiple-resttemplate-builder-beans]]
=== Multiple `RestTemplateBuilder` beans

If you want a `RestTemplateBuilder` that is not load-balanced, create a `RestTemplateBuilder` bean and inject it.
To access the load-balanced `RestTemplateBuilder`, use the `@LoadBalanced` qualifier when you create your `@Bean`, as the following example shows:

[source,java,indent=0]
----
@Configuration
public class MyConfiguration {
@LoadBalanced
@Bean
RestTemplateBuilder loadBalancedRestTemplateBuilder() {
return new RestTemplateBuilder();
}
@Primary
@Bean
RestTemplateBuilder restTemplateBuilder() {
return new RestTemplateBuilder();
}
}
public class MyClass {
@Autowired
private RestTemplateBuilder restTemplateBuilder;
@Autowired
@LoadBalanced
private RestTemplateBuilder loadBalanced;
public String doOtherStuff() {
return loadBalanced.getForObject("http://stores/stores", String.class);
}
public String doStuff() {
return restTemplateBuilder.build().getForObject("http://example.com", String.class);
}
}
----

IMPORTANT: Notice the use of the `@Primary` annotation on the plain `RestTemplateBuilder` declaration in the preceding example to disambiguate the unqualified `@Autowired` injection.


[[rest-client-loadbalancer-client]]
== Spring RestClient as a LoadBalancer Client

Expand Down Expand Up @@ -256,7 +339,7 @@ IMPORTANT: To use it, add xref:spring-cloud-commons/loadbalancer.adoc#spring-clo
=== Multiple `RestClient.Builder` Objects

If you want a `RestClient.Builder` that is not load-balanced, create a `RestClient.Builder` bean and inject it.
To access the load-balanced `RestClient`, use the `@LoadBalanced` qualifier when you create your `@Bean`, as the following example shows:
To access the load-balanced `RestClient.Builder`, use the `@LoadBalanced` qualifier when you create your `@Bean`, as the following example shows:

[source,java,indent=0]
----
Expand Down Expand Up @@ -296,7 +379,7 @@ public class MyClass {
}
----

IMPORTANT: Notice the use of the `@Primary` annotation on the plain `RestTemplate` declaration in the preceding example to disambiguate the unqualified `@Autowired` injection.
IMPORTANT: Notice the use of the `@Primary` annotation on the plain `RestClient.Builder` declaration in the preceding example to disambiguate the unqualified `@Autowired` injection.

[[webclinet-loadbalancer-client]]
== Spring WebClient as a LoadBalancer Client
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.http.client.ClientHttpRequestInterceptor;

/**
* A {@link BeanPostProcessor} that adds the provided {@link ClientHttpRequestInterceptor}
* to bean instances annotated with {@link LoadBalanced}.
*
* @author Olga Maciaszek-Sharma
* @since 4.2.0
*/
public abstract class AbstractLoadBalancerBlockingBuilderBeanPostProcessor<T extends ClientHttpRequestInterceptor>
implements BeanPostProcessor {

protected final ObjectProvider<T> loadBalancerInterceptorProvider;

protected final ApplicationContext context;

AbstractLoadBalancerBlockingBuilderBeanPostProcessor(ObjectProvider<T> loadBalancerInterceptorProvider,
ApplicationContext context) {
this.loadBalancerInterceptorProvider = loadBalancerInterceptorProvider;
this.context = context;
}

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// a separate call to verify supported type before searching for annotation for
// performance reasons
if (isSupported(bean)) {
if (context.findAnnotationOnBean(beanName, LoadBalanced.class) == null) {
return bean;
}
ClientHttpRequestInterceptor interceptor = loadBalancerInterceptorProvider.getIfAvailable();
if (interceptor == null) {
throw new IllegalStateException(ClientHttpRequestInterceptor.class.getSimpleName() + " not available.");
}
bean = apply(bean, interceptor);
}
return bean;
}

protected abstract Object apply(Object bean, ClientHttpRequestInterceptor interceptor);

protected abstract boolean isSupported(Object bean);

}
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ static LoadBalancerRestClientBuilderBeanPostProcessor<DeferringLoadBalancerInter
return new LoadBalancerRestClientBuilderBeanPostProcessor<>(loadBalancerInterceptorProvider, context);
}

@Bean
@ConditionalOnBean(DeferringLoadBalancerInterceptor.class)
@ConditionalOnMissingBean(LoadBalancerRestTemplateBuilderBeanPostProcessor.class)
static LoadBalancerRestTemplateBuilderBeanPostProcessor<DeferringLoadBalancerInterceptor> lbRestTemplateBuilderPostProcessor(
ObjectProvider<DeferringLoadBalancerInterceptor> loadBalancerInterceptorProvider,
ApplicationContext context) {
return new LoadBalancerRestTemplateBuilderBeanPostProcessor<>(loadBalancerInterceptorProvider, context);
}

}

@AutoConfiguration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,22 @@

package org.springframework.cloud.client.loadbalancer;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestClient;

/**
* A {@link BeanPostProcessor} that adds the provided {@link ClientHttpRequestInterceptor}
* to all {@link RestClient.Builder} instances annotated with {@link LoadBalanced}.
* {@link RestClient.Builder}-specific
* {@link AbstractLoadBalancerBlockingBuilderBeanPostProcessor} implementation. Adds the
* provided {@link ClientHttpRequestInterceptor} to all {@link RestClient.Builder}
* instances annotated with {@link LoadBalanced}.
*
* @author Olga Maciaszek-Sharma
* @since 4.1.0
*/
public class LoadBalancerRestClientBuilderBeanPostProcessor<T extends ClientHttpRequestInterceptor>
implements BeanPostProcessor {

private final ObjectProvider<T> loadBalancerInterceptorProvider;

private final ApplicationContext context;
extends AbstractLoadBalancerBlockingBuilderBeanPostProcessor<T> {

/**
* Creates a {@link LoadBalancerRestClientBuilderBeanPostProcessor} instance using a
Expand All @@ -48,8 +44,7 @@ public class LoadBalancerRestClientBuilderBeanPostProcessor<T extends ClientHttp
*/
@Deprecated(forRemoval = true)
public LoadBalancerRestClientBuilderBeanPostProcessor(T loadBalancerInterceptor, ApplicationContext context) {
this.loadBalancerInterceptorProvider = new SimpleObjectProvider<>(loadBalancerInterceptor);
this.context = context;
this(new SimpleObjectProvider<>(loadBalancerInterceptor), context);
}

/**
Expand All @@ -61,23 +56,17 @@ public LoadBalancerRestClientBuilderBeanPostProcessor(T loadBalancerInterceptor,
*/
public LoadBalancerRestClientBuilderBeanPostProcessor(ObjectProvider<T> loadBalancerInterceptorProvider,
ApplicationContext context) {
this.loadBalancerInterceptorProvider = loadBalancerInterceptorProvider;
this.context = context;
super(loadBalancerInterceptorProvider, context);
}

@Override
protected Object apply(Object bean, ClientHttpRequestInterceptor interceptor) {
return ((RestClient.Builder) bean).requestInterceptor(interceptor);
}

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof RestClient.Builder) {
if (context.findAnnotationOnBean(beanName, LoadBalanced.class) == null) {
return bean;
}
ClientHttpRequestInterceptor interceptor = loadBalancerInterceptorProvider.getIfAvailable();
if (interceptor == null) {
throw new IllegalStateException(ClientHttpRequestInterceptor.class.getSimpleName() + " not available.");
}
((RestClient.Builder) bean).requestInterceptor(interceptor);
}
return bean;
protected boolean isSupported(Object bean) {
return bean instanceof RestClient.Builder;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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.beans.factory.ObjectProvider;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.http.client.ClientHttpRequestInterceptor;

/**
* {@link RestTemplateBuilder}-specific
* {@link AbstractLoadBalancerBlockingBuilderBeanPostProcessor} implementation. Adds the
* provided {@link ClientHttpRequestInterceptor} to all {@link RestTemplateBuilder}
* instances annotated with {@link LoadBalanced}.
*
* @author Olga Maciaszek-Sharma
* @since 4.2.0
*/
public class LoadBalancerRestTemplateBuilderBeanPostProcessor<T extends ClientHttpRequestInterceptor>
extends AbstractLoadBalancerBlockingBuilderBeanPostProcessor<T> {

public LoadBalancerRestTemplateBuilderBeanPostProcessor(ObjectProvider<T> loadBalancerInterceptorProvider,
ApplicationContext context) {
super(loadBalancerInterceptorProvider, context);
}

@Override
protected boolean isSupported(Object bean) {
return bean instanceof RestTemplateBuilder;
}

@Override
protected Object apply(Object bean, ClientHttpRequestInterceptor interceptor) {
return ((RestTemplateBuilder) bean).interceptors(interceptor);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package org.springframework.cloud.client.loadbalancer;

import java.io.IOException;
import java.net.URI;

import org.junit.jupiter.api.Test;
Expand All @@ -25,7 +24,6 @@
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;

Expand All @@ -41,17 +39,16 @@
properties = "spring.cloud.loadbalancer.retry.enabled=false")
public class LoadBalancedRestClientIntegrationTests {

private final RestClient client;
private final RestClient.Builder restClientBuilder;

@Autowired
ApplicationContext context;

public LoadBalancedRestClientIntegrationTests(@Autowired RestClient.Builder clientBuilder) {
this.client = clientBuilder.build();
public LoadBalancedRestClientIntegrationTests(@Autowired RestClient.Builder restClientBuilder) {
this.restClientBuilder = restClientBuilder;
}

@Test
void shouldBuildLoadBalancedRestClientInConstructor() {
RestClient client = restClientBuilder.build();

// Interceptors are not visible in RestClient
assertThatThrownBy(() -> client.get().uri("http://test-service").retrieve())
.hasMessage("LoadBalancerInterceptor invoked.");
Expand All @@ -70,13 +67,13 @@ RestClient.Builder restClientBuilder() {
LoadBalancerClient testLoadBalancerClient() {
return new LoadBalancerClient() {
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) {
throw new UnsupportedOperationException("LoadBalancerInterceptor invoked.");
}

@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request)
throws IOException {
public <T> T execute(String serviceId, ServiceInstance serviceInstance,
LoadBalancerRequest<T> request) {
throw new UnsupportedOperationException("LoadBalancerInterceptor invoked.");
}

Expand Down
Loading

0 comments on commit 2cf7717

Please sign in to comment.