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 extends Annotation> 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 extends T> getScopedFactory(
+ Dependency dependency,
+ Class extends Annotation> 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 extends T> 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 extends T> typedScopedFactory =
+ (InternalFactory extends T>) scopedFactory;
+ return typedScopedFactory;
+ }
+
SingleParameterInjector createParameterInjector(
final Dependency dependency, final Errors errors) throws ErrorsException {
BindingImpl extends T> 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 extends T> getFieldFactory(
+ Dependency dependency,
+ Annotation[] fieldAnnotations,
+ Map, Map, InternalFactory>>> scopedFactories,
+ Errors errors)
+ throws ErrorsException {
+ int numErrorsBefore = errors.size();
+ Class extends Annotation> 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 extends T> binding) {
+ this(dependency, binding.getInternalFactory());
+ }
+
+ SingleParameterInjector(
+ Dependency dependency, InternalFactory extends T> 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";