Revision: 1586
Author:   [email protected]
Date:     Tue Sep 27 08:37:30 2011
Log:
Add Scopes.isCircularProxy, for use by Scope implementations. The basic problem is that somewhere along the line, Guice is breaking the type-safety of <T> in the scope(Key<T>, Provider<T>) method. This happens when <T> is involved in a circular dependency, and the ConstructionContext creates a circular proxy for a given 'expectedType' of <T> (the type in the parameter where it's being injected). Expected type is a superclass or superinterface of <T>, not a subclass or subinterface, so if a Scope caches the result of Key<T> -> Provider<T>.get(), and then tries to reuse it, it's possible that we return something that isn't compatible with <T>. This results in either a ClassCastException (if cglib is involved) or IllegalArgumentException (java reflection) when trying to construct the object, because the parameters don't match the arguments.


Revision created by MOE tool push_codebase.
MOE_MIGRATION=3341

http://code.google.com/p/google-guice/source/detail?r=1586

Modified:
 /trunk/core/src/com/google/inject/Scopes.java
 /trunk/core/test/com/google/inject/CircularDependencyTest.java
 /trunk/extensions/servlet/src/com/google/inject/servlet/ServletScopes.java

=======================================
--- /trunk/core/src/com/google/inject/Scopes.java       Sun Oct 24 17:41:10 2010
+++ /trunk/core/src/com/google/inject/Scopes.java       Tue Sep 27 08:37:30 2011
@@ -65,7 +65,7 @@
                 T provided = creator.get();

// don't remember proxies; these exist only to serve circular dependencies
-                if (provided instanceof CircularDependencyProxy) {
+                if (isCircularProxy(provided)) {
                   return provided;
                 }

@@ -87,6 +87,7 @@
           return returnedInstance;
         }

+        @Override
         public String toString() {
           return String.format("%s[%s]", creator, SINGLETON);
         }
@@ -154,7 +155,7 @@

       if (binding instanceof LinkedBindingImpl) {
         LinkedBindingImpl<?> linkedBinding = (LinkedBindingImpl) binding;
-        Injector injector = (Injector) linkedBinding.getInjector();
+        Injector injector = linkedBinding.getInjector();
         if (injector != null) {
           binding = injector.getBinding(linkedBinding.getLinkedKey());
           continue;
@@ -171,4 +172,16 @@
       return false;
     } while (true);
   }
-}
+
+  /**
+   * Returns true if the object is a proxy for a circular dependency,
+ * constructed by Guice because it encountered a circular dependency. Scope + * implementations should be careful to <b>not cache circular proxies</b>, + * because the proxies are not intended for general purpose use. (They are
+   * designed just to fulfill the immediate injection, not all injections.
+ * Caching them can lead to IllegalArgumentExceptions or ClassCastExceptions.)
+   */
+  public static boolean isCircularProxy(Object object) {
+    return object instanceof CircularDependencyProxy;
+  }
+}
=======================================
--- /trunk/core/test/com/google/inject/CircularDependencyTest.java Thu Jul 7 17:34:16 2011 +++ /trunk/core/test/com/google/inject/CircularDependencyTest.java Tue Sep 27 08:37:30 2011
@@ -17,11 +17,19 @@
 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 junit.framework.TestCase;

+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;

 /**
  * @author [email protected] (Bob Lee)
@@ -501,4 +509,90 @@
   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.
+   *
+   * 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();
+ public <T> Provider<T> scope(final Key<T> key, final Provider<T> unscoped) {
+      return new Provider<T>() {
+        @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);
+        }
+      };
+    }
+  }
+}
=======================================
--- /trunk/extensions/servlet/src/com/google/inject/servlet/ServletScopes.java Tue Sep 27 08:36:19 2011 +++ /trunk/extensions/servlet/src/com/google/inject/servlet/ServletScopes.java Tue Sep 27 08:37:30 2011
@@ -23,6 +23,7 @@
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
 import com.google.inject.Scope;
+import com.google.inject.Scopes;

 import java.util.Map;
 import java.util.concurrent.Callable;
@@ -76,8 +77,10 @@

               if (t == null) {
                 t = creator.get();
-                // Store a sentinel for provider-given null values.
-                scopeMap.put(name, t != null ? t : NullObject.INSTANCE);
+                if (!Scopes.isCircularProxy(t)) {
+                  // Store a sentinel for provider-given null values.
+                  scopeMap.put(name, t != null ? t : NullObject.INSTANCE);
+                }
               }

               return t;
@@ -105,18 +108,22 @@
             T t = (T) obj;
             if (t == null) {
               t = creator.get();
- request.setAttribute(name, (t != null) ? t : NullObject.INSTANCE);
+              if (!Scopes.isCircularProxy(t)) {
+ request.setAttribute(name, (t != null) ? t : NullObject.INSTANCE);
+              }
             }
             return t;
           }
         }

+        @Override
         public String toString() {
           return String.format("%s[%s]", creator, REQUEST);
         }
       };
     }

+    @Override
     public String toString() {
       return "ServletScopes.REQUEST";
     }
@@ -140,17 +147,21 @@
             T t = (T) obj;
             if (t == null) {
               t = creator.get();
- session.setAttribute(name, (t != null) ? t : NullObject.INSTANCE);
+              if (!Scopes.isCircularProxy(t)) {
+ session.setAttribute(name, (t != null) ? t : NullObject.INSTANCE);
+              }
             }
             return t;
           }
         }
+        @Override
         public String toString() {
           return String.format("%s[%s]", creator, SESSION);
         }
       };
     }

+    @Override
     public String toString() {
       return "ServletScopes.SESSION";
     }

--
You received this message because you are subscribed to the Google Groups 
"google-guice-dev" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/google-guice-dev?hl=en.

Reply via email to