From 64777204d8187ee101cbee21d5d0ce4ffd338774 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 20 Jun 2017 14:56:18 +0200 Subject: [PATCH] Apply codecs auto-configuration to WebFlux This commit introduces `CodecCustomizer`, a new callback-based interface for customizing the codecs configuration for WebFlux server and client. Instances of those customizers are applied to the `WebClient.Builder` and to the `WebFluxAutoConfiguration` (which deals with both WebFlux and WebFlux.fn). For now, only Jackson codecs are auto-configured, by getting the `ObjectMapper` instance created by Spring Boot. Other codecs can be configured as soon as WebFlux supports those. Closes gh-9166 --- .../http/codec/CodecsAutoConfiguration.java | 60 +++++++++++++++++++ .../http/codec/package-info.java | 20 +++++++ .../reactive/WebFluxAutoConfiguration.java | 16 ++++- .../client/WebClientAutoConfiguration.java | 21 +++++++ .../client/WebClientCodecCustomizer.java | 47 +++++++++++++++ .../main/resources/META-INF/spring.factories | 1 + .../WebFluxAutoConfigurationTests.java | 22 +++++++ .../WebClientAutoConfigurationTests.java | 22 +++++++ .../main/resources/META-INF/spring.factories | 1 + .../boot/web/codec/CodecCustomizer.java | 36 +++++++++++ 10 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/codec/CodecsAutoConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/codec/package-info.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientCodecCustomizer.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/web/codec/CodecCustomizer.java 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 0000000000..ec4b000516 --- /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 0000000000..b734b106b5 --- /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 6e35f2527c..b5a50c07d7 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.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.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.core.convert.converter.GenericConverter; 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 @@ import org.springframework.web.reactive.result.view.ViewResolver; @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 class WebFluxAutoConfiguration { private final List argumentResolvers; + private final List codecCustomizers; + private final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer; private final List viewResolvers; @@ -105,12 +110,14 @@ public class WebFluxAutoConfiguration { 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 class WebFluxAutoConfiguration { } } + @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 index d068a457d2..fd77faaa0e 100644 --- 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 @@ -20,14 +20,19 @@ 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; @@ -41,6 +46,7 @@ import org.springframework.web.reactive.function.client.WebClient; */ @Configuration @ConditionalOnClass(WebClient.class) +@AutoConfigureAfter(CodecsAutoConfiguration.class) public class WebClientAutoConfiguration { private final WebClient.Builder webClientBuilder; @@ -62,4 +68,19 @@ public class WebClientAutoConfiguration { 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 0000000000..877cd78d40 --- /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 03559d5683..21846af9f0 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,\ 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 35be60e0db..7434ebe446 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.validation.ValidatorAdapter; 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.context.annotation.Import; 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.ViewResolutionResultHandler; 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}. @@ -112,6 +116,15 @@ public class WebFluxAutoConfigurationTests { 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 class WebFluxAutoConfigurationTests { } + @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 index 3c98e035cc..7c1bf9ec99 100644 --- 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 @@ -22,12 +22,14 @@ 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; @@ -54,6 +56,17 @@ public class WebClientAutoConfigurationTests { } } + @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); @@ -104,6 +117,15 @@ public class WebClientAutoConfigurationTests { this.context = ctx; } + @Configuration + static class CodecConfiguration { + + @Bean + public CodecCustomizer myCodecCustomizer() { + return mock(CodecCustomizer.class); + } + } + @Configuration static class WebClientCustomizerConfig { 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 9edc8e10b6..0d335481a5 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,6 +109,7 @@ 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.reactive.function.client.WebClientAutoConfiguration 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 0000000000..0f8edbef11 --- /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); + +}