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
This commit is contained in:
parent
4eae8a096c
commit
7ec22d8668
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
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.boot.convert.ApplicationConversionService;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.context.ConfigurableApplicationContext;
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
import org.springframework.core.convert.ConversionService;
|
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.core.convert.support.DefaultConversionService;
|
||||||
import org.springframework.format.Formatter;
|
|
||||||
import org.springframework.format.FormatterRegistry;
|
|
||||||
import org.springframework.format.support.FormattingConversionService;
|
import org.springframework.format.support.FormattingConversionService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,13 +55,9 @@ class ConversionServiceDeducer {
|
||||||
|
|
||||||
private List<ConversionService> getConversionServices(ConfigurableApplicationContext applicationContext) {
|
private List<ConversionService> getConversionServices(ConfigurableApplicationContext applicationContext) {
|
||||||
List<ConversionService> conversionServices = new ArrayList<>();
|
List<ConversionService> conversionServices = new ArrayList<>();
|
||||||
ConverterBeans converterBeans = new ConverterBeans(applicationContext);
|
|
||||||
if (!converterBeans.isEmpty()) {
|
|
||||||
FormattingConversionService beansConverterService = new FormattingConversionService();
|
FormattingConversionService beansConverterService = new FormattingConversionService();
|
||||||
DefaultConversionService.addCollectionConverters(beansConverterService);
|
Map<String, Object> converterBeans = addBeans(applicationContext, beansConverterService);
|
||||||
beansConverterService
|
if (!converterBeans.isEmpty()) {
|
||||||
.addConverter(new ConfigurationPropertiesCharSequenceToObjectConverter(beansConverterService));
|
|
||||||
converterBeans.addTo(beansConverterService);
|
|
||||||
conversionServices.add(beansConverterService);
|
conversionServices.add(beansConverterService);
|
||||||
}
|
}
|
||||||
if (applicationContext.getBeanFactory().getConversionService() != null) {
|
if (applicationContext.getBeanFactory().getConversionService() != null) {
|
||||||
|
@ -83,50 +73,18 @@ class ConversionServiceDeducer {
|
||||||
return conversionServices;
|
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() {
|
private boolean hasUserDefinedConfigurationServiceBean() {
|
||||||
String beanName = ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME;
|
String beanName = ConfigurableApplicationContext.CONVERSION_SERVICE_BEAN_NAME;
|
||||||
return this.applicationContext.containsBean(beanName) && this.applicationContext.getAutowireCapableBeanFactory()
|
return this.applicationContext.containsBean(beanName) && this.applicationContext.getAutowireCapableBeanFactory()
|
||||||
.isTypeMatch(beanName, ConversionService.class);
|
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import java.util.function.Consumer;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import org.springframework.beans.factory.ListableBeanFactory;
|
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.BeanDefinition;
|
||||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||||
import org.springframework.context.ConfigurableApplicationContext;
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
@ -290,13 +291,30 @@ public class ApplicationConversionService extends FormattingConversionService {
|
||||||
* @since 2.2.0
|
* @since 2.2.0
|
||||||
*/
|
*/
|
||||||
public static void addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory) {
|
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);
|
ConfigurableListableBeanFactory configurableBeanFactory = getConfigurableListableBeanFactory(beanFactory);
|
||||||
getBeans(beanFactory).forEach((beanName, bean) -> {
|
Map<String, Object> beans = getBeans(beanFactory, qualifier);
|
||||||
|
beans.forEach((beanName, bean) -> {
|
||||||
BeanDefinition beanDefinition = (configurableBeanFactory != null)
|
BeanDefinition beanDefinition = (configurableBeanFactory != null)
|
||||||
? configurableBeanFactory.getMergedBeanDefinition(beanName) : null;
|
? configurableBeanFactory.getMergedBeanDefinition(beanName) : null;
|
||||||
ResolvableType type = (beanDefinition != null) ? beanDefinition.getResolvableType() : null;
|
ResolvableType type = (beanDefinition != null) ? beanDefinition.getResolvableType() : null;
|
||||||
addBean(registry, bean, type);
|
addBean(registry, bean, type);
|
||||||
});
|
});
|
||||||
|
return beans;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ConfigurableListableBeanFactory getConfigurableListableBeanFactory(ListableBeanFactory beanFactory) {
|
private static ConfigurableListableBeanFactory getConfigurableListableBeanFactory(ListableBeanFactory beanFactory) {
|
||||||
|
@ -309,19 +327,20 @@ public class ApplicationConversionService extends FormattingConversionService {
|
||||||
return null;
|
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<>();
|
Map<String, Object> beans = new LinkedHashMap<>();
|
||||||
beans.putAll(getBeans(beanFactory, Printer.class));
|
beans.putAll(getBeans(beanFactory, Printer.class, qualifier));
|
||||||
beans.putAll(getBeans(beanFactory, Parser.class));
|
beans.putAll(getBeans(beanFactory, Parser.class, qualifier));
|
||||||
beans.putAll(getBeans(beanFactory, Formatter.class));
|
beans.putAll(getBeans(beanFactory, Formatter.class, qualifier));
|
||||||
beans.putAll(getBeans(beanFactory, Converter.class));
|
beans.putAll(getBeans(beanFactory, Converter.class, qualifier));
|
||||||
beans.putAll(getBeans(beanFactory, ConverterFactory.class));
|
beans.putAll(getBeans(beanFactory, ConverterFactory.class, qualifier));
|
||||||
beans.putAll(getBeans(beanFactory, GenericConverter.class));
|
beans.putAll(getBeans(beanFactory, GenericConverter.class, qualifier));
|
||||||
return beans;
|
return beans;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static <T> Map<String, T> getBeans(ListableBeanFactory beanFactory, Class<T> type) {
|
private static <T> Map<String, T> getBeans(ListableBeanFactory beanFactory, Class<T> type, String qualifier) {
|
||||||
return beanFactory.getBeansOfType(type);
|
return (!StringUtils.hasLength(qualifier)) ? beanFactory.getBeansOfType(type)
|
||||||
|
: BeanFactoryAnnotationUtils.qualifiedBeansOfType(beanFactory, type, qualifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void addBean(FormatterRegistry registry, Object bean, ResolvableType beanType) {
|
static void addBean(FormatterRegistry registry, Object bean, ResolvableType beanType) {
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -16,8 +16,10 @@
|
||||||
|
|
||||||
package org.springframework.boot.context.properties;
|
package org.springframework.boot.context.properties;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
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.context.annotation.Configuration;
|
||||||
import org.springframework.core.convert.ConversionService;
|
import org.springframework.core.convert.ConversionService;
|
||||||
import org.springframework.core.convert.converter.Converter;
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.format.Printer;
|
||||||
import org.springframework.format.support.FormattingConversionService;
|
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;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@ -82,6 +87,28 @@ class ConversionServiceDeducerTests {
|
||||||
assertThat(conversionServices.get(1)).isSameAs(ApplicationConversionService.getSharedInstance());
|
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)
|
@Configuration(proxyBeanMethods = false)
|
||||||
static class CustomConverterServiceConfiguration {
|
static class CustomConverterServiceConfiguration {
|
||||||
|
|
||||||
|
@ -114,6 +141,36 @@ class ConversionServiceDeducerTests {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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 {
|
private static final class TestApplicationConversionService extends ApplicationConversionService {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue