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");
|
||||
* 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<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) {
|
||||
|
@ -83,50 +73,18 @@ class ConversionServiceDeducer {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<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) {
|
||||
|
@ -309,19 +327,20 @@ public class ApplicationConversionService extends FormattingConversionService {
|
|||
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) {
|
||||
|
|
|
@ -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<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 {
|
||||
|
||||
|
@ -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 {
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue