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 06f49fe4daac..c5354ed12688 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 @@ -17,12 +17,23 @@ package org.springframework.boot.convert; import java.lang.annotation.Annotation; +import java.util.Collections; import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Optional; import java.util.Set; import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalConverter; +import org.springframework.core.convert.converter.ConditionalGenericConverter; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.core.convert.converter.ConverterRegistry; @@ -37,6 +48,7 @@ import org.springframework.format.Printer; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.FormattingConversionService; +import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; /** @@ -49,6 +61,7 @@ * against registry instance. * * @author Phillip Webb + * @author Shixiong Guo(viviel) * @since 2.0.0 */ public class ApplicationConversionService extends FormattingConversionService { @@ -272,28 +285,313 @@ public static void addApplicationFormatters(FormatterRegistry registry) { * @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); + Set> entries = new LinkedHashSet<>(); + entries.addAll(beanFactory.getBeansOfType(GenericConverter.class).entrySet()); + entries.addAll(beanFactory.getBeansOfType(Converter.class).entrySet()); + entries.addAll(beanFactory.getBeansOfType(Printer.class).entrySet()); + entries.addAll(beanFactory.getBeansOfType(Parser.class).entrySet()); + for (Map.Entry e : entries) { + String beanName = e.getKey(); + Object bean = e.getValue(); + try { + doAddBean(registry, bean); } - else if (bean instanceof Converter) { - registry.addConverter((Converter) bean); + catch (IllegalArgumentException ex) { + if (!tryAddFactoryMethodBean(registry, beanFactory, beanName, bean)) { + throw ex; + } + } + } + } + + private static void doAddBean(FormatterRegistry registry, Object bean) { + 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); + } + } + + private static boolean tryAddFactoryMethodBean(FormatterRegistry registry, ListableBeanFactory beanFactory, + String beanName, Object bean) { + ConfigurableListableBeanFactory clbf = getConfigurableListableBeanFactory(beanFactory); + if (clbf == null) { + return false; + } + if (!isFactoryMethod(clbf, beanName)) { + return false; + } + if (bean instanceof Converter) { + return addConverter(registry, clbf, beanName, (Converter) bean); + } + else if (bean instanceof Printer) { + return addPrinter(registry, clbf, beanName, (Printer) bean); + } + else if (bean instanceof Parser) { + return addParser(registry, clbf, beanName, (Parser) bean); + } + return false; + } + + private static ConfigurableListableBeanFactory getConfigurableListableBeanFactory(ListableBeanFactory beanFactory) { + ListableBeanFactory bf = beanFactory; + if (bf instanceof ConfigurableApplicationContext) { + bf = ((ConfigurableApplicationContext) bf).getBeanFactory(); + } + if (bf instanceof ConfigurableListableBeanFactory) { + return (ConfigurableListableBeanFactory) bf; + } + return null; + } + + private static boolean isFactoryMethod(ConfigurableListableBeanFactory clbf, String beanName) { + BeanDefinition bd = clbf.getMergedBeanDefinition(beanName); + return bd.getFactoryMethodName() != null; + } + + private static boolean addConverter(FormatterRegistry registry, ConfigurableListableBeanFactory beanFactory, + String beanName, Converter converter) { + ConverterAdapter adapter = getConverterAdapter(beanFactory, beanName, converter); + if (adapter == null) { + return false; + } + registry.addConverter(adapter); + return true; + } + + private static ConverterAdapter getConverterAdapter(ConfigurableListableBeanFactory beanFactory, String beanName, + Converter converter) { + ResolvableType[] types = getResolvableType(beanFactory, beanName); + if (types.length < 2) { + return null; + } + return new ConverterAdapter(converter, types[0], types[1]); + } + + private static ResolvableType[] getResolvableType(ConfigurableListableBeanFactory beanFactory, String beanName) { + BeanDefinition beanDefinition = beanFactory.getMergedBeanDefinition(beanName); + ResolvableType resolvableType = beanDefinition.getResolvableType(); + return resolvableType.getGenerics(); + } + + private static boolean addPrinter(FormatterRegistry registry, ConfigurableListableBeanFactory beanFactory, + String beanName, Printer printer) { + PrinterAdapter adapter = getPrinterAdapter(beanFactory, beanName, printer); + if (adapter == null) { + return false; + } + registry.addConverter(adapter); + return true; + } + + private static PrinterAdapter getPrinterAdapter(ConfigurableListableBeanFactory beanFactory, String beanName, + Printer printer) { + ResolvableType[] types = getResolvableType(beanFactory, beanName); + if (types.length < 1) { + return null; + } + ConversionService conversionService = beanFactory.getBean(ConversionService.class); + return new PrinterAdapter(types[0].resolve(), printer, conversionService); + } + + private static boolean addParser(FormatterRegistry registry, ConfigurableListableBeanFactory beanFactory, + String beanName, Parser parser) { + ParserAdapter adapter = getParserAdapter(beanFactory, beanName, parser); + if (adapter == null) { + return false; + } + registry.addConverter(adapter); + return true; + } + + private static ParserAdapter getParserAdapter(ConfigurableListableBeanFactory beanFactory, String beanName, + Parser parser) { + ResolvableType[] types = getResolvableType(beanFactory, beanName); + if (types.length < 1) { + return null; + } + ConversionService conversionService = beanFactory.getBean(ConversionService.class); + return new ParserAdapter(types[0].resolve(), parser, conversionService); + } + + /** + * Adapts a {@link Converter} to a {@link GenericConverter}. + *

+ * Reference from + * {@link org.springframework.core.convert.support.GenericConversionService.ConverterAdapter} + */ + @SuppressWarnings("unchecked") + private static final class ConverterAdapter implements ConditionalGenericConverter { + + private final Converter converter; + + private final ConvertiblePair typeInfo; + + private final ResolvableType targetType; + + ConverterAdapter(Converter converter, ResolvableType sourceType, ResolvableType targetType) { + this.converter = (Converter) converter; + this.typeInfo = new ConvertiblePair(sourceType.toClass(), targetType.toClass()); + this.targetType = targetType; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(this.typeInfo); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + // Check raw type first... + if (this.typeInfo.getTargetType() != targetType.getObjectType()) { + return false; } - else if (bean instanceof Formatter) { - registry.addFormatter((Formatter) bean); + // Full check for complex generic type match required? + ResolvableType rt = targetType.getResolvableType(); + if (!(rt.getType() instanceof Class) && !rt.isAssignableFrom(this.targetType) + && !this.targetType.hasUnresolvableGenerics()) { + return false; } - else if (bean instanceof Printer) { - registry.addPrinter((Printer) bean); + return !(this.converter instanceof ConditionalConverter) + || ((ConditionalConverter) this.converter).matches(sourceType, targetType); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (source == null) { + return convertNullSource(sourceType, targetType); } - else if (bean instanceof Parser) { - registry.addParser((Parser) bean); + return this.converter.convert(source); + } + + @Override + public String toString() { + return (this.typeInfo + " : " + this.converter); + } + + /** + * Template method to convert a {@code null} source. + *

+ * The default implementation returns {@code null} or the Java 8 + * {@link java.util.Optional#empty()} instance if the target type is + * {@code java.util.Optional}. Subclasses may override this to return custom + * {@code null} objects for specific target types. + * @param sourceType the source type to convert from + * @param targetType the target type to convert to + * @return the converted null object + */ + private Object convertNullSource(TypeDescriptor sourceType, TypeDescriptor targetType) { + if (targetType.getObjectType() == Optional.class) { + return Optional.empty(); } + return null; + } + + } + + private static class PrinterAdapter implements GenericConverter { + + private final Class fieldType; + + private final TypeDescriptor printerObjectType; + + @SuppressWarnings("rawtypes") + private final Printer printer; + + private final ConversionService conversionService; + + PrinterAdapter(Class fieldType, Printer printer, ConversionService conversionService) { + this.fieldType = fieldType; + this.printerObjectType = TypeDescriptor.valueOf(resolvePrinterObjectType(printer)); + this.printer = printer; + this.conversionService = conversionService; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(this.fieldType, String.class)); } + + @Override + @SuppressWarnings("unchecked") + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + if (!sourceType.isAssignableTo(this.printerObjectType)) { + source = this.conversionService.convert(source, sourceType, this.printerObjectType); + } + if (source == null) { + return ""; + } + return this.printer.print(source, LocaleContextHolder.getLocale()); + } + + private Class resolvePrinterObjectType(Printer printer) { + return GenericTypeResolver.resolveTypeArgument(printer.getClass(), Printer.class); + } + + @Override + public String toString() { + return (this.fieldType.getName() + " -> " + String.class.getName() + " : " + this.printer); + } + + } + + private static class ParserAdapter implements GenericConverter { + + private final Class fieldType; + + private final Parser parser; + + private final ConversionService conversionService; + + ParserAdapter(Class fieldType, Parser parser, ConversionService conversionService) { + this.fieldType = fieldType; + this.parser = parser; + this.conversionService = conversionService; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, this.fieldType)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + String text = (String) source; + if (!StringUtils.hasText(text)) { + return null; + } + Object result; + try { + result = this.parser.parse(text, LocaleContextHolder.getLocale()); + } + catch (IllegalArgumentException ex) { + throw ex; + } + catch (Throwable ex) { + throw new IllegalArgumentException("Parse attempt failed for value [" + text + "]", ex); + } + TypeDescriptor resultType = TypeDescriptor.valueOf(result.getClass()); + if (!resultType.isAssignableTo(targetType)) { + result = this.conversionService.convert(result, resultType, targetType); + } + return result; + } + + @Override + public String toString() { + return (String.class.getName() + " -> " + this.fieldType.getName() + ": " + this.parser); + } + } } 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 index 1599587f9554..77977efa4bc0 100644 --- 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 @@ -26,6 +26,9 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.GenericConverter; @@ -36,6 +39,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -44,6 +49,7 @@ * Tests for {@link ApplicationConversionService}. * * @author Phillip Webb + * @author Shixiong Guo(viviel) */ class ApplicationConversionServiceTests { @@ -95,6 +101,48 @@ void addBeansWhenHasParserBeanAddParser() { } } + @SuppressWarnings("unchecked") + @Test + void addBeansWhenHasFactoryMethodConverterBeanAddConverter() { + try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( + FactoryMethodConverter.class)) { + Converter converter = (Converter) context.getBean("converter"); + willThrow(IllegalArgumentException.class).given(this.registry).addConverter(converter); + ApplicationConversionService.addBeans(this.registry, context); + verify(this.registry).addConverter(converter); + verify(this.registry).addConverter(any(GenericConverter.class)); + verifyNoMoreInteractions(this.registry); + } + } + + @SuppressWarnings("unchecked") + @Test + void addBeansWhenHasFactoryMethodPrinterBeanAddPrinter() { + try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( + FactoryMethodPrinter.class)) { + Printer printer = (Printer) context.getBean("printer"); + willThrow(IllegalArgumentException.class).given(this.registry).addPrinter(printer); + ApplicationConversionService.addBeans(this.registry, context); + verify(this.registry).addPrinter(printer); + verify(this.registry).addConverter(any(GenericConverter.class)); + verifyNoMoreInteractions(this.registry); + } + } + + @SuppressWarnings("unchecked") + @Test + void addBeansWhenHasFactoryMethodParserBeanAddParser() { + try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( + FactoryMethodParser.class)) { + Parser parser = (Parser) context.getBean("parser"); + willThrow(IllegalArgumentException.class).given(this.registry).addParser(parser); + ApplicationConversionService.addBeans(this.registry, context); + verify(this.registry).addParser(parser); + verify(this.registry).addConverter(any(GenericConverter.class)); + verifyNoMoreInteractions(this.registry); + } + } + @Test void isConvertViaObjectSourceTypeWhenObjectSourceReturnsTrue() { // Uses ObjectToCollectionConverter @@ -192,4 +240,44 @@ public String print(Integer object, Locale locale) { } + @Configuration + static class FactoryMethodConverter { + + @Bean + Converter converter() { + return Integer::valueOf; + } + + } + + @Configuration + static class FactoryMethodPrinter { + + @Bean + Printer printer() { + return (object, locale) -> object.toString(); + } + + @Bean + ConversionService conversionService() { + return mock(ConversionService.class); + } + + } + + @Configuration + static class FactoryMethodParser { + + @Bean + Parser parser() { + return (text, locale) -> Integer.valueOf(text); + } + + @Bean + ConversionService conversionService() { + return mock(ConversionService.class); + } + + } + }