Support lambda based converters via bean method signature generics

Update `ApplicationConversionService` to support beans that are
implemented using lambdas. The updated code now uses the result of
`beanDefinition.getResolvableType()` if the type itself has unresolvable
generics.

See gh-22885
This commit is contained in:
viviel
2021-12-04 19:51:58 +08:00
committed by Phillip Webb
parent 1bee5f3942
commit 2921b1428e
2 changed files with 407 additions and 19 deletions

View File

@@ -17,12 +17,23 @@
package org.springframework.boot.convert;
import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalConverter;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.core.convert.converter.ConverterRegistry;
@@ -37,6 +48,7 @@ import org.springframework.format.Parser;
import org.springframework.format.Printer;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
/**
@@ -49,6 +61,7 @@ import org.springframework.util.StringValueResolver;
* against registry instance.
*
* @author Phillip Webb
* @author Shixiong Guo(viviel)
* @since 2.0.0
*/
public class ApplicationConversionService extends FormattingConversionService {
@@ -272,28 +285,313 @@ public class ApplicationConversionService extends FormattingConversionService {
* @since 2.2.0
*/
public static void addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory) {
Set<Object> beans = new LinkedHashSet<>();
beans.addAll(beanFactory.getBeansOfType(GenericConverter.class).values());
beans.addAll(beanFactory.getBeansOfType(Converter.class).values());
beans.addAll(beanFactory.getBeansOfType(Printer.class).values());
beans.addAll(beanFactory.getBeansOfType(Parser.class).values());
for (Object bean : beans) {
if (bean instanceof GenericConverter genericConverter) {
registry.addConverter(genericConverter);
Set<Map.Entry<String, ?>> entries = new LinkedHashSet<>();
entries.addAll(beanFactory.getBeansOfType(GenericConverter.class).entrySet());
entries.addAll(beanFactory.getBeansOfType(Converter.class).entrySet());
entries.addAll(beanFactory.getBeansOfType(Printer.class).entrySet());
entries.addAll(beanFactory.getBeansOfType(Parser.class).entrySet());
for (Map.Entry<String, ?> e : entries) {
String beanName = e.getKey();
Object bean = e.getValue();
try {
doAddBean(registry, bean);
}
else if (bean instanceof Converter<?, ?> converter) {
registry.addConverter(converter);
}
else if (bean instanceof Formatter<?> formatter) {
registry.addFormatter(formatter);
}
else if (bean instanceof Printer<?> printer) {
registry.addPrinter(printer);
}
else if (bean instanceof Parser<?> parser) {
registry.addParser(parser);
catch (IllegalArgumentException ex) {
if (!tryAddFactoryMethodBean(registry, beanFactory, beanName, bean)) {
throw ex;
}
}
}
}
private static void doAddBean(FormatterRegistry registry, Object bean) {
if (bean instanceof GenericConverter) {
registry.addConverter((GenericConverter) bean);
}
else if (bean instanceof Converter) {
registry.addConverter((Converter<?, ?>) bean);
}
else if (bean instanceof Formatter) {
registry.addFormatter((Formatter<?>) bean);
}
else if (bean instanceof Printer) {
registry.addPrinter((Printer<?>) bean);
}
else if (bean instanceof Parser) {
registry.addParser((Parser<?>) bean);
}
}
private static boolean tryAddFactoryMethodBean(FormatterRegistry registry, ListableBeanFactory beanFactory,
String beanName, Object bean) {
ConfigurableListableBeanFactory clbf = getConfigurableListableBeanFactory(beanFactory);
if (clbf == null) {
return false;
}
if (!isFactoryMethod(clbf, beanName)) {
return false;
}
if (bean instanceof Converter) {
return addConverter(registry, clbf, beanName, (Converter<?, ?>) bean);
}
else if (bean instanceof Printer) {
return addPrinter(registry, clbf, beanName, (Printer<?>) bean);
}
else if (bean instanceof Parser) {
return addParser(registry, clbf, beanName, (Parser<?>) bean);
}
return false;
}
private static ConfigurableListableBeanFactory getConfigurableListableBeanFactory(ListableBeanFactory beanFactory) {
ListableBeanFactory bf = beanFactory;
if (bf instanceof ConfigurableApplicationContext) {
bf = ((ConfigurableApplicationContext) bf).getBeanFactory();
}
if (bf instanceof ConfigurableListableBeanFactory) {
return (ConfigurableListableBeanFactory) bf;
}
return null;
}
private static boolean isFactoryMethod(ConfigurableListableBeanFactory clbf, String beanName) {
BeanDefinition bd = clbf.getMergedBeanDefinition(beanName);
return bd.getFactoryMethodName() != null;
}
private static boolean addConverter(FormatterRegistry registry, ConfigurableListableBeanFactory beanFactory,
String beanName, Converter<?, ?> converter) {
ConverterAdapter adapter = getConverterAdapter(beanFactory, beanName, converter);
if (adapter == null) {
return false;
}
registry.addConverter(adapter);
return true;
}
private static ConverterAdapter getConverterAdapter(ConfigurableListableBeanFactory beanFactory, String beanName,
Converter<?, ?> converter) {
ResolvableType[] types = getResolvableType(beanFactory, beanName);
if (types.length < 2) {
return null;
}
return new ConverterAdapter(converter, types[0], types[1]);
}
private static ResolvableType[] getResolvableType(ConfigurableListableBeanFactory beanFactory, String beanName) {
BeanDefinition beanDefinition = beanFactory.getMergedBeanDefinition(beanName);
ResolvableType resolvableType = beanDefinition.getResolvableType();
return resolvableType.getGenerics();
}
private static boolean addPrinter(FormatterRegistry registry, ConfigurableListableBeanFactory beanFactory,
String beanName, Printer<?> printer) {
PrinterAdapter adapter = getPrinterAdapter(beanFactory, beanName, printer);
if (adapter == null) {
return false;
}
registry.addConverter(adapter);
return true;
}
private static PrinterAdapter getPrinterAdapter(ConfigurableListableBeanFactory beanFactory, String beanName,
Printer<?> printer) {
ResolvableType[] types = getResolvableType(beanFactory, beanName);
if (types.length < 1) {
return null;
}
ConversionService conversionService = beanFactory.getBean(ConversionService.class);
return new PrinterAdapter(types[0].resolve(), printer, conversionService);
}
private static boolean addParser(FormatterRegistry registry, ConfigurableListableBeanFactory beanFactory,
String beanName, Parser<?> parser) {
ParserAdapter adapter = getParserAdapter(beanFactory, beanName, parser);
if (adapter == null) {
return false;
}
registry.addConverter(adapter);
return true;
}
private static ParserAdapter getParserAdapter(ConfigurableListableBeanFactory beanFactory, String beanName,
Parser<?> parser) {
ResolvableType[] types = getResolvableType(beanFactory, beanName);
if (types.length < 1) {
return null;
}
ConversionService conversionService = beanFactory.getBean(ConversionService.class);
return new ParserAdapter(types[0].resolve(), parser, conversionService);
}
/**
* Adapts a {@link Converter} to a {@link GenericConverter}.
* <p>
* Reference from
* {@link org.springframework.core.convert.support.GenericConversionService.ConverterAdapter}
*/
@SuppressWarnings("unchecked")
private static final class ConverterAdapter implements ConditionalGenericConverter {
private final Converter<Object, Object> converter;
private final ConvertiblePair typeInfo;
private final ResolvableType targetType;
ConverterAdapter(Converter<?, ?> converter, ResolvableType sourceType, ResolvableType targetType) {
this.converter = (Converter<Object, Object>) converter;
this.typeInfo = new ConvertiblePair(sourceType.toClass(), targetType.toClass());
this.targetType = targetType;
}
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(this.typeInfo);
}
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
// Check raw type first...
if (this.typeInfo.getTargetType() != targetType.getObjectType()) {
return false;
}
// Full check for complex generic type match required?
ResolvableType rt = targetType.getResolvableType();
if (!(rt.getType() instanceof Class) && !rt.isAssignableFrom(this.targetType)
&& !this.targetType.hasUnresolvableGenerics()) {
return false;
}
return !(this.converter instanceof ConditionalConverter)
|| ((ConditionalConverter) this.converter).matches(sourceType, targetType);
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return convertNullSource(sourceType, targetType);
}
return this.converter.convert(source);
}
@Override
public String toString() {
return (this.typeInfo + " : " + this.converter);
}
/**
* Template method to convert a {@code null} source.
* <p>
* The default implementation returns {@code null} or the Java 8
* {@link java.util.Optional#empty()} instance if the target type is
* {@code java.util.Optional}. Subclasses may override this to return custom
* {@code null} objects for specific target types.
* @param sourceType the source type to convert from
* @param targetType the target type to convert to
* @return the converted null object
*/
private Object convertNullSource(TypeDescriptor sourceType, TypeDescriptor targetType) {
if (targetType.getObjectType() == Optional.class) {
return Optional.empty();
}
return null;
}
}
private static class PrinterAdapter implements GenericConverter {
private final Class<?> fieldType;
private final TypeDescriptor printerObjectType;
@SuppressWarnings("rawtypes")
private final Printer printer;
private final ConversionService conversionService;
PrinterAdapter(Class<?> fieldType, Printer<?> printer, ConversionService conversionService) {
this.fieldType = fieldType;
this.printerObjectType = TypeDescriptor.valueOf(resolvePrinterObjectType(printer));
this.printer = printer;
this.conversionService = conversionService;
}
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(this.fieldType, String.class));
}
@Override
@SuppressWarnings("unchecked")
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (!sourceType.isAssignableTo(this.printerObjectType)) {
source = this.conversionService.convert(source, sourceType, this.printerObjectType);
}
if (source == null) {
return "";
}
return this.printer.print(source, LocaleContextHolder.getLocale());
}
private Class<?> resolvePrinterObjectType(Printer<?> printer) {
return GenericTypeResolver.resolveTypeArgument(printer.getClass(), Printer.class);
}
@Override
public String toString() {
return (this.fieldType.getName() + " -> " + String.class.getName() + " : " + this.printer);
}
}
private static class ParserAdapter implements GenericConverter {
private final Class<?> fieldType;
private final Parser<?> parser;
private final ConversionService conversionService;
ParserAdapter(Class<?> fieldType, Parser<?> parser, ConversionService conversionService) {
this.fieldType = fieldType;
this.parser = parser;
this.conversionService = conversionService;
}
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(String.class, this.fieldType));
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
String text = (String) source;
if (!StringUtils.hasText(text)) {
return null;
}
Object result;
try {
result = this.parser.parse(text, LocaleContextHolder.getLocale());
}
catch (IllegalArgumentException ex) {
throw ex;
}
catch (Throwable ex) {
throw new IllegalArgumentException("Parse attempt failed for value [" + text + "]", ex);
}
TypeDescriptor resultType = TypeDescriptor.valueOf(result.getClass());
if (!resultType.isAssignableTo(targetType)) {
result = this.conversionService.convert(result, resultType, targetType);
}
return result;
}
@Override
public String toString() {
return (String.class.getName() + " -> " + this.fieldType.getName() + ": " + this.parser);
}
}
}

