当使用 lambda 表达式而不是匿名内部类时,Spring 无法确定泛型类型

2022-09-03 15:05:47

我正在玩Spring的,添加一个简单的转换器来将(Java 8)转换为:ConversionServiceZonedDateTimeString

@Bean
public ConversionServiceFactoryBean conversionServiceFactoryBean() {
    ConversionServiceFactoryBean conversionServiceFactoryBean =
        new ConversionServiceFactoryBean();

    Converter<ZonedDateTime, String> dateTimeConverter =
        new Converter<ZonedDateTime, String>() {
            @Override
            public String convert(ZonedDateTime source) {
                return source.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
            }
        };

    conversionServiceFactoryBean.setConverters(
        new HashSet<>(Arrays.asList(dateTimeConverter)));
    return conversionServiceFactoryBean;
}

这工作正常。但是我的IDE(IntelliJ)建议用lambda表达式替换匿名内部类:

Converter<ZonedDateTime, String> dateTimeConverter =
    source -> source.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);

如果我这样做,那么它就不再起作用了,我得到一个关于Spring无法确定泛型类型的错误:

Caused by: java.lang.IllegalArgumentException: Unable to the determine sourceType <S> and targetType <T> which your Converter<S, T> converts between; declare these generic types.
    at org.springframework.util.Assert.notNull(Assert.java:112)
    at org.springframework.core.convert.support.GenericConversionService.addConverter(GenericConversionService.java:100)
    at org.springframework.core.convert.support.ConversionServiceFactory.registerConverters(ConversionServiceFactory.java:50)
    at org.springframework.context.support.ConversionServiceFactoryBean.afterPropertiesSet(ConversionServiceFactoryBean.java:70)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1627)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1564)

表示 lambda 表达式的对象显然与匿名内部类的对象有很大不同,以至于 Spring 无法再确定泛型类型。Java 8 如何使用 lambda 表达式来做到这一点?这是Spring中一个可以修复的错误,还是Java 8没有提供必要的信息?ClassClass

我使用的是Spring版本4.1.0.RELEASE和Java 8 update 20。


答案 1

艾伦·斯托克斯(Alan Stokes)评论中链接的这篇文章很好地解释了这个问题。

基本上,在当前的JDK中,lambda的实际实现被编译到声明类中,JVM生成一个Lambda类,其方法是擦除接口中声明的方法。

所以

Converter<ZonedDateTime, String> dateTimeConverter =
    source -> source.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);

产生一种合成方法,如

private static java.lang.String com.example.Test.lambda$0(java.time.ZonedDateTime source)  {
    return source.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}

它由生成的 lambda 类实例调用。在内部,函数接口方法只是简单地转换为上述方法的参数类型。JLS声明

如果被覆盖方法类型的擦除在其签名上与 函数类型的擦除不同,则在计算或执行 lambda 主体之前,该方法的主体会检查每个参数值是否是 U 函数类型中相应参数类型擦除的子类或子接口;如果不是,则抛出 a。UClassCastException

VM 本身会生成一个重写方法,该方法与接口中声明的方法基本等效。

您拥有的有关类型的唯一信息位于上面的方法中。由于此方法是声明类的一部分,因此在给定从 lambda 表达式生成的实例的情况下,Spring 无法检索它。static

但是,您可以执行

interface ZonedDateTimeToStringConverter extends Converter<ZonedDateTime, String> {
}

Converter<ZonedDateTime, String> dateTimeConverter = (ZonedDateTimeToStringConverter)
    source -> source.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);

ZonedDateTimeToStringConverter dateTimeConverter =  source -> source.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);

这强制 lambda 声明一个类似

public String convert(ZonedDateTime zdt);

Spring将能够找到它并解析目标和源类型。


答案 2

查看类型工具

Converter<ZonedDateTime, String> converter = source -> source.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
Class<?>[] typeArgs = TypeResolver.resolveRawArguments(Converter.class, converter.getClass());

assert typeArgs[0] == ZonedDateTime.class;
assert typeArgs[1] == String.class;

这种方法适用于Oracle /OpenJDK,因为它使用API。sun.reflect.ConstantPool

注意:我被要求为Spring贡献这项工作,但我还没有空闲时间(快速浏览一下,与Spring现有的泛型类型解析内容完全集成似乎并非易事)。


推荐