Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support @LoadBalanced RestTemplateBuilder #1403

Merged
merged 4 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,63 +16,57 @@

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 provided {@link ClientHttpRequestInterceptor} and application context.
* @param loadBalancerInterceptor a {@link ClientHttpRequestInterceptor} used for load-balancing
* Creates a {@link LoadBalancerRestClientBuilderBeanPostProcessor} instance using a
* provided {@link ClientHttpRequestInterceptor} and application context.
* @param loadBalancerInterceptor a {@link ClientHttpRequestInterceptor} used for
* load-balancing
* @param context {@link ApplicationContext}
* @deprecated in favour of {@link LoadBalancerRestClientBuilderBeanPostProcessor#LoadBalancerRestClientBuilderBeanPostProcessor(ObjectProvider, ApplicationContext)}
* @deprecated in favour of
* {@link LoadBalancerRestClientBuilderBeanPostProcessor#LoadBalancerRestClientBuilderBeanPostProcessor(ObjectProvider, ApplicationContext)}
*/
@Deprecated(forRemoval = true)
public LoadBalancerRestClientBuilderBeanPostProcessor(T loadBalancerInterceptor, ApplicationContext context) {
this.loadBalancerInterceptorProvider = new SimpleObjectProvider<>(loadBalancerInterceptor);
this.context = context;
this(new SimpleObjectProvider<>(loadBalancerInterceptor), context);
}

/**
* Creates a {@link LoadBalancerRestClientBuilderBeanPostProcessor} instance using interceptor {@link ObjectProvider} and application context.
* @param loadBalancerInterceptorProvider an {@link ObjectProvider} for {@link ClientHttpRequestInterceptor} used for load-balancing
* Creates a {@link LoadBalancerRestClientBuilderBeanPostProcessor} instance using
* interceptor {@link ObjectProvider} and application context.
* @param loadBalancerInterceptorProvider an {@link ObjectProvider} for
* {@link ClientHttpRequestInterceptor} used for load-balancing
* @param context {@link ApplicationContext}
*/
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