Add initial support for RestClient

Introduce initial support for Spring Framework's `RestClient`, in the
form of a `RestClientCustomizer` and `RestClientAutoConfiguration`.

See gh-36213
This commit is contained in:
Arjen Poutsma
2023-07-04 15:50:53 +02:00
committed by Phillip Webb
parent 8b3070e027
commit a1a5acf128
3 changed files with 222 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2012-2023 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.boot.autoconfigure.web.client;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Scope;
import org.springframework.web.client.RestClient;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link RestClient}.
* <p>
* This will produce a {@link RestClient.Builder RestClient.Builder} bean with the
* {@code prototype} scope, meaning each injection point will receive a newly cloned
* instance of the builder.
*
* @author Arjen Poutsma
* @since 3.2.0
*/
@AutoConfiguration(after = HttpMessageConvertersAutoConfiguration.class)
@ConditionalOnClass(RestClient.class)
@Conditional(RestClientAutoConfiguration.NotReactiveWebApplicationCondition.class)
public class RestClientAutoConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
public RestClient.Builder webClientBuilder(ObjectProvider<RestClientCustomizer> customizerProvider) {
RestClient.Builder builder = RestClient.builder();
customizerProvider.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder;
}
static class NotReactiveWebApplicationCondition extends NoneNestedConditions {
NotReactiveWebApplicationCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
private static class ReactiveWebApplication {
}
}
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2012-2023 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.boot.autoconfigure.web.client;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.boot.web.codec.CodecCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RestClientAutoConfiguration}
*
* @author Arjen Poutsma
*/
class RestClientAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class));
@Test
void shouldCreateBuilder() {
this.contextRunner.run((context) -> {
RestClient.Builder builder = context.getBean(RestClient.Builder.class);
RestClient restClient = builder.build();
assertThat(restClient).isNotNull();
});
}
@Test
void restClientShouldApplyCustomizers() {
this.contextRunner.withUserConfiguration(RestClientCustomizerConfig.class).run((context) -> {
RestClient.Builder builder = context.getBean(RestClient.Builder.class);
RestClientCustomizer customizer = context.getBean("webClientCustomizer", RestClientCustomizer.class);
builder.build();
then(customizer).should().customize(any(RestClient.Builder.class));
});
}
@Test
void shouldGetPrototypeScopedBean() {
this.contextRunner.withUserConfiguration(RestClientCustomizerConfig.class).run((context) -> {
RestClient.Builder firstBuilder = context.getBean(RestClient.Builder.class);
RestClient.Builder secondBuilder = context.getBean(RestClient.Builder.class);
assertThat(firstBuilder).isNotEqualTo(secondBuilder);
});
}
@Test
void shouldNotCreateClientBuilderIfAlreadyPresent() {
this.contextRunner.withUserConfiguration(CustomRestClientBuilderConfig.class).run((context) -> {
RestClient.Builder builder = context.getBean(RestClient.Builder.class);
assertThat(builder).isInstanceOf(MyWebClientBuilder.class);
});
}
@Configuration(proxyBeanMethods = false)
static class CodecConfiguration {
@Bean
CodecCustomizer myCodecCustomizer() {
return mock(CodecCustomizer.class);
}
}
@Configuration(proxyBeanMethods = false)
static class RestClientCustomizerConfig {
@Bean
RestClientCustomizer webClientCustomizer() {
return mock(RestClientCustomizer.class);
}
}
@Configuration(proxyBeanMethods = false)
static class CustomRestClientBuilderConfig {
@Bean
MyWebClientBuilder myWebClientBuilder() {
return mock(MyWebClientBuilder.class);
}
}
interface MyWebClientBuilder extends RestClient.Builder {
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2012-2023 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.boot.web.client;
import org.springframework.web.client.RestClient;
/**
* Callback interface that can be used to customize a
* {@link org.springframework.web.client.RestClient.Builder RestClient.Builder}.
*
* @author Arjen Poutsma
* @since 3.2.0
*/
@FunctionalInterface
public interface RestClientCustomizer {
/**
* Callback to customize a {@link org.springframework.web.client.RestClient.Builder
* RestClient.Builder} instance.
* @param restClientBuilder the client builder to customize
*/
void customize(RestClient.Builder restClientBuilder);
}