如何确定实现具有lambda的通用FunctionalInterface的Bean的类型参数?

flu*_*lup 6 java reflection lambda spring

我有一个通用的功能界面:

@FunctionalInterface
public interface Feeder<T extends Animal> {
  void feed(T t);
}
Run Code Online (Sandbox Code Playgroud)

还有一些bean为不同的Animal子类实现了这个接口.

@Configuration
public class Config {
  @Bean
  public Feeder<Dog> dogFeeder() {
    return dog -> dogService.feedDog(dog);
  }
  @Bean
  public Feeder<Cat> catFeeder() {
    return cat -> catService.feedCat(cat);
  }
}
Run Code Online (Sandbox Code Playgroud)

现在已经为这些bean注入了一个服务类,并给出了一个实例Animal.如何确定使用正确的Feeder bean?

@Service
public class PetStore {
  @Autowired
  private List<Feeder<? extends Animal> feeders;

  private void feed(Animal animal) {
    //TODO: How to determine the correct feeder from feeders?
    Feeder<? extends Animal> correctFeeder = ....
    correctFeeder.feed(animal);
  }
}
Run Code Online (Sandbox Code Playgroud)

我试过的事情:

我最初认为我可以使用如何获取泛型类型T的类实例,遇到的问题是使用l​​ambda函数实现bean,并且在调用时返回的类型GenericTypeResolver.resolveTypeArgument(feeder.getClass(), Feeder.class)Animal(!)

然后我尝试使用bean的匿名子类.然后GenericTypeResolver可以确定每个Feeder将提供的动物的特定类型.但IntelliJ正在尖叫着我应该为它创建一个lambda,其他人也会使用PetStore.

我在Feeder接口中添加了一个getAnimalClass()方法.IntelliJ停止尖叫.但它确实感觉非常笨拙.

我第一次得到一个我尚未喂食的动物实例,我尝试/捕捉使用每个候选喂食器,直到找到一个有效的.然后我记得结果以备将来使用.也觉得很笨拙.

所以我的问题是:这样做的正确方法是什么?

Nic*_*ven 3

简短回答

恐怕没有真正干净的方法。由于类型被删除,您需要将类型信息保留在某处,而您使用的第三个建议getAnimalClass()是一种方法(但是您的问题中不清楚您稍后如何使用它)。

我个人会去掉 lambda 并添加 acanFeed(animal)Feeder委托决策(而不是添加getAnimalClass())。这样,他们Feeder就有责任知道它可以喂养哪些动物。

因此,类型信息将保留在 Feeder 类中,例如使用通过构造传递的实例(或者像您可能所做的那样Class覆盖 a ):getAnimalClass()

final Class<T> typeParameterClass;

public Feeder(Class<T> typeParameterClass){
    typeParameterClass = typeParameterClass;
}
Run Code Online (Sandbox Code Playgroud)

这样就可以通过以下canFeed方法使用:

public boolean canFeed(Animal animal) {
   return typeParameterClass.isAssignableFrom(animal.getClass());
}
Run Code Online (Sandbox Code Playgroud)

这将使代码非常PetStore干净:

private Feeder feederFor(Animal animal) {
    return feeders.stream()
                  .filter(feeder -> feeder.canFeed(animal))
                  .findFirst()
                  .orElse((Feeder) unknownAnimal -> {});
}
Run Code Online (Sandbox Code Playgroud)

替代答案

正如您所说,存储类型信息使其变得笨拙。然而,您可以以另一种不太严格的方式传递类型信息,而不会使 Feeders 混乱,例如通过依赖您注入的 Spring bean 的名称。

假设您将所有 Feeder 注入PetStorea 中Map

@Autowired
Map<String, Feeder<? extends Animal>> feederMap;
Run Code Online (Sandbox Code Playgroud)

现在您有了一个包含 Feeder 名称(及其隐式类型)和相应 Feeder 的映射。该canFeed方法现在只是检查子字符串:

private boolean canFeed(String feederName, Animal animal) {
    return feederName.contains(animal.getClass().getTypeName());
}
Run Code Online (Sandbox Code Playgroud)

Feeder您可以使用它从地图中获取正确的信息:

