diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java index ecc4d7bb696..5f03ff870f3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java @@ -185,18 +185,10 @@ final class BindConverter { private static class TypeConverterConversionService extends GenericConversionService { TypeConverterConversionService(Consumer initializer) { - addConverter(new TypeConverterConverter(createTypeConverter(initializer))); + addConverter(new TypeConverterConverter(initializer)); ApplicationConversionService.addDelimitedStringConverters(this); } - private SimpleTypeConverter createTypeConverter(Consumer initializer) { - SimpleTypeConverter typeConverter = new SimpleTypeConverter(); - if (initializer != null) { - initializer.accept(typeConverter); - } - return typeConverter; - } - @Override public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) { // Prefer conversion service to handle things like String to char[]. @@ -213,10 +205,15 @@ final class BindConverter { */ private static class TypeConverterConverter implements ConditionalGenericConverter { - private final SimpleTypeConverter typeConverter; + private final Consumer initializer; + + // SimpleTypeConverter is not thread-safe to use for conversion but we can use it + // in a thread-safe way to check if conversion is possible. + private final SimpleTypeConverter matchesOnlyTypeConverter; - TypeConverterConverter(SimpleTypeConverter typeConverter) { - this.typeConverter = typeConverter; + TypeConverterConverter(Consumer initializer) { + this.initializer = initializer; + this.matchesOnlyTypeConverter = createTypeConverter(); } @Override @@ -226,32 +223,32 @@ final class BindConverter { @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { - return getPropertyEditor(targetType.getType()) != null; - } - - @Override - public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { - SimpleTypeConverter typeConverter = this.typeConverter; - return typeConverter.convertIfNecessary(source, targetType.getType()); - } - - private PropertyEditor getPropertyEditor(Class type) { + Class type = targetType.getType(); if (type == null || type == Object.class || Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type)) { - return null; + return false; } - SimpleTypeConverter typeConverter = this.typeConverter; - PropertyEditor editor = typeConverter.getDefaultEditor(type); + PropertyEditor editor = this.matchesOnlyTypeConverter.getDefaultEditor(type); if (editor == null) { - editor = typeConverter.findCustomEditor(type, null); + editor = this.matchesOnlyTypeConverter.findCustomEditor(type, null); } if (editor == null && String.class != type) { editor = BeanUtils.findEditorByConvention(type); } - if (editor == null || EXCLUDED_EDITORS.contains(editor.getClass())) { - return null; + return (editor != null && !EXCLUDED_EDITORS.contains(editor.getClass())); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return createTypeConverter().convertIfNecessary(source, targetType.getType()); + } + + private SimpleTypeConverter createTypeConverter() { + SimpleTypeConverter typeConverter = new SimpleTypeConverter(); + if (this.initializer != null) { + this.initializer.accept(typeConverter); } - return editor; + return typeConverter; } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindConverterTests.java index c8b4627f9b3..1cca31b4551 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindConverterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BindConverterTests.java @@ -19,6 +19,8 @@ package org.springframework.boot.context.properties.bind; import java.beans.PropertyEditorSupport; import java.io.File; import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.function.Consumer; @@ -192,6 +194,30 @@ class BindConverterTests { .withRootCauseInstanceOf(ClassNotFoundException.class); } + @Test + void convertWhenUsingTypeConverterConversionServiceFromMultipleThreads() { + BindConverter bindConverter = getPropertyEditorOnlyBindConverter(this::registerSampleTypeEditor); + ResolvableType type = ResolvableType.forClass(SampleType.class); + List threads = new ArrayList<>(); + List results = Collections.synchronizedList(new ArrayList<>()); + for (int i = 0; i < 40; i++) { + threads.add(new Thread(() -> { + for (int j = 0; j < 20; j++) { + results.add(bindConverter.convert("test", type)); + } + })); + } + threads.forEach(Thread::start); + for (Thread thread : threads) { + try { + thread.join(); + } + catch (InterruptedException ex) { + } + } + assertThat(results).isNotEmpty().doesNotContainNull(); + } + private BindConverter getPropertyEditorOnlyBindConverter( Consumer propertyEditorInitializer) { return BindConverter.get(new ThrowingConversionService(), propertyEditorInitializer); @@ -221,9 +247,12 @@ class BindConverterTests { @Override public void setAsText(String text) throws IllegalArgumentException { - SampleType value = new SampleType(); - value.text = text; - setValue(value); + setValue(null); + if (text != null) { + SampleType value = new SampleType(); + value.text = text; + setValue(value); + } } }