diff --git a/core/src/com/google/inject/ScopeAnnotation.java b/core/src/com/google/inject/ScopeAnnotation.java index e1196cb2e9..98af253d7f 100644 --- a/core/src/com/google/inject/ScopeAnnotation.java +++ b/core/src/com/google/inject/ScopeAnnotation.java @@ -24,14 +24,16 @@ /** * Annotates annotations which are used for scoping. Only one such annotation may apply to a single - * implementation class. You must also annotate scope annotations with {@code @Retention(RUNTIME)}. - * For example: + * implementation class, injectable constructor parameter, or injectable field. You must also + * annotate scope annotations with {@code @Retention(RUNTIME)}. To permit constructor-parameter- or + * field-local scoping, the scope annotation must target {@code PARAMETER} or {@code FIELD} + * respectively. For example: * *
  *   {@code @}Retention(RUNTIME)
- *   {@code @}Target(TYPE, METHOD)
+ *   {@code @}Target({TYPE, METHOD, PARAMETER, FIELD})
  *   {@code @}ScopeAnnotation
- *   public {@code @}interface SessionScoped {}
+ *   public {@code @}interface CustomScoped {}
  * 
* * @author crazybob@google.com (Bob Lee) diff --git a/core/src/com/google/inject/Singleton.java b/core/src/com/google/inject/Singleton.java index 59a2b44d6d..4531c8d651 100644 --- a/core/src/com/google/inject/Singleton.java +++ b/core/src/com/google/inject/Singleton.java @@ -24,11 +24,13 @@ /** * Apply this to implementation classes when you want only one instance (per {@link Injector}) to be - * reused for all injections for that binding. + * reused for all injections for that binding. Apply this to an injectable constructor parameter or + * field to reuse one instance for that dependency across all objects created by that constructor, + * or across all fields of that instance sharing the same key, respectively. * * @author crazybob@google.com (Bob Lee) */ -@Target({ElementType.TYPE, ElementType.METHOD}) +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) @Retention(RUNTIME) @ScopeAnnotation public @interface Singleton {} diff --git a/core/src/com/google/inject/internal/ConstructorInjectorStore.java b/core/src/com/google/inject/internal/ConstructorInjectorStore.java index 4ef9af8d4e..1927ce0665 100644 --- a/core/src/com/google/inject/internal/ConstructorInjectorStore.java +++ b/core/src/com/google/inject/internal/ConstructorInjectorStore.java @@ -71,7 +71,7 @@ private ConstructorInjector createConstructor(InjectionPoint injectionPoi int numErrorsBefore = errors.size(); SingleParameterInjector[] constructorParameterInjectors = - injector.getParametersInjectors(injectionPoint.getDependencies(), errors); + injector.getConstructorParameterInjectors(injectionPoint, errors); @SuppressWarnings("unchecked") // the injector type agrees with the injection point type MembersInjectorImpl membersInjector = diff --git a/core/src/com/google/inject/internal/InjectorImpl.java b/core/src/com/google/inject/internal/InjectorImpl.java index 25d0b2c442..bb8dee022c 100644 --- a/core/src/com/google/inject/internal/InjectorImpl.java +++ b/core/src/com/google/inject/internal/InjectorImpl.java @@ -52,10 +52,12 @@ import com.google.inject.spi.TypeConverterBinding; import com.google.inject.util.Providers; import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; import java.lang.reflect.GenericArrayType; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -1083,16 +1085,39 @@ public Map, List> getAllMembersInjectorInjectionP /** Returns parameter injectors, or {@code null} if there are no parameters. */ SingleParameterInjector[] getParametersInjectors(List> parameters, Errors errors) throws ErrorsException { + return getParametersInjectors(parameters, null, errors); + } + + /** Returns scoped parameter injectors for an injectable constructor. */ + SingleParameterInjector[] getConstructorParameterInjectors( + InjectionPoint injectionPoint, Errors errors) throws ErrorsException { + Constructor constructor = (Constructor) injectionPoint.getMember(); + return getParametersInjectors( + injectionPoint.getDependencies(), constructor.getParameterAnnotations(), errors); + } + + private SingleParameterInjector[] getParametersInjectors( + List> parameters, + @Nullable Annotation[][] parameterAnnotations, + Errors errors) + throws ErrorsException { if (parameters.isEmpty()) { return null; } int numErrorsBefore = errors.size(); SingleParameterInjector[] result = new SingleParameterInjector[parameters.size()]; - int i = 0; - for (Dependency parameter : parameters) { + Map, Map, InternalFactory>> scopedFactories = + parameterAnnotations == null ? null : new HashMap<>(); + for (int i = 0; i < parameters.size(); i++) { + Dependency parameter = parameters.get(i); try { - result[i++] = createParameterInjector(parameter, errors.withSource(parameter)); + Errors parameterErrors = errors.withSource(parameter); + result[i] = + parameterAnnotations == null + ? createParameterInjector(parameter, parameterErrors) + : createConstructorParameterInjector( + parameter, parameterAnnotations[i], scopedFactories, parameterErrors); } catch (ErrorsException rethrownBelow) { // rethrown below } @@ -1102,6 +1127,62 @@ SingleParameterInjector[] getParametersInjectors(List> paramete return result; } + private SingleParameterInjector createConstructorParameterInjector( + Dependency dependency, + Annotation[] parameterAnnotations, + Map, Map, InternalFactory>> scopedFactories, + Errors errors) + throws ErrorsException { + int numErrorsBefore = errors.size(); + Class scopeAnnotation = + findScopeAnnotation(errors, parameterAnnotations); + errors.throwIfNewErrors(numErrorsBefore); + if (scopeAnnotation == null) { + return createParameterInjector(dependency, errors); + } + + return new SingleParameterInjector<>( + dependency, getScopedFactory(dependency, scopeAnnotation, scopedFactories, errors)); + } + + /** + * Returns a factory for {@code dependency} that's scoped by {@code scopeAnnotation}, reusing an + * existing scoped factory from {@code scopedFactories} if one was already created for the same + * scope annotation and key (e.g. for another parameter or field sharing the same declaring + * element). + */ + InternalFactory getScopedFactory( + Dependency dependency, + Class scopeAnnotation, + Map, Map, InternalFactory>> scopedFactories, + Errors errors) + throws ErrorsException { + Map, InternalFactory> factoriesForScope = + scopedFactories.computeIfAbsent(scopeAnnotation, unused -> new HashMap<>()); + InternalFactory scopedFactory = factoriesForScope.get(dependency.getKey()); + if (scopedFactory == null) { + int numErrorsBefore = errors.size(); + Scoping scoping = + Scoping.makeInjectable(Scoping.forAnnotation(scopeAnnotation), this, errors); + errors.throwIfNewErrors(numErrorsBefore); + + BindingImpl binding = + getBindingOrThrow(dependency.getKey(), errors, JitLimitation.NO_JIT); + + Key privateKey = + Key.get(dependency.getKey().getTypeLiteral(), UniqueAnnotations.create()); + scopedFactory = + Scoping.scope( + privateKey, this, binding.getInternalFactory(), dependency, scoping); + factoriesForScope.put(dependency.getKey(), scopedFactory); + } + + @SuppressWarnings("unchecked") // The factory was created for this dependency key. + InternalFactory typedScopedFactory = + (InternalFactory) scopedFactory; + return typedScopedFactory; + } + SingleParameterInjector createParameterInjector( final Dependency dependency, final Errors errors) throws ErrorsException { BindingImpl binding = @@ -1109,6 +1190,28 @@ SingleParameterInjector createParameterInjector( return new SingleParameterInjector(dependency, binding); } + /** + * Returns the factory for an injectable field, honoring any scope annotation on the field. The + * {@code scopedFactories} map is shared across the fields (and methods) of a single declaring + * type, so that multiple fields scoped by the same annotation and requesting the same key reuse + * one scoped instance, mirroring the behavior for scoped constructor parameters. + */ + InternalFactory getFieldFactory( + Dependency dependency, + Annotation[] fieldAnnotations, + Map, Map, InternalFactory>> scopedFactories, + Errors errors) + throws ErrorsException { + int numErrorsBefore = errors.size(); + Class scopeAnnotation = findScopeAnnotation(errors, fieldAnnotations); + errors.throwIfNewErrors(numErrorsBefore); + if (scopeAnnotation == null) { + return getBindingOrThrow(dependency.getKey(), errors, JitLimitation.NO_JIT) + .getInternalFactory(); + } + return getScopedFactory(dependency, scopeAnnotation, scopedFactories, errors); + } + /** Cached constructor injectors for each type */ final ConstructorInjectorStore constructors = new ConstructorInjectorStore(this); diff --git a/core/src/com/google/inject/internal/MembersInjectorStore.java b/core/src/com/google/inject/internal/MembersInjectorStore.java index 760efa02ee..112e9ae3fc 100644 --- a/core/src/com/google/inject/internal/MembersInjectorStore.java +++ b/core/src/com/google/inject/internal/MembersInjectorStore.java @@ -23,12 +23,16 @@ import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.inject.ConfigurationException; +import com.google.inject.Key; import com.google.inject.TypeLiteral; import com.google.inject.spi.InjectionPoint; import com.google.inject.spi.TypeListener; import com.google.inject.spi.TypeListenerBinding; +import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -120,6 +124,10 @@ private MembersInjectorImpl createWithListeners(TypeLiteral type, Erro ImmutableList getInjectors( Set injectionPoints, Errors errors) { List injectors = Lists.newArrayList(); + // Shared across all fields of the type being processed, so that multiple scoped fields + // requesting the same key and scope annotation reuse a single scoped instance. + Map, Map, InternalFactory>> scopedFieldFactories = + new HashMap<>(); for (InjectionPoint injectionPoint : injectionPoints) { try { Errors errorsForMember = @@ -128,7 +136,8 @@ ImmutableList getInjectors( : errors.withSource(injectionPoint); SingleMemberInjector injector = injectionPoint.getMember() instanceof Field - ? new SingleFieldInjector(this.injector, injectionPoint, errorsForMember) + ? new SingleFieldInjector( + this.injector, injectionPoint, scopedFieldFactories, errorsForMember) : new SingleMethodInjector(this.injector, injectionPoint, errorsForMember); injectors.add(injector); } catch (ErrorsException ignoredForNow) { diff --git a/core/src/com/google/inject/internal/SingleFieldInjector.java b/core/src/com/google/inject/internal/SingleFieldInjector.java index 4debda6488..98c03baa68 100644 --- a/core/src/com/google/inject/internal/SingleFieldInjector.java +++ b/core/src/com/google/inject/internal/SingleFieldInjector.java @@ -19,13 +19,15 @@ import static com.google.inject.internal.InternalMethodHandles.castReturnTo; import static java.lang.invoke.MethodType.methodType; -import com.google.inject.internal.InjectorImpl.JitLimitation; +import com.google.inject.Key; import com.google.inject.spi.Dependency; import com.google.inject.spi.InjectionPoint; +import java.lang.annotation.Annotation; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.util.Map; /** Sets an injectable field. */ final class SingleFieldInjector implements SingleMemberInjector { @@ -34,7 +36,11 @@ final class SingleFieldInjector implements SingleMemberInjector { final Dependency dependency; final InternalFactory factory; - public SingleFieldInjector(InjectorImpl injector, InjectionPoint injectionPoint, Errors errors) + public SingleFieldInjector( + InjectorImpl injector, + InjectionPoint injectionPoint, + Map, Map, InternalFactory>> scopedFactories, + Errors errors) throws ErrorsException { this.injectionPoint = injectionPoint; this.field = (Field) injectionPoint.getMember(); @@ -43,9 +49,7 @@ public SingleFieldInjector(InjectorImpl injector, InjectionPoint injectionPoint, // Ewwwww... field.setAccessible(true); factory = - injector - .getBindingOrThrow(dependency.getKey(), errors, JitLimitation.NO_JIT) - .getInternalFactory(); + injector.getFieldFactory(dependency, field.getAnnotations(), scopedFactories, errors); } @Override diff --git a/core/src/com/google/inject/internal/SingleParameterInjector.java b/core/src/com/google/inject/internal/SingleParameterInjector.java index d41e39ae1f..d99bbf1b29 100644 --- a/core/src/com/google/inject/internal/SingleParameterInjector.java +++ b/core/src/com/google/inject/internal/SingleParameterInjector.java @@ -32,8 +32,13 @@ final class SingleParameterInjector { @LazyInit private MethodHandle handle; SingleParameterInjector(Dependency dependency, BindingImpl binding) { + this(dependency, binding.getInternalFactory()); + } + + SingleParameterInjector( + Dependency dependency, InternalFactory factory) { this.dependency = dependency; - this.factory = binding.getInternalFactory(); + this.factory = factory; } T inject(InternalContext context) throws InternalProvisionException { diff --git a/core/test/com/google/inject/ScopesTest.java b/core/test/com/google/inject/ScopesTest.java index 51c7606166..8bd8c3072a 100644 --- a/core/test/com/google/inject/ScopesTest.java +++ b/core/test/com/google/inject/ScopesTest.java @@ -60,6 +60,7 @@ import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -493,6 +494,185 @@ protected void configure() { } } + @Test + public void testSingletonScopeOnConstructorParameter() { + AtomicInteger nextInstanceId = new AtomicInteger(); + Key dependencyKey = + Key.get(ConstructorScopedDependency.class, named("constructor")); + Injector injector = + Guice.createInjector( + new AbstractModule() { + @Override + protected void configure() { + bind(dependencyKey) + .toProvider( + () -> new ConstructorScopedDependency(nextInstanceId.getAndIncrement())); + } + }); + + SingletonParameterConsumer first = injector.getInstance(SingletonParameterConsumer.class); + SingletonParameterConsumer second = injector.getInstance(SingletonParameterConsumer.class); + OtherSingletonParameterConsumer other = + injector.getInstance(OtherSingletonParameterConsumer.class); + ConstructorScopedDependency direct = injector.getInstance(dependencyKey); + + assertSame(first.first, first.second); + assertSame(first.first, second.first); + assertNotSame(first.first, other.dependency); + assertNotSame(first.first, direct); + assertEquals(3, nextInstanceId.get()); + } + + @Test + public void testCustomScopeOnConstructorParameter() { + AtomicInteger nextInstanceId = new AtomicInteger(); + Injector injector = + Guice.createInjector( + new AbstractModule() { + @Override + protected void configure() { + bindScope(ConstructorScoped.class, Scopes.SINGLETON); + bind(ConstructorScopedDependency.class) + .toProvider( + () -> new ConstructorScopedDependency(nextInstanceId.getAndIncrement())); + } + }); + + CustomScopeParameterConsumer first = + injector.getInstance(CustomScopeParameterConsumer.class); + CustomScopeParameterConsumer second = + injector.getInstance(CustomScopeParameterConsumer.class); + + assertSame(first.dependency, second.dependency); + assertEquals(1, nextInstanceId.get()); + } + + @Test + public void testConstructorParameterScopeUsedButNotBound() { + CreationException exception = + assertThrows( + CreationException.class, + () -> + Guice.createInjector( + new AbstractModule() { + @Override + protected void configure() { + bind(UnboundScopeParameterConsumer.class); + } + })); + + assertContains( + exception.getMessage(), "No scope is bound to ScopesTest$UnboundConstructorScoped."); + } + + @Test + public void testDuplicateScopesOnConstructorParameter() { + CreationException exception = + assertThrows( + CreationException.class, + () -> + Guice.createInjector( + new AbstractModule() { + @Override + protected void configure() { + bindScope(ConstructorScoped.class, Scopes.SINGLETON); + bind(DuplicateScopeParameterConsumer.class); + } + })); + + assertContains( + exception.getMessage(), + "More than one scope annotation was found: Singleton and" + + " ScopesTest$ConstructorScoped."); + } + + @Test + public void testSingletonScopeOnInjectedField() { + AtomicInteger nextInstanceId = new AtomicInteger(); + Key dependencyKey = + Key.get(ConstructorScopedDependency.class, named("field")); + Injector injector = + Guice.createInjector( + new AbstractModule() { + @Override + protected void configure() { + bind(dependencyKey) + .toProvider( + () -> new ConstructorScopedDependency(nextInstanceId.getAndIncrement())); + } + }); + + SingletonFieldConsumer first = injector.getInstance(SingletonFieldConsumer.class); + SingletonFieldConsumer second = injector.getInstance(SingletonFieldConsumer.class); + OtherSingletonFieldConsumer other = injector.getInstance(OtherSingletonFieldConsumer.class); + ConstructorScopedDependency direct = injector.getInstance(dependencyKey); + + assertSame(first.first, first.second); + assertSame(first.first, second.first); + assertNotSame(first.first, other.dependency); + assertNotSame(first.first, direct); + assertEquals(3, nextInstanceId.get()); + } + + @Test + public void testCustomScopeOnInjectedField() { + AtomicInteger nextInstanceId = new AtomicInteger(); + Injector injector = + Guice.createInjector( + new AbstractModule() { + @Override + protected void configure() { + bindScope(FieldScoped.class, Scopes.SINGLETON); + bind(ConstructorScopedDependency.class) + .toProvider( + () -> new ConstructorScopedDependency(nextInstanceId.getAndIncrement())); + } + }); + + CustomScopeFieldConsumer first = injector.getInstance(CustomScopeFieldConsumer.class); + CustomScopeFieldConsumer second = injector.getInstance(CustomScopeFieldConsumer.class); + + assertSame(first.dependency, second.dependency); + assertEquals(1, nextInstanceId.get()); + } + + @Test + public void testFieldScopeUsedButNotBound() { + CreationException exception = + assertThrows( + CreationException.class, + () -> + Guice.createInjector( + new AbstractModule() { + @Override + protected void configure() { + bind(UnboundScopeFieldConsumer.class); + } + })); + + assertContains(exception.getMessage(), "No scope is bound to ScopesTest$UnboundFieldScoped."); + } + + @Test + public void testDuplicateScopesOnInjectedField() { + CreationException exception = + assertThrows( + CreationException.class, + () -> + Guice.createInjector( + new AbstractModule() { + @Override + protected void configure() { + bindScope(FieldScoped.class, Scopes.SINGLETON); + bind(DuplicateScopeFieldConsumer.class); + } + })); + + assertContains( + exception.getMessage(), + "More than one scope annotation was found: Singleton and ScopesTest$FieldScoped."); + } + @Test public void testNullScopedAsASingleton() { Injector injector = @@ -545,6 +725,127 @@ public static class Injected implements In {} @ScopeAnnotation public @interface CustomScoped {} + @Target(ElementType.PARAMETER) + @Retention(RUNTIME) + @ScopeAnnotation + public @interface ConstructorScoped {} + + @Target(ElementType.PARAMETER) + @Retention(RUNTIME) + @ScopeAnnotation + public @interface UnboundConstructorScoped {} + + static final class ConstructorScopedDependency { + final int instanceId; + + ConstructorScopedDependency(int instanceId) { + this.instanceId = instanceId; + } + } + + @SuppressWarnings("MisplacedScopeAnnotations") + static final class SingletonParameterConsumer { + final ConstructorScopedDependency first; + final ConstructorScopedDependency second; + + @Inject + SingletonParameterConsumer( + @Named("constructor") @Singleton ConstructorScopedDependency first, + @Named("constructor") @Singleton ConstructorScopedDependency second) { + this.first = first; + this.second = second; + } + } + + @SuppressWarnings("MisplacedScopeAnnotations") + static final class OtherSingletonParameterConsumer { + final ConstructorScopedDependency dependency; + + @Inject + OtherSingletonParameterConsumer( + @Named("constructor") @Singleton ConstructorScopedDependency dependency) { + this.dependency = dependency; + } + } + + @SuppressWarnings("MisplacedScopeAnnotations") + static final class CustomScopeParameterConsumer { + final ConstructorScopedDependency dependency; + + @Inject + CustomScopeParameterConsumer(@ConstructorScoped ConstructorScopedDependency dependency) { + this.dependency = dependency; + } + } + + @SuppressWarnings("MisplacedScopeAnnotations") + static final class UnboundScopeParameterConsumer { + @Inject + UnboundScopeParameterConsumer( + @UnboundConstructorScoped ConstructorScopedDependency dependency) {} + } + + @SuppressWarnings({ + "MisplacedScopeAnnotations", + "MoreThanOneScopeAnnotationOnClass", + "multiple-scope" + }) + static final class DuplicateScopeParameterConsumer { + @Inject + DuplicateScopeParameterConsumer( + @Singleton @ConstructorScoped ConstructorScopedDependency dependency) {} + } + + @Target(ElementType.FIELD) + @Retention(RUNTIME) + @ScopeAnnotation + public @interface FieldScoped {} + + @Target(ElementType.FIELD) + @Retention(RUNTIME) + @ScopeAnnotation + public @interface UnboundFieldScoped {} + + @SuppressWarnings("MisplacedScopeAnnotations") + static final class SingletonFieldConsumer { + @Inject + @Named("field") + @Singleton + ConstructorScopedDependency first; + + @Inject + @Named("field") + @Singleton + ConstructorScopedDependency second; + } + + @SuppressWarnings("MisplacedScopeAnnotations") + static final class OtherSingletonFieldConsumer { + @Inject + @Named("field") + @Singleton + ConstructorScopedDependency dependency; + } + + @SuppressWarnings("MisplacedScopeAnnotations") + static final class CustomScopeFieldConsumer { + @Inject @FieldScoped ConstructorScopedDependency dependency; + } + + @SuppressWarnings("MisplacedScopeAnnotations") + static final class UnboundScopeFieldConsumer { + @Inject @UnboundFieldScoped ConstructorScopedDependency dependency; + } + + @SuppressWarnings({ + "MisplacedScopeAnnotations", + "MoreThanOneScopeAnnotationOnClass", + "multiple-scope" + }) + static final class DuplicateScopeFieldConsumer { + @Inject @Singleton @FieldScoped ConstructorScopedDependency dependency; + } + static final Scope CUSTOM_SCOPE = new Scope() { @Override diff --git a/extensions/servlet/src/com/google/inject/servlet/RequestScoped.java b/extensions/servlet/src/com/google/inject/servlet/RequestScoped.java index 340a5b6dad..d25ff7e673 100644 --- a/extensions/servlet/src/com/google/inject/servlet/RequestScoped.java +++ b/extensions/servlet/src/com/google/inject/servlet/RequestScoped.java @@ -23,11 +23,14 @@ import jakarta.inject.Scope; /** - * Apply this to implementation classes when you want one instance per request. + * Apply this to implementation classes when you want one instance per request. Apply this to an + * injectable constructor parameter or field to reuse one instance per request for that dependency + * across all objects created by that constructor, or across all fields of that instance sharing + * the same key, respectively. * * @author crazybob@google.com (Bob Lee) */ -@Target({ElementType.TYPE, ElementType.METHOD}) +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Scope public @interface RequestScoped {} diff --git a/extensions/servlet/test/com/google/inject/servlet/ScopeRequestIntegrationTest.java b/extensions/servlet/test/com/google/inject/servlet/ScopeRequestIntegrationTest.java index 99f8a34e23..b9039e4a8c 100644 --- a/extensions/servlet/test/com/google/inject/servlet/ScopeRequestIntegrationTest.java +++ b/extensions/servlet/test/com/google/inject/servlet/ScopeRequestIntegrationTest.java @@ -35,6 +35,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import jakarta.servlet.ServletException; import junit.framework.TestCase; @@ -146,6 +147,84 @@ protected void configureServlets() { assertNull(callable.call()); } + public final void testRequestScopeOnConstructorParameter() throws Exception { + AtomicInteger nextInstanceId = new AtomicInteger(); + Injector injector = + Guice.createInjector( + new ServletModule() { + @Override + protected void configureServlets() { + bind(ConstructorScopedDependency.class) + .toProvider( + () -> new ConstructorScopedDependency(nextInstanceId.getAndIncrement())); + } + }); + + ConstructorScopedDependency[] firstRequest = + ServletScopes.scopeRequest( + () -> { + RequestParameterConsumer first = + injector.getInstance(RequestParameterConsumer.class); + RequestParameterConsumer second = + injector.getInstance(RequestParameterConsumer.class); + OtherRequestParameterConsumer other = + injector.getInstance(OtherRequestParameterConsumer.class); + ConstructorScopedDependency direct = + injector.getInstance(ConstructorScopedDependency.class); + return new ConstructorScopedDependency[] { + first.first, first.second, second.first, other.dependency, direct + }; + }, + ImmutableMap.of()) + .call(); + + assertSame(firstRequest[0], firstRequest[1]); + assertSame(firstRequest[0], firstRequest[2]); + assertNotSame(firstRequest[0], firstRequest[3]); + assertNotSame(firstRequest[0], firstRequest[4]); + + ConstructorScopedDependency secondRequest = + ServletScopes.scopeRequest( + () -> injector.getInstance(RequestParameterConsumer.class).first, + ImmutableMap.of()) + .call(); + + assertNotSame(firstRequest[0], secondRequest); + assertEquals(4, nextInstanceId.get()); + } + + private static final class ConstructorScopedDependency { + final int instanceId; + + ConstructorScopedDependency(int instanceId) { + this.instanceId = instanceId; + } + } + + @SuppressWarnings("MisplacedScopeAnnotations") + private static final class RequestParameterConsumer { + final ConstructorScopedDependency first; + final ConstructorScopedDependency second; + + @Inject + RequestParameterConsumer( + @RequestScoped ConstructorScopedDependency first, + @RequestScoped ConstructorScopedDependency second) { + this.first = first; + this.second = second; + } + } + + @SuppressWarnings("MisplacedScopeAnnotations") + private static final class OtherRequestParameterConsumer { + final ConstructorScopedDependency dependency; + + @Inject + OtherRequestParameterConsumer(@RequestScoped ConstructorScopedDependency dependency) { + this.dependency = dependency; + } + } + @RequestScoped public static class SomeObject { private static final String INVALID = "invalid";