How to determine the type parameter of Bean implementing generic FunctionalInterface with a lambda?

flup :

I have a generic functional interface:

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

And a couple of beans implementing that interface for different Animal subclasses.

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

Now a service class has been injected with these beans and is given an instance of Animal. How can it determine the correct Feeder bean to use?

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

Things I've tried:

I initially thought I'd be alright using How to get a class instance of generics type T but am running into issues that the bean is implemented using a lambda function and the type returned when I call GenericTypeResolver.resolveTypeArgument(feeder.getClass(), Feeder.class) is Animal(!)

I then tried to use an anonymous subclass for the beans. Then GenericTypeResolver can determine the specific type of Animal each Feeder will feed. But IntelliJ is screaming at me I should create a lambda for it and so will other people using the PetStore.

I added a getAnimalClass() method to the Feeder interface. IntelliJ stops screaming. It does feel very clumsy though.

The first time I get an Animal instance of a class I've not yet fed, I try/catch to use each candidate feeder, until I find one that works. Then I remember the result for future use. Also feels very clumsy.

So my question: What is the proper way to do this?

Nick Vanderhoven :

Short answer

I'm afraid there is no really clean way. Since the types are erased, you need to keep the type info somewhere and your third suggestion using the getAnimalClass() is one way to do it (however it's unclear in your question how you put it to use later on).

I would personally get rid of the lambda's and add a canFeed(animal) to the Feeder to delegate the decision (instead of adding the getAnimalClass()). That way, the Feeder is responsible for knowing what animals it can feed.

So the type info will be kept in the Feeder class, for example using a Class instance passed with construction (or by overriding a getAnimalClass() as you presumably did):

final Class<T> typeParameterClass;

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

So that it can be used by the canFeed method:

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

This will keep the code in the PetStore pretty clean:

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

Alternative Answer

As you said, storing the type info makes it clumsy. You could however pass the type info in another, less strict way, without cluttering the Feeders, for example by relying on the names of the Spring beans you inject.

Let's say you inject all the Feeders in the PetStore in a Map:

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

Now you have a map that contains the feeder name (and implicit it's type) and the corresponding Feeder. The canFeed method is now simply checking the substring:

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

And you can use it to fetch the correct Feeder from the map:

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

Deep dive

The lambda's are clean and concise, so what if we want to keep the lambda expressions in the config. We need the type information, so the first try can be to add the canFeed method as a default method on the Feeder<T> interface:

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

Of course we can't do A == T, and because of type erasure, there is no way to compare the generic types A and T. Normally, there is the trick you referred to, that only works when using generic supertypes. You mentioned the Spring toolbox method, but let's look at the Java implementation:

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

Since we have multiple Feeder implementations with a Feeder<T> superinterface, you might think we can adopt this strategy. However, we can't, because we are dealing with lambda's here and lambda's are not implemented as anonymous inner classes (they are not compiled to a class but use an invokedynamic instruction) and we loose the typing info.

So back to the drawing board, what if we make it an abstract class instead, and use it as a lambda. This however is not possible and Brian Goetz, chief language architect, explains why on the mailinglist:

Before throwing this use case under the bus, we did some corpus analysis to found how often abstract class SAMs are used compared to interface SAMs. We found that in that corpus, only 3% of the lambda candidate inner class instances had abstract classes as their target. And most of them were amenable to simple refactorings where you added a constructor/factory that accepted a lambda that was interface-targeted.

We could go and create factory methods to work around this, but that again would be clumsy and get us too far.

Since we got this far, let's take our lambda's back and try to fetch the type info in some other way. Spring's GenericTypeResolver did not bring the expected result Animal, but we could get hacky and take advantage of the way the Type info is stored in bytecode.

When compiling a lambda, the compiler inserts a dynamic invoke instruction that points to a LambdaMetafactory and a synthetic method with the body of the lambda. The method handle in the constant pool contains the generic type, since the generics in our Config our explicit. At runtime, ASM generates a class that implements the functional interface. Unfortunately, this specific generated class does not store the generic signatures and you can not use reflection to get around the erasure, since it is defined using Unsafe.defineAnonymousClass. There is a a hack to get the info out of the Class.getConstantPool that uses the ASM to parse and return the argument types, but this hack relies on undocumented methods and classes and is vulnerable to code changes in the JDK. You could hack it in yourself (by coping pasting the code from the reference) or use a library that implements this approach such as TypeTools. Other hacks could work too, such as adding Serialization support to the lambda and try to fetch the method signature of the instantiated interface from the serialized form. Unfortunately, I did not find a way yet to resolve the type info with the new Spring api's.

If we take this approach to add the default method in our interface, you can keep all your code (e.g. the config) and the actual Feeder-hack hack reduces to:

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

The PetStore stays clean:

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

So unfortunately, there is no straightforward approach and I guess we can safely conclude we checked all (or at least more than a few basic) options.

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=470664&siteId=1