Merge pull request #22885 from viviel

* pr/22885:
  Polish 'Support lambda based converters via bean method signature generics'
  Support lambda based converters via bean method signature generics

Closes gh-22885
This commit is contained in:
Phillip Webb 2025-01-30 15:05:38 -08:00
commit 4eae8a096c
2 changed files with 524 additions and 27 deletions

View File

@ -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.
@ -17,12 +17,24 @@
package org.springframework.boot.convert;
import java.lang.annotation.Annotation;
import java.util.LinkedHashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Supplier;
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.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 +49,8 @@ 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.Assert;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;
/**
@ -49,10 +63,13 @@ import org.springframework.util.StringValueResolver;
* against registry instance.
*
* @author Phillip Webb
* @author Shixiong Guo
* @since 2.0.0
*/
public class ApplicationConversionService extends FormattingConversionService {
private static final ResolvableType STRING = ResolvableType.forClass(String.class);
private static volatile ApplicationConversionService sharedInstance;
private final boolean unmodifiable;
@ -265,35 +282,284 @@ public class ApplicationConversionService extends FormattingConversionService {
}
/**
* Add {@link GenericConverter}, {@link Converter}, {@link Printer}, {@link Parser}
* and {@link Formatter} beans from the specified context.
* 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
* @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);
}
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);
}
ConfigurableListableBeanFactory configurableBeanFactory = getConfigurableListableBeanFactory(beanFactory);
getBeans(beanFactory).forEach((beanName, bean) -> {
BeanDefinition beanDefinition = (configurableBeanFactory != null)
? configurableBeanFactory.getMergedBeanDefinition(beanName) : null;
ResolvableType type = (beanDefinition != null) ? beanDefinition.getResolvableType() : null;
addBean(registry, bean, type);
});
}
private static ConfigurableListableBeanFactory getConfigurableListableBeanFactory(ListableBeanFactory beanFactory) {
if (beanFactory instanceof ConfigurableApplicationContext applicationContext) {
return applicationContext.getBeanFactory();
}
if (beanFactory instanceof ConfigurableListableBeanFactory configurableListableBeanFactory) {
return configurableListableBeanFactory;
}
return null;
}
private static Map<String, Object> getBeans(ListableBeanFactory beanFactory) {
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));
return beans;
}
private static <T> Map<String, T> getBeans(ListableBeanFactory beanFactory, Class<T> type) {
return beanFactory.getBeansOfType(type);
}
static void addBean(FormatterRegistry registry, Object bean, ResolvableType beanType) {
if (bean instanceof GenericConverter converterBean) {
addBean(registry, converterBean, beanType, GenericConverter.class, registry::addConverter, (Runnable) null);
}
else if (bean instanceof Converter<?, ?> converterBean) {
addBean(registry, converterBean, beanType, Converter.class, registry::addConverter,
ConverterBeanAdapter::new);
}
else if (bean instanceof ConverterFactory<?, ?> converterBean) {
addBean(registry, converterBean, beanType, ConverterFactory.class, registry::addConverterFactory,
ConverterFactoryBeanAdapter::new);
}
else if (bean instanceof Formatter<?> formatterBean) {
addBean(registry, formatterBean, beanType, Formatter.class, registry::addFormatter, () -> {
registry.addConverter(new PrinterBeanAdapter(formatterBean, beanType));
registry.addConverter(new ParserBeanAdapter(formatterBean, beanType));
});
}
else if (bean instanceof Printer<?> printerBean) {
addBean(registry, printerBean, beanType, Printer.class, registry::addPrinter, PrinterBeanAdapter::new);
}
else if (bean instanceof Parser<?> parserBean) {
addBean(registry, parserBean, beanType, Parser.class, registry::addParser, ParserBeanAdapter::new);
}
}
private static <B, T> void addBean(FormatterRegistry registry, B bean, ResolvableType beanType, Class<T> type,
Consumer<B> standardRegistrar, BiFunction<B, ResolvableType, BeanAdapter<?>> beanAdapterFactory) {
addBean(registry, bean, beanType, type, standardRegistrar,
() -> registry.addConverter(beanAdapterFactory.apply(bean, beanType)));
}
private static <B, T> void addBean(FormatterRegistry registry, B bean, ResolvableType beanType, Class<T> type,
Consumer<B> standardRegistrar, Runnable beanAdapterRegistrar) {
if (beanType != null && beanAdapterRegistrar != null
&& ResolvableType.forInstance(bean).as(type).hasUnresolvableGenerics()) {
beanAdapterRegistrar.run();
return;
}
standardRegistrar.accept(bean);
}
/**
* Base class for adapters that adapt a bean to a {@link GenericConverter}.
*
* @param <B> the base type of the bean
*/
abstract static class BeanAdapter<B> implements ConditionalGenericConverter {
private final B bean;
private final ResolvableTypePair types;
BeanAdapter(B bean, ResolvableType beanType) {
Assert.isInstanceOf(beanType.toClass(), bean);
ResolvableType type = ResolvableType.forClass(getClass()).as(BeanAdapter.class).getGeneric();
ResolvableType[] generics = beanType.as(type.toClass()).getGenerics();
this.bean = bean;
this.types = getResolvableTypePair(generics);
}
protected ResolvableTypePair getResolvableTypePair(ResolvableType[] generics) {
return new ResolvableTypePair(generics[0], generics[1]);
}
protected B bean() {
return this.bean;
}
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Set.of(new ConvertiblePair(this.types.source().toClass(), this.types.target().toClass()));
}
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return (this.types.target().toClass() == targetType.getObjectType()
&& matchesTargetType(targetType.getResolvableType()));
}
private boolean matchesTargetType(ResolvableType targetType) {
ResolvableType ours = this.types.target();
return targetType.getType() instanceof Class || targetType.isAssignableFrom(ours)
|| this.types.target().hasUnresolvableGenerics();
}
protected final boolean conditionalConverterCandidateMatches(Object conditionalConverterCandidate,
TypeDescriptor sourceType, TypeDescriptor targetType) {
return (conditionalConverterCandidate instanceof ConditionalConverter conditionalConverter)
? conditionalConverter.matches(sourceType, targetType) : true;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
protected final Object convert(Object source, TypeDescriptor targetType, Converter<?, ?> converter) {
return (source != null) ? ((Converter) converter).convert(source) : convertNull(targetType);
}
private Object convertNull(TypeDescriptor targetType) {
return (targetType.getObjectType() != Optional.class) ? null : Optional.empty();
}
@Override
public String toString() {
return this.types + " : " + this.bean;
}
}
/**
* Adapts a {@link Printer} bean to a {@link GenericConverter}.
*/
static class PrinterBeanAdapter extends BeanAdapter<Printer<?>> {
PrinterBeanAdapter(Printer<?> bean, ResolvableType beanType) {
super(bean, beanType);
}
@Override
protected ResolvableTypePair getResolvableTypePair(ResolvableType[] generics) {
return new ResolvableTypePair(generics[0], STRING);
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
return (source != null) ? print(source) : "";
}
@SuppressWarnings("unchecked")
private String print(Object object) {
return ((Printer<Object>) bean()).print(object, LocaleContextHolder.getLocale());
}
}
/**
* Adapts a {@link Parser} bean to a {@link GenericConverter}.
*/
static class ParserBeanAdapter extends BeanAdapter<Parser<?>> {
ParserBeanAdapter(Parser<?> bean, ResolvableType beanType) {
super(bean, beanType);
}
@Override
protected ResolvableTypePair getResolvableTypePair(ResolvableType[] generics) {
return new ResolvableTypePair(STRING, generics[0]);
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
String text = (String) source;
return (!StringUtils.hasText(text)) ? null : parse(text);
}
private Object parse(String text) {
try {
return bean().parse(text, LocaleContextHolder.getLocale());
}
catch (IllegalArgumentException ex) {
throw ex;
}
catch (Throwable ex) {
throw new IllegalArgumentException("Parse attempt failed for value [" + text + "]", ex);
}
}
}
/**
* Adapts a {@link Converter} bean to a {@link GenericConverter}.
*/
static final class ConverterBeanAdapter extends BeanAdapter<Converter<?, ?>> {
ConverterBeanAdapter(Converter<?, ?> bean, ResolvableType beanType) {
super(bean, beanType);
}
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return super.matches(sourceType, targetType)
&& conditionalConverterCandidateMatches(bean(), sourceType, targetType);
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
return convert(source, targetType, bean());
}
}
/**
* Adapts a {@link ConverterFactory} bean to a {@link GenericConverter}.
*/
private static final class ConverterFactoryBeanAdapter extends BeanAdapter<ConverterFactory<?, ?>> {
ConverterFactoryBeanAdapter(ConverterFactory<?, ?> bean, ResolvableType beanType) {
super(bean, beanType);
}
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return super.matches(sourceType, targetType)
&& conditionalConverterCandidateMatches(bean(), sourceType, targetType)
&& conditionalConverterCandidateMatches(getConverter(targetType::getType), sourceType, targetType);
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
return convert(source, targetType, getConverter(targetType::getObjectType));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private Converter<Object, ?> getConverter(Supplier<Class<?>> typeSupplier) {
return ((ConverterFactory) bean()).getConverter(typeSupplier.get());
}
}
/**
* Convertible type information as extracted from bean generics.
*
* @param source the source type
* @param target the target type
*/
record ResolvableTypePair(ResolvableType source, ResolvableType target) {
ResolvableTypePair {
Assert.notNull(source.resolve(), "'source' cannot be resolved");
Assert.notNull(target.resolve(), "'target' cannot be resolved");
}
@Override
public final String toString() {
return source() + " -> " + target();
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 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,34 +19,53 @@ package org.springframework.boot.convert;
import java.text.ParseException;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.junit.jupiter.api.Test;
import org.springframework.boot.convert.ApplicationConversionService.ConverterBeanAdapter;
import org.springframework.boot.convert.ApplicationConversionService.ParserBeanAdapter;
import org.springframework.boot.convert.ApplicationConversionService.PrinterBeanAdapter;
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.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalConverter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.Parser;
import org.springframework.format.Printer;
import org.springframework.format.support.FormattingConversionService;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
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.never;
import static org.mockito.Mockito.withSettings;
/**
* Tests for {@link ApplicationConversionService}.
*
* @author Phillip Webb
* @author Shixiong Guo
*/
class ApplicationConversionServiceTests {
private final FormatterRegistry registry = mock(FormatterRegistry.class);
private final FormatterRegistry registry = mock(FormatterRegistry.class,
withSettings().extraInterfaces(ConversionService.class));
@Test
void addBeansWhenHasGenericConverterBeanAddConverter() {
@ -94,6 +113,47 @@ class ApplicationConversionServiceTests {
}
}
@Test
@SuppressWarnings("unchecked")
void addBeansWhenHasConverterBeanMethodAddConverter() {
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
ConverterBeanMethodConfiguration.class)) {
Converter<String, Integer> converter = (Converter<String, Integer>) context.getBean("converter");
willThrow(IllegalArgumentException.class).given(this.registry).addConverter(converter);
ApplicationConversionService.addBeans(this.registry, context);
then(this.registry).should().addConverter(any(ConverterBeanAdapter.class));
then(this.registry).shouldHaveNoMoreInteractions();
}
}
@Test
@SuppressWarnings("unchecked")
void addBeansWhenHasPrinterBeanMethodAddPrinter() {
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
PrinterBeanMethodConfiguration.class)) {
Printer<Integer> printer = (Printer<Integer>) context.getBean("printer");
willThrow(IllegalArgumentException.class).given(this.registry).addPrinter(printer);
ApplicationConversionService.addBeans(this.registry, context);
then(this.registry).should(never()).addPrinter(printer);
then(this.registry).should().addConverter(any(PrinterBeanAdapter.class));
then(this.registry).shouldHaveNoMoreInteractions();
}
}
@Test
@SuppressWarnings("unchecked")
void addBeansWhenHasParserBeanMethodAddParser() {
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
ParserBeanMethodConfiguration.class)) {
Parser<Integer> parser = (Parser<Integer>) context.getBean("parser");
willThrow(IllegalArgumentException.class).given(this.registry).addParser(parser);
ApplicationConversionService.addBeans(this.registry, context);
then(this.registry).should(never()).addParser(parser);
then(this.registry).should().addConverter(any(ParserBeanAdapter.class));
then(this.registry).shouldHaveNoMoreInteractions();
}
}
@Test
void isConvertViaObjectSourceTypeWhenObjectSourceReturnsTrue() {
// Uses ObjectToCollectionConverter
@ -131,6 +191,130 @@ class ApplicationConversionServiceTests {
assertUnmodifiableExceptionThrown(() -> instance.removeConvertible(null, null));
}
@Test
void addPrinterBeanWithTypeConvertsUsingTypeInformation() {
FormattingConversionService conversionService = new FormattingConversionService();
Printer<?> printer = (object, locale) -> object.toString().toUpperCase(locale);
ApplicationConversionService.addBean(conversionService, printer,
ResolvableType.forClassWithGenerics(Printer.class, ExampleRecord.class));
assertThat(conversionService.convert(new ExampleRecord("test"), String.class)).isEqualTo("TEST");
assertThatExceptionOfType(ConverterNotFoundException.class)
.isThrownBy(() -> conversionService.convert(new OtherRecord("test"), String.class));
assertThatIllegalArgumentException().isThrownBy(() -> conversionService.addPrinter(printer))
.withMessageContaining("Unable to extract");
}
@Test
void addParserBeanWithTypeConvertsUsingTypeInformation() {
FormattingConversionService conversionService = new FormattingConversionService();
Parser<?> parser = (text, locale) -> new ExampleRecord(text.toString());
ApplicationConversionService.addBean(conversionService, parser,
ResolvableType.forClassWithGenerics(Parser.class, ExampleRecord.class));
assertThat(conversionService.convert("test", ExampleRecord.class)).isEqualTo(new ExampleRecord("test"));
assertThatExceptionOfType(ConverterNotFoundException.class)
.isThrownBy(() -> conversionService.convert("test", OtherRecord.class));
assertThatIllegalArgumentException().isThrownBy(() -> conversionService.addParser(parser))
.withMessageContaining("Unable to extract");
}
@Test
@SuppressWarnings("rawtypes")
void addFormatterBeanWithTypeConvertsUsingTypeInformation() {
FormattingConversionService conversionService = new FormattingConversionService();
Formatter<?> formatter = new Formatter() {
@Override
public String print(Object object, Locale locale) {
return object.toString().toUpperCase(locale);
}
@Override
public Object parse(String text, Locale locale) throws ParseException {
return new ExampleRecord(text.toString());
}
};
ApplicationConversionService.addBean(conversionService, formatter,
ResolvableType.forClassWithGenerics(Formatter.class, ExampleRecord.class));
assertThat(conversionService.convert(new ExampleRecord("test"), String.class)).isEqualTo("TEST");
assertThat(conversionService.convert("test", ExampleRecord.class)).isEqualTo(new ExampleRecord("test"));
assertThatExceptionOfType(ConverterNotFoundException.class)
.isThrownBy(() -> conversionService.convert(new OtherRecord("test"), String.class));
assertThatExceptionOfType(ConverterNotFoundException.class)
.isThrownBy(() -> conversionService.convert("test", OtherRecord.class));
assertThatIllegalArgumentException().isThrownBy(() -> conversionService.addFormatter(formatter))
.withMessageContaining("Unable to extract");
}
@Test
void addConverterBeanWithTypeConvertsUsingTypeInformation() {
FormattingConversionService conversionService = new FormattingConversionService();
Converter<?, ?> converter = (source) -> new ExampleRecord(source.toString());
ApplicationConversionService.addBean(conversionService, converter,
ResolvableType.forClassWithGenerics(Converter.class, CharSequence.class, ExampleRecord.class));
assertThat(conversionService.convert("test", ExampleRecord.class)).isEqualTo(new ExampleRecord("test"));
assertThat(conversionService.convert(new StringBuilder("test"), ExampleRecord.class))
.isEqualTo(new ExampleRecord("test"));
assertThatExceptionOfType(ConverterNotFoundException.class)
.isThrownBy(() -> conversionService.convert("test", OtherRecord.class));
assertThatIllegalArgumentException().isThrownBy(() -> conversionService.addConverter(converter))
.withMessageContaining("Unable to determine");
}
@Test
@SuppressWarnings("rawtypes")
void addConverterBeanWithTypeWhenConditionalChecksCondition() {
FormattingConversionService conversionService = new FormattingConversionService();
ConditionalConverterConverter<?, ?> converter = new ConditionalConverterConverter() {
@Override
public Object convert(Object source) {
return new ExampleRecord(source.toString());
}
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return sourceType.getType() != StringBuilder.class;
}
};
ApplicationConversionService.addBean(conversionService, converter,
ResolvableType.forClassWithGenerics(Converter.class, CharSequence.class, ExampleRecord.class));
assertThat(conversionService.convert("test", ExampleRecord.class)).isEqualTo(new ExampleRecord("test"));
assertThatExceptionOfType(ConverterNotFoundException.class)
.isThrownBy(() -> conversionService.convert(new StringBuilder("test"), ExampleRecord.class));
assertThatIllegalArgumentException().isThrownBy(() -> conversionService.addConverter(converter))
.withMessageContaining("Unable to determine");
}
@Test
@SuppressWarnings("unchecked")
void addConverterBeanWithTypeWhenNullSourceCanConvertToOptionEmpty() {
FormattingConversionService conversionService = new FormattingConversionService();
Converter<?, ?> converter = (source) -> new ExampleRecord(source.toString());
ApplicationConversionService.addBean(conversionService, converter,
ResolvableType.forClassWithGenerics(Converter.class, CharSequence.class, ExampleRecord.class));
assertThat(conversionService.convert(null, ExampleRecord.class)).isNull();
assertThat(conversionService.convert(null, Optional.class)).isEmpty();
}
@Test
@SuppressWarnings("rawtypes")
void addConverterFactoryBeanWithTypeConvertsUsingTypeInformation() {
FormattingConversionService conversionService = new FormattingConversionService();
Converter<?, ?> converter = (source) -> new ExampleRecord(source.toString());
ConverterFactory converterFactory = (targetType) -> converter;
ApplicationConversionService.addBean(conversionService, converterFactory,
ResolvableType.forClassWithGenerics(ConverterFactory.class, CharSequence.class, ExampleRecord.class));
assertThat(conversionService.convert("test", ExampleRecord.class)).isEqualTo(new ExampleRecord("test"));
assertThat(conversionService.convert(new StringBuilder("test"), ExampleRecord.class))
.isEqualTo(new ExampleRecord("test"));
assertThatExceptionOfType(ConverterNotFoundException.class)
.isThrownBy(() -> conversionService.convert("test", OtherRecord.class));
assertThatIllegalArgumentException().isThrownBy(() -> conversionService.addConverterFactory(converterFactory))
.withMessageContaining("Unable to determine");
}
private void assertUnmodifiableExceptionThrown(ThrowingCallable throwingCallable) {
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(throwingCallable)
.withMessage("This ApplicationConversionService cannot be modified");
@ -191,4 +375,51 @@ class ApplicationConversionServiceTests {
}
@Configuration
static class ConverterBeanMethodConfiguration {
@Bean
Converter<String, Integer> converter() {
return Integer::valueOf;
}
}
@Configuration
static class PrinterBeanMethodConfiguration {
@Bean
Printer<Integer> printer() {
return (object, locale) -> object.toString();
}
}
@Configuration
static class ParserBeanMethodConfiguration {
@Bean
Parser<Integer> parser() {
return (text, locale) -> Integer.valueOf(text);
}
}
record ExampleRecord(String value) {
@Override
public final String toString() {
return value();
}
}
record OtherRecord(String value) {
}
interface ConditionalConverterConverter<S, T> extends Converter<S, T>, ConditionalConverter {
}
}