Support date conversion format for java.time types

Prior to this change, the Spring MVC auto-configuration would add a new
formatter to convert `java.util.Date` to/from `String` using the
configured configuration property `spring.mvc.date-format`.

This commit adds a new `WebConversionService` class that registers
date formatters with a custom date format, or register the default ones
if no custom configuration is provided.
This avoids duplicating equivalent formatters in the registry.

With this change, date types from `java.util`, `org.joda.time` and
`java.time` are now all supported.

This commit also replicates this feature for WebFlux applications by
adding a new `spring.webflux.date-format` configuration property.

Closes gh-5523
Closes gh-11402
pull/11388/merge
Brian Clozel 7 years ago
parent ec26488ff1
commit 2fa0539e7f

@ -0,0 +1,118 @@
/*
* 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.format;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;
import org.joda.time.format.DateTimeFormatterBuilder;
import org.springframework.format.datetime.DateFormatter;
import org.springframework.format.datetime.DateFormatterRegistrar;
import org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar;
import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar;
import org.springframework.format.number.NumberFormatAnnotationFormatterFactory;
import org.springframework.format.number.money.CurrencyUnitFormatter;
import org.springframework.format.number.money.Jsr354NumberFormatAnnotationFormatterFactory;
import org.springframework.format.number.money.MonetaryAmountFormatter;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* {@link org.springframework.format.support.FormattingConversionService} dedicated
* to web applications for formatting and converting values to/from the web.
*
* <p>This service replaces the default implementations provided by
* {@link org.springframework.web.servlet.config.annotation.EnableWebMvc}
* and {@link org.springframework.web.reactive.config.EnableWebFlux}.
*
* @author Brian Clozel
* @since 2.0.0
*/
public class WebConversionService extends DefaultFormattingConversionService {
private static final boolean jsr354Present = ClassUtils
.isPresent("javax.money.MonetaryAmount", WebConversionService.class.getClassLoader());
private static final boolean jodaTimePresent = ClassUtils
.isPresent("org.joda.time.LocalDate", WebConversionService.class.getClassLoader());
private String dateFormat;
/**
* Create a new WebConversionService that configures formatters with the provided date format,
* or register the default ones if no custom format is provided.
* @param dateFormat the custom date format to use for date conversions
*/
public WebConversionService(String dateFormat) {
super(false);
if (StringUtils.hasText(dateFormat)) {
this.dateFormat = dateFormat;
addFormatters();
}
else {
addDefaultFormatters(this);
}
}
private void addFormatters() {
addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());
if (jsr354Present) {
addFormatter(new CurrencyUnitFormatter());
addFormatter(new MonetaryAmountFormatter());
addFormatterForFieldAnnotation(new Jsr354NumberFormatAnnotationFormatterFactory());
}
registerJsr310();
if (jodaTimePresent) {
registerJodaTime();
}
registerJavaDate();
}
private void registerJsr310() {
DateTimeFormatterRegistrar dateTime = new DateTimeFormatterRegistrar();
if (this.dateFormat != null) {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter
.ofPattern(this.dateFormat)
.withResolverStyle(ResolverStyle.STRICT);
dateTime.setDateFormatter(dateTimeFormatter);
}
dateTime.registerFormatters(this);
}
private void registerJodaTime() {
JodaTimeFormatterRegistrar jodaTime = new JodaTimeFormatterRegistrar();
if (this.dateFormat != null) {
org.joda.time.format.DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
.appendPattern(this.dateFormat)
.toFormatter();
jodaTime.setDateFormatter(dateTimeFormatter);
}
jodaTime.registerFormatters(this);
}
private void registerJavaDate() {
DateFormatterRegistrar dateFormatterRegistrar = new DateFormatterRegistrar();
if (this.dateFormat != null) {
DateFormatter dateFormatter = new DateFormatter(this.dateFormat);
dateFormatterRegistrar.setFormatter(dateFormatter);
}
dateFormatterRegistrar.registerFormatters(this);
}
}

