guice assisted inject + multibinding + generics
Asked Answered
H

1

6

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?

Handtomouth answered 11/4, 2018 at 8:11 Comment(0)
M
5

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. TypeTokens 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.

Microdot answered 8/5, 2018 at 15:37 Comment(1)
+1 to this answer, but unless you're already depending on Guava, you might be able to compute your type expression with Types.newParameterizedType instead. Check out the rest of Types, too; it's a miniature complement to TypeToken's APIs.Zillion

© 2022 - 2024 — McMap. All rights reserved.