View File

@@ -26,6 +26,9 @@ import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
@@ -36,13 +39,18 @@ import org.springframework.format.Printer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.then;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link ApplicationConversionService}.
*
* @author Phillip Webb
* @author Shixiong Guo(viviel)
*/
class ApplicationConversionServiceTests {
@@ -94,6 +102,48 @@ class ApplicationConversionServiceTests {
}
}
@SuppressWarnings("unchecked")
@Test
void addBeansWhenHasFactoryMethodConverterBeanAddConverter() {
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
FactoryMethodConverter.class)) {
Converter<String, Integer> converter = (Converter<String, Integer>) context.getBean("converter");
willThrow(IllegalArgumentException.class).given(this.registry).addConverter(converter);
ApplicationConversionService.addBeans(this.registry, context);
verify(this.registry).addConverter(converter);
verify(this.registry).addConverter(any(GenericConverter.class));
verifyNoMoreInteractions(this.registry);
}
}
@SuppressWarnings("unchecked")
@Test
void addBeansWhenHasFactoryMethodPrinterBeanAddPrinter() {
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
FactoryMethodPrinter.class)) {
Printer<Integer> printer = (Printer<Integer>) context.getBean("printer");
willThrow(IllegalArgumentException.class).given(this.registry).addPrinter(printer);
ApplicationConversionService.addBeans(this.registry, context);
verify(this.registry).addPrinter(printer);
verify(this.registry).addConverter(any(GenericConverter.class));
verifyNoMoreInteractions(this.registry);
}
}
@SuppressWarnings("unchecked")
@Test
void addBeansWhenHasFactoryMethodParserBeanAddParser() {
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
FactoryMethodParser.class)) {
Parser<Integer> parser = (Parser<Integer>) context.getBean("parser");
willThrow(IllegalArgumentException.class).given(this.registry).addParser(parser);
ApplicationConversionService.addBeans(this.registry, context);
verify(this.registry).addParser(parser);
verify(this.registry).addConverter(any(GenericConverter.class));
verifyNoMoreInteractions(this.registry);
}
}
@Test
void isConvertViaObjectSourceTypeWhenObjectSourceReturnsTrue() {
// Uses ObjectToCollectionConverter
@@ -191,4 +241,44 @@ class ApplicationConversionServiceTests {
}
@Configuration
static class FactoryMethodConverter {
@Bean
Converter<String, Integer> converter() {
return Integer::valueOf;
}
}
@Configuration
static class FactoryMethodPrinter {
@Bean
Printer<Integer> printer() {
return (object, locale) -> object.toString();
}
@Bean
ConversionService conversionService() {
return mock(ConversionService.class);
}
}
@Configuration
static class FactoryMethodParser {
@Bean
Parser<Integer> parser() {
return (text, locale) -> Integer.valueOf(text);
}
@Bean
ConversionService conversionService() {
return mock(ConversionService.class);
}
}
}