pombreda / google-guice

Automatically exported from code.google.com/p/google-guice
Apache License 2.0
0 stars 1 forks source link

Not obvious behavior of JIT bindings creations by parent #740

Open GoogleCodeExporter opened 9 years ago

GoogleCodeExporter commented 9 years ago
Hi guys, 

Today I recognized easy one tricky part of JIT binding based on TypeListeners 
and ChildInjectors usage. 
Let me explain step by step:

First
For every binding bind(Foo.class).to(FooImpl.class) one JIT binding will be 
created. Binding for FooImpl.class implemented by ConstructorBindingImpl.class. 
As soon as FooImpl.class is not explicitly bind, it'll be bind just-in-time. As 
it always happened with final implementations.

Second
Guice tried to create JIT binding in parent injector first. If it's not 
possible then in would be created in a child.

Let's review the example. We have some base classes for binding:

   class Dependency {} //just a dependency

    interface Foo {} //interface for binding

    class FooImpl implements Foo {} //base implementation without dependencies

    class EnhancedFooImpl implements Foo { //enhanced implementation with dependency

        @Inject
        EnhancedFooImpl(Dependency dependency) {
        }

    }

Next, let's make injectors hierarchy: bind nothing in parent and bind our 
classes in child.

      Injector parentInjector = Guice.createInjector(new AbstractModule() {
            @Override
            protected void configure() {
                //nothing here
            }
        });

        parentInjector.createChildInjector(new AbstractModule() {
            @Override
            protected void configure() {
                bind(Dependency.class);
                bind(Foo.class).annotatedWith(Names.named("simple")).to(FooImpl.class);
                bind(Foo.class).annotatedWith(Names.named("enhanced")).to(EnhancedFooImpl.class);
            }
        });

At the child injector initialization time, two just-in-time bindings will be 
created: for FooImpl.class and EnhancedFooImpl.class.
But due to "JIT parent first strategy" FooImpl.class bindings will be created 
by parent injector due to both classes have no dependencies at all.
As well as EnhancedFooImpl.class will be created by child injector, due to 
dependency on binding from child injector space (Dependency.class).

That means, JIT binding will be created by injector which can resolve all 
dependencies by himself. 

Tricky
It's tricky because we don't know, what injector actually will build 
implementation instance. Depends on implementation dependencies it can be one 
from all injectors hierarchy. 
And more, what about type listeners defined by child injector?

Let's add type listener for all classes in child injection:

Injector childInjector = parentInjector.createChildInjector(new 
AbstractModule() {

            @Override
            protected void configure() {
                bind(Dependency.class);
                bind(Foo.class).annotatedWith(Names.named("simple")).to(FooImpl.class);
                bind(Foo.class).annotatedWith(Names.named("enhanced")).to(EnhancedFooImpl.class);

                //bind listener for all injections in this injector
                bindListener(Matchers.any(), new TypeListener() {
                    @Override
                    public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
                        encounter.register(new InjectionListener<I>() {
                            @Override
                            public void afterInjection(I injectee) {
                                 //this call was never happened for FooImpl creation
                                //but will successfully called for EnhancedFooImpl creation 
                            }
                        });
                    }
                });
            }
});

Due to the fact FooImpl.class binding created by parent injector, injection 
listener defined in the child injector would never called. 
Parent injector just doesn't no about some injection listeners in child 
containers :(

So behavior is not obvious. If we have a hierarchy of injector as well as 
hierarchy of type listeners, we can't guarantee what of them will be processed 
and what not. Depends on what level dependencies is enough to create JIT 
binding for implementation class. I think it can create some floating issues.  

In addition, to reproduce the same effect, try to inject Injector dependency in 
both implementation classes.  For FooImpl.class it'll be parent injector 
instance. For EnhancedFooImpl.class - child injector instance.

I'm not sure, what happened if we will always create JIT binding in an injector 
where binding will be requested. 

P.S. Test file attached.

Original issue reported on code.google.com by aleksey....@gmail.com on 5 Feb 2013 at 5:27

Attachments:

GoogleCodeExporter commented 9 years ago
Bad things will happen if the JIT binding is always created in the same 
injector by default, because of the way JIT bindings work in general.

However, if you call Binder.requireExplicitBindings(), it will disable JIT 
bindings while still allowing implicit bindings [the FooImpl in 
bind(Foo.class).to(FooImpl.class)], and that will force the implicit binding to 
be created in the same injector.

Original comment by sberlin on 7 Feb 2013 at 2:21

GoogleCodeExporter commented 9 years ago
Thanks for the answer. Yep, that's really good to have 
Binder.requireExplicitBindings() workaround, got it.
By the way, what kind of bad things will happen? Just professional curiosity :)

Original comment by aleksey....@gmail.com on 13 Feb 2013 at 7:54