diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java index 901f621a93..cd3aea64b5 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java @@ -30,6 +30,7 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.format.Formatter; /** * Utility to deduce the {@link ConversionService} to use for configuration properties @@ -62,9 +63,13 @@ class ConversionServiceDeducer { private final List genericConverters; + @SuppressWarnings("rawtypes") + private final List formatters; + Factory(BeanFactory beanFactory) { this.converters = beans(beanFactory, Converter.class, ConfigurationPropertiesBinding.VALUE); this.genericConverters = beans(beanFactory, GenericConverter.class, ConfigurationPropertiesBinding.VALUE); + this.formatters = beans(beanFactory, Formatter.class, ConfigurationPropertiesBinding.VALUE); } private List beans(BeanFactory beanFactory, Class type, String qualifier) { @@ -80,7 +85,7 @@ class ConversionServiceDeducer { } ConversionService create() { - if (this.converters.isEmpty() && this.genericConverters.isEmpty()) { + if (this.converters.isEmpty() && this.genericConverters.isEmpty() && this.formatters.isEmpty()) { return ApplicationConversionService.getSharedInstance(); } ApplicationConversionService conversionService = new ApplicationConversionService(); @@ -90,6 +95,9 @@ class ConversionServiceDeducer { for (GenericConverter genericConverter : this.genericConverters) { conversionService.addConverter(genericConverter); } + for (Formatter formatter : this.formatters) { + conversionService.addFormatter(formatter); + } return conversionService; } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java index 3cc651c200..e9e1e635e1 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java @@ -18,12 +18,14 @@ package org.springframework.boot.context.properties; import java.beans.PropertyEditorSupport; import java.io.File; +import java.text.ParseException; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; @@ -73,6 +75,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ProtocolResolver; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; +import org.springframework.format.Formatter; import org.springframework.mock.env.MockEnvironment; import org.springframework.stereotype.Component; import org.springframework.test.context.support.TestPropertySourceUtils; @@ -609,7 +612,7 @@ class ConfigurationPropertiesTests { } @Test - void loadShouldUseConfigurationConverter() { + void loadShouldUseConverterBean() { prepareConverterContext(ConverterConfiguration.class, PersonProperties.class); Person person = this.context.getBean(PersonProperties.class).getPerson(); assertThat(person.firstName).isEqualTo("John"); @@ -625,13 +628,21 @@ class ConfigurationPropertiesTests { } @Test - void loadShouldUseGenericConfigurationConverter() { + void loadShouldUseGenericConverterBean() { prepareConverterContext(GenericConverterConfiguration.class, PersonProperties.class); Person person = this.context.getBean(PersonProperties.class).getPerson(); assertThat(person.firstName).isEqualTo("John"); assertThat(person.lastName).isEqualTo("Smith"); } + @Test + void loadShouldUseFormatterBean() { + prepareConverterContext(FormatterConfiguration.class, PersonProperties.class); + Person person = this.context.getBean(PersonProperties.class).getPerson(); + assertThat(person.firstName).isEqualTo("John"); + assertThat(person.lastName).isEqualTo("Smith"); + } + @Test void loadWhenGenericConfigurationConverterIsNotQualifiedShouldNotConvert() { assertThatExceptionOfType(BeanCreationException.class).isThrownBy( @@ -1246,6 +1257,17 @@ class ConfigurationPropertiesTests { } + @Configuration(proxyBeanMethods = false) + static class FormatterConfiguration { + + @Bean + @ConfigurationPropertiesBinding + Formatter personFormatter() { + return new PersonFormatter(); + } + + } + @Configuration(proxyBeanMethods = false) static class NonQualifiedGenericConverterConfiguration { @@ -2011,12 +2033,27 @@ class ConfigurationPropertiesTests { } + static class PersonFormatter implements Formatter { + + @Override + public String print(Person person, Locale locale) { + return person.getFirstName() + " " + person.getLastName(); + } + + @Override + public Person parse(String text, Locale locale) throws ParseException { + String[] content = text.split(" "); + return new Person(content[0], content[1]); + } + + } + static class PersonPropertyEditor extends PropertyEditorSupport { @Override public void setAsText(String text) throws IllegalArgumentException { - String[] split = text.split(","); - setValue(new Person(split[1], split[0])); + String[] content = text.split(","); + setValue(new Person(content[1], content[0])); } } @@ -2032,6 +2069,14 @@ class ConfigurationPropertiesTests { this.lastName = lastName; } + String getFirstName() { + return this.firstName; + } + + String getLastName() { + return this.lastName; + } + } static class Foo {