Skip to content

Commit 37a6634

Browse files
committed
Fall back to application conversion service in BindConverter
Previously, if a user declared a custom conversionService bean that was not an ApplicationConversionService instance, the binder lost the ability to convert a String to a Duration (along with any other conversions that are specific to ApplicationConversionService). This commit updates BindConverter so that, if the ConversionService with which it is created is not an ApplicationConversionService, it will use one as an additional service when performing conversion. Closes gh-12237
1 parent 30f79f2 commit 37a6634

File tree

2 files changed

+84
-15
lines changed

2 files changed

+84
-15
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@
1818

1919
import java.beans.PropertyEditor;
2020
import java.lang.annotation.Annotation;
21+
import java.util.ArrayList;
2122
import java.util.Collection;
2223
import java.util.Collections;
2324
import java.util.HashSet;
25+
import java.util.List;
2426
import java.util.Map;
2527
import java.util.Set;
2628
import java.util.function.Consumer;
29+
import java.util.function.Function;
30+
import java.util.function.Predicate;
2731

2832
import org.springframework.beans.BeanUtils;
2933
import org.springframework.beans.PropertyEditorRegistry;
@@ -42,6 +46,7 @@
4246
* and so a new instance is created for each top-level bind.
4347
*
4448
* @author Phillip Webb
49+
* @author Andy Wilkinson
4550
*/
4651
class BindConverter {
4752

@@ -52,24 +57,21 @@ class BindConverter {
5257
EXCLUDED_EDITORS = Collections.unmodifiableSet(excluded);
5358
}
5459

55-
private final ConversionService typeConverterConversionService;
56-
5760
private final ConversionService conversionService;
5861

5962
BindConverter(ConversionService conversionService,
6063
Consumer<PropertyEditorRegistry> propertyEditorInitializer) {
6164
Assert.notNull(conversionService, "ConversionService must not be null");
62-
this.typeConverterConversionService = new TypeConverterConversionService(
63-
propertyEditorInitializer);
64-
this.conversionService = conversionService;
65+
this.conversionService = new CompositeConversionService(
66+
new TypeConverterConversionService(propertyEditorInitializer),
67+
conversionService);
6568
}
6669

6770
public boolean canConvert(Object value, ResolvableType type,
6871
Annotation... annotations) {
6972
TypeDescriptor sourceType = TypeDescriptor.forObject(value);
7073
TypeDescriptor targetType = new ResolvableTypeDescriptor(type, annotations);
71-
return this.typeConverterConversionService.canConvert(sourceType, targetType)
72-
|| this.conversionService.canConvert(sourceType, targetType);
74+
return this.conversionService.canConvert(sourceType, targetType);
7375
}
7476

7577
public <T> T convert(Object result, Bindable<T> target) {
@@ -83,10 +85,6 @@ public <T> T convert(Object value, ResolvableType type, Annotation... annotation
8385
}
8486
TypeDescriptor sourceType = TypeDescriptor.forObject(value);
8587
TypeDescriptor targetType = new ResolvableTypeDescriptor(type, annotations);
86-
if (this.typeConverterConversionService.canConvert(sourceType, targetType)) {
87-
return (T) this.typeConverterConversionService.convert(value, sourceType,
88-
targetType);
89-
}
9088
return (T) this.conversionService.convert(value, sourceType, targetType);
9189
}
9290

@@ -180,4 +178,66 @@ private PropertyEditor getPropertyEditor(Class<?> type) {
180178

181179
}
182180

181+
private static final class CompositeConversionService implements ConversionService {
182+
183+
private final List<ConversionService> delegates;
184+
185+
private CompositeConversionService(
186+
TypeConverterConversionService typeConverterConversionService,
187+
ConversionService conversionService) {
188+
List<ConversionService> delegates = new ArrayList<ConversionService>();
189+
delegates.add(typeConverterConversionService);
190+
delegates.add(conversionService);
191+
if (!(conversionService instanceof ApplicationConversionService)) {
192+
delegates.add(ApplicationConversionService.getSharedInstance());
193+
}
194+
this.delegates = delegates;
195+
}
196+
197+
@Override
198+
public boolean canConvert(Class<?> sourceType, Class<?> targetType) {
199+
return canConvert((delegate) -> delegate.canConvert(sourceType, targetType));
200+
}
201+
202+
@Override
203+
public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) {
204+
return canConvert((delegate) -> delegate.canConvert(sourceType, targetType));
205+
}
206+
207+
private boolean canConvert(Predicate<ConversionService> canConvert) {
208+
for (ConversionService delegate : this.delegates) {
209+
if (canConvert.test(delegate)) {
210+
return true;
211+
}
212+
}
213+
return false;
214+
}
215+
216+
@Override
217+
public <T> T convert(Object source, Class<T> targetType) {
218+
Class<?> sourceType = source.getClass();
219+
return convert((delegate) -> delegate.canConvert(sourceType, targetType),
220+
(delegate) -> delegate.convert(source, targetType));
221+
}
222+
223+
@Override
224+
public Object convert(Object source, TypeDescriptor sourceType,
225+
TypeDescriptor targetType) {
226+
return convert((delegate) -> delegate.canConvert(sourceType, targetType),
227+
(delegate) -> delegate.convert(source, sourceType, targetType));
228+
}
229+
230+
public <T> T convert(Predicate<ConversionService> canConvert,
231+
Function<ConversionService, T> convert) {
232+
for (int i = 0; i < this.delegates.size() - 1; i++) {
233+
ConversionService delegate = this.delegates.get(i);
234+
if (canConvert.test(delegate)) {
235+
return convert.apply(delegate);
236+
}
237+
}
238+
return convert.apply(this.delegates.get(this.delegates.size() - 1));
239+
}
240+
241+
}
242+
183243
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindConverterTests.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.beans.PropertyEditorSupport;
2020
import java.io.File;
21+
import java.time.Duration;
2122
import java.util.List;
2223
import java.util.function.Consumer;
2324

@@ -45,6 +46,7 @@
4546
* Tests for {@link BindConverter}.
4647
*
4748
* @author Phillip Webb
49+
* @author Andy Wilkinson
4850
*/
4951
public class BindConverterTests {
5052

@@ -202,10 +204,17 @@ public void convertWhenConvertingToFileShouldExcludeFileEditor() {
202204
// classpath resource reference. See gh-12163
203205
BindConverter bindConverter = new BindConverter(new GenericConversionService(),
204206
null);
205-
assertThat(bindConverter.canConvert(".", ResolvableType.forClass(File.class)))
206-
.isFalse();
207-
this.thrown.expect(ConverterNotFoundException.class);
208-
bindConverter.convert(".", ResolvableType.forClass(File.class));
207+
File result = bindConverter.convert(".", ResolvableType.forClass(File.class));
208+
assertThat(result.getPath()).isEqualTo(".");
209+
}
210+
211+
@Test
212+
public void fallsBackToApplicationConversionService() {
213+
BindConverter bindConverter = new BindConverter(new GenericConversionService(),
214+
null);
215+
Duration result = bindConverter.convert("10s",
216+
ResolvableType.forClass(Duration.class));
217+
assertThat(result.getSeconds()).isEqualTo(10);
209218
}
210219

211220
private BindConverter getPropertyEditorOnlyBindConverter(

0 commit comments

Comments
 (0)