/* * Copyright (C) 2006 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.inject; import static com.google.inject.Asserts.assertContains; import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.util.ArrayList; import java.util.List; import java.util.Map; import junit.framework.TestCase; /** * @author crazybob@google.com (Bob Lee) * @author sameb@google.com (Sam Berlin) */ public class CircularDependencyTest extends TestCase { @Override protected void setUp() throws Exception { AImpl.nextId = 0; BImpl.nextId = 0; } public void testCircularlyDependentConstructors() throws CreationException { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(A.class).to(AImpl.class); bind(B.class).to(BImpl.class); } }); assertCircularDependencies(injector); } public void testCircularlyDependentConstructorsWithProviderMethods() throws CreationException { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() {} @Provides @Singleton A a(B b) { return new AImpl(b); } @Provides B b(A a) { return new BImpl(a); } }); assertCircularDependencies(injector); } public void testCircularlyDependentConstructorsWithProviderInstances() throws CreationException { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(A.class) .toProvider( new Provider<A>() { @Inject Provider<B> bp; @Override public A get() { return new AImpl(bp.get()); } }) .in(Singleton.class); bind(B.class) .toProvider( new Provider<B>() { @Inject Provider<A> ap; @Override public B get() { return new BImpl(ap.get()); } }); } }); assertCircularDependencies(injector); } public void testCircularlyDependentConstructorsWithProviderKeys() throws CreationException { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(A.class).toProvider(AP.class).in(Singleton.class); bind(B.class).toProvider(BP.class); } }); assertCircularDependencies(injector); } public void testCircularlyDependentConstructorsWithProvidedBy() throws CreationException { Injector injector = Guice.createInjector(); assertCircularDependencies(injector); } private void assertCircularDependencies(Injector injector) { A a = injector.getInstance(A.class); assertNotNull(a.getB().getA()); assertEquals(0, a.id()); assertEquals(a.id(), a.getB().getA().id()); assertEquals(0, a.getB().id()); assertEquals(1, AImpl.nextId); assertEquals(1, BImpl.nextId); assertSame(a, injector.getInstance(A.class)); } @ProvidedBy(AutoAP.class) public interface A { B getB(); int id(); } @Singleton static class AImpl implements A { static int nextId; int id = nextId++; final B b; @Inject public AImpl(B b) { this.b = b; } @Override public int id() { return id; } @Override public B getB() { return b; } } static class AP implements Provider<A> { @Inject Provider<B> bp; @Override public A get() { return new AImpl(bp.get()); } } @Singleton static class AutoAP implements Provider<A> { @Inject Provider<B> bp; A a; @Override public A get() { if (a == null) { a = new AImpl(bp.get()); } return a; } } @ProvidedBy(BP.class) public interface B { A getA(); int id(); } static class BImpl implements B { static int nextId; int id = nextId++; final A a; @Inject public BImpl(A a) { this.a = a; } @Override public int id() { return id; } @Override public A getA() { return a; } } static class BP implements Provider<B> { Provider<A> ap; @Inject BP(Provider<A> ap) { this.ap = ap; } @Override public B get() { return new BImpl(ap.get()); } } public void testUnresolvableCircularDependency() { try { Guice.createInjector().getInstance(C.class); fail(); } catch (ProvisionException expected) { assertContains( expected.getMessage(), "Tried proxying " + C.class.getName() + " to support a circular dependency, ", "but it is not an interface."); } } public void testUnresolvableCircularDependenciesWithProviderInstances() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() {} @Provides C c(D d) { return null; } @Provides D d(C c) { return null; } }) .getInstance(C.class); fail(); } catch (ProvisionException expected) { assertContains( expected.getMessage(), "Tried proxying " + C.class.getName() + " to support a circular dependency, ", "but it is not an interface."); } } public void testUnresolvableCircularDependenciesWithProviderKeys() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(C2.class).toProvider(C2P.class); bind(D2.class).toProvider(D2P.class); } }) .getInstance(C2.class); fail(); } catch (ProvisionException expected) { assertContains( expected.getMessage(), "Tried proxying " + C2.class.getName() + " to support a circular dependency, ", "but it is not an interface."); } } public void testUnresolvableCircularDependenciesWithProvidedBy() { try { Guice.createInjector().getInstance(C2.class); fail(); } catch (ProvisionException expected) { assertContains( expected.getMessage(), "Tried proxying " + C2.class.getName() + " to support a circular dependency, ", "but it is not an interface."); } } static class C { @Inject C(D d) {} } static class D { @Inject D(C c) {} } static class C2P implements Provider<C2> { @Inject Provider<D2> dp; @Override public C2 get() { dp.get(); return null; } } static class D2P implements Provider<D2> { @Inject Provider<C2> cp; @Override public D2 get() { cp.get(); return null; } } @ProvidedBy(C2P.class) static class C2 { @Inject C2(D2 d) {} } @ProvidedBy(D2P.class) static class D2 { @Inject D2(C2 c) {} } public void testDisabledCircularDependency() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() { binder().disableCircularProxies(); } }) .getInstance(C.class); fail(); } catch (ProvisionException expected) { assertContains( expected.getMessage(), "Found a circular dependency involving " + C.class.getName() + ", and circular dependencies are disabled."); } } public void testDisabledCircularDependenciesWithProviderInstances() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() { binder().disableCircularProxies(); } @Provides C c(D d) { return null; } @Provides D d(C c) { return null; } }) .getInstance(C.class); fail(); } catch (ProvisionException expected) { assertContains( expected.getMessage(), "Found a circular dependency involving " + C.class.getName() + ", and circular dependencies are disabled."); } } public void testDisabledCircularDependenciesWithProviderKeys() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() { binder().disableCircularProxies(); bind(C2.class).toProvider(C2P.class); bind(D2.class).toProvider(D2P.class); } }) .getInstance(C2.class); fail(); } catch (ProvisionException expected) { assertContains( expected.getMessage(), "Found a circular dependency involving " + C2.class.getName() + ", and circular dependencies are disabled."); } } public void testDisabledCircularDependenciesWithProvidedBy() { try { Guice.createInjector( new AbstractModule() { @Override protected void configure() { binder().disableCircularProxies(); } }) .getInstance(C2.class); fail(); } catch (ProvisionException expected) { assertContains( expected.getMessage(), "Found a circular dependency involving " + C2.class.getName() + ", and circular dependencies are disabled."); } } /** * As reported by issue 349, we give a lousy trace when a class is circularly dependent on itself * in multiple ways. */ public void testCircularlyDependentMultipleWays() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { binder.bind(A.class).to(E.class); binder.bind(B.class).to(E.class); } }); injector.getInstance(A.class); } public void testDisablingCircularDependencies() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { binder().disableCircularProxies(); binder.bind(A.class).to(E.class); binder.bind(B.class).to(E.class); } }); try { injector.getInstance(A.class); fail("expected exception"); } catch (ProvisionException expected) { assertContains( expected.getMessage(), "Found a circular dependency involving " + A.class.getName() + ", and circular dependencies are disabled.", "Found a circular dependency involving " + B.class.getName() + ", and circular dependencies are disabled."); } } @Singleton static class E implements A, B { @Inject public E(A a, B b) {} @Override public B getB() { return this; } @Override public A getA() { return this; } @Override public int id() { return 0; } } public void testCircularDependencyProxyDelegateNeverInitialized() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(F.class).to(RealF.class); bind(G.class).to(RealG.class); } }); F f = injector.getInstance(F.class); assertEquals("F", f.g().f().toString()); assertEquals("G", f.g().f().g().toString()); } public interface F { G g(); } @Singleton public static class RealF implements F { private final G g; @Inject RealF(G g) { this.g = g; } @Override public G g() { return g; } @Override public String toString() { return "F"; } } public interface G { F f(); } @Singleton public static class RealG implements G { private final F f; @Inject RealG(F f) { this.f = f; } @Override public F f() { return f; } @Override public String toString() { return "G"; } } /** * Tests that ProviderInternalFactory can detect circular dependencies before it gets to * Scopes.SINGLETON. This is especially important because the failure in Scopes.SINGLETON doesn't * have enough context to provide a decent error message. */ public void testCircularDependenciesDetectedEarlyWhenDependenciesHaveDifferentTypes() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bind(Number.class).to(Integer.class); } @Provides @Singleton Integer provideInteger(List list) { return 2; } @Provides List provideList(Integer integer) { return new ArrayList(); } }); try { injector.getInstance(Number.class); fail(); } catch (ProvisionException expected) { assertContains( expected.getMessage(), "Tried proxying " + Integer.class.getName() + " to support a circular dependency, ", "but it is not an interface."); } } public void testPrivateModulesDontTriggerCircularErrorsInProviders() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { install( new PrivateModule() { @Override protected void configure() { bind(Foo.class); expose(Foo.class); } @Provides String provideString(Bar bar) { return new String("private 1, " + bar.string); } }); install( new PrivateModule() { @Override protected void configure() { bind(Bar.class); expose(Bar.class); } @Provides String provideString() { return new String("private 2"); } }); } }); Foo foo = injector.getInstance(Foo.class); assertEquals("private 1, private 2", foo.string); } static class Foo { @Inject String string; } static class Bar { @Inject String string; } /** * When Scope Providers call their unscoped Provider's get() methods are called, it's possible * that the result is a circular proxy designed for one specific parameter (not for all possible * parameters). But custom scopes typically cache the results without checking to see if the * result is a proxy. This leads to caching a result that is unsuitable for reuse for other * parameters. * * <p>This means that custom proxies have to do an {@code if(Scopes.isCircularProxy(..))} in order * to avoid exceptions. */ public void testCustomScopeCircularProxies() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { bindScope(SimpleSingleton.class, new BasicSingleton()); bind(H.class).to(HImpl.class); bind(I.class).to(IImpl.class); bind(J.class).to(JImpl.class); } }); // The reason this happens is because the Scope gets these requests, in order: // entry: Key<IImpl> (1 - from getInstance call) // entry: Key<HImpl> // entry: Key<IImpl> (2 - circular dependency from HImpl) // result of 2nd Key<IImpl> - a com.google.inject.$Proxy, because it's a circular proxy // result of Key<HImpl> - an HImpl // entry: Key<JImpl> // entry: Key<IImpl> (3 - another circular dependency, this time from JImpl) // At this point, if the first Key<Impl> result was cached, our cache would have // Key<IImpl> caching to an instanceof of I, but not an an instanceof of IImpl. // If returned this, it would result in cglib giving a ClassCastException or // java reflection giving an IllegalArgumentException when filling in parameters // for the constructor, because JImpl wants an IImpl, not an I. try { injector.getInstance(IImpl.class); fail(); } catch (ProvisionException pe) { assertContains( Iterables.getOnlyElement(pe.getErrorMessages()).getMessage(), "Tried proxying " + IImpl.class.getName() + " to support a circular dependency, but it is not an interface."); } } interface H {} interface I {} interface J {} @SimpleSingleton static class HImpl implements H { @Inject HImpl(I i) {} } @SimpleSingleton static class IImpl implements I { @Inject IImpl(HImpl i, J j) {} } @SimpleSingleton static class JImpl implements J { @Inject JImpl(IImpl i) {} } @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RUNTIME) @ScopeAnnotation public @interface SimpleSingleton {} public static class BasicSingleton implements Scope { private static Map<Key<?>, Object> cache = Maps.newHashMap(); @Override public <T> Provider<T> scope(final Key<T> key, final Provider<T> unscoped) { return new Provider<T>() { @Override @SuppressWarnings("unchecked") public T get() { if (!cache.containsKey(key)) { T t = unscoped.get(); if (Scopes.isCircularProxy(t)) { return t; } cache.put(key, t); } return (T) cache.get(key); } }; } } public void testDisabledNonConstructorCircularDependencies() { Injector injector = Guice.createInjector( new AbstractModule() { @Override protected void configure() { binder().disableCircularProxies(); } }); try { injector.getInstance(K.class); fail("expected exception"); } catch (ProvisionException expected) { assertContains( expected.getMessage(), "Found a circular dependency involving " + K.class.getName() + ", and circular dependencies are disabled."); } try { injector.getInstance(L.class); fail("expected exception"); } catch (ProvisionException expected) { assertContains( expected.getMessage(), "Found a circular dependency involving " + L.class.getName() + ", and circular dependencies are disabled."); } } static class K { @Inject L l; } static class L { @Inject void inject(K k) {} } }