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的类实例,
但遇到的问题是使用lambda函数实现bean,并且在调用时返回的类型GenericTypeResolver.resolveTypeArgument(feeder.getClass(), Feeder.class)
是Animal
(!)
然后我尝试使用bean的匿名子类.然后GenericTypeResolver可以确定每个Feeder将提供的动物的特定类型.但IntelliJ正在尖叫着我应该为它创建一个lambda,其他人也会使用PetStore.
我在Feeder接口中添加了一个getAnimalClass()方法.IntelliJ停止尖叫.但它确实感觉非常笨拙.
我第一次得到一个我尚未喂食的动物实例,我尝试/捕捉使用每个候选喂食器,直到找到一个有效的.然后我记得结果以备将来使用.也觉得很笨拙.
所以我的问题是:这样做的正确方法是什么?
简短回答
恐怕没有真正干净的方法。由于类型被删除,您需要将类型信息保留在某处,而您使用的第三个建议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 注入PetStore
a 中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)
因此,不幸的是,没有直接的方法,我想我们可以安全地得出结论,我们检查了所有(或至少多个基本)选项。