diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/codec/CodecsAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/codec/CodecsAutoConfiguration.java new file mode 100644 index 000000000000..ec4b000516e6 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/codec/CodecsAutoConfiguration.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2017 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 + * + * http://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.http.codec; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.web.codec.CodecCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.codec.CodecConfigurer; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.util.MimeType; + +/** + * {@link EnableAutoConfiguration Auto-configuration} + * for {@link org.springframework.core.codec.Encoder}s and {@link org.springframework.core.codec.Decoder}s. + * @author Brian Clozel + */ +@Configuration +@ConditionalOnClass(CodecConfigurer.class) +@AutoConfigureAfter(JacksonAutoConfiguration.class) +public class CodecsAutoConfiguration { + + @Configuration + @ConditionalOnClass(ObjectMapper.class) + static class JacksonCodecConfiguration { + + @Bean + @ConditionalOnBean(ObjectMapper.class) + public CodecCustomizer jacksonCodecCustomizer(ObjectMapper objectMapper) { + return configurer -> { + CodecConfigurer.DefaultCodecs defaults = configurer.defaultCodecs(); + defaults.jackson2Decoder(new Jackson2JsonDecoder(objectMapper, new MimeType[0])); + defaults.jackson2Encoder(new Jackson2JsonEncoder(objectMapper, new MimeType[0])); + }; + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/codec/package-info.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/codec/package-info.java new file mode 100644 index 000000000000..b734b106b5d3 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/codec/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2017 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 + * + * http://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. + */ + +/** + * Auto-configuration for HTTP codecs. + */ +package org.springframework.boot.autoconfigure.http.codec; diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java index 6e35f2527c22..b5a50c07d751 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java @@ -32,10 +32,12 @@ 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.http.codec.CodecsAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain; import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -46,6 +48,7 @@ import org.springframework.format.Formatter; import org.springframework.format.FormatterRegistry; import org.springframework.http.CacheControl; +import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.util.ClassUtils; import org.springframework.validation.Validator; import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration; @@ -79,7 +82,7 @@ @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) @ConditionalOnClass(WebFluxConfigurer.class) @ConditionalOnMissingBean({ WebFluxConfigurationSupport.class }) -@AutoConfigureAfter(ReactiveWebServerAutoConfiguration.class) +@AutoConfigureAfter({ ReactiveWebServerAutoConfiguration.class, CodecsAutoConfiguration.class }) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) public class WebFluxAutoConfiguration { @@ -98,6 +101,8 @@ public static class WebFluxConfig implements WebFluxConfigurer { private final List argumentResolvers; + private final List codecCustomizers; + private final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer; private final List viewResolvers; @@ -105,12 +110,14 @@ public static class WebFluxConfig implements WebFluxConfigurer { public WebFluxConfig(ResourceProperties resourceProperties, WebFluxProperties webFluxProperties, ListableBeanFactory beanFactory, ObjectProvider> resolvers, + ObjectProvider> codecCustomizers, ObjectProvider resourceHandlerRegistrationCustomizer, ObjectProvider> viewResolvers) { this.resourceProperties = resourceProperties; this.webFluxProperties = webFluxProperties; this.beanFactory = beanFactory; this.argumentResolvers = resolvers.getIfAvailable(); + this.codecCustomizers = codecCustomizers.getIfAvailable(); this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizer .getIfAvailable(); this.viewResolvers = viewResolvers.getIfAvailable(); @@ -123,6 +130,13 @@ public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { } } + @Override + public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { + if (this.codecCustomizers != null) { + this.codecCustomizers.forEach(codecCustomizer -> codecCustomizer.customize(configurer)); + } + } + @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfiguration.java new file mode 100644 index 000000000000..fd77faaa0e39 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfiguration.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2017 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 + * + * http://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.reactive.function.client; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration; +import org.springframework.boot.web.codec.CodecCustomizer; +import org.springframework.boot.web.reactive.function.client.WebClientCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.annotation.Order; +import org.springframework.util.CollectionUtils; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for {@link WebClient}. + *

