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 3f29066f03c..4a74e54f260 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,19 +19,13 @@ package org.springframework.boot.context.properties; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; -import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.convert.ApplicationConversionService; import org.springframework.context.ApplicationContext; 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.core.convert.support.DefaultConversionService; -import org.springframework.format.Formatter; -import org.springframework.format.FormatterRegistry; import org.springframework.format.support.FormattingConversionService; /** @@ -61,13 +55,9 @@ class ConversionServiceDeducer { private List getConversionServices(ConfigurableApplicationContext applicationContext) { List conversionServices = new ArrayList<>(); - ConverterBeans converterBeans = new ConverterBeans(applicationContext); + FormattingConversionService beansConverterService = new FormattingConversionService(); + Map converterBeans = addBeans(applicationContext, beansConverterService); if (!converterBeans.isEmpty()) { - FormattingConversionService beansConverterService = new FormattingConversionService(); - DefaultConversionService.addCollectionConverters(beansConverterService); - beansConverterService - .addConverter(new ConfigurationPropertiesCharSequenceToObjectConverter(beansConverterService)); - converterBeans.addTo(beansConverterService); conversionServices.add(beansConverterService); } if (applicationContext.getBeanFactory().getConversionService() != null) { @@ -83,50 +73,18 @@ class ConversionServiceDeducer { return conversionServices; } + private Map addBeans(ConfigurableApplicationContext applicationContext, + FormattingConversionService converterService) { + DefaultConversionService.addCollectionConverters(converterService); + converterService.addConverter(new ConfigurationPropertiesCharSequenceToObjectConverter(converterService)); + return ApplicationConversionService.addBeans(converterService, applicationContext.getBeanFactory(), + ConfigurationPropertiesBinding.VALUE); + } + 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") - private final List converters; - - private final List genericConverters; - - @SuppressWarnings("rawtypes") - private final List formatters; - - ConverterBeans(ConfigurableApplicationContext applicationContext) { - ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory(); - this.converters = beans(Converter.class, ConfigurationPropertiesBinding.VALUE, beanFactory); - this.genericConverters = beans(GenericConverter.class, ConfigurationPropertiesBinding.VALUE, beanFactory); - this.formatters = beans(Formatter.class, ConfigurationPropertiesBinding.VALUE, beanFactory); - } - - private List beans(Class type, String qualifier, ListableBeanFactory beanFactory) { - return new ArrayList<>( - BeanFactoryAnnotationUtils.qualifiedBeansOfType(beanFactory, type, qualifier).values()); - } - - boolean isEmpty() { - return this.converters.isEmpty() && this.genericConverters.isEmpty() && this.formatters.isEmpty(); - } - - void addTo(FormatterRegistry registry) { - for (Converter converter : this.converters) { - registry.addConverter(converter); - } - for (GenericConverter genericConverter : this.genericConverters) { - registry.addConverter(genericConverter); - } - for (Formatter formatter : this.formatters) { - registry.addFormatter(formatter); - } - } - - } - } 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 92e4479b00a..e76b6a3be0d 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 @@ -26,6 +26,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.ConfigurableApplicationContext; @@ -290,13 +291,30 @@ public class ApplicationConversionService extends FormattingConversionService { * @since 2.2.0 */ public static void addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory) { + addBeans(registry, beanFactory, null); + } + + /** + * Add {@link Printer}, {@link Parser}, {@link Formatter}, {@link Converter}, + * {@link ConverterFactory}, {@link GenericConverter}, and beans from the specified + * bean factory. + * @param registry the service to register beans with + * @param beanFactory the bean factory to get the beans from + * @param qualifier the qualifier required on the beans or {@code null} + * @return the beans that were added + * @since 3.5.0 + */ + public static Map addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory, + String qualifier) { ConfigurableListableBeanFactory configurableBeanFactory = getConfigurableListableBeanFactory(beanFactory); - getBeans(beanFactory).forEach((beanName, bean) -> { + Map beans = getBeans(beanFactory, qualifier); + beans.forEach((beanName, bean) -> { BeanDefinition beanDefinition = (configurableBeanFactory != null) ? configurableBeanFactory.getMergedBeanDefinition(beanName) : null; ResolvableType type = (beanDefinition != null) ? beanDefinition.getResolvableType() : null; addBean(registry, bean, type); }); + return beans; } private static ConfigurableListableBeanFactory getConfigurableListableBeanFactory(ListableBeanFactory beanFactory) { @@ -309,19 +327,20 @@ public class ApplicationConversionService extends FormattingConversionService { return null; } - private static Map getBeans(ListableBeanFactory beanFactory) { + private static Map getBeans(ListableBeanFactory beanFactory, String qualifier) { Map beans = new LinkedHashMap<>(); - beans.putAll(getBeans(beanFactory, Printer.class)); - beans.putAll(getBeans(beanFactory, Parser.class)); - beans.putAll(getBeans(beanFactory, Formatter.class)); - beans.putAll(getBeans(beanFactory, Converter.class)); - beans.putAll(getBeans(beanFactory, ConverterFactory.class)); - beans.putAll(getBeans(beanFactory, GenericConverter.class)); + beans.putAll(getBeans(beanFactory, Printer.class, qualifier)); + beans.putAll(getBeans(beanFactory, Parser.class, qualifier)); + beans.putAll(getBeans(beanFactory, Formatter.class, qualifier)); + beans.putAll(getBeans(beanFactory, Converter.class, qualifier)); + beans.putAll(getBeans(beanFactory, ConverterFactory.class, qualifier)); + beans.putAll(getBeans(beanFactory, GenericConverter.class, qualifier)); return beans; } - private static Map getBeans(ListableBeanFactory beanFactory, Class type) { - return beanFactory.getBeansOfType(type); + private static Map getBeans(ListableBeanFactory beanFactory, Class type, String qualifier) { + return (!StringUtils.hasLength(qualifier)) ? beanFactory.getBeansOfType(type) + : BeanFactoryAnnotationUtils.qualifiedBeansOfType(beanFactory, type, qualifier); } static void addBean(FormatterRegistry registry, Object bean, ResolvableType beanType) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConversionServiceDeducerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConversionServiceDeducerTests.java index 0027cb1d7cf..482e81917d4 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConversionServiceDeducerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConversionServiceDeducerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,10 @@ package org.springframework.boot.context.properties; +import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.util.List; import org.junit.jupiter.api.Test; @@ -30,7 +32,10 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; +import org.springframework.format.Printer; import org.springframework.format.support.FormattingConversionService; +import org.springframework.util.StreamUtils; +import org.springframework.util.function.ThrowingSupplier; import static org.assertj.core.api.Assertions.assertThat; @@ -82,6 +87,28 @@ class ConversionServiceDeducerTests { assertThat(conversionServices.get(1)).isSameAs(ApplicationConversionService.getSharedInstance()); } + @Test + void getConversionServiceWhenHasQualifiedConverterLambdaBeansContainsCustomizedFormattingService() { + ApplicationContext applicationContext = new AnnotationConfigApplicationContext( + CustomLambdaConverterConfiguration.class); + ConversionServiceDeducer deducer = new ConversionServiceDeducer(applicationContext); + List conversionServices = deducer.getConversionServices(); + assertThat(conversionServices).hasSize(2); + assertThat(conversionServices.get(0)).isExactlyInstanceOf(FormattingConversionService.class); + assertThat(conversionServices.get(0).canConvert(InputStream.class, OutputStream.class)).isTrue(); + assertThat(conversionServices.get(0).canConvert(CharSequence.class, InputStream.class)).isTrue(); + assertThat(conversionServices.get(1)).isSameAs(ApplicationConversionService.getSharedInstance()); + } + + @Test + void getConversionServiceWhenHasPrinterBean() { + ApplicationContext applicationContext = new AnnotationConfigApplicationContext(PrinterConfiguration.class); + ConversionServiceDeducer deducer = new ConversionServiceDeducer(applicationContext); + List conversionServices = deducer.getConversionServices(); + InputStream inputStream = new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8)); + assertThat(conversionServices.get(0).convert(inputStream, String.class)).isEqualTo("test"); + } + @Configuration(proxyBeanMethods = false) static class CustomConverterServiceConfiguration { @@ -114,6 +141,36 @@ class ConversionServiceDeducerTests { } + @Configuration(proxyBeanMethods = false) + static class CustomLambdaConverterConfiguration { + + @Bean + @ConfigurationPropertiesBinding + Converter testConverter() { + return (source) -> new TestConverter().convert(source); + } + + @Bean + @ConfigurationPropertiesBinding + Converter stringConverter() { + return (source) -> new StringConverter().convert(source); + } + + } + + @Configuration(proxyBeanMethods = false) + static class PrinterConfiguration { + + @Bean + @ConfigurationPropertiesBinding + Printer inputStreamPrinter() { + return (source, locale) -> ThrowingSupplier + .of(() -> StreamUtils.copyToString(source, StandardCharsets.UTF_8)) + .get(); + } + + } + private static final class TestApplicationConversionService extends ApplicationConversionService { }