问题
I'm trying to combine this 3 features of Guice: inject, multibinding, generics. I create a prototype of production project, so here it is:
First, this is a little hierarchy for generics(in production case there is hierarchy of N entities):
public interface Type {
}
public class Type1 implements Type{
}
public class Type2 implements Type {
}
Next, classes ToCreate1 and ToCreate2 I want to create by Factory.
Base class:
public abstract class AbstractToCreate<T extends Type> {
public T type;
public Integer param;
public AbstractToCreate(T type, Integer param){
this.type = type;
this.param = param;
}
}
It's inheritors:
public class ToCreate1 extends AbstractToCreate<Type1>{
@Inject
public ToCreate1(Type1 type, @Assisted Integer param) {
super(type, param);
}
}
public class ToCreate2 extends AbstractToCreate<Type2> {
@Inject
public ToCreate2(Type2 type, @Assisted Integer param) {
super(type, param);
}
}
Then, the Factory itself:
public interface Factory<T extends Type> {
AbstractToCreate<T> create(Integer param);
}
So, now I want to Inject a map, containing Factory<Type1> and Factory<Type2> to create ToInject1 and ToInject2 respectively.
So, I create Guice's AbstractModule with configure method:
protected void configure() {
install(new FactoryModuleBuilder()
.implement(new TypeLiteral<AbstractToCreate<Type1>>(){}, ToCreate1.class)
.build(new TypeLiteral<Factory<Type1>>(){}));
install(new FactoryModuleBuilder()
.implement(new TypeLiteral<AbstractToCreate<Type2>>(){}, ToCreate2.class)
.build(new TypeLiteral<Factory<Type2>>(){}));
MapBinder<String, Factory> mapBinder = MapBinder.newMapBinder(binder(), String.class, Factory.class);
mapBinder.addBinding("type1").to(new TypeLiteral<Factory<Type1>>(){});
mapBinder.addBinding("type2").to(new TypeLiteral<Factory<Type2>>(){});
}
So, I inject it @Inject public Map<String, Factory> map;
and all is Ok:
Factory<Type1> factory1 = main.map.get("type1");
Factory<Type2> factory2 = main.map.get("type2");
AbstractToCreate<Type1> create1 = factory1.create(1);//create1 is ToCreate1 instance
AbstractToCreate<Type2> create2 = factory2.create(2);//create2 is ToCreate2 instance
As I mentioned before, there is much more Types in my production system, so the AbstractModule becomes too cumbersome. I tried to avoid duplicate code and modified configure method:
@Override
protected void configure() {
this.<Type1>inst(ToCreate1.class);
this.<Type2>inst(ToCreate2.class);
}
private <V extends Type> void inst(Class<? extends AbstractToCreate<V>> clazz) {
install(new FactoryModuleBuilder()
.implement(new TypeLiteral<AbstractToCreate<V>>(){}, clazz)
.build(new TypeLiteral<Factory<V>>(){}));
}
And it doesn't work! Guice says:
1) ru.test.genericassistedinject.AbstractToCreate<V> cannot be used as a key; It is not fully specified.
What's wrong?
回答1:
The problem here is with type erasure. In particular, this code:
private <V extends Type> void inst(Class<? extends AbstractToCreate<V>> clazz) {
install(new FactoryModuleBuilder()
.implement(new TypeLiteral<AbstractToCreate<V>>(){}, clazz)
.build(new TypeLiteral<Factory<V>>(){}));
}
can't work because it's relying on type parameter V
to help make a runtime decision (what binding to use), but the type parameter V
has no runtime representation, so its value can never directly affect runtime. Another way of thinking about this: Java can't "read" a type parameter's value in a generic; new TypeLiteral<Factory<V>>(){}
is always the same value regardless of what V
is instantiated with in the caller.
As is often the case when you run into erasure-related problems, the trick is to add a runtime value that represents the type you want. In this case that's especially tricky, since what you want to do is represent a value for a type parameter to a larger type.
There are a few ways to get runtime values that represent static types. TypeToken
is one and Class
is another, but neither of them will allow you to represent a type with a parameter and then programmatically fill that value. Luckily, Google Guava contains another representation, com.google.common.reflect.TypeToken
, that will work for us. TypeToken
s can represent a type with a variable and support programmatically "filling in" that variable with a concrete representation, e.g:
new TypeToken<List<V>>() {}.where(new TypeParameter<V>() {}, Integer.class)
represents the type List<Integer>
at runtime.
Using TypeToken
we can build our types, like so:
private <V extends Type> void inst(Class<? extends AbstractToCreate<V>> clazz, Class<V> binding) {
TypeToken<AbstractToCreate<V>> implementationType = new TypeToken<AbstractToCreate<V>>() {}
.where(new TypeParameter<V>() {}, binding);
TypeToken<Factory<V>> factoryType = new TypeToken<Factory<V>>() {}
.where(new TypeParameter<V>() {}, binding);
@SuppressWarnings("unchecked") // The type returned by TypeToken::getType is always the type it represents
Key<AbstractToCreate<V>> key = (Key<AbstractToCreate<V>>) Key.get(implementationType.getType());
@SuppressWarnings("unchecked") // as above
Key<Factory<V>> factoryKey = (Key<Factory<V>>) Key.get(factoryType.getType());
install(
new FactoryModuleBuilder()
.implement(key, clazz)
.build(factoryKey));
}
Now we can call inst
with:
inst(ToCreate1.class, Type1.class);
inst(ToCreate2.class, Type2.class);
and everything will work as desired.
This is pretty fancy stuff, though, and understanding it hinges on having a pretty good understanding of the difference between compile-time and runtime representations of types. If it were me, I wouldn't do this if it was something you only expected to use once or twice, since the confusion burden is pretty high; I'd only do it if this was part of a library or something and you could save some work for every caller.
来源:https://stackoverflow.com/questions/49769830/guice-assisted-inject-multibinding-generics