This will produce a {@link WebClient.Builder} bean with the {@code prototype} scope, + * meaning each injection point will receive a newly cloned instance of the builder. + * + * @author Brian Clozel + * @since 2.0.0 + */ +@Configuration +@ConditionalOnClass(WebClient.class) +@AutoConfigureAfter(CodecsAutoConfiguration.class) +public class WebClientAutoConfiguration { + + private final WebClient.Builder webClientBuilder; + + + public WebClientAutoConfiguration(ObjectProvider> customizerProvider) { + this.webClientBuilder = WebClient.builder(); + List customizers = customizerProvider.getIfAvailable(); + if (!CollectionUtils.isEmpty(customizers)) { + customizers = new ArrayList<>(customizers); + AnnotationAwareOrderComparator.sort(customizers); + customizers.forEach(customizer -> customizer.customize(this.webClientBuilder)); + } + } + + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + public WebClient.Builder webClientBuilder(List customizers) { + return this.webClientBuilder.clone(); + } + + @Configuration + @ConditionalOnBean(CodecCustomizer.class) + protected static class WebClientCodecsConfiguration { + + @Bean + @ConditionalOnMissingBean + @Order(0) + public WebClientCodecCustomizer exchangeStrategiesCustomizer( + List codecCustomizers) { + return new WebClientCodecCustomizer(codecCustomizers); + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientCodecCustomizer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientCodecCustomizer.java new file mode 100644 index 000000000000..877cd78d4029 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientCodecCustomizer.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2017 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 + * + * http://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.reactive.function.client; + +import java.util.List; + +import org.springframework.boot.web.codec.CodecCustomizer; +import org.springframework.boot.web.reactive.function.client.WebClientCustomizer; +import org.springframework.web.reactive.function.client.ExchangeStrategies; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * {@link WebClientCustomizer} that configures codecs for the HTTP client. + * @author Brian Clozel + * @since 2.0.0 + */ +public class WebClientCodecCustomizer implements WebClientCustomizer { + + private final List codecCustomizers; + + public WebClientCodecCustomizer(List codecCustomizers) { + this.codecCustomizers = codecCustomizers; + } + + @Override + public void customize(WebClient.Builder webClientBuilder) { + webClientBuilder + .exchangeStrategies(ExchangeStrategies.builder() + .codecs(codecs -> { + this.codecCustomizers.forEach(codecCustomizer -> codecCustomizer.customize(codecs)); + }).build()); + } +} diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 81a43a9aa2aa..21846af9f047 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -62,6 +62,7 @@ org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\ org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\ org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\ org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\ +org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\ org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\ org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\ org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\ @@ -114,6 +115,7 @@ org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration, org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\ +org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\ org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\ org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\ org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\ diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index c2cd67338783..7434ebe44669 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfigurationTests.Config; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.boot.web.reactive.context.GenericReactiveWebApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -39,6 +40,7 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.core.io.ClassPathResource; +import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.ObjectUtils; @@ -60,7 +62,9 @@ import org.springframework.web.reactive.result.view.ViewResolver; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * Tests for {@link WebFluxAutoConfiguration}. @@ -105,13 +109,22 @@ public void shouldRegisterCustomHandlerMethodArgumentResolver() throws Exception .getBean(RequestMappingHandlerAdapter.class); assertThat((List) ReflectionTestUtils .getField(adapter.getArgumentResolverConfigurer(), "customResolvers")) - .contains( - this.context.getBean("firstResolver", - HandlerMethodArgumentResolver.class), + .contains( + this.context.getBean("firstResolver", + HandlerMethodArgumentResolver.class), this.context.getBean("secondResolver", HandlerMethodArgumentResolver.class)); } + @Test + public void shouldCustomizeCodecs() throws Exception { + load(CustomCodecCustomizers.class); + CodecCustomizer codecCustomizer = + this.context.getBean("firstCodecCustomizer", CodecCustomizer.class); + assertThat(codecCustomizer).isNotNull(); + verify(codecCustomizer).customize(any(ServerCodecConfigurer.class)); + } + @Test public void shouldRegisterResourceHandlerMapping() throws Exception { load(); @@ -316,6 +329,15 @@ public HandlerMethodArgumentResolver secondResolver() { } + @Configuration + protected static class CustomCodecCustomizers { + + @Bean + public CodecCustomizer firstCodecCustomizer() { + return mock(CodecCustomizer.class); + } + } + @Configuration protected static class ViewResolvers { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfigurationTests.java new file mode 100644 index 000000000000..7c1bf9ec9915 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfigurationTests.java @@ -0,0 +1,153 @@ +/* + * Copyright 2012-2017 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 + * + * http://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.reactive.function.client; + +import java.net.URI; + +import org.junit.After; +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.web.codec.CodecCustomizer; +import org.springframework.boot.web.reactive.function.client.WebClientCustomizer; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.codec.CodecConfigurer; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link WebClientAutoConfiguration} + * + * @author Brian Clozel + */ +public class WebClientAutoConfigurationTests { + + private AnnotationConfigApplicationContext context; + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void shouldCustomizeClientCodecs() throws Exception { + load(CodecConfiguration.class); + WebClient.Builder builder = this.context.getBean(WebClient.Builder.class); + CodecCustomizer codecCustomizer = this.context.getBean(CodecCustomizer.class); + WebClientCodecCustomizer clientCustomizer = this.context.getBean(WebClientCodecCustomizer.class); + builder.build(); + assertThat(clientCustomizer).isNotNull(); + verify(codecCustomizer).customize(any(CodecConfigurer.class)); + } + + @Test + public void webClientShouldApplyCustomizers() throws Exception { + load(WebClientCustomizerConfig.class); + WebClient.Builder builder = this.context.getBean(WebClient.Builder.class); + WebClientCustomizer customizer = this.context.getBean(WebClientCustomizer.class); + builder.build(); + verify(customizer).customize(any(WebClient.Builder.class)); + } + + @Test + public void shouldGetPrototypeScopedBean() throws Exception { + load(WebClientCustomizerConfig.class); + + ClientHttpConnector firstConnector = mock(ClientHttpConnector.class); + given(firstConnector.connect(any(), any(), any())).willReturn(Mono.empty()); + WebClient.Builder firstBuilder = this.context.getBean(WebClient.Builder.class); + firstBuilder.clientConnector(firstConnector).baseUrl("http://first.example.org"); + + ClientHttpConnector secondConnector = mock(ClientHttpConnector.class); + given(secondConnector.connect(any(), any(), any())).willReturn(Mono.empty()); + WebClient.Builder secondBuilder = this.context.getBean(WebClient.Builder.class); + secondBuilder.clientConnector(secondConnector).baseUrl("http://second.example.org"); + + assertThat(firstBuilder).isNotEqualTo(secondBuilder); + + firstBuilder.build().get().uri("/foo").exchange().block(); + secondBuilder.build().get().uri("/foo").exchange().block(); + + verify(firstConnector).connect(eq(HttpMethod.GET), eq(URI.create("http://first.example.org/foo")), any()); + verify(secondConnector).connect(eq(HttpMethod.GET), eq(URI.create("http://second.example.org/foo")), any()); + WebClientCustomizer customizer = this.context.getBean(WebClientCustomizer.class); + verify(customizer, times(1)).customize(any(WebClient.Builder.class)); + } + + @Test + public void shouldNotCreateClientBuilderIfAlreadyPresent() throws Exception { + load(WebClientCustomizerConfig.class, CustomWebClientBuilderConfig.class); + WebClient.Builder builder = this.context.getBean(WebClient.Builder.class); + assertThat(builder).isInstanceOf(MyWebClientBuilder.class); + } + + + private void load(Class... config) { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(config); + ctx.register(WebClientAutoConfiguration.class); + ctx.refresh(); + this.context = ctx; + } + + @Configuration + static class CodecConfiguration { + + @Bean + public CodecCustomizer myCodecCustomizer() { + return mock(CodecCustomizer.class); + } + } + + @Configuration + static class WebClientCustomizerConfig { + + @Bean + public WebClientCustomizer webClientCustomizer() { + return mock(WebClientCustomizer.class); + } + + } + + @Configuration + static class CustomWebClientBuilderConfig { + + @Bean + public MyWebClientBuilder myWebClientBuilder() { + return mock(MyWebClientBuilder.class); + } + + } + + interface MyWebClientBuilder extends WebClient.Builder { + + } + +} diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/reactive/WebTestClientAutoConfiguration.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/reactive/WebTestClientAutoConfiguration.java index 7d7a7ba769c1..a4184e49cef3 100644 --- a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/reactive/WebTestClientAutoConfiguration.java +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/reactive/WebTestClientAutoConfiguration.java @@ -16,12 +16,19 @@ package org.springframework.boot.test.autoconfigure.web.reactive; +import java.util.Collection; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration; +import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.CollectionUtils; +import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; /** @@ -31,13 +38,32 @@ * @since 2.0.0 */ @Configuration -@ConditionalOnClass({ WebClient.class, WebTestClient.class }) +@ConditionalOnClass({WebClient.class, WebTestClient.class}) +@AutoConfigureAfter(CodecsAutoConfiguration.class) public class WebTestClientAutoConfiguration { @Bean @ConditionalOnMissingBean public WebTestClient webTestClient(ApplicationContext applicationContext) { - return WebTestClient.bindToApplicationContext(applicationContext).build(); + + WebTestClient.Builder clientBuilder = WebTestClient + .bindToApplicationContext(applicationContext).configureClient(); + customizeWebTestClientCodecs(clientBuilder, applicationContext); + return clientBuilder.build(); + } + + private void customizeWebTestClientCodecs(WebTestClient.Builder clientBuilder, + ApplicationContext applicationContext) { + + Collection codecCustomizers = applicationContext + .getBeansOfType(CodecCustomizer.class).values(); + if (!CollectionUtils.isEmpty(codecCustomizers)) { + clientBuilder.exchangeStrategies(ExchangeStrategies.builder() + .codecs(codecs -> { + codecCustomizers.forEach(codecCustomizer -> codecCustomizer.customize(codecs)); + }) + .build()); + } } } diff --git a/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories index 08255a6c2c7c..0d335481a554 100644 --- a/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories @@ -109,8 +109,10 @@ org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient=\ org.springframework.boot.test.autoconfigure.web.client.WebClientRestTemplateAutoConfiguration,\ org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\ org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\ +org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\ org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\ -org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration +org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\ +org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration # AutoConfigureWebMvc auto-configuration imports org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc=\ diff --git a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/WebTestClientAutoConfigurationTests.java b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/WebTestClientAutoConfigurationTests.java new file mode 100644 index 000000000000..75359dcf7629 --- /dev/null +++ b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/WebTestClientAutoConfigurationTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2017 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 + * + * http://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.test.autoconfigure.web.reactive; + +import org.junit.After; +import org.junit.Test; + +import org.springframework.boot.web.codec.CodecCustomizer; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.codec.CodecConfigurer; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.server.WebHandler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link WebTestClientAutoConfiguration} + * + * @author Brian Clozel + */ +public class WebTestClientAutoConfigurationTests { + + private AnnotationConfigApplicationContext context; + + @After + public void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void shouldCustomizeClientCodecs() throws Exception { + load(CodecConfiguration.class); + WebTestClient webTestClient = this.context.getBean(WebTestClient.class); + CodecCustomizer codecCustomizer = this.context.getBean(CodecCustomizer.class); + assertThat(webTestClient).isNotNull(); + verify(codecCustomizer).customize(any(CodecConfigurer.class)); + } + + + private void load(Class... config) { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(config); + ctx.register(WebTestClientAutoConfiguration.class); + ctx.refresh(); + this.context = ctx; + } + + @Configuration + static class BaseConfiguration { + + @Bean + public WebHandler webHandler() { + return mock(WebHandler.class); + } + } + + @Configuration + @Import(BaseConfiguration.class) + static class CodecConfiguration { + + @Bean + public CodecCustomizer myCodecCustomizer() { + return mock(CodecCustomizer.class); + } + + } + +} diff --git a/spring-boot-test/pom.xml b/spring-boot-test/pom.xml index e0308842c895..e4011bd9c461 100644 --- a/spring-boot-test/pom.xml +++ b/spring-boot-test/pom.xml @@ -105,6 +105,11 @@ spring-web true + + org.springframework + spring-webflux + true + net.sourceforge.htmlunit htmlunit @@ -162,11 +167,6 @@ spring-webmvc test - - org.springframework - spring-webflux - test - diff --git a/spring-boot-test/src/main/java/org/springframework/boot/test/web/reactive/WebTestClientContextCustomizer.java b/spring-boot-test/src/main/java/org/springframework/boot/test/web/reactive/WebTestClientContextCustomizer.java index e0a6b71108a0..ac4c1fc2502d 100644 --- a/spring-boot-test/src/main/java/org/springframework/boot/test/web/reactive/WebTestClientContextCustomizer.java +++ b/spring-boot-test/src/main/java/org/springframework/boot/test/web/reactive/WebTestClientContextCustomizer.java @@ -16,6 +16,8 @@ package org.springframework.boot.test.web.reactive; +import java.util.Collection; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -23,6 +25,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -31,6 +34,8 @@ import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.CollectionUtils; +import org.springframework.web.reactive.function.client.ExchangeStrategies; /** * {@link ContextCustomizer} for {@link WebTestClient}. @@ -115,7 +120,9 @@ private WebTestClient createWebTestClient() { String port = this.applicationContext.getEnvironment() .getProperty("local.server.port", "8080"); String baseUrl = (sslEnabled ? "https" : "http") + "://localhost:" + port; - return WebTestClient.bindToServer().baseUrl(baseUrl).build(); + WebTestClient.Builder builder = WebTestClient.bindToServer(); + customizeWebTestClientCodecs(builder, this.applicationContext); + return builder.baseUrl(baseUrl).build(); } private boolean isSslEnabled(ApplicationContext context) { @@ -130,6 +137,18 @@ private boolean isSslEnabled(ApplicationContext context) { } } + private void customizeWebTestClientCodecs(WebTestClient.Builder clientBuilder, + ApplicationContext context) { + Collection codecCustomizers = context.getBeansOfType(CodecCustomizer.class).values(); + if (!CollectionUtils.isEmpty(codecCustomizers)) { + clientBuilder.exchangeStrategies(ExchangeStrategies.builder() + .codecs(codecs -> { + codecCustomizers.forEach(codecCustomizer -> codecCustomizer.customize(codecs)); + }) + .build()); + } + } + } } diff --git a/spring-boot/src/main/java/org/springframework/boot/web/codec/CodecCustomizer.java b/spring-boot/src/main/java/org/springframework/boot/web/codec/CodecCustomizer.java new file mode 100644 index 000000000000..0f8edbef119c --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/web/codec/CodecCustomizer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2017 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 + * + * http://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.codec; + +import org.springframework.http.codec.CodecConfigurer; + +/** + * Callback interface that can be used to customize codecs configuration + * for an HTTP client and/or server with a {@link CodecConfigurer}. + * @author Brian Clozel + * @since 2.0 + */ +@FunctionalInterface +public interface CodecCustomizer { + + /** + * Callback to customize a {@link CodecConfigurer} instance. + * @param configurer codec configurer to customize + */ + void customize(CodecConfigurer configurer); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/web/reactive/function/client/WebClientCustomizer.java b/spring-boot/src/main/java/org/springframework/boot/web/reactive/function/client/WebClientCustomizer.java new file mode 100644 index 000000000000..3c4e90cd5c76 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/web/reactive/function/client/WebClientCustomizer.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2017 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 + * + * http://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.reactive.function.client; + +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Callback interface that can be used to customize a {@link WebClient.Builder}. + * + * @author Brian Clozel + * @since 2.0.0 + */ +@FunctionalInterface +public interface WebClientCustomizer { + + /** + * Callback to customize a {@link WebClient.Builder} instance. + * @param webClientBuilder the client builder to customize + */ + void customize(WebClient.Builder webClientBuilder); +}