Commit 51c3e180 authored by Phillip Webb's avatar Phillip Webb

Support binding with BeanFactory ConversionService

Update `BindConverter` so that multiple `ConverterServices` can be
specified when binding. This change allows `ConversionServiceDeducer`
to add both the `BeanFactory` conversion service as well as a
custom `ApplicationConversionService` when beans annotated with
`@ConfigurationPropertiesBinding` are found.

Fixes gh-26089
parent 5581ec0e
...@@ -166,7 +166,7 @@ class ConfigurationPropertiesBinder { ...@@ -166,7 +166,7 @@ class ConfigurationPropertiesBinder {
private Binder getBinder() { private Binder getBinder() {
if (this.binder == null) { if (this.binder == null) {
this.binder = new Binder(getConfigurationPropertySources(), getPropertySourcesPlaceholdersResolver(), this.binder = new Binder(getConfigurationPropertySources(), getPropertySourcesPlaceholdersResolver(),
getConversionService(), getPropertyEditorInitializer(), null, getConversionServices(), getPropertyEditorInitializer(), null,
ConfigurationPropertiesBindConstructorProvider.INSTANCE); ConfigurationPropertiesBindConstructorProvider.INSTANCE);
} }
return this.binder; return this.binder;
...@@ -180,8 +180,8 @@ class ConfigurationPropertiesBinder { ...@@ -180,8 +180,8 @@ class ConfigurationPropertiesBinder {
return new PropertySourcesPlaceholdersResolver(this.propertySources); return new PropertySourcesPlaceholdersResolver(this.propertySources);
} }
private ConversionService getConversionService() { private List<ConversionService> getConversionServices() {
return new ConversionServiceDeducer(this.applicationContext).getConversionService(); return new ConversionServiceDeducer(this.applicationContext).getConversionServices();
} }
private Consumer<PropertyEditorRegistry> getPropertyEditorInitializer() { private Consumer<PropertyEditorRegistry> getPropertyEditorInitializer() {
......
/* /*
* Copyright 2012-2020 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -20,10 +20,9 @@ import java.util.ArrayList; ...@@ -20,10 +20,9 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.convert.ApplicationConversionService; import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
...@@ -31,6 +30,7 @@ import org.springframework.core.convert.ConversionService; ...@@ -31,6 +30,7 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.format.Formatter; import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry;
/** /**
* Utility to deduce the {@link ConversionService} to use for configuration properties * Utility to deduce the {@link ConversionService} to use for configuration properties
...@@ -46,17 +46,38 @@ class ConversionServiceDeducer { ...@@ -46,17 +46,38 @@ class ConversionServiceDeducer {
this.applicationContext = applicationContext; this.applicationContext = applicationContext;
} }
ConversionService getConversionService() { List<ConversionService> getConversionServices() {
try { if (hasUserDefinedConfigurationServiceBean()) {
return this.applicationContext.getBean(ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME, return Collections.singletonList(this.applicationContext
ConversionService.class); .getBean(ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
} }
catch (NoSuchBeanDefinitionException ex) { if (this.applicationContext instanceof ConfigurableApplicationContext) {
return new Factory(this.applicationContext.getAutowireCapableBeanFactory()).create(); return getConversionServices((ConfigurableApplicationContext) this.applicationContext);
} }
return null;
} }
private static class Factory { private List<ConversionService> getConversionServices(ConfigurableApplicationContext applicationContext) {
List<ConversionService> conversionServices = new ArrayList<>();
if (applicationContext.getBeanFactory().getConversionService() != null) {
conversionServices.add(applicationContext.getBeanFactory().getConversionService());
}
ConverterBeans converterBeans = new ConverterBeans(applicationContext);
if (!converterBeans.isEmpty()) {
ApplicationConversionService beansConverterService = new ApplicationConversionService();
converterBeans.addTo(beansConverterService);
conversionServices.add(beansConverterService);
}
return conversionServices;
}
private boolean hasUserDefinedConfigurationServiceBean() {
String beanName = ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME;
return this.applicationContext.containsBean(beanName) && this.applicationContext.getAutowireCapableBeanFactory()
.isTypeMatch(beanName, ConversionService.class);
}
private static class ConverterBeans {
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
private final List<Converter> converters; private final List<Converter> converters;
...@@ -66,17 +87,11 @@ class ConversionServiceDeducer { ...@@ -66,17 +87,11 @@ class ConversionServiceDeducer {
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
private final List<Formatter> formatters; private final List<Formatter> formatters;
Factory(BeanFactory beanFactory) { ConverterBeans(ConfigurableApplicationContext applicationContext) {
this.converters = beans(beanFactory, Converter.class, ConfigurationPropertiesBinding.VALUE); ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
this.genericConverters = beans(beanFactory, GenericConverter.class, ConfigurationPropertiesBinding.VALUE); this.converters = beans(Converter.class, ConfigurationPropertiesBinding.VALUE, beanFactory);
this.formatters = beans(beanFactory, Formatter.class, ConfigurationPropertiesBinding.VALUE); this.genericConverters = beans(GenericConverter.class, ConfigurationPropertiesBinding.VALUE, beanFactory);
} this.formatters = beans(Formatter.class, ConfigurationPropertiesBinding.VALUE, beanFactory);
private <T> List<T> beans(BeanFactory beanFactory, Class<T> type, String qualifier) {
if (beanFactory instanceof ListableBeanFactory) {
return beans(type, qualifier, (ListableBeanFactory) beanFactory);
}
return Collections.emptyList();
} }
private <T> List<T> beans(Class<T> type, String qualifier, ListableBeanFactory beanFactory) { private <T> List<T> beans(Class<T> type, String qualifier, ListableBeanFactory beanFactory) {
...@@ -84,21 +99,20 @@ class ConversionServiceDeducer { ...@@ -84,21 +99,20 @@ class ConversionServiceDeducer {
BeanFactoryAnnotationUtils.qualifiedBeansOfType(beanFactory, type, qualifier).values()); BeanFactoryAnnotationUtils.qualifiedBeansOfType(beanFactory, type, qualifier).values());
} }
ConversionService create() { boolean isEmpty() {
if (this.converters.isEmpty() && this.genericConverters.isEmpty() && this.formatters.isEmpty()) { return this.converters.isEmpty() && this.genericConverters.isEmpty() && this.formatters.isEmpty();
return ApplicationConversionService.getSharedInstance();
} }
ApplicationConversionService conversionService = new ApplicationConversionService();
void addTo(FormatterRegistry registry) {
for (Converter<?, ?> converter : this.converters) { for (Converter<?, ?> converter : this.converters) {
conversionService.addConverter(converter); registry.addConverter(converter);
} }
for (GenericConverter genericConverter : this.genericConverters) { for (GenericConverter genericConverter : this.genericConverters) {
conversionService.addConverter(genericConverter); registry.addConverter(genericConverter);
} }
for (Formatter<?> formatter : this.formatters) { for (Formatter<?> formatter : this.formatters) {
conversionService.addFormatter(formatter); registry.addFormatter(formatter);
} }
return conversionService;
} }
} }
......
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -38,7 +38,7 @@ import org.springframework.core.convert.ConversionService; ...@@ -38,7 +38,7 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalGenericConverter; import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.util.Assert; import org.springframework.util.CollectionUtils;
/** /**
* Utility to handle any conversion needed during binding. This class is not thread-safe * Utility to handle any conversion needed during binding. This class is not thread-safe
...@@ -49,97 +49,33 @@ import org.springframework.util.Assert; ...@@ -49,97 +49,33 @@ import org.springframework.util.Assert;
*/ */
final class BindConverter { final class BindConverter {
private static final Set<Class<?>> EXCLUDED_EDITORS;
static {
Set<Class<?>> excluded = new HashSet<>();
excluded.add(FileEditor.class); // gh-12163
EXCLUDED_EDITORS = Collections.unmodifiableSet(excluded);
}
private static BindConverter sharedInstance; private static BindConverter sharedInstance;
private final ConversionService conversionService; private final List<ConversionService> delegates;
private BindConverter(ConversionService conversionService,
Consumer<PropertyEditorRegistry> propertyEditorInitializer) {
Assert.notNull(conversionService, "ConversionService must not be null");
List<ConversionService> conversionServices = getConversionServices(conversionService,
propertyEditorInitializer);
this.conversionService = new CompositeConversionService(conversionServices);
}
private List<ConversionService> getConversionServices(ConversionService conversionService,
Consumer<PropertyEditorRegistry> propertyEditorInitializer) {
List<ConversionService> services = new ArrayList<>();
services.add(new TypeConverterConversionService(propertyEditorInitializer));
services.add(conversionService);
if (!(conversionService instanceof ApplicationConversionService)) {
services.add(ApplicationConversionService.getSharedInstance());
}
return services;
}
boolean canConvert(Object value, ResolvableType type, Annotation... annotations) {
return this.conversionService.canConvert(TypeDescriptor.forObject(value),
new ResolvableTypeDescriptor(type, annotations));
}
<T> T convert(Object result, Bindable<T> target) {
return convert(result, target.getType(), target.getAnnotations());
}
@SuppressWarnings("unchecked")
<T> T convert(Object value, ResolvableType type, Annotation... annotations) {
if (value == null) {
return null;
}
return (T) this.conversionService.convert(value, TypeDescriptor.forObject(value),
new ResolvableTypeDescriptor(type, annotations));
}
static BindConverter get(ConversionService conversionService, private BindConverter(List<ConversionService> conversionServices,
Consumer<PropertyEditorRegistry> propertyEditorInitializer) { Consumer<PropertyEditorRegistry> propertyEditorInitializer) {
if (conversionService == ApplicationConversionService.getSharedInstance() List<ConversionService> delegates = new ArrayList<>();
&& propertyEditorInitializer == null) { delegates.add(new TypeConverterConversionService(propertyEditorInitializer));
if (sharedInstance == null) { boolean hasApplication = false;
sharedInstance = new BindConverter(conversionService, propertyEditorInitializer); if (!CollectionUtils.isEmpty(conversionServices)) {
for (ConversionService conversionService : conversionServices) {
delegates.add(conversionService);
hasApplication = hasApplication || conversionService instanceof ApplicationConversionService;
} }
return sharedInstance;
} }
return new BindConverter(conversionService, propertyEditorInitializer); if (!hasApplication) {
delegates.add(ApplicationConversionService.getSharedInstance());
} }
this.delegates = Collections.unmodifiableList(delegates);
/**
* A {@link TypeDescriptor} backed by a {@link ResolvableType}.
*/
private static class ResolvableTypeDescriptor extends TypeDescriptor {
ResolvableTypeDescriptor(ResolvableType resolvableType, Annotation[] annotations) {
super(resolvableType, null, annotations);
}
}
/**
* Composite {@link ConversionService} used to call multiple services.
*/
static class CompositeConversionService implements ConversionService {
private final List<ConversionService> delegates;
CompositeConversionService(List<ConversionService> delegates) {
this.delegates = delegates;
} }
@Override boolean canConvert(Object source, ResolvableType targetType, Annotation... targetAnnotations) {
public boolean canConvert(Class<?> sourceType, Class<?> targetType) { return canConvert(TypeDescriptor.forObject(source),
Assert.notNull(targetType, "Target type to convert to cannot be null"); new ResolvableTypeDescriptor(targetType, targetAnnotations));
return canConvert((sourceType != null) ? TypeDescriptor.valueOf(sourceType) : null,
TypeDescriptor.valueOf(targetType));
} }
@Override private boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
for (ConversionService service : this.delegates) { for (ConversionService service : this.delegates) {
if (service.canConvert(sourceType, targetType)) { if (service.canConvert(sourceType, targetType)) {
return true; return true;
...@@ -148,15 +84,20 @@ final class BindConverter { ...@@ -148,15 +84,20 @@ final class BindConverter {
return false; return false;
} }
@Override <T> T convert(Object source, Bindable<T> target) {
return convert(source, target.getType(), target.getAnnotations());
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <T> T convert(Object source, Class<T> targetType) { <T> T convert(Object source, ResolvableType targetType, Annotation... targetAnnotations) {
Assert.notNull(targetType, "Target type to convert to cannot be null"); if (source == null) {
return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType)); return null;
}
return (T) convert(source, TypeDescriptor.forObject(source),
new ResolvableTypeDescriptor(targetType, targetAnnotations));
} }
@Override private Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
for (int i = 0; i < this.delegates.size() - 1; i++) { for (int i = 0; i < this.delegates.size() - 1; i++) {
try { try {
ConversionService delegate = this.delegates.get(i); ConversionService delegate = this.delegates.get(i);
...@@ -170,6 +111,32 @@ final class BindConverter { ...@@ -170,6 +111,32 @@ final class BindConverter {
return this.delegates.get(this.delegates.size() - 1).convert(source, sourceType, targetType); return this.delegates.get(this.delegates.size() - 1).convert(source, sourceType, targetType);
} }
static BindConverter get(List<ConversionService> conversionServices,
Consumer<PropertyEditorRegistry> propertyEditorInitializer) {
boolean sharedApplicationConversionService = (conversionServices == null) || (conversionServices.size() == 1
&& conversionServices.get(0) == ApplicationConversionService.getSharedInstance());
if (propertyEditorInitializer == null && sharedApplicationConversionService) {
return getSharedInstance();
}
return new BindConverter(conversionServices, propertyEditorInitializer);
}
private static BindConverter getSharedInstance() {
if (sharedInstance == null) {
sharedInstance = new BindConverter(null, null);
}
return sharedInstance;
}
/**
* A {@link TypeDescriptor} backed by a {@link ResolvableType}.
*/
private static class ResolvableTypeDescriptor extends TypeDescriptor {
ResolvableTypeDescriptor(ResolvableType resolvableType, Annotation[] annotations) {
super(resolvableType, null, annotations);
}
} }
/** /**
...@@ -208,6 +175,13 @@ final class BindConverter { ...@@ -208,6 +175,13 @@ final class BindConverter {
*/ */
private static class TypeConverterConverter implements ConditionalGenericConverter { private static class TypeConverterConverter implements ConditionalGenericConverter {
private static final Set<Class<?>> EXCLUDED_EDITORS;
static {
Set<Class<?>> excluded = new HashSet<>();
excluded.add(FileEditor.class); // gh-12163
EXCLUDED_EDITORS = Collections.unmodifiableSet(excluded);
}
private final SimpleTypeConverter typeConverter; private final SimpleTypeConverter typeConverter;
TypeConverterConverter(SimpleTypeConverter typeConverter) { TypeConverterConverter(SimpleTypeConverter typeConverter) {
......
...@@ -60,9 +60,7 @@ public class Binder { ...@@ -60,9 +60,7 @@ public class Binder {
private final PlaceholdersResolver placeholdersResolver; private final PlaceholdersResolver placeholdersResolver;
private final ConversionService conversionService; private final BindConverter bindConverter;
private final Consumer<PropertyEditorRegistry> propertyEditorInitializer;
private final BindHandler defaultBindHandler; private final BindHandler defaultBindHandler;
...@@ -159,12 +157,34 @@ public class Binder { ...@@ -159,12 +157,34 @@ public class Binder {
public Binder(Iterable<ConfigurationPropertySource> sources, PlaceholdersResolver placeholdersResolver, public Binder(Iterable<ConfigurationPropertySource> sources, PlaceholdersResolver placeholdersResolver,
ConversionService conversionService, Consumer<PropertyEditorRegistry> propertyEditorInitializer, ConversionService conversionService, Consumer<PropertyEditorRegistry> propertyEditorInitializer,
BindHandler defaultBindHandler, BindConstructorProvider constructorProvider) { BindHandler defaultBindHandler, BindConstructorProvider constructorProvider) {
this(sources, placeholdersResolver,
(conversionService != null) ? Collections.singletonList(conversionService)
: (List<ConversionService>) null,
propertyEditorInitializer, defaultBindHandler, constructorProvider);
}
/**
* Create a new {@link Binder} instance for the specified sources.
* @param sources the sources used for binding
* @param placeholdersResolver strategy to resolve any property placeholders
* @param conversionServices the conversion services to convert values (or
* {@code null} to use {@link ApplicationConversionService})
* @param propertyEditorInitializer initializer used to configure the property editors
* that can convert values (or {@code null} if no initialization is required). Often
* used to call {@link ConfigurableListableBeanFactory#copyRegisteredEditorsTo}.
* @param defaultBindHandler the default bind handler to use if none is specified when
* binding
* @param constructorProvider the constructor provider which provides the bind
* constructor to use when binding
* @since 2.5.0
*/
public Binder(Iterable<ConfigurationPropertySource> sources, PlaceholdersResolver placeholdersResolver,
List<ConversionService> conversionServices, Consumer<PropertyEditorRegistry> propertyEditorInitializer,
BindHandler defaultBindHandler, BindConstructorProvider constructorProvider) {
Assert.notNull(sources, "Sources must not be null"); Assert.notNull(sources, "Sources must not be null");
this.sources = sources; this.sources = sources;
this.placeholdersResolver = (placeholdersResolver != null) ? placeholdersResolver : PlaceholdersResolver.NONE; this.placeholdersResolver = (placeholdersResolver != null) ? placeholdersResolver : PlaceholdersResolver.NONE;
this.conversionService = (conversionService != null) ? conversionService this.bindConverter = BindConverter.get(conversionServices, propertyEditorInitializer);
: ApplicationConversionService.getSharedInstance();
this.propertyEditorInitializer = propertyEditorInitializer;
this.defaultBindHandler = (defaultBindHandler != null) ? defaultBindHandler : BindHandler.DEFAULT; this.defaultBindHandler = (defaultBindHandler != null) ? defaultBindHandler : BindHandler.DEFAULT;
if (constructorProvider == null) { if (constructorProvider == null) {
constructorProvider = BindConstructorProvider.DEFAULT; constructorProvider = BindConstructorProvider.DEFAULT;
...@@ -513,8 +533,6 @@ public class Binder { ...@@ -513,8 +533,6 @@ public class Binder {
*/ */
final class Context implements BindContext { final class Context implements BindContext {
private final BindConverter converter;
private int depth; private int depth;
private final List<ConfigurationPropertySource> source = Arrays.asList((ConfigurationPropertySource) null); private final List<ConfigurationPropertySource> source = Arrays.asList((ConfigurationPropertySource) null);
...@@ -527,10 +545,6 @@ public class Binder { ...@@ -527,10 +545,6 @@ public class Binder {
private ConfigurationProperty configurationProperty; private ConfigurationProperty configurationProperty;
Context() {
this.converter = BindConverter.get(Binder.this.conversionService, Binder.this.propertyEditorInitializer);
}
private void increaseDepth() { private void increaseDepth() {
this.depth++; this.depth++;
} }
...@@ -602,7 +616,7 @@ public class Binder { ...@@ -602,7 +616,7 @@ public class Binder {
} }
BindConverter getConverter() { BindConverter getConverter() {
return this.converter; return Binder.this.bindConverter;
} }
@Override @Override
......
...@@ -76,6 +76,7 @@ import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; ...@@ -76,6 +76,7 @@ import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.StandardEnvironment; import org.springframework.core.env.StandardEnvironment;
...@@ -645,6 +646,20 @@ class ConfigurationPropertiesTests { ...@@ -645,6 +646,20 @@ class ConfigurationPropertiesTests {
assertThat(person.lastName).isEqualTo("Smith"); assertThat(person.lastName).isEqualTo("Smith");
} }
@Test
void loadWhenBeanFactoryConversionServiceAndConverterBean() {
DefaultConversionService conversionService = new DefaultConversionService();
conversionService.addConverter(new AlienConverter());
this.context.getBeanFactory().setConversionService(conversionService);
load(new Class<?>[] { ConverterConfiguration.class, PersonAndAlienProperties.class }, "test.person=John Smith",
"test.alien=Alf Tanner");
PersonAndAlienProperties properties = this.context.getBean(PersonAndAlienProperties.class);
assertThat(properties.getPerson().firstName).isEqualTo("John");
assertThat(properties.getPerson().lastName).isEqualTo("Smith");
assertThat(properties.getAlien().firstName).isEqualTo("Alf");
assertThat(properties.getAlien().lastName).isEqualTo("Tanner");
}
@Test @Test
void loadWhenConfigurationConverterIsNotQualifiedShouldNotConvert() { void loadWhenConfigurationConverterIsNotQualifiedShouldNotConvert() {
assertThatExceptionOfType(BeanCreationException.class) assertThatExceptionOfType(BeanCreationException.class)
...@@ -1908,6 +1923,32 @@ class ConfigurationPropertiesTests { ...@@ -1908,6 +1923,32 @@ class ConfigurationPropertiesTests {
} }
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "test")
static class PersonAndAlienProperties {
private Person person;
private Alien alien;
Person getPerson() {
return this.person;
}
void setPerson(Person person) {
this.person = person;
}
Alien getAlien() {
return this.alien;
}
void setAlien(Alien alien) {
this.alien = alien;
}
}
@EnableConfigurationProperties @EnableConfigurationProperties
@ConfigurationProperties(prefix = "sample") @ConfigurationProperties(prefix = "sample")
static class MapWithNumericKeyProperties { static class MapWithNumericKeyProperties {
...@@ -2201,6 +2242,16 @@ class ConfigurationPropertiesTests { ...@@ -2201,6 +2242,16 @@ class ConfigurationPropertiesTests {
} }
static class AlienConverter implements Converter<String, Alien> {
@Override
public Alien convert(String source) {
String[] content = StringUtils.split(source, " ");
return new Alien(content[0], content[1]);
}
}
static class GenericPersonConverter implements GenericConverter { static class GenericPersonConverter implements GenericConverter {
@Override @Override
...@@ -2262,6 +2313,27 @@ class ConfigurationPropertiesTests { ...@@ -2262,6 +2313,27 @@ class ConfigurationPropertiesTests {
} }
static class Alien {
private final String firstName;
private final String lastName;
Alien(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
String getFirstName() {
return this.firstName;
}
String getLastName() {
return this.lastName;
}
}
static class Foo { static class Foo {
private String a; private String a;
......
/* /*
* Copyright 2012-2020 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -18,6 +18,7 @@ package org.springframework.boot.context.properties; ...@@ -18,6 +18,7 @@ package org.springframework.boot.context.properties;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
...@@ -40,28 +41,42 @@ import static org.assertj.core.api.Assertions.assertThat; ...@@ -40,28 +41,42 @@ import static org.assertj.core.api.Assertions.assertThat;
class ConversionServiceDeducerTests { class ConversionServiceDeducerTests {
@Test @Test
void getConversionServiceWhenHasConversionServiceBeanReturnsBean() { void getConversionServicesWhenHasConversionServiceBeanContainsOnlyBean() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext( ApplicationContext applicationContext = new AnnotationConfigApplicationContext(
CustomConverterServiceConfiguration.class); CustomConverterServiceConfiguration.class);
ConversionServiceDeducer deducer = new ConversionServiceDeducer(applicationContext); ConversionServiceDeducer deducer = new ConversionServiceDeducer(applicationContext);
assertThat(deducer.getConversionService()).isInstanceOf(TestApplicationConversionService.class); TestApplicationConversionService expected = applicationContext.getBean(TestApplicationConversionService.class);
assertThat(deducer.getConversionServices()).containsExactly(expected);
} }
@Test @Test
void getConversionServiceWhenHasNoConversionServiceBeanAndNoQualifiedBeansReturnsSharedInstance() { void getConversionServiceWhenHasNoConversionServiceBeanAndNoQualifiedBeansAndNoBeanFactoryConversionServiceReturnsEmptyList() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(EmptyConfiguration.class); ApplicationContext applicationContext = new AnnotationConfigApplicationContext(EmptyConfiguration.class);
ConversionServiceDeducer deducer = new ConversionServiceDeducer(applicationContext); ConversionServiceDeducer deducer = new ConversionServiceDeducer(applicationContext);
assertThat(deducer.getConversionService()).isSameAs(ApplicationConversionService.getSharedInstance()); assertThat(deducer.getConversionServices()).isEmpty();
} }
@Test @Test
void getConversionServiceWhenHasQualifiedConverterBeansReturnsNewInstance() { void getConversionServiceWhenHasNoConversionServiceBeanAndNoQualifiedBeansAndBeanFactoryConversionServiceContainsOnlyBeanFactoryInstance() {
ConfigurableApplicationContext applicationContext = new AnnotationConfigApplicationContext(
EmptyConfiguration.class);
ConversionService conversionService = new ApplicationConversionService();
applicationContext.getBeanFactory().setConversionService(conversionService);
ConversionServiceDeducer deducer = new ConversionServiceDeducer(applicationContext);
List<ConversionService> conversionServices = deducer.getConversionServices();
assertThat(conversionServices).containsOnly(conversionService);
assertThat(conversionServices.get(0)).isSameAs(conversionService);
}
@Test
void getConversionServiceWhenHasQualifiedConverterBeansContainsCustomizedApplicationService() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext( ApplicationContext applicationContext = new AnnotationConfigApplicationContext(
CustomConverterConfiguration.class); CustomConverterConfiguration.class);
ConversionServiceDeducer deducer = new ConversionServiceDeducer(applicationContext); ConversionServiceDeducer deducer = new ConversionServiceDeducer(applicationContext);
ConversionService conversionService = deducer.getConversionService(); List<ConversionService> conversionServices = deducer.getConversionServices();
assertThat(conversionService).isNotSameAs(ApplicationConversionService.getSharedInstance()); assertThat(conversionServices).hasSize(1);
assertThat(conversionService.canConvert(InputStream.class, OutputStream.class)).isTrue(); assertThat(conversionServices.get(0)).isNotSameAs(ApplicationConversionService.getSharedInstance());
assertThat(conversionServices.get(0).canConvert(InputStream.class, OutputStream.class)).isTrue();
} }
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
......
/* /*
* Copyright 2012-2020 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -19,6 +19,7 @@ package org.springframework.boot.context.properties.bind; ...@@ -19,6 +19,7 @@ package org.springframework.boot.context.properties.bind;
import java.beans.PropertyEditorSupport; import java.beans.PropertyEditorSupport;
import java.io.File; import java.io.File;
import java.time.Duration; import java.time.Duration;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
...@@ -28,7 +29,6 @@ import org.mockito.Mock; ...@@ -28,7 +29,6 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.PropertyEditorRegistry;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.ConverterNotFoundException;
...@@ -38,7 +38,6 @@ import org.springframework.core.convert.support.GenericConversionService; ...@@ -38,7 +38,6 @@ import org.springframework.core.convert.support.GenericConversionService;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
...@@ -54,20 +53,14 @@ class BindConverterTests { ...@@ -54,20 +53,14 @@ class BindConverterTests {
@Mock @Mock
private Consumer<PropertyEditorRegistry> propertyEditorInitializer; private Consumer<PropertyEditorRegistry> propertyEditorInitializer;
@Test
void createWhenConversionServiceIsNullShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> BindConverter.get(null, null))
.withMessageContaining("ConversionService must not be null");
}
@Test @Test
void createWhenPropertyEditorInitializerIsNullShouldCreate() { void createWhenPropertyEditorInitializerIsNullShouldCreate() {
BindConverter.get(ApplicationConversionService.getSharedInstance(), null); BindConverter.get(null, null);
} }
@Test @Test
void createWhenPropertyEditorInitializerIsNotNullShouldUseToInitialize() { void createWhenPropertyEditorInitializerIsNotNullShouldUseToInitialize() {
BindConverter.get(ApplicationConversionService.getSharedInstance(), this.propertyEditorInitializer); BindConverter.get(null, this.propertyEditorInitializer);
verify(this.propertyEditorInitializer).accept(any(PropertyEditorRegistry.class)); verify(this.propertyEditorInitializer).accept(any(PropertyEditorRegistry.class));
} }
...@@ -111,7 +104,7 @@ class BindConverterTests { ...@@ -111,7 +104,7 @@ class BindConverterTests {
@Test @Test
void canConvertWhenNotPropertyEditorAndConversionServiceCannotConvertShouldReturnFalse() { void canConvertWhenNotPropertyEditorAndConversionServiceCannotConvertShouldReturnFalse() {
BindConverter bindConverter = BindConverter.get(ApplicationConversionService.getSharedInstance(), null); BindConverter bindConverter = BindConverter.get(null, null);
assertThat(bindConverter.canConvert("test", ResolvableType.forClass(SampleType.class))).isFalse(); assertThat(bindConverter.canConvert("test", ResolvableType.forClass(SampleType.class))).isFalse();
} }
...@@ -162,7 +155,7 @@ class BindConverterTests { ...@@ -162,7 +155,7 @@ class BindConverterTests {
@Test @Test
void convertWhenNotPropertyEditorAndConversionServiceCannotConvertShouldThrowException() { void convertWhenNotPropertyEditorAndConversionServiceCannotConvertShouldThrowException() {
BindConverter bindConverter = BindConverter.get(ApplicationConversionService.getSharedInstance(), null); BindConverter bindConverter = BindConverter.get(null, null);
assertThatExceptionOfType(ConverterNotFoundException.class) assertThatExceptionOfType(ConverterNotFoundException.class)
.isThrownBy(() -> bindConverter.convert("test", ResolvableType.forClass(SampleType.class))); .isThrownBy(() -> bindConverter.convert("test", ResolvableType.forClass(SampleType.class)));
} }
...@@ -171,27 +164,29 @@ class BindConverterTests { ...@@ -171,27 +164,29 @@ class BindConverterTests {
void convertWhenConvertingToFileShouldExcludeFileEditor() { void convertWhenConvertingToFileShouldExcludeFileEditor() {
// For back compatibility we want true file conversion and not an accidental // For back compatibility we want true file conversion and not an accidental
// classpath resource reference. See gh-12163 // classpath resource reference. See gh-12163
BindConverter bindConverter = BindConverter.get(new GenericConversionService(), null); BindConverter bindConverter = BindConverter.get(Collections.singletonList(new GenericConversionService()),
null);
File result = bindConverter.convert(".", ResolvableType.forClass(File.class)); File result = bindConverter.convert(".", ResolvableType.forClass(File.class));
assertThat(result.getPath()).isEqualTo("."); assertThat(result.getPath()).isEqualTo(".");
} }
@Test @Test
void fallsBackToApplicationConversionService() { void fallsBackToApplicationConversionService() {
BindConverter bindConverter = BindConverter.get(new GenericConversionService(), null); BindConverter bindConverter = BindConverter.get(Collections.singletonList(new GenericConversionService()),
null);
Duration result = bindConverter.convert("10s", ResolvableType.forClass(Duration.class)); Duration result = bindConverter.convert("10s", ResolvableType.forClass(Duration.class));
assertThat(result.getSeconds()).isEqualTo(10); assertThat(result.getSeconds()).isEqualTo(10);
} }
private BindConverter getPropertyEditorOnlyBindConverter( private BindConverter getPropertyEditorOnlyBindConverter(
Consumer<PropertyEditorRegistry> propertyEditorInitializer) { Consumer<PropertyEditorRegistry> propertyEditorInitializer) {
return BindConverter.get(new ThrowingConversionService(), propertyEditorInitializer); return BindConverter.get(Collections.singletonList(new ThrowingConversionService()), propertyEditorInitializer);
} }
private BindConverter getBindConverter(Converter<?, ?> converter) { private BindConverter getBindConverter(Converter<?, ?> converter) {
GenericConversionService conversionService = new GenericConversionService(); GenericConversionService conversionService = new GenericConversionService();
conversionService.addConverter(converter); conversionService.addConverter(converter);
return BindConverter.get(conversionService, null); return BindConverter.get(Collections.singletonList(conversionService), null);
} }
private void registerSampleTypeEditor(PropertyEditorRegistry registry) { private void registerSampleTypeEditor(PropertyEditorRegistry registry) {
......
/* /*
* Copyright 2012-2020 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -38,6 +38,7 @@ import org.springframework.boot.context.properties.source.ConfigurationPropertyS ...@@ -38,6 +38,7 @@ import org.springframework.boot.context.properties.source.ConfigurationPropertyS
import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; import org.springframework.boot.context.properties.source.MockConfigurationPropertySource;
import org.springframework.boot.convert.Delimiter; import org.springframework.boot.convert.Delimiter;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
...@@ -340,7 +341,7 @@ class JavaBeanBinderTests { ...@@ -340,7 +341,7 @@ class JavaBeanBinderTests {
@Test @Test
void bindToInstanceWhenNoDefaultConstructorShouldBind() { void bindToInstanceWhenNoDefaultConstructorShouldBind() {
Binder binder = new Binder(this.sources, null, null, null, null, Binder binder = new Binder(this.sources, null, (ConversionService) null, null, null,
(bindable, isNestedConstructorBinding) -> null); (bindable, isNestedConstructorBinding) -> null);
MockConfigurationPropertySource source = new MockConfigurationPropertySource(); MockConfigurationPropertySource source = new MockConfigurationPropertySource();
source.put("foo.value", "bar"); source.put("foo.value", "bar");
......
/* /*
* Copyright 2012-2020 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -31,6 +31,7 @@ import org.springframework.boot.context.properties.source.ConfigurationPropertyN ...@@ -31,6 +31,7 @@ import org.springframework.boot.context.properties.source.ConfigurationPropertyN
import org.springframework.boot.context.properties.source.ConfigurationPropertySource; import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; import org.springframework.boot.context.properties.source.MockConfigurationPropertySource;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.util.Assert; import org.springframework.util.Assert;
...@@ -109,7 +110,7 @@ class ValueObjectBinderTests { ...@@ -109,7 +110,7 @@ class ValueObjectBinderTests {
this.sources.add(source); this.sources.add(source);
Constructor<?>[] constructors = MultipleConstructorsBean.class.getDeclaredConstructors(); Constructor<?>[] constructors = MultipleConstructorsBean.class.getDeclaredConstructors();
Constructor<?> constructor = (constructors[0].getParameterCount() == 1) ? constructors[0] : constructors[1]; Constructor<?> constructor = (constructors[0].getParameterCount() == 1) ? constructors[0] : constructors[1];
Binder binder = new Binder(this.sources, null, null, null, null, Binder binder = new Binder(this.sources, null, (ConversionService) null, null, null,
(bindable, isNestedConstructorBinding) -> constructor); (bindable, isNestedConstructorBinding) -> constructor);
MultipleConstructorsBean bound = binder.bind("foo", Bindable.of(MultipleConstructorsBean.class)).get(); MultipleConstructorsBean bound = binder.bind("foo", Bindable.of(MultipleConstructorsBean.class)).get();
assertThat(bound.getIntValue()).isEqualTo(12); assertThat(bound.getIntValue()).isEqualTo(12);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment