Revision: 1049 Author: limpbizkit Date: Thu Jul 23 17:48:12 2009 Log: New scope checker feature.
The implementation is somewhat complex; if you're reading this please consider reading through the code to find some mistakes! I'm considering moving this out to be a proper extension; for now it'll live in util. http://code.google.com/p/google-guice/source/detail?r=1049 Added: /trunk/src/com/google/inject/util/Node.java /trunk/src/com/google/inject/util/ScopeChecker.java /trunk/test/com/google/inject/util/ScopeCheckerTest.java Modified: /trunk/src/com/google/inject/Injector.java /trunk/src/com/google/inject/internal/InheritingState.java /trunk/src/com/google/inject/internal/InjectorBuilder.java /trunk/src/com/google/inject/internal/InjectorImpl.java /trunk/src/com/google/inject/internal/State.java ======================================= --- /dev/null +++ /trunk/src/com/google/inject/util/Node.java Thu Jul 23 17:48:12 2009 @@ -0,0 +1,114 @@ +/** + * Copyright (C) 2009 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.util; + +import com.google.inject.Key; +import com.google.inject.internal.Errors; +import com.google.inject.internal.ImmutableSet; +import com.google.inject.internal.Sets; +import java.lang.annotation.Annotation; +import java.util.Set; + +/** + * A node in the scoped dependency graph. Each node has two scopes. The <i>applied scope</i> is the + * scope directly assigned to the binding by the user, such as in an {...@code in()} clause. The + * <i>effective scope</i> is the narrowest scope in which this object is used. It is derived from + * the narrowest scope of the node's transitive dependencies. Each scope is modelled as a rank; + * higher numbers represent narrower scopes. + */ +class Node { + private final Key<?> key; + + private int appliedScope = Integer.MAX_VALUE; + private Node effectiveScopeDependency; + + private int effectiveScope = Integer.MIN_VALUE; + private Class<? extends Annotation> appliedScopeAnnotation; + + /** Places that this node is injected. */ + private Set<Node> users = ImmutableSet.of(); + + Node(Key<?> key) { + this.key = key; + } + + /** + * Initialize the scope ranks for this node. Called at most once per node. + */ + void setScopeRank(int rank, Class<? extends Annotation> annotation) { + this.appliedScope = rank; + this.effectiveScope = rank; + this.appliedScopeAnnotation = annotation; + } + + /** + * Sets this node's effective scope unless it's already better. + */ + private void setEffectiveScope(int effectiveScope, Node effectiveScopeDependency) { + if (this.effectiveScope >= effectiveScope) { + return; + } + + this.effectiveScope = effectiveScope; + this.effectiveScopeDependency = effectiveScopeDependency; + pushScopeToUsers(); + } + + /** + * Pushes the narrowness of this node's effective scope to everyone that depends on this node. + */ + void pushScopeToUsers() { + for (Node user : users) { + user.setEffectiveScope(effectiveScope, this); + } + } + + /** + * Returns true if this node has no dependency whose scope is narrower than itself. + */ + boolean isScopedCorrectly() { + return appliedScope >= effectiveScope; + } + + boolean isEffectiveScopeAppliedScope() { + return appliedScope == effectiveScope; + } + + /** + * Returns the most narrowly scoped dependency. If multiple such dependencies exist, the selection + * of which is returned is arbitrary. + */ + Node effectiveScopeDependency() { + return effectiveScopeDependency; + } + + /** + * Mark this as a dependency of {...@code node}. + */ + public void addUser(Node node) { + if (users.isEmpty()) { + users = Sets.newHashSet(); + } + users.add(node); + } + + @Override public String toString() { + return appliedScopeAnnotation != null + ? Errors.convert(key) + " in @" + appliedScopeAnnotation.getSimpleName() + : Errors.convert(key).toString(); + } +} ======================================= --- /dev/null +++ /trunk/src/com/google/inject/util/ScopeChecker.java Thu Jul 23 17:48:12 2009 @@ -0,0 +1,180 @@ +/** + * Copyright (C) 2009 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.util; + +import com.google.inject.Binding; +import com.google.inject.ConfigurationException; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Scope; +import com.google.inject.Scopes; +import com.google.inject.internal.ImmutableList; +import com.google.inject.internal.ImmutableMap; +import com.google.inject.internal.Lists; +import com.google.inject.internal.Maps; +import static com.google.inject.internal.Preconditions.checkArgument; +import com.google.inject.spi.BindingScopingVisitor; +import com.google.inject.spi.Dependency; +import com.google.inject.spi.HasDependencies; +import com.google.inject.spi.Message; +import com.google.inject.spi.ProviderBinding; +import java.lang.annotation.Annotation; +import static java.util.Arrays.asList; +import java.util.List; +import java.util.Map; + +/** + * Inspects an injector for scoping violations. Scoping violations exist whenever a long-lived + * object (such as a singleton) depends on a short-lived object (such as a request-scoped object). + * To use, create an scope checker and call it's {...@code check()} method with your scoping + * annotations in decreasing duration: + * <pre><code> + * ScopeChecker scopeChecker = new ScopeChecker(injector); + * scopeChecker.check(Singleton.class, SessionScoped.class, RequestScoped.class); + * </code></pre> + * If there are scoping violations in the injector, the call will fail with a detailed {...@code + * ConfigurationException}. + * + * @author [email protected] (Jesse Wilson) + */ +public class ScopeChecker { + + private final Injector injector; + + public ScopeChecker(Injector injector) { + this.injector = injector; + } + + /** + * Checks this checker's injector for scoping violations. + * + * @param longest the outermost scope, such as {...@code Singleton.class}. + * @param nested a scope immediately nested within {...@code longest} + * @param furtherNested any scopes nested within {...@code nested}, in decreasing duration. + * @throws ConfigurationException if any violations are found. + */ + public void check(Class<? extends Annotation> longest, Class<? extends Annotation> nested, + Class<? extends Annotation>... furtherNested) { + Ranker ranker = new Ranker(longest, nested, furtherNested); + Map<Key<?>, Node> nodes = Maps.newHashMap(); + + // build the graph of node dependencies with scope ranks + for (Binding<?> binding : injector.getAllBindings().values()) { + Key<?> key = binding.getKey(); + Node node = getNode(nodes, key); + ranker.rank(binding, node); + + // explicitly ignore dependencies that come via providers. + if (binding instanceof ProviderBinding) { + continue; + } + + if (binding instanceof HasDependencies) { + HasDependencies hasDependencies = (HasDependencies) binding; + for (Dependency<?> dependency : hasDependencies.getDependencies()) { + getNode(nodes, dependency.getKey()).addUser(node); + } + } + } + + // walk through the nodes, pushing effective scopes through dependencies + for (Node node : nodes.values()) { + node.pushScopeToUsers(); + } + + // on the nodes with dependencies narrower than themselves, print an error + List<Message> messages = Lists.newArrayList(); + for (Node node : nodes.values()) { + if (node.isScopedCorrectly()) { + continue; + } + + StringBuilder error = new StringBuilder("Illegal scoped dependency: ").append(node); + Node dependency = node; + do { + dependency = dependency.effectiveScopeDependency(); + error.append("\n depends on ").append(dependency); + } while (!dependency.isEffectiveScopeAppliedScope()); + messages.add(new Message(error.toString())); + } + + if (!messages.isEmpty()) { + throw new ConfigurationException(messages); + } + } + + private Node getNode(Map<Key<?>, Node> nodes, Key<?> key) { + Node node = nodes.get(key); + if (node == null) { + node = new Node(key); + nodes.put(key, node); + } + return node; + } + + /** + * Applies the scoping rank to a node. Scopes are stored as integers, and narrower scopes get + * greater values. + */ + private class Ranker implements BindingScopingVisitor<Scope> { + private final ImmutableList<Class<? extends Annotation>> scopeAnnotations; + private final ImmutableMap<Scope, Integer> scopeToRank; + + private Ranker(Class<? extends Annotation> longest, Class<? extends Annotation> nested, + Class<? extends Annotation>... furtherNested) { + scopeAnnotations = new ImmutableList.Builder<Class<? extends Annotation>>() + .add(longest) + .add(nested) + .addAll(asList(furtherNested)) + .build(); + + ImmutableMap.Builder<Scope, Integer> scopeToRankBuilder = ImmutableMap.builder(); + Map<Class<? extends Annotation>, Scope> annotationToScope = injector.getScopeBindings(); + int i = 0; + for (Class<? extends Annotation> scopeAnnotation : scopeAnnotations) { + Scope scope = annotationToScope.get(scopeAnnotation); + checkArgument(scope != null, "No scope binding for %s", scopeAnnotation); + scopeToRankBuilder.put(scope, i++); + } + scopeToRank = scopeToRankBuilder.build(); + } + + public void rank(Binding<?> binding, Node node) { + Scope scope = binding.acceptScopingVisitor(this); + Integer rank = scopeToRank.get(scope); + if (rank != null) { + node.setScopeRank(rank, scopeAnnotations.get(rank)); + } + } + + public Scope visitEagerSingleton() { + return Scopes.SINGLETON; + } + + public com.google.inject.Scope visitScope(com.google.inject.Scope scope) { + return scope; + } + + public Scope visitScopeAnnotation(Class<? extends Annotation> scopeAnnotation) { + throw new AssertionError(); + } + + public Scope visitNoScoping() { + return Scopes.NO_SCOPE; + } + } +} ======================================= --- /dev/null +++ /trunk/test/com/google/inject/util/ScopeCheckerTest.java Thu Jul 23 17:48:12 2009 @@ -0,0 +1,226 @@ +/** + * Copyright (C) 2009 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.util; + +import com.google.inject.AbstractModule; +import static com.google.inject.Asserts.assertContains; +import com.google.inject.ConfigurationException; +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import com.google.inject.Provider; +import com.google.inject.Scope; +import com.google.inject.ScopeAnnotation; +import com.google.inject.Singleton; +import static java.lang.annotation.ElementType.TYPE; +import java.lang.annotation.Retention; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import java.lang.annotation.Target; +import junit.framework.TestCase; + +/** + * @author [email protected] (Jesse Wilson) + */ +public class ScopeCheckerTest extends TestCase { + + @Target(TYPE) @Retention(RUNTIME) @ScopeAnnotation + @interface Annually {} + + @Target(TYPE) @Retention(RUNTIME) @ScopeAnnotation + @interface Seasonally {} + + @Target(TYPE) @Retention(RUNTIME) @ScopeAnnotation + @interface Daily {} + + Module scopesModule = new AbstractModule() { + protected void configure() { + bindScope(Annually.class, newScope()); + bindScope(Seasonally.class, newScope()); + bindScope(Daily.class, newScope()); + } + }; + + /** change your shirt daily. Depends on the sleeve length appropriate for the weather */ + static class Shirt { + @Inject SleeveLenth sleeveLenth; + } + + /** long sleeves in the winter, short sleeves in the summer, etc. */ + static class SleeveLenth { + @Inject Style style; + } + + /** fashion evolves over time */ + static class Style {} + + /** pants can be tweaked (with belts) to fit a changing style */ + static class Pants { + @Inject Provider<Style> style; + } + + public void testProperlyNestedScopes() { + Module module = new AbstractModule() { + protected void configure() { + bind(Style.class).in(Annually.class); + bind(SleeveLenth.class).in(Seasonally.class); + bind(Shirt.class).in(Daily.class); + } + }; + + ScopeChecker scopeChecker = new ScopeChecker(Guice.createInjector(scopesModule, module)); + scopeChecker.check(Annually.class, Seasonally.class, Daily.class); + } + + public void testDependingOnUnscoped() { + Module module = new AbstractModule() { + protected void configure() { + bind(Style.class); + bind(SleeveLenth.class); + bind(Shirt.class).in(Daily.class); + } + }; + + ScopeChecker scopeChecker = new ScopeChecker(Guice.createInjector(scopesModule, module)); + scopeChecker.check(Annually.class, Seasonally.class, Daily.class); + } + + public void testUsedByUnscoped() { + Module module = new AbstractModule() { + protected void configure() { + bind(Style.class).in(Annually.class); + bind(SleeveLenth.class); + bind(Shirt.class); + } + }; + + ScopeChecker scopeChecker = new ScopeChecker(Guice.createInjector(scopesModule, module)); + scopeChecker.check(Annually.class, Seasonally.class, Daily.class); + } + + public void testDirectViolation() { + Module module = new AbstractModule() { + protected void configure() { + bind(Style.class).in(Annually.class); + bind(SleeveLenth.class).in(Seasonally.class); + bind(Shirt.class).in(Annually.class); + } + }; + + ScopeChecker scopeChecker = new ScopeChecker(Guice.createInjector(scopesModule, module)); + try { + scopeChecker.check(Annually.class, Seasonally.class, Daily.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + "1) Illegal scoped dependency: " + Shirt.class.getName() + " in @Annually", + " depends on " + SleeveLenth.class.getName() + " in @Seasonally"); + } + } + + public void testDirectDependencyOnProvider() { + Module module = new AbstractModule() { + protected void configure() { + bind(Style.class).in(Daily.class); + bind(Pants.class).in(Seasonally.class); + } + }; + + ScopeChecker scopeChecker = new ScopeChecker(Guice.createInjector(scopesModule, module)); + scopeChecker.check(Annually.class, Seasonally.class, Daily.class); + } + + public void testIndirectViolation() { + Module module = new AbstractModule() { + protected void configure() { + bind(Style.class).in(Seasonally.class); + bind(Shirt.class).in(Annually.class); + } + }; + + ScopeChecker scopeChecker = new ScopeChecker(Guice.createInjector(scopesModule, module)); + try { + scopeChecker.check(Annually.class, Seasonally.class, Daily.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + "1) Illegal scoped dependency: " + Shirt.class.getName() + " in @Annually", + " depends on " + SleeveLenth.class.getName(), + " depends on " + Style.class.getName() + " in @Seasonally"); + } + } + + public void testValidCircularDependency() { + Module module = new AbstractModule() { + protected void configure() { + bind(Chicken.class).in(Daily.class); + bind(Egg.class).in(Daily.class); + } + }; + + ScopeChecker scopeChecker = new ScopeChecker(Guice.createInjector(scopesModule, module)); + scopeChecker.check(Annually.class, Seasonally.class, Daily.class); + } + + public void testInvalidCircularDependency() { + Module module = new AbstractModule() { + protected void configure() { + bind(Chicken.class).in(Seasonally.class); + bind(Egg.class).in(Daily.class); + } + }; + + ScopeChecker scopeChecker = new ScopeChecker(Guice.createInjector(scopesModule, module)); + try { + scopeChecker.check(Annually.class, Seasonally.class, Daily.class); + fail(); + } catch (ConfigurationException expected) { + assertContains(expected.getMessage(), + "1) Illegal scoped dependency: " + Chicken.class.getName() + " in @Seasonally", + " depends on " + Egg.class.getName() + " in @Daily"); + } + } + + public void testCheckUnboundScope() { + Injector injector = Guice.createInjector(); + ScopeChecker scopeChecker = new ScopeChecker(injector); + + try { + scopeChecker.check(Singleton.class, Daily.class); + fail(); + } catch (IllegalArgumentException expected) { + assertContains(expected.getMessage(), + "No scope binding for " + Daily.class); + } + } + + static class Chicken { + @Inject Egg source; + } + static class Egg { + @Inject Chicken source; + } + + private Scope newScope() { + return new Scope() { + public <T> Provider<T> scope(Key<T> key, Provider<T> unscoped) { + return unscoped; + } + }; + } +} ======================================= --- /trunk/src/com/google/inject/Injector.java Sat Jul 4 09:20:55 2009 +++ /trunk/src/com/google/inject/Injector.java Thu Jul 23 17:48:12 2009 @@ -16,6 +16,7 @@ package com.google.inject; +import java.lang.annotation.Annotation; import java.util.List; import java.util.Map; @@ -222,4 +223,13 @@ * @since 2.0 */ Injector createChildInjector(Module... modules); -} + + /** + * Returns a map containing all scopes in the injector. The maps keys are scoping annotations + * like {...@code Singleton.class}, and the values are scope instances, such as {...@code + * Scopes.SINGLETON. The returned map is immutable. + * + * <p>This method is part of the Guice SPI and is intended for use by tools and extensions. + */ + Map<Class<? extends Annotation>, Scope> getScopeBindings(); +} ======================================= --- /trunk/src/com/google/inject/internal/InheritingState.java Sat Jun 6 10:51:27 2009 +++ /trunk/src/com/google/inject/internal/InheritingState.java Thu Jul 23 17:48:12 2009 @@ -142,4 +142,8 @@ public Object lock() { return lock; } -} + + public Map<Class<? extends Annotation>, Scope> getScopes() { + return scopes; + } +} ======================================= --- /trunk/src/com/google/inject/internal/InjectorBuilder.java Sat Jul 4 09:20:55 2009 +++ /trunk/src/com/google/inject/internal/InjectorBuilder.java Thu Jul 23 17:48:12 2009 @@ -24,11 +24,13 @@ import com.google.inject.Provider; import com.google.inject.Stage; import com.google.inject.TypeLiteral; +import com.google.inject.Scope; import com.google.inject.spi.Dependency; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; +import java.lang.annotation.Annotation; /** * Builds a tree of injectors. This is a primary injector, plus child injectors needed for each @@ -266,6 +268,9 @@ public Injector createChildInjector(Module... modules) { return delegateInjector.createChildInjector(modules); } + public Map<Class<? extends Annotation>, Scope> getScopeBindings() { + return delegateInjector.getScopeBindings(); + } public <T> Provider<T> getProvider(Key<T> key) { throw new UnsupportedOperationException( "Injector.getProvider(Key<T>) is not supported in Stage.TOOL"); ======================================= --- /trunk/src/com/google/inject/internal/InjectorImpl.java Thu Jul 16 20:10:13 2009 +++ /trunk/src/com/google/inject/internal/InjectorImpl.java Thu Jul 23 17:48:12 2009 @@ -28,17 +28,20 @@ import com.google.inject.Provider; import com.google.inject.ProvisionException; import com.google.inject.TypeLiteral; +import com.google.inject.Scope; import com.google.inject.spi.BindingTargetVisitor; import com.google.inject.spi.ConvertedConstantBinding; import com.google.inject.spi.Dependency; import com.google.inject.spi.InjectionPoint; import com.google.inject.spi.ProviderBinding; import com.google.inject.spi.ProviderKeyBinding; +import com.google.inject.spi.HasDependencies; import com.google.inject.util.Providers; import java.lang.reflect.GenericArrayType; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.lang.annotation.Annotation; import java.util.Collections; import java.util.List; import java.util.Map; @@ -217,7 +220,7 @@ } private static class ProviderBindingImpl<T> extends BindingImpl<Provider<T>> - implements ProviderBinding<Provider<T>> { + implements ProviderBinding<Provider<T>>, HasDependencies { final BindingImpl<T> providedBinding; ProviderBindingImpl(InjectorImpl injector, Key<Provider<T>> key, Binding<T> providedBinding) { @@ -253,6 +256,10 @@ .add("providedKey", getProvidedKey()) .toString(); } + + public Set<Dependency<?>> getDependencies() { + return ImmutableSet.<Dependency<?>>of(Dependency.get(getProvidedKey())); + } } /** @@ -643,6 +650,10 @@ .build(); } } + + public Map<Class<? extends Annotation>, Scope> getScopeBindings() { + return state.getScopes(); + } private static class BindingsMultimap { final Map<TypeLiteral<?>, List<Binding<?>>> multimap = Maps.newHashMap(); ======================================= --- /trunk/src/com/google/inject/internal/State.java Sat Jun 6 10:51:27 2009 +++ /trunk/src/com/google/inject/internal/State.java Thu Jul 23 17:48:12 2009 @@ -99,6 +99,10 @@ public Object lock() { throw new UnsupportedOperationException(); } + + public Map<Class<? extends Annotation>, Scope> getScopes() { + return ImmutableMap.of(); + } }; State parent(); @@ -153,4 +157,9 @@ * to be used when reading mutable data (ie. just-in-time bindings, and binding blacklists). */ Object lock(); -} + + /** + * Returns all the scope bindings at this level and parent levels. + */ + Map<Class<? extends Annotation>, Scope> getScopes(); +} --~--~---------~--~----~------------~-------~--~----~ 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 -~----------~----~----~----~------~----~------~--~---
