google / guice

Guice (pronounced 'juice') is a lightweight dependency injection framework for Java 11 and above, brought to you by Google.
https://github.com/google/guice
Apache License 2.0
12.46k stars 1.67k forks source link

AOP bug - Child injector doesn't intercepts JIT bindings #893

Closed pabloromanelli closed 9 years ago

pabloromanelli commented 9 years ago

I can intercept JIT bindings over the root injector, but when trying to intercept a JIT binding configured in a child injector, it doesn't intercept the methods.

Apparently it works if the class has a dependency registered in the child injector (maybe it's related to #888).

I have some tests to reproduce the issue. Tested with guice 3 and guice 4.0-SNAPSHOT (guice-4.0-20141205.235717-17.jar):

https://gist.github.com/pabloromanelli/0b14c31ecb20d19139d2

package com.google.inject;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.junit.Assert.assertEquals;

import java.lang.annotation.*;

import org.aopalliance.intercept.*;
import org.junit.Test;

import com.google.inject.matcher.Matchers;

public class GuiceChildInjectorInterceptionTest {

    @Test
    public void childInjectorDOESNOTInterceptsJITBindings() {
        Injector childInjector = Guice.createInjector().createChildInjector(new JITModule());
        NoDependencies noDependencies = childInjector.getInstance(NoDependencies.class);
        // ¿? interception not done
        assertEquals("m", noDependencies.m());
    }

    @Test
    public void childInjectorInterceptsJITBindingsWithDependencies() {
        Injector childInjector = Guice.createInjector().createChildInjector(new JITModule());
        WithDependency withDependency = childInjector.getInstance(WithDependency.class);
        assertEquals("XmX", withDependency.m());
    }

    /**
     * When manually bounded, both cases work
     */
    @Test
    public void childInjectorCanInterceptsLinkedBindings() {
        Injector childInjector = Guice.createInjector().createChildInjector(new LinkedBindingModule());

        WithDependency withDependency = childInjector.getInstance(WithDependency.class);
        assertEquals("XmX", withDependency.m());

        NoDependencies noDependencies = childInjector.getInstance(NoDependencies.class);
        assertEquals("XmX", noDependencies.m());
    }

    static class NoDependencies {
        @MyInterception
        public String m() {
            return "m";
        }
    }

    static class WithDependency {
        @Inject
        public String someString;

        @MyInterception
        public String m() {
            return "m";
        }
    }

    class JITModule extends AbstractModule {
        @Override
        protected void configure() {
            bindInterceptor(Matchers.any(), Matchers.annotatedWith(MyInterception.class), new XInterceptor());
            bind(String.class).toInstance("my dependency");
        }
    }

    class LinkedBindingModule extends AbstractModule {
        @Override
        protected void configure() {
            bindInterceptor(Matchers.any(), Matchers.annotatedWith(MyInterception.class), new XInterceptor());
            bind(String.class).toInstance("my dependency");
            bind(WithDependency.class);
            bind(NoDependencies.class);
        }
    }

    public static class XInterceptor implements MethodInterceptor {
        public Object invoke(MethodInvocation invocation) throws Throwable {
            return "X" + invocation.proceed() + "X";
        }
    }

    @BindingAnnotation
    @Target({ FIELD, PARAMETER, METHOD })
    @Retention(RUNTIME)
    @interface MyInterception {}

}
sameb commented 9 years ago

This is confusing, but by design. You're binding the interceptor in the child, so it only applies to types in the child injector. By default, Guice promotes dependencies to the parent-most-injector that can house them -- so if you're using just-in-time bindings (e.g, you didn't explicitly bind the type in the child injector's module) and the type doesn't have dependencies that can only be satisfied in the child, then Guice will create the binding in the parent injector. This is mostly necessary for correctness, otherwise programs become non-deterministic depending on the timing of instantiation. (Imagine if something in the parent also wanted the type -- if it was created in the child first, then the parent's use of it would fail.)

To avoid this, bind your type in the child injector's module. You can also use requireExplicitBindings to prevent all just-in-time bindings, which prevents this class of errors.

pabloromanelli commented 9 years ago

Very clear explanation. Thank you @sameb !