Skip to content

Commit

Permalink
Allow @ConfigurationPropertiesBinding to work with lambdas
Browse files Browse the repository at this point in the history
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
  • Loading branch information
philwebb committed Jan 31, 2025
1 parent 4eae8a0 commit 7ec22d8
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 65 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -19,19 +19,13 @@
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;

/**
Expand Down Expand Up @@ -61,13 +55,9 @@ List<ConversionService> getConversionServices() {

private List<ConversionService> getConversionServices(ConfigurableApplicationContext applicationContext) {
List<ConversionService> conversionServices = new ArrayList<>();
ConverterBeans converterBeans = new ConverterBeans(applicationContext);
FormattingConversionService beansConverterService = new FormattingConversionService();
Map<String, Object> 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) {
Expand All @@ -83,50 +73,18 @@ private List<ConversionService> getConversionServices(ConfigurableApplicationCon
return conversionServices;
}

private Map<String, Object> 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<Converter> converters;

private final List<GenericConverter> genericConverters;

@SuppressWarnings("rawtypes")
private final List<Formatter> 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 <T> List<T> beans(Class<T> 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);
}
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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;
Expand Down Expand Up @@ -290,13 +291,30 @@ public static void addApplicationFormatters(FormatterRegistry registry) {
* @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<String, Object> addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory,
String qualifier) {
ConfigurableListableBeanFactory configurableBeanFactory = getConfigurableListableBeanFactory(beanFactory);
getBeans(beanFactory).forEach((beanName, bean) -> {
Map<String, Object> 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) {
Expand All @@ -309,19 +327,20 @@ private static ConfigurableListableBeanFactory getConfigurableListableBeanFactor
return null;
}

private static Map<String, Object> getBeans(ListableBeanFactory beanFactory) {
private static Map<String, Object> getBeans(ListableBeanFactory beanFactory, String qualifier) {
Map<String, Object> 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 <T> Map<String, T> getBeans(ListableBeanFactory beanFactory, Class<T> type) {
return beanFactory.getBeansOfType(type);
private static <T> Map<String, T> getBeans(ListableBeanFactory beanFactory, Class<T> type, String qualifier) {
return (!StringUtils.hasLength(qualifier)) ? beanFactory.getBeansOfType(type)
: BeanFactoryAnnotationUtils.qualifiedBeansOfType(beanFactory, type, qualifier);
}

static void addBean(FormatterRegistry registry, Object bean, ResolvableType beanType) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -30,7 +32,10 @@
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;

Expand Down Expand Up @@ -82,6 +87,28 @@ void getConversionServiceWhenHasQualifiedConverterBeansContainsCustomizedFormatt
assertThat(conversionServices.get(1)).isSameAs(ApplicationConversionService.getSharedInstance());
}

@Test
void getConversionServiceWhenHasQualifiedConverterLambdaBeansContainsCustomizedFormattingService() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(
CustomLambdaConverterConfiguration.class);
ConversionServiceDeducer deducer = new ConversionServiceDeducer(applicationContext);
List<ConversionService> 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<ConversionService> 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 {

Expand Down Expand Up @@ -114,6 +141,36 @@ StringConverter stringConverter() {

}

@Configuration(proxyBeanMethods = false)
static class CustomLambdaConverterConfiguration {

@Bean
@ConfigurationPropertiesBinding
Converter<InputStream, OutputStream> testConverter() {
return (source) -> new TestConverter().convert(source);
}

@Bean
@ConfigurationPropertiesBinding
Converter<String, InputStream> stringConverter() {
return (source) -> new StringConverter().convert(source);
}

}

@Configuration(proxyBeanMethods = false)
static class PrinterConfiguration {

@Bean
@ConfigurationPropertiesBinding
Printer<InputStream> inputStreamPrinter() {
return (source, locale) -> ThrowingSupplier
.of(() -> StreamUtils.copyToString(source, StandardCharsets.UTF_8))
.get();
}

}

private static final class TestApplicationConversionService extends ApplicationConversionService {

}
Expand Down

0 comments on commit 7ec22d8

Please sign in to comment.