Skip to content

Commit 7ec22d8

Browse files
committed
Allow @ConfigurationPropertiesBinding to work with lambdas
Update `ConversionServiceDeducer` so that lambdas can be used with `@ConfigurationPropertiesBinding` annotated `@Bean` methods. This commit also allows more converter types to be detected. Closes gh-44018
1 parent 4eae8a0 commit 7ec22d8

File tree

3 files changed

+99
-65
lines changed

3 files changed

+99
-65
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,19 +19,13 @@
1919
import java.util.ArrayList;
2020
import java.util.Collections;
2121
import java.util.List;
22+
import java.util.Map;
2223

23-
import org.springframework.beans.factory.ListableBeanFactory;
24-
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
25-
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
2624
import org.springframework.boot.convert.ApplicationConversionService;
2725
import org.springframework.context.ApplicationContext;
2826
import org.springframework.context.ConfigurableApplicationContext;
2927
import org.springframework.core.convert.ConversionService;
30-
import org.springframework.core.convert.converter.Converter;
31-
import org.springframework.core.convert.converter.GenericConverter;
3228
import org.springframework.core.convert.support.DefaultConversionService;
33-
import org.springframework.format.Formatter;
34-
import org.springframework.format.FormatterRegistry;
3529
import org.springframework.format.support.FormattingConversionService;
3630

