我有一个通用的功能界面:
@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停止尖叫。但它确实感觉非常笨拙。
我第一次得到一个我尚未喂食的动物实例,我尝试/捕捉使用每个候选喂食器,直到找到一个有效的。然后我记得结果以备将来使用。也觉得很笨拙。
所以我的问题: 这样做的正确方法是什么?
答案 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的名称。
假设您在PetStore
中Map
注入了所有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中代码更改的影响。您可以自己破解它(通过粘贴来自参考的代码)或使用实现此方法的库,例如TypeTools。 Other 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
。
我知道我没有给你代码看哪个看起来最好,但是你真的应该为你的域决定如何处理这些问题,并确定一些后果这些设计决定有。祝你好运。