Skip to content

CustomScopes

Googler edited this page Aug 2, 2023 · 12 revisions

Custom Scopes

It is generally recommended that users do not write their own custom scopes — the built-in scopes should be sufficient for most applications. If you're writing a web application, the ServletModule provides simple, well tested scope implementations for HTTP requests and HTTP sessions.

Creating custom scopes is a multistep process:

  1. Define a scoping annotation
  2. Implementing the Scope interface
  3. Attaching the scope annotation to the implementation
  4. Triggering scope entry and exit

Defining a scoping annotation

The scoping annotation identifies your scope. You'll use it to annotate Guice-constructed types, @Provides methods, and in the in() clause of a bind statement. Copy-and-customize this code to define your scoping annotation:

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;

@Target({ TYPE, METHOD })
@Retention(RUNTIME)
@ScopeAnnotation
public @interface BatchScoped {}

Tip: If your scope represents a request or session (such as for SOAP requests), consider using the RequestScoped and SessionScoped annotations from Guice's servlet extension.

When using a custom scope, make sure you have imported the correct scope annotation. Otherwise you may get a SCOPE_NOT_FOUND error.

Implementing Scope

The scope interface ensures there's at most one type instance for each scope instance. SimpleScope is a decent starting point for a per-thread implementation. Copy this class into your project and tweak it to suit your needs.

import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.Maps;
import com.google.inject.Key;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.Scope;
import java.util.Map;

/**
 * Scopes a single execution of a block of code. Apply this scope with a
 * try/finally block: <pre><code>
 *
 *   scope.enter();
 *   try {
 *     // explicitly seed some seed objects...
 *     scope.seed(Key.get(SomeObject.class), someObject);
 *     // create and access scoped objects
 *   } finally {
 *     scope.exit();
 *   }
 * </code></pre>
 *
 * The scope can be initialized with one or more seed values by calling
 * <code>seed(key, value)</code> before the injector will be called upon to
 * provide for this key. A typical use is for a servlet filter to enter/exit the
 * scope, representing a Request Scope, and seed HttpServletRequest and
 * HttpServletResponse.  For each key inserted with seed(), you must include a
 * corresponding binding:

 *  <pre><code>
 *   bind(key)
 *       .toProvider(SimpleScope.&lt;KeyClass&gt;seededKeyProvider())
 *       .in(ScopeAnnotation.class);
 * </code></pre>
 *
 * @author Jesse Wilson
 * @author Fedor Karpelevitch
 */
public class SimpleScope implements Scope {

  private static final Provider<Object> SEEDED_KEY_PROVIDER =
      new Provider<Object>() {
        public Object get() {
          throw new IllegalStateException("If you got here then it means that" +
              " your code asked for scoped object which should have been" +
              " explicitly seeded in this scope by calling" +
              " SimpleScope.seed(), but was not.");
        }
      };
  private final ThreadLocal<Map<Key<?>, Object>> values
      = new ThreadLocal<Map<Key<?>, Object>>();

  public void enter() {
    checkState(values.get() == null, "A scoping block is already in progress");
    values.set(Maps.<Key<?>, Object>newHashMap());
  }

  public void exit() {
    checkState(values.get() != null, "No scoping block in progress");
    values.remove();
  }

  public <T> void seed(Key<T> key, T value) {
    Map<Key<?>, Object> scopedObjects = getScopedObjectMap(key);
    checkState(!scopedObjects.containsKey(key), "A value for the key %s was " +
        "already seeded in this scope. Old value: %s New value: %s", key,
        scopedObjects.get(key), value);
    scopedObjects.put(key, value);
  }

  public <T> void seed(Class<T> clazz, T value) {
    seed(Key.get(clazz), value);
  }

  public <T> Provider<T> scope(final Key<T> key, final Provider<T> unscoped) {
    return new Provider<T>() {
      public T get() {
        Map<Key<?>, Object> scopedObjects = getScopedObjectMap(key);

        @SuppressWarnings("unchecked")
        T current = (T) scopedObjects.get(key);
        if (current == null && !scopedObjects.containsKey(key)) {
          current = unscoped.get();

          // don't remember proxies; these exist only to serve circular dependencies
          if (Scopes.isCircularProxy(current)) {
            return current;
          }

          scopedObjects.put(key, current);
        }
        return current;
      }
    };
  }

  private <T> Map<Key<?>, Object> getScopedObjectMap(Key<T> key) {
    Map<Key<?>, Object> scopedObjects = values.get();
    if (scopedObjects == null) {
      throw new OutOfScopeException("Cannot access " + key
          + " outside of a scoping block");
    }
    return scopedObjects;
  }

  /**
   * Returns a provider that always throws exception complaining that the object
   * in question must be seeded before it can be injected.
   *
   * @return typed provider
   */
  @SuppressWarnings({"unchecked"})
  public static <T> Provider<T> seededKeyProvider() {
    return (Provider<T>) SEEDED_KEY_PROVIDER;
  }
}

Registering the Scope

You must attach your scoping annotation to the corresponding scope implementation. Just like bindings, you can configure this in your module's configure() method.

public final class BatchScopeModule extends AbstractModule {
  private final SimpleScope batchScope = new SimpleScope();

  @Override
  protected void configure() {
    // tell Guice about the scope
    bindScope(BatchScoped.class, batchScope);
  }

  @Provides
  @Named("batchScope")
  SimpleScope provideBatchScope() {
    return batchScope;
  }
}

public final class ApplicationModule extends AbstractModule {
  @Override
  protected void configure() {
    install(new BatchScopeModule());
  }

  // Foo is bound in BatchScoped
  @Provides
  @BatchScoped
  Foo provideFoo() {
    return FooFactory.createFoo();
  }
}

We also bind the scope implementation as @Named("batchScope") SimpleScope. This is necessary since we will need to use batchScope to trigger the scope in the next step.

Triggering the Scope

Custom Scope implementation requires that you manually enter and exit the scope. Usually this lives in some low-level infrastructure code, such as the entry point of a web server. For example, to run a piece of code with SimpleScope enabled:

  @Inject @Named("batchScope") SimpleScope scope;

  /**
   * Runs {@code runnable} in batch scope.
   */
  public void scopeRunnable(Runnable runnable) {
    scope.enter();
    try {
      // explicitly seed some seed objects...
      scope.seed(Key.get(SomeObject.class), someObject);

      // create and access scoped objects
      runnable.run();

    } finally {
      scope.exit();
    }
  }

Be sure to call exit() in a finally clause, otherwise the scope will be left open when an exception is thrown.

Clone this wiki locally