3731
/**
@@ -61,13 +55,9 @@ List<ConversionService> getConversionServices() {
6155

6256
private List<ConversionService> getConversionServices(ConfigurableApplicationContext applicationContext) {
6357
List<ConversionService> conversionServices = new ArrayList<>();
64-
ConverterBeans converterBeans = new ConverterBeans(applicationContext);
58+
FormattingConversionService beansConverterService = new FormattingConversionService();
59+
Map<String, Object> converterBeans = addBeans(applicationContext, beansConverterService);
6560
if (!converterBeans.isEmpty()) {
66-
FormattingConversionService beansConverterService = new FormattingConversionService();
67-
DefaultConversionService.addCollectionConverters(beansConverterService);
68-
beansConverterService
69-
.addConverter(new ConfigurationPropertiesCharSequenceToObjectConverter(beansConverterService));
70-
converterBeans.addTo(beansConverterService);
7161
conversionServices.add(beansConverterService);
7262
}
7363
if (applicationContext.getBeanFactory().getConversionService() != null) {
@@ -83,50 +73,18 @@ private List<ConversionService> getConversionServices(ConfigurableApplicationCon
8373
return conversionServices;
8474
}
8575

76+
private Map<String, Object> addBeans(ConfigurableApplicationContext applicationContext,
77+
FormattingConversionService converterService) {
78+
DefaultConversionService.addCollectionConverters(converterService);
79+
converterService.addConverter(new ConfigurationPropertiesCharSequenceToObjectConverter(converterService));
80+
return ApplicationConversionService.addBeans(converterService, applicationContext.getBeanFactory(),
81+
ConfigurationPropertiesBinding.VALUE);
82+
}
83+
8684
private boolean hasUserDefinedConfigurationServiceBean() {
8785
String beanName = ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME;
8886
return this.applicationContext.containsBean(beanName) && this.applicationContext.getAutowireCapableBeanFactory()
8987
.isTypeMatch(beanName, ConversionService.class);
9088
}
9189

92-
private static class ConverterBeans {
93-
94-
@SuppressWarnings("rawtypes")
95-
private final List<Converter> converters;
96-
97-
private final List<GenericConverter> genericConverters;
98-
99-
@SuppressWarnings("rawtypes")
100-
private final List<Formatter> formatters;
101-
102-
ConverterBeans(ConfigurableApplicationContext applicationContext) {
103-
ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
104-
this.converters = beans(Converter.class, ConfigurationPropertiesBinding.VALUE, beanFactory);
105-
this.genericConverters = beans(GenericConverter.class, ConfigurationPropertiesBinding.VALUE, beanFactory);
106-
this.formatters = beans(Formatter.class, ConfigurationPropertiesBinding.VALUE, beanFactory);
107-
}
108-
109-
private <T> List<T> beans(Class<T> type, String qualifier, ListableBeanFactory beanFactory) {
110-
return new ArrayList<>(
111-
BeanFactoryAnnotationUtils.qualifiedBeansOfType(beanFactory, type, qualifier).values());
112-
}
113-
114-
boolean isEmpty() {
115-
return this.converters.isEmpty() && this.genericConverters.isEmpty() && this.formatters.isEmpty();
116-
}
117-
118-
void addTo(FormatterRegistry registry) {
119-
for (Converter<?, ?> converter : this.converters) {
120-
registry.addConverter(converter);
121-
}
122-
for (GenericConverter genericConverter : this.genericConverters) {
123-
registry.addConverter(genericConverter);
124-
}
125-
for (Formatter<?> formatter : this.formatters) {
126-
registry.addFormatter(formatter);
127-
}
128-
}
129-
130-
}
131-
13290
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.function.Supplier;
2727

2828
import org.springframework.beans.factory.ListableBeanFactory;
29+
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
2930
import org.springframework.beans.factory.config.BeanDefinition;
3031
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
3132
import org.springframework.context.ConfigurableApplicationContext;
@@ -290,13 +291,30 @@ public static void addApplicationFormatters(FormatterRegistry registry) {
290291
* @since 2.2.0
291292
*/
292293
public static void addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory) {
294+
addBeans(registry, beanFactory, null);
295+
}
296+
297+
/**
298+
* Add {@link Printer}, {@link Parser}, {@link Formatter}, {@link Converter},
299+
* {@link ConverterFactory}, {@link GenericConverter}, and beans from the specified
300+
* bean factory.
301+
* @param registry the service to register beans with
302+
* @param beanFactory the bean factory to get the beans from
303+
* @param qualifier the qualifier required on the beans or {@code null}
304+
* @return the beans that were added
305+
* @since 3.5.0
306+
*/
307+
public static Map<String, Object> addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory,
308+
String qualifier) {
293309
ConfigurableListableBeanFactory configurableBeanFactory = getConfigurableListableBeanFactory(beanFactory);
294-
getBeans(beanFactory).forEach((beanName, bean) -> {
310+
Map<String, Object> beans = getBeans(beanFactory, qualifier);
311+
beans.forEach((beanName, bean) -> {
295312
BeanDefinition beanDefinition = (configurableBeanFactory != null)
296313
? configurableBeanFactory.getMergedBeanDefinition(beanName) : null;
297314
ResolvableType type = (beanDefinition != null) ? beanDefinition.getResolvableType() : null;
298315
addBean(registry, bean, type);
299316
});
317+
return beans;
300318
}
301319

302320
private static ConfigurableListableBeanFactory getConfigurableListableBeanFactory(ListableBeanFactory beanFactory) {
@@ -309,19 +327,20 @@ private static ConfigurableListableBeanFactory getConfigurableListableBeanFactor
309327
return null;
310328
}
311329

312-
private static Map<String, Object> getBeans(ListableBeanFactory beanFactory) {
330+
private static Map<String, Object> getBeans(ListableBeanFactory beanFactory, String qualifier) {
313331
Map<String, Object> beans = new LinkedHashMap<>();
314-
beans.putAll(getBeans(beanFactory, Printer.class));
315-
beans.putAll(getBeans(beanFactory, Parser.class));
316-
beans.putAll(getBeans(beanFactory, Formatter.class));
317-
beans.putAll(getBeans(beanFactory, Converter.class));
318-
beans.putAll(getBeans(beanFactory, ConverterFactory.class));
319-
beans.putAll(getBeans(beanFactory, GenericConverter.class));
332+
beans.putAll(getBeans(beanFactory, Printer.class, qualifier));
333+
beans.putAll(getBeans(beanFactory, Parser.class, qualifier));
334+
beans.putAll(getBeans(beanFactory, Formatter.class, qualifier));
335+
beans.putAll(getBeans(beanFactory, Converter.class, qualifier));
336+
beans.putAll(getBeans(beanFactory, ConverterFactory.class, qualifier));
337+
beans.putAll(getBeans(beanFactory, GenericConverter.class, qualifier));
320338
return beans;
321339
}
322340

323-
private static <T> Map<String, T> getBeans(ListableBeanFactory beanFactory, Class<T> type) {
324-
return beanFactory.getBeansOfType(type);
341+
private static <T> Map<String, T> getBeans(ListableBeanFactory beanFactory, Class<T> type, String qualifier) {
342+
return (!StringUtils.hasLength(qualifier)) ? beanFactory.getBeansOfType(type)
343+
: BeanFactoryAnnotationUtils.qualifiedBeansOfType(beanFactory, type, qualifier);
325344
}
326345

327346
static void addBean(FormatterRegistry registry, Object bean, ResolvableType beanType) {

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

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,8 +16,10 @@
1616

1717
package org.springframework.boot.context.properties;
1818

19+
import java.io.ByteArrayInputStream;
1920
import java.io.InputStream;
2021
import java.io.OutputStream;
22+
import java.nio.charset.StandardCharsets;
2123
import java.util.List;
2224

2325
import org.junit.jupiter.api.Test;
@@ -30,7 +32,10 @@
3032
import org.springframework.context.annotation.Configuration;
3133
import org.springframework.core.convert.ConversionService;
3234
import org.springframework.core.convert.converter.Converter;
35+
import org.springframework.format.Printer;
3336
import org.springframework.format.support.FormattingConversionService;
37+
import org.springframework.util.StreamUtils;
38+
import org.springframework.util.function.ThrowingSupplier;
3439

3540
import static org.assertj.core.api.Assertions.assertThat;
3641

@@ -82,6 +87,28 @@ void getConversionServiceWhenHasQualifiedConverterBeansContainsCustomizedFormatt
8287
assertThat(conversionServices.get(1)).isSameAs(ApplicationConversionService.getSharedInstance());
8388
}
8489

90+
@Test
91+
void getConversionServiceWhenHasQualifiedConverterLambdaBeansContainsCustomizedFormattingService() {
92+
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(
93+
CustomLambdaConverterConfiguration.class);
94+
ConversionServiceDeducer deducer = new ConversionServiceDeducer(applicationContext);
95+
List<ConversionService> conversionServices = deducer.getConversionServices();
96+
assertThat(conversionServices).hasSize(2);
97+
assertThat(conversionServices.get(0)).isExactlyInstanceOf(FormattingConversionService.class);
98+
assertThat(conversionServices.get(0).canConvert(InputStream.class, OutputStream.class)).isTrue();
99+
assertThat(conversionServices.get(0).canConvert(CharSequence.class, InputStream.class)).isTrue();
100+
assertThat(conversionServices.get(1)).isSameAs(ApplicationConversionService.getSharedInstance());
101+
}
102+
103+
@Test
104+
void getConversionServiceWhenHasPrinterBean() {
105+
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(PrinterConfiguration.class);
106+
ConversionServiceDeducer deducer = new ConversionServiceDeducer(applicationContext);
107+
List<ConversionService> conversionServices = deducer.getConversionServices();
108+
InputStream inputStream = new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8));
109+
assertThat(conversionServices.get(0).convert(inputStream, String.class)).isEqualTo("test");
110+
}
111+
85112
@Configuration(proxyBeanMethods = false)
86113
static class CustomConverterServiceConfiguration {
87114

@@ -114,6 +141,36 @@ StringConverter stringConverter() {
114141

115142
}
116143

144+
@Configuration(proxyBeanMethods = false)
145+
static class CustomLambdaConverterConfiguration {
146+
147+
@Bean
148+
@ConfigurationPropertiesBinding
149+
Converter<InputStream, OutputStream> testConverter() {
150+
return (source) -> new TestConverter().convert(source);
151+
}
152+
153+
@Bean
154+
@ConfigurationPropertiesBinding
155+
Converter<String, InputStream> stringConverter() {
156+
return (source) -> new StringConverter().convert(source);
157+
}
158+
159+
}
160+
161+
@Configuration(proxyBeanMethods = false)
162+
static class PrinterConfiguration {
163+
164+
@Bean
165+
@ConfigurationPropertiesBinding
166+
Printer<InputStream> inputStreamPrinter() {
167+
return (source, locale) -> ThrowingSupplier
168+
.of(() -> StreamUtils.copyToString(source, StandardCharsets.UTF_8))
169+
.get();
170+
}
171+
172+
}
173+
117174
private static final class TestApplicationConversionService extends ApplicationConversionService {
118175

119176
}

0 commit comments

Comments
 (0)