@ -34,9 +34,11 @@ 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.ValidationAutoConfiguration;
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.autoconfigure.web.format.WebConversionService;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.codec.CodecCustomizer;
import org.springframework.context.annotation.Bean;
@ -48,6 +50,7 @@ import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.CacheControl;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.util.ClassUtils;
@ -84,7 +87,7 @@ import org.springframework.web.reactive.result.view.ViewResolver;
@ConditionalOnClass(WebFluxConfigurer.class)
@ConditionalOnMissingBean({ WebFluxConfigurationSupport.class })
@AutoConfigureAfter({ ReactiveWebServerAutoConfiguration.class,
CodecsAutoConfiguration.class })
CodecsAutoConfiguration.class, ValidationAutoConfiguration.class })
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
public class WebFluxAutoConfiguration {
@ -211,8 +214,22 @@ public class WebFluxAutoConfiguration {
public static class EnableWebFluxConfiguration
extends DelegatingWebFluxConfiguration {
private final WebFluxProperties webFluxProperties;
public EnableWebFluxConfiguration(WebFluxProperties webFluxProperties) {
this.webFluxProperties = webFluxProperties;
}
@Bean
@Override
public FormattingConversionService webFluxConversionService() {
WebConversionService conversionService = new WebConversionService(this.webFluxProperties.getDateFormat());
addFormatters(conversionService);
return conversionService;
}
@Bean
@Override
public Validator webFluxValidator() {
if (!ClassUtils.isPresent("javax.validation.Validator",
getClass().getClassLoader())) {

@ -27,11 +27,24 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "spring.webflux")
public class WebFluxProperties {
/**
* Date format to use. For instance, "dd/MM/yyyy".
*/
private String dateFormat;
/**
* Path pattern used for static resources.
*/
private String staticPathPattern = "/**";
public String getDateFormat() {
return this.dateFormat;
}
public void setDateFormat(String dateFormat) {
this.dateFormat = dateFormat;
}
public String getStaticPathPattern() {
return this.staticPathPattern;
}

@ -21,7 +21,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
@ -54,6 +53,7 @@ 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.autoconfigure.web.ResourceProperties.Strategy;
import org.springframework.boot.autoconfigure.web.format.WebConversionService;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter;
import org.springframework.boot.web.servlet.filter.OrderedHttpPutFormContentFilter;
@ -73,7 +73,7 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.datetime.DateFormatter;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.CacheControl;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
@ -266,12 +266,6 @@ public class WebMvcAutoConfiguration {
return localeResolver;
}
@Bean
@ConditionalOnProperty(prefix = "spring.mvc", name = "date-format")
public Formatter<Date> dateFormatter() {
return new DateFormatter(this.mvcProperties.getDateFormat());
}
@Override
public MessageCodesResolver getMessageCodesResolver() {
if (this.mvcProperties.getMessageCodesResolverFormat() != null) {
@ -478,6 +472,14 @@ public class WebMvcAutoConfiguration {
return super.requestMappingHandlerMapping();
}
@Bean
@Override
public FormattingConversionService mvcConversionService() {
WebConversionService conversionService = new WebConversionService(this.mvcProperties.getDateFormat());
addFormatters(conversionService);
return conversionService;
}
@Bean
@Override
public Validator mvcValidator() {

@ -0,0 +1,50 @@
/*
* 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.format;
import java.util.Date;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WebConversionService}.
*
* @author Brian Clozel
*/
public class WebConversionServiceTests {
@Test
public void customDateFormat() {
WebConversionService conversionService = new WebConversionService("dd*MM*yyyy");
Date date = new DateTime(2018, 1, 1, 20, 30).toDate();
assertThat(conversionService.convert(date, String.class))
.isEqualTo("01*01*2018");
LocalDate jodaDate = LocalDate.fromDateFields(date);
assertThat(conversionService.convert(jodaDate, String.class))
.isEqualTo("01*01*2018");
java.time.LocalDate localDate = java.time.LocalDate.of(2018, 1, 1);
assertThat(conversionService.convert(localDate, String.class))
.isEqualTo("01*01*2018");
}
}

@ -16,32 +16,29 @@
package org.springframework.boot.autoconfigure.web.reactive;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import javax.validation.ValidatorFactory;
import org.joda.time.DateTime;
import org.junit.Test;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
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.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.web.codec.CodecCustomizer;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.format.support.FormattingConversionService;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.reactive.HandlerMapping;
@ -72,240 +69,262 @@ import static org.mockito.Mockito.verify;
*/
public class WebFluxAutoConfigurationTests {
private AnnotationConfigReactiveWebApplicationContext context;
private static final MockReactiveWebServerFactory mockReactiveWebServerFactory
= new MockReactiveWebServerFactory();
private ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class))
.withUserConfiguration(Config.class);
@Test
public void shouldNotProcessIfExistingWebReactiveConfiguration() {
load(WebFluxConfigurationSupport.class);
assertThat(this.context.getBeansOfType(RequestMappingHandlerMapping.class).size())
.isEqualTo(1);
assertThat(this.context.getBeansOfType(RequestMappingHandlerAdapter.class).size())
.isEqualTo(1);
this.contextRunner.withUserConfiguration(WebFluxConfigurationSupport.class)
.run(context -> {
assertThat(context).getBeans(RequestMappingHandlerMapping.class).hasSize(1);
assertThat(context).getBeans(RequestMappingHandlerAdapter.class).hasSize(1);
});
}
@Test
public void shouldCreateDefaultBeans() {
load();
assertThat(this.context.getBeansOfType(RequestMappingHandlerMapping.class).size())
.isEqualTo(1);
assertThat(this.context.getBeansOfType(RequestMappingHandlerAdapter.class).size())
.isEqualTo(1);
assertThat(this.context.getBeansOfType(RequestedContentTypeResolver.class).size())
.isEqualTo(1);
assertThat(this.context.getBean("resourceHandlerMapping", HandlerMapping.class))
.isNotNull();
this.contextRunner.run(context -> {
assertThat(context).getBeans(RequestMappingHandlerMapping.class).hasSize(1);
assertThat(context).getBeans(RequestMappingHandlerAdapter.class).hasSize(1);
assertThat(context).getBeans(RequestedContentTypeResolver.class).hasSize(1);
assertThat(context.getBean("resourceHandlerMapping", HandlerMapping.class))
.isNotNull();
});
}
@SuppressWarnings("unchecked")
@Test
public void shouldRegisterCustomHandlerMethodArgumentResolver() {
load(CustomArgumentResolvers.class);
RequestMappingHandlerAdapter adapter = this.context
.getBean(RequestMappingHandlerAdapter.class);
List<HandlerMethodArgumentResolver> customResolvers = (List<HandlerMethodArgumentResolver>) ReflectionTestUtils
.getField(adapter.getArgumentResolverConfigurer(), "customResolvers");
assertThat(customResolvers).contains(
this.context.getBean("firstResolver",
HandlerMethodArgumentResolver.class),
this.context.getBean("secondResolver",
HandlerMethodArgumentResolver.class));
this.contextRunner.withUserConfiguration(CustomArgumentResolvers.class).run(context -> {
RequestMappingHandlerAdapter adapter = context
.getBean(RequestMappingHandlerAdapter.class);
List<HandlerMethodArgumentResolver> customResolvers =
(List<HandlerMethodArgumentResolver>) ReflectionTestUtils
.getField(adapter.getArgumentResolverConfigurer(), "customResolvers");
assertThat(customResolvers).contains(
context.getBean("firstResolver",
HandlerMethodArgumentResolver.class),
context.getBean("secondResolver",
HandlerMethodArgumentResolver.class));
});
}
@Test
public void shouldCustomizeCodecs() {
load(CustomCodecCustomizers.class);
CodecCustomizer codecCustomizer = this.context.getBean("firstCodecCustomizer",
CodecCustomizer.class);
assertThat(codecCustomizer).isNotNull();
verify(codecCustomizer).customize(any(ServerCodecConfigurer.class));
this.contextRunner.withUserConfiguration(CustomCodecCustomizers.class).run(context -> {
CodecCustomizer codecCustomizer = context.getBean("firstCodecCustomizer",
CodecCustomizer.class);
assertThat(codecCustomizer).isNotNull();
verify(codecCustomizer).customize(any(ServerCodecConfigurer.class));
});
}
@Test
public void shouldRegisterResourceHandlerMapping() {
load();
SimpleUrlHandlerMapping hm = this.context.getBean("resourceHandlerMapping",
SimpleUrlHandlerMapping.class);
assertThat(hm.getUrlMap().get("/**")).isInstanceOf(ResourceWebHandler.class);
ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap().get("/**");
assertThat(staticHandler.getLocations()).hasSize(4);
assertThat(hm.getUrlMap().get("/webjars/**"))
.isInstanceOf(ResourceWebHandler.class);
ResourceWebHandler webjarsHandler = (ResourceWebHandler) hm.getUrlMap()
.get("/webjars/**");
assertThat(webjarsHandler.getLocations()).hasSize(1);
assertThat(webjarsHandler.getLocations().get(0))
.isEqualTo(new ClassPathResource("/META-INF/resources/webjars/"));
this.contextRunner.run(context -> {
SimpleUrlHandlerMapping hm = context.getBean("resourceHandlerMapping",
SimpleUrlHandlerMapping.class);
assertThat(hm.getUrlMap().get("/**")).isInstanceOf(ResourceWebHandler.class);
ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap().get("/**");
assertThat(staticHandler.getLocations()).hasSize(4);
assertThat(hm.getUrlMap().get("/webjars/**"))
.isInstanceOf(ResourceWebHandler.class);
ResourceWebHandler webjarsHandler = (ResourceWebHandler) hm.getUrlMap()
.get("/webjars/**");
assertThat(webjarsHandler.getLocations()).hasSize(1);
assertThat(webjarsHandler.getLocations().get(0))
.isEqualTo(new ClassPathResource("/META-INF/resources/webjars/"));
});
}
@Test
public void shouldMapResourcesToCustomPath() {
load("spring.webflux.static-path-pattern:/static/**");
SimpleUrlHandlerMapping hm = this.context.getBean("resourceHandlerMapping",
SimpleUrlHandlerMapping.class);
assertThat(hm.getUrlMap().get("/static/**"))
.isInstanceOf(ResourceWebHandler.class);
ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap()
.get("/static/**");
assertThat(staticHandler.getLocations()).hasSize(4);
this.contextRunner.withPropertyValues("spring.webflux.static-path-pattern:/static/**")
.run(context -> {
SimpleUrlHandlerMapping hm = context.getBean("resourceHandlerMapping",
SimpleUrlHandlerMapping.class);
assertThat(hm.getUrlMap().get("/static/**"))
.isInstanceOf(ResourceWebHandler.class);
ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap()
.get("/static/**");
assertThat(staticHandler.getLocations()).hasSize(4);
});
}
@Test
public void shouldNotMapResourcesWhenDisabled() {
load("spring.resources.add-mappings:false");
assertThat(this.context.getBean("resourceHandlerMapping"))
.isNotInstanceOf(SimpleUrlHandlerMapping.class);
this.contextRunner.withPropertyValues("spring.resources.add-mappings:false")
.run(context -> {
assertThat(context.getBean("resourceHandlerMapping"))
.isNotInstanceOf(SimpleUrlHandlerMapping.class);
});
}
@Test
public void resourceHandlerChainEnabled() {
load("spring.resources.chain.enabled:true");
SimpleUrlHandlerMapping hm = this.context.getBean("resourceHandlerMapping",
SimpleUrlHandlerMapping.class);
assertThat(hm.getUrlMap().get("/**")).isInstanceOf(ResourceWebHandler.class);
ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap().get("/**");
assertThat(staticHandler.getResourceResolvers()).extractingResultOf("getClass")
.containsOnly(CachingResourceResolver.class, PathResourceResolver.class);
assertThat(staticHandler.getResourceTransformers()).extractingResultOf("getClass")
.containsOnly(CachingResourceTransformer.class);
this.contextRunner.withPropertyValues("spring.resources.chain.enabled:true")
.run(context -> {
SimpleUrlHandlerMapping hm = context.getBean("resourceHandlerMapping",
SimpleUrlHandlerMapping.class);
assertThat(hm.getUrlMap().get("/**")).isInstanceOf(ResourceWebHandler.class);
ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap().get("/**");
assertThat(staticHandler.getResourceResolvers()).extractingResultOf("getClass")
.containsOnly(CachingResourceResolver.class, PathResourceResolver.class);
assertThat(staticHandler.getResourceTransformers()).extractingResultOf("getClass")
.containsOnly(CachingResourceTransformer.class);
});
}
@Test
public void shouldRegisterViewResolvers() {
load(ViewResolvers.class);
ViewResolutionResultHandler resultHandler = this.context
.getBean(ViewResolutionResultHandler.class);
assertThat(resultHandler.getViewResolvers()).containsExactly(
this.context.getBean("aViewResolver", ViewResolver.class),
this.context.getBean("anotherViewResolver", ViewResolver.class));
this.contextRunner.withUserConfiguration(ViewResolvers.class).run(context -> {
ViewResolutionResultHandler resultHandler = context
.getBean(ViewResolutionResultHandler.class);
assertThat(resultHandler.getViewResolvers()).containsExactly(
context.getBean("aViewResolver", ViewResolver.class),
context.getBean("anotherViewResolver", ViewResolver.class));
});
}
@Test
public void noDateFormat() {
this.contextRunner.run((context) -> {
FormattingConversionService conversionService = context
.getBean(FormattingConversionService.class);
Date date = new DateTime(1988, 6, 25, 20, 30).toDate();
// formatting conversion service should use simple toString()
assertThat(conversionService.convert(date, String.class))
.isEqualTo(date.toString());
});
}
@Test
public void overrideDateFormat() {
this.contextRunner.withPropertyValues("spring.webflux.date-format:dd*MM*yyyy")
.run((context) -> {
FormattingConversionService conversionService = context
.getBean(FormattingConversionService.class);
Date date = new DateTime(1988, 6, 25, 20, 30).toDate();
assertThat(conversionService.convert(date, String.class))
.isEqualTo("25*06*1988");
});
}
@Test
public void validatorWhenNoValidatorShouldUseDefault() {
load(null, new Class<?>[] { ValidationAutoConfiguration.class });
assertThat(this.context.getBeansOfType(ValidatorFactory.class)).isEmpty();
assertThat(this.context.getBeansOfType(javax.validation.Validator.class))
.isEmpty();
String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class);
assertThat(springValidatorBeans).containsExactly("webFluxValidator");
this.contextRunner
.run(context -> {
assertThat(context).doesNotHaveBean(ValidatorFactory.class);
assertThat(context).doesNotHaveBean(javax.validation.Validator.class);
assertThat(context).getBeanNames(Validator.class).containsExactly("webFluxValidator");
});
}
@Test
public void validatorWhenNoCustomizationShouldUseAutoConfigured() {
load();
String[] jsrValidatorBeans = this.context
.getBeanNamesForType(javax.validation.Validator.class);
String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class);
assertThat(jsrValidatorBeans).containsExactly("defaultValidator");
assertThat(springValidatorBeans).containsExactly("defaultValidator",
"webFluxValidator");
Validator validator = this.context.getBean("webFluxValidator", Validator.class);
assertThat(validator).isInstanceOf(ValidatorAdapter.class);
Object defaultValidator = this.context.getBean("defaultValidator");
assertThat(((ValidatorAdapter) validator).getTarget()).isSameAs(defaultValidator);
// Primary Spring validator is the one used by WebFlux behind the scenes
assertThat(this.context.getBean(Validator.class)).isEqualTo(defaultValidator);
this.contextRunner.withConfiguration(
AutoConfigurations.of(ValidationAutoConfiguration.class))
.run(context -> {
assertThat(context).getBeanNames(javax.validation.Validator.class)
.containsExactly("defaultValidator");
assertThat(context).getBeanNames(Validator.class)
.containsExactlyInAnyOrder("defaultValidator", "webFluxValidator");
Validator validator = context.getBean("webFluxValidator", Validator.class);
assertThat(validator).isInstanceOf(ValidatorAdapter.class);
Object defaultValidator = context.getBean("defaultValidator");
assertThat(((ValidatorAdapter) validator).getTarget()).isSameAs(defaultValidator);
// Primary Spring validator is the one used by WebFlux behind the scenes
assertThat(context.getBean(Validator.class)).isEqualTo(defaultValidator);
});
}
@Test
public void validatorWithConfigurerShouldUseSpringValidator() {
load(ValidatorWebFluxConfigurer.class,
new Class<?>[] { ValidationAutoConfiguration.class });
assertThat(this.context.getBeansOfType(ValidatorFactory.class)).isEmpty();
assertThat(this.context.getBeansOfType(javax.validation.Validator.class))
.isEmpty();
String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class);
assertThat(springValidatorBeans).containsExactly("webFluxValidator");
assertThat(this.context.getBean("webFluxValidator")).isSameAs(
this.context.getBean(ValidatorWebFluxConfigurer.class).validator);
this.contextRunner.withUserConfiguration(ValidatorWebFluxConfigurer.class)
.run(context -> {
assertThat(context).doesNotHaveBean(ValidatorFactory.class);
assertThat(context).doesNotHaveBean(javax.validation.Validator.class);
assertThat(context).getBeanNames(Validator.class)
.containsOnly("webFluxValidator");
assertThat(context.getBean("webFluxValidator"))
.isSameAs(context.getBean(ValidatorWebFluxConfigurer.class).validator);
});
}
@Test
public void validatorWithConfigurerDoesNotExposeJsr303() {
load(ValidatorJsr303WebFluxConfigurer.class,
new Class<?>[] { ValidationAutoConfiguration.class });
assertThat(this.context.getBeansOfType(ValidatorFactory.class)).isEmpty();
assertThat(this.context.getBeansOfType(javax.validation.Validator.class))
.isEmpty();
String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class);
assertThat(springValidatorBeans).containsExactly("webFluxValidator");
Validator validator = this.context.getBean("webFluxValidator", Validator.class);
assertThat(validator).isInstanceOf(ValidatorAdapter.class);
assertThat(((ValidatorAdapter) validator).getTarget()).isSameAs(
this.context.getBean(ValidatorJsr303WebFluxConfigurer.class).validator);
this.contextRunner
.withUserConfiguration(ValidatorJsr303WebFluxConfigurer.class)
.run(context -> {
assertThat(context).doesNotHaveBean(ValidatorFactory.class);
assertThat(context).doesNotHaveBean(javax.validation.Validator.class);
assertThat(context).getBeanNames(Validator.class)
.containsOnly("webFluxValidator");
Validator validator = context.getBean("webFluxValidator",
Validator.class);
assertThat(validator).isInstanceOf(ValidatorAdapter.class);
assertThat(((ValidatorAdapter) validator).getTarget()).isSameAs(
context.getBean(ValidatorJsr303WebFluxConfigurer.class).validator);
});
}
@Test
public void validationCustomConfigurerTakesPrecedence() {
load(ValidatorWebFluxConfigurer.class);
assertThat(this.context.getBeansOfType(ValidatorFactory.class)).hasSize(1);
assertThat(this.context.getBeansOfType(javax.validation.Validator.class))
.hasSize(1);
String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class);
assertThat(springValidatorBeans).containsExactly("defaultValidator",
"webFluxValidator");
assertThat(this.context.getBean("webFluxValidator")).isSameAs(
this.context.getBean(ValidatorWebFluxConfigurer.class).validator);
// Primary Spring validator is the auto-configured one as the WebFlux one has been
// customized via a WebFluxConfigurer
assertThat(this.context.getBean(Validator.class))
.isEqualTo(this.context.getBean("defaultValidator"));
this.contextRunner
.withConfiguration(AutoConfigurations.of(ValidationAutoConfiguration.class))
.withUserConfiguration(ValidatorWebFluxConfigurer.class)
.run(context -> {
assertThat(context).getBeans(ValidatorFactory.class).hasSize(1);
assertThat(context).getBeans(javax.validation.Validator.class).hasSize(1);
assertThat(context).getBeanNames(Validator.class)
.containsExactlyInAnyOrder("defaultValidator", "webFluxValidator");
assertThat(context.getBean("webFluxValidator")).isSameAs(
context.getBean(ValidatorWebFluxConfigurer.class).validator);
// Primary Spring validator is the auto-configured one as the WebFlux one has been
// customized via a WebFluxConfigurer
assertThat(context.getBean(Validator.class))
.isEqualTo(context.getBean("defaultValidator"));
});
}
@Test
public void validatorWithCustomSpringValidatorIgnored() {
load(CustomSpringValidator.class);
String[] jsrValidatorBeans = this.context
.getBeanNamesForType(javax.validation.Validator.class);
String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class);
assertThat(jsrValidatorBeans).containsExactly("defaultValidator");
assertThat(springValidatorBeans).containsExactly("customValidator",
"defaultValidator", "webFluxValidator");
Validator validator = this.context.getBean("webFluxValidator", Validator.class);
assertThat(validator).isInstanceOf(ValidatorAdapter.class);
Object defaultValidator = this.context.getBean("defaultValidator");
assertThat(((ValidatorAdapter) validator).getTarget()).isSameAs(defaultValidator);
// Primary Spring validator is the one used by WebFlux behind the scenes
assertThat(this.context.getBean(Validator.class)).isEqualTo(defaultValidator);
this.contextRunner
.withConfiguration(AutoConfigurations.of(ValidationAutoConfiguration.class))
.withUserConfiguration(CustomSpringValidator.class)
.run(context -> {
assertThat(context).getBeanNames(javax.validation.Validator.class)
.containsExactly("defaultValidator");
assertThat(context).getBeanNames(Validator.class)
.containsExactlyInAnyOrder("customValidator", "defaultValidator", "webFluxValidator");
Validator validator = context.getBean("webFluxValidator", Validator.class);
assertThat(validator).isInstanceOf(ValidatorAdapter.class);
Object defaultValidator = context.getBean("defaultValidator");
assertThat(((ValidatorAdapter) validator).getTarget()).isSameAs(defaultValidator);
// Primary Spring validator is the one used by WebFlux behind the scenes
assertThat(context.getBean(Validator.class)).isEqualTo(defaultValidator);
});
}
@Test
public void validatorWithCustomJsr303ValidatorExposedAsSpringValidator() {
load(CustomJsr303Validator.class);
assertThat(this.context.getBeansOfType(ValidatorFactory.class)).isEmpty();
String[] jsrValidatorBeans = this.context
.getBeanNamesForType(javax.validation.Validator.class);
String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class);
assertThat(jsrValidatorBeans).containsExactly("customValidator");
assertThat(springValidatorBeans).containsExactly("webFluxValidator");
Validator validator = this.context.getBean(Validator.class);
assertThat(validator).isInstanceOf(ValidatorAdapter.class);
Validator target = ((ValidatorAdapter) validator).getTarget();
assertThat(new DirectFieldAccessor(target).getPropertyValue("targetValidator"))
.isSameAs(this.context.getBean("customValidator"));
}
private void load(String... environment) {
load(null, environment);
}
private void load(Class<?> config, String... environment) {
load(config, null, environment);
}
private void load(Class<?> config, Class<?>[] exclude, String... environment) {
this.context = new AnnotationConfigReactiveWebApplicationContext();
TestPropertyValues.of(environment).applyTo(this.context);
List<Class<?>> configClasses = new ArrayList<>();
if (config != null) {
configClasses.add(config);
}
configClasses.addAll(Arrays.asList(Config.class,
ValidationAutoConfiguration.class, BaseConfiguration.class));
if (!ObjectUtils.isEmpty(exclude)) {
configClasses.removeAll(Arrays.asList(exclude));
}
this.context.register(configClasses.toArray(new Class<?>[configClasses.size()]));
this.context.refresh();
this.contextRunner.withUserConfiguration(CustomJsr303Validator.class)
.run(context -> {
assertThat(context).doesNotHaveBean(ValidatorFactory.class);
assertThat(context).getBeanNames(javax.validation.Validator.class)
.containsExactly("customValidator");
assertThat(context).getBeanNames(Validator.class)
.containsExactly("webFluxValidator");
Validator validator = context.getBean(Validator.class);
assertThat(validator).isInstanceOf(ValidatorAdapter.class);
Validator target = ((ValidatorAdapter) validator).getTarget();
assertThat(new DirectFieldAccessor(target).getPropertyValue("targetValidator"))
.isSameAs(context.getBean("customValidator"));
});
}
@Configuration
@ -348,13 +367,11 @@ public class WebFluxAutoConfigurationTests {
}
@Configuration
@Import({ WebFluxAutoConfiguration.class })
@EnableConfigurationProperties(WebFluxProperties.class)
protected static class BaseConfiguration {
protected static class Config {
@Bean
public MockReactiveWebServerFactory mockReactiveWebServerFactory() {
return new MockReactiveWebServerFactory();
return mockReactiveWebServerFactory;
}
}

@ -465,7 +465,8 @@ content into your application. Rather, pick only the properties that you need.
spring.thymeleaf.template-resolver-order= # Order of the template resolver in the chain.
spring.thymeleaf.view-names= # Comma-separated list of view names that can be resolved.
# SPRING WEB FLUX ({sc-spring-boot-autoconfigure}/web/reactive/WebFluxProperties.{sc-ext}[WebFluxProperties])
# SPRING WEBFLUX ({sc-spring-boot-autoconfigure}/web/reactive/WebFluxProperties.{sc-ext}[WebFluxProperties])
spring.webflux.date-format= # Date format to use. For instance, `dd/MM/yyyy`.
spring.webflux.static-path-pattern=/** # Path pattern used for static resources.
# SPRING WEB SERVICES ({sc-spring-boot-autoconfigure}/webservices/WebServicesProperties.{sc-ext}[WebServicesProperties])

Loading…
Cancel
Save