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

时间:2017-05-12 12:28:08

标签: java spring reflection lambda

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

@FunctionalInterface
public interface Feeder<T extends Animal> {
  void feed(T t);
}

还有几个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);
  }
}

现在已向服务类注入了这些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);
  }
}

我尝试过的事情:

我最初认为我可以使用How to get a class instance of generics type T 但是我遇到了使用lambda函数实现bean的问题,并且当我调用GenericTypeResolver.resolveTypeArgument(feeder.getClass(), Feeder.class)时返回的类型是Animal(!)

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

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

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

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

2 个答案:

答案 0 :(得分:2)

简短回答

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

我个人会摆脱lambda并向canFeed(animal)添加Feeder来委派决定(而不是添加getAnimalClass())。这样,Feeder负责了解它可以喂养什么动物。

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

final Class<T> typeParameterClass;

public Feeder(Class<T> typeParameterClass){
    typeParameterClass = typeParameterClass;
}

因此canFeed方法可以使用它:

public boolean canFeed(Animal animal) {
   return typeParameterClass.isAssignableFrom(animal.getClass());
}

这会使代码保持在PetStore非常干净:

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

替代答案

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

假设您在PetStoreMap注入了所有Feedders:

@Autowired
Map<String, Feeder<? extends Animal>> feederMap;

现在您有一张地图,其中包含进纸器名称(以及隐式它的类型)和相应的进纸器。 canFeed方法现在只是检查子字符串:

private boolean canFeed(String feederName, Animal animal) {
    return feederName.contains(animal.getClass().getTypeName());
}

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

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

深度潜水

lambda是干净简洁的,所以如果我们想在配置中保留lambda表达式呢。我们需要类型信息,因此第一次尝试可以在canFeed界面上将default方法添加为Feeder<T>方法:

default <A extends Animal> boolean canFeed(A animalToFeed) {
    return A == T;
}

当然我们不能做A == T,并且由于类型擦除,没有办法比较泛型类型A和T.通常,你引用the trick,只能工作使用通用超类型时。您提到了Spring工具箱方法,但让我们看一下Java实现:

this.entityBeanType = ((Class) ((ParameterizedType) getClass()
    .getGenericSuperclass()).getActualTypeArguments()[0]);

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

回到绘图板,如果我们将它改为抽象类,并将其用作lambda。然而,这是不可能的,首席语言架构师Brian Goetz解释了为什么on the mailinglist

  

在将此用例丢弃到总线之前,我们进行了一些语料库分析,以发现抽象类SAM的使用频率与   接口SAMs。我们发现在那个语料库中,只有3%的lambda   候选内部类实例将抽象类作为目标。   并且他们中的大多数都适合你添加的简单重构   接受lambda的构造函数/工厂   界面定位。

我们可以去创造工厂方法来解决这个问题,但这又是笨拙的,让我们走得太远。

由于我们已经做到了这一点,让我们把lambda放回去,尝试以其他方式获取类型信息。 Spring的GenericTypeResolver没有带来预期的结果Animal,但我们可能会变得hacky并利用Type信息存储在字节码中的方式。

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

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 -> {});
}

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

答案 1 :(得分:0)

我想说,没有真正“干净”的方法。以前已经处理过带有类型参数的自动布线组件的问题。但要申请你的美味赏金,我会描述你可以采用的一些方法。这样做的正确方法是你需要自己决定的。但是由于你已经说过在Java中实现了泛型,你可以看到在类型安全的庄园中没有一个很好的方法。无论您提出什么解决方案,您都会遇到这样的问题:要么不确定服务是否已经提供了正确的动物饲养类型,要么在编译时检索到正确的喂食服务

无论解决方案如何,这都是您的问题。我想大多数开发人员都会接受的正确方法是一个解决方案,保证你在编译时有正确的 Feeder Animal ,但你不能,这只是打破我的心。

但是我再说谎,因为肯定有一种方法可以在Java中以类型安全的方式执行此操作,但实际上并非如此,也就是说,您可以提出一些类型安全且保证正确服务的方法。动物,但它会涉及黑魔法,无论是反射,AspectJ,一些讨厌的构建插件等等,没有人会喜欢它,尤其不是那些寻求这种“正确”解决方案的人。

说实话,我喜欢一堆服务的想法,每个服务都试图喂你的动物传递给下一个服务,听起来很像策略模式。如果我看到我会得到你想要的东西,每个人都会理解类似Feeding策略的东西。当Animal被赋予它无法提供它时会抛出NoFeedingServiceForThatAnimal或其他东西。另一种方法是为动物定义Feed查找服务,这意味着每只动物的ID(enum可能会给你一些类型安全性),或者使用反射但没有人喜欢反射。恕我直言策略比查找型解决方案更通用,因为您可能为同一动物提供不同的喂食服务,例如您可能有一个婴儿大象喂食器和一个成人大象喂食器,这可能更有意义保持这些分开而不是只有一只大象Feeder

我知道我没有给你代码看哪个看起来最好,但是你真的应该为你的域决定如何处理这些问题,并确定一些后果这些设计决定有。祝你好运。