diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java index 920a62f16a6..c7ebde4a345 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java @@ -17,7 +17,6 @@ package org.springframework.boot.autoconfigure.web.reactive; import java.time.Duration; -import java.util.Collection; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -38,15 +37,13 @@ import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceCh 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.convert.ApplicationConversionService; import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.boot.web.reactive.filter.OrderedHiddenHttpMethodFilter; 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.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.codec.ServerCodecConfigurer; @@ -176,26 +173,13 @@ public class WebFluxAutoConfiguration { @Override public void addFormatters(FormatterRegistry registry) { - for (Converter converter : getBeansOfType(Converter.class)) { - registry.addConverter(converter); - } - for (GenericConverter converter : getBeansOfType(GenericConverter.class)) { - registry.addConverter(converter); - } - for (Formatter formatter : getBeansOfType(Formatter.class)) { - registry.addFormatter(formatter); - } - } - - private Collection getBeansOfType(Class type) { - return this.beanFactory.getBeansOfType(type).values(); + ApplicationConversionService.addBeans(registry, this.beanFactory); } private void customizeResourceHandlerRegistration(ResourceHandlerRegistration registration) { if (this.resourceHandlerRegistrationCustomizer != null) { this.resourceHandlerRegistrationCustomizer.customize(registration); } - } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java index 07e0d7ecf6f..b742edc6cdf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java @@ -19,7 +19,6 @@ package org.springframework.boot.autoconfigure.web.servlet; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.ListIterator; @@ -55,6 +54,7 @@ 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.convert.ApplicationConversionService; import org.springframework.boot.web.servlet.filter.OrderedFormContentFilter; import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter; import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter; @@ -66,13 +66,10 @@ import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Primary; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; -import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.task.AsyncTaskExecutor; -import org.springframework.format.Formatter; import org.springframework.format.FormatterRegistry; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.CacheControl; @@ -298,19 +295,7 @@ public class WebMvcAutoConfiguration { @Override public void addFormatters(FormatterRegistry registry) { - for (Converter converter : getBeansOfType(Converter.class)) { - registry.addConverter(converter); - } - for (GenericConverter converter : getBeansOfType(GenericConverter.class)) { - registry.addConverter(converter); - } - for (Formatter formatter : getBeansOfType(Formatter.class)) { - registry.addFormatter(formatter); - } - } - - private Collection getBeansOfType(Class type) { - return this.beanFactory.getBeansOfType(type).values(); + ApplicationConversionService.addBeans(registry, this.beanFactory); } @Override diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index 63cff395959..3a67b946304 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.web.reactive; import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -40,7 +41,10 @@ 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.convert.ConversionService; import org.springframework.core.io.ClassPathResource; +import org.springframework.format.Parser; +import org.springframework.format.Printer; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.CacheControl; import org.springframework.http.codec.ServerCodecConfigurer; @@ -373,6 +377,16 @@ class WebFluxAutoConfigurationTests { Assertions.setExtractBareNamePropertyMethods(true); } + @Test + void customPrinterAndParserShouldBeRegisteredAsConverters() { + this.contextRunner.withUserConfiguration(ParserConfiguration.class, PrinterConfiguration.class) + .run((context) -> { + ConversionService service = context.getBean(ConversionService.class); + assertThat(service.convert(new Example("spring", new Date()), String.class)).isEqualTo("spring"); + assertThat(service.convert("boot", Example.class)).extracting(Example::getName).isEqualTo("boot"); + }); + } + private Map getHandlerMap(ApplicationContext context) { HandlerMapping mapping = context.getBean("resourceHandlerMapping", HandlerMapping.class); if (mapping instanceof SimpleUrlHandlerMapping) { @@ -545,4 +559,56 @@ class WebFluxAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class PrinterConfiguration { + + @Bean + public Printer examplePrinter() { + return new ExamplePrinter(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ParserConfiguration { + + @Bean + public Parser exampleParser() { + return new ExampleParser(); + } + + } + + static final class Example { + + private final String name; + + private Example(String name, Date date) { + this.name = name; + } + + public String getName() { + return this.name; + } + + } + + private static class ExamplePrinter implements Printer { + + @Override + public String print(Example example, Locale locale) { + return example.getName(); + } + + } + + private static class ExampleParser implements Parser { + + @Override + public Example parse(String source, Locale locale) { + return new Example(source, new Date()); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java index ab7ee064f50..dfb97202fda 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java @@ -56,6 +56,8 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.format.Parser; +import org.springframework.format.Printer; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; @@ -773,6 +775,16 @@ class WebMvcAutoConfigurationTests { }); } + @Test + void customPrinterAndParserShouldBeRegisteredAsConverters() { + this.contextRunner.withUserConfiguration(ParserConfiguration.class, PrinterConfiguration.class) + .run((context) -> { + ConversionService service = context.getBean(ConversionService.class); + assertThat(service.convert(new Example("spring", new Date()), String.class)).isEqualTo("spring"); + assertThat(service.convert("boot", Example.class)).extracting(Example::getName).isEqualTo("boot"); + }); + } + private void assertCacheControl(AssertableWebApplicationContext context) { Map handlerMap = getHandlerMap(context.getBean("resourceHandlerMapping", HandlerMapping.class)); assertThat(handlerMap).hasSize(2); @@ -1093,4 +1105,56 @@ class WebMvcAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class PrinterConfiguration { + + @Bean + public Printer examplePrinter() { + return new ExamplePrinter(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ParserConfiguration { + + @Bean + public Parser exampleParser() { + return new ExampleParser(); + } + + } + + static final class Example { + + private final String name; + + private Example(String name, Date date) { + this.name = name; + } + + public String getName() { + return this.name; + } + + } + + private static class ExamplePrinter implements Printer { + + @Override + public String print(Example example, Locale locale) { + return example.getName(); + } + + } + + private static class ExampleParser implements Parser { + + @Override + public Example parse(String source, Locale locale) { + return new Example(source, new Date()); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java index 959745ef343..15e059a52c6 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-2019 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. @@ -16,11 +16,20 @@ package org.springframework.boot.convert; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.format.Formatter; import org.springframework.format.FormatterRegistry; +import org.springframework.format.Parser; +import org.springframework.format.Printer; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.FormattingConversionService; import org.springframework.util.StringValueResolver; @@ -134,4 +143,36 @@ public class ApplicationConversionService extends FormattingConversionService { registry.addFormatter(new IsoOffsetFormatter()); } + /** + * Add {@link GenericConverter}, {@link Converter}, {@link Printer}, {@link Parser} + * and {@link Formatter} beans from the specified context. + * @param registry the service to register beans with + * @param beanFactory the bean factory to get the beans from + * @since 2.2.0 + */ + public static void addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory) { + Set beans = new LinkedHashSet<>(); + beans.addAll(beanFactory.getBeansOfType(GenericConverter.class).values()); + beans.addAll(beanFactory.getBeansOfType(Converter.class).values()); + beans.addAll(beanFactory.getBeansOfType(Printer.class).values()); + beans.addAll(beanFactory.getBeansOfType(Parser.class).values()); + for (Object bean : beans) { + if (bean instanceof GenericConverter) { + registry.addConverter((GenericConverter) bean); + } + else if (bean instanceof Converter) { + registry.addConverter((Converter) bean); + } + else if (bean instanceof Formatter) { + registry.addFormatter((Formatter) bean); + } + else if (bean instanceof Printer) { + registry.addPrinter((Printer) bean); + } + else if (bean instanceof Parser) { + registry.addParser((Parser) bean); + } + } + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ApplicationConversionServiceTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ApplicationConversionServiceTests.java new file mode 100644 index 00000000000..0fae7814a15 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ApplicationConversionServiceTests.java @@ -0,0 +1,149 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.convert; + +import java.text.ParseException; +import java.util.Locale; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.convert.TypeDescriptor; +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.Parser; +import org.springframework.format.Printer; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Tests for {@link ApplicationConversionService}. + * + * @author Phillip Webb + */ +class ApplicationConversionServiceTests { + + private FormatterRegistry registry = mock(FormatterRegistry.class); + + @Test + void addBeansWhenHasGenericConverterBeanAddConverter() { + try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( + ExampleGenericConverter.class)) { + ApplicationConversionService.addBeans(this.registry, context); + verify(this.registry).addConverter(context.getBean(ExampleGenericConverter.class)); + verifyNoMoreInteractions(this.registry); + } + } + + @Test + void addBeansWhenHasConverterBeanAddConverter() { + try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(ExampleConverter.class)) { + ApplicationConversionService.addBeans(this.registry, context); + verify(this.registry).addConverter(context.getBean(ExampleConverter.class)); + verifyNoMoreInteractions(this.registry); + } + } + + @Test + void addBeansWhenHasFormatterBeanAddsOnlyFormatter() { + try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(ExampleFormatter.class)) { + ApplicationConversionService.addBeans(this.registry, context); + verify(this.registry).addFormatter(context.getBean(ExampleFormatter.class)); + verifyNoMoreInteractions(this.registry); + } + } + + @Test + void addBeansWhenHasPrinterBeanAddPrinter() { + try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(ExamplePrinter.class)) { + ApplicationConversionService.addBeans(this.registry, context); + verify(this.registry).addPrinter(context.getBean(ExamplePrinter.class)); + verifyNoMoreInteractions(this.registry); + } + } + + @Test + void addBeansWhenHasParserBeanAddParser() { + try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(ExampleParser.class)) { + ApplicationConversionService.addBeans(this.registry, context); + verify(this.registry).addParser(context.getBean(ExampleParser.class)); + verifyNoMoreInteractions(this.registry); + } + } + + private static class ExampleGenericConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + return null; + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return null; + } + + } + + private static class ExampleConverter implements Converter { + + @Override + public Integer convert(String source) { + return null; + } + + } + + private static class ExampleFormatter implements Formatter { + + @Override + public String print(Integer object, Locale locale) { + return null; + } + + @Override + public Integer parse(String text, Locale locale) throws ParseException { + return null; + } + + } + + private static class ExampleParser implements Parser { + + @Override + public Integer parse(String text, Locale locale) throws ParseException { + return null; + } + + } + + private static class ExamplePrinter implements Printer { + + @Override + public String print(Integer object, Locale locale) { + return null; + } + + } + +}