private Feeder feederFor(Animal animal) {
    return feederMap.entrySet().stream()
                    .filter(entry -> canFeed(entry.getKey(), animal))
                    .map(Map.Entry::getValue)
                    .findFirst()
                    .orElse((Feeder) unknownAnimal -> {});
}
Run Code Online (Sandbox Code Playgroud)

深潜

lambda 干净简洁,那么如果我们想将 lambda 表达式保留在配置中怎么办?我们需要类型信息,因此第一个尝试可以是将canFeed方法添加为接口default上的方法Feeder<T>

default <A extends Animal> boolean canFeed(A animalToFeed) {
    return A == T;
}
Run Code Online (Sandbox Code Playgroud)

当然,我们不能做到 A == T,并且由于类型擦除,无法比较泛型类型 A 和 T。通常,有您提到的技巧,该技巧仅在使用泛型超类型时有效。你提到了Spring工具箱方法,但让我们看看Java实现:

this.entityBeanType = ((Class) ((ParameterizedType) getClass()
    .getGenericSuperclass()).getActualTypeArguments()[0]);
Run Code Online (Sandbox Code Playgroud)

由于我们有多个带有Feeder<T>超级接口的 Feeder 实现,您可能会认为我们可以采用这种策略。但是,我们不能,因为我们在这里处理 lambda,并且 lambda 不是作为匿名内部类实现的(它们没有编译为类,而是使用指令invokedynamic),并且我们丢失了类型信息。

那么回到绘图板,如果我们将其改为抽象类,并将其用作 lambda,会怎样呢?然而这是不可能的,首席语言架构师 Brian Goetz在邮件列表中解释了原因:

在将此用例抛诸脑后之前,我们进行了一些语料库分析,以了解与接口 SAM 相比,抽象类 SAM 的使用频率。我们发现,在该语料库中,只有 3% 的 lambda 候选内部类实例以抽象类作为目标。其中大多数都适合简单的重构,您可以在其中添加接受面向接口的 lambda 的构造函数/工厂。

我们可以创建工厂方法来解决这个问题,但这又会很笨拙,让我们走得太远。

既然我们已经到了这里,让我们收回 lambda 并尝试以其他方式获取类型信息。Spring的GenericTypeResolver并没有带来预期的结果Animal,但我们可以进行一些 hacky 并利用类型信息存储在字节码中的方式。

编译 lambda 时,编译器会插入一条动态调用指令,该指令指向 LambdaMetafactory 和带有 lambda 主体的合成方法。常量池中的方法句柄包含泛型类型,因为泛型在我们的Config显式中。在运行时,ASM 生成一个实现函数式接口的类。不幸的是,这个特定的生成类不存储通用签名,并且您不能使用反射来绕过擦除,因为它是使用Unsafe.defineAnonymousClass. 有一个hackClass.getConstantPool可以使用 ASM 解析并返回参数类型来获取信息,但是这个 hack 依赖于未记录的方法和类,并且容易受到 JDK 中代码更改的影响。您可以自己破解它(通过粘贴参考中的代码)或使用实现此方法的库,例如TypeTools其他技巧也可以发挥作用,例如添加Serialization对 lambda 的支持并尝试从序列化形式中获取实例化接口的方法签名。不幸的是,我还没有找到一种方法来使用新的 Spring api 解析类型信息。

如果我们采用这种方法在界面中添加默认方法,您可以保留所有代码(例如配置),并且实际的Feeder-hack hack 减少为:

default <A extends Animal> boolean canFeed(A animalToFeed) {
    Class<?> feederType = TypeResolver.resolveRawArgument(Feeder.class, this.getClass());
    return feederType.isAssignableFrom(animalToFeed.getClass());
}
Run Code Online (Sandbox Code Playgroud)

保持PetStore清洁:

@Autowired
private List<Feeder<? extends Animal>> feeders;

public void feed(Animal animal) {
    feederFor(animal).feed(animal);
}

private Feeder feederFor(Animal animal) {
    return feeders.stream()
                  .filter(feeder -> feeder.canFeed(animal))
                  .findFirst()
                  .orElse(unknownAnimal -> {});
}
Run Code Online (Sandbox Code Playgroud)

因此,不幸的是,没有直接的方法,我想我们可以安全地得出结论,我们检查了所有(或至少多个基本)选项。