Locked History Actions

guice/Manual/Extensions/CustomScopes

カスタムスコープ

ユーザが自身のカスタムスコープは通常推奨されない。 多くのアプリケーションではビルトインスコープで十分である。 もしウェブアプリケーションを記述しているのであれば、ServletModuleがシンプルでよくテストされたHTTPリクエスト及びHTTPセッション向けのスコープ実装を提供してくれる。

カスタムスコープの作成は複数のステップからなる。

  1. スコーピングアノテーションを定義する
  2. スコープインターフェースを実装する
  3. スコープアノテーションを実装にアタッチする
  4. スコープ入り口出口をトリガリングする

スコーピングアノテーションを定義する

スコーピングアノテーションで目的のスコープを識別する。 これを使って、Guiceの生成するタイプ、@Providesメソッド、そしてbindステートメントのin()句をアノテートする。 以下のコードをコピーして変更し、スコーピングアノテーションを定義すること。

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: 目的のスコープがリクエストやセッションを表すならば(SOAPリクエストのような), Guiceのサーブレット拡張に存在するRequestScopedSessionScopedを考慮すべきである。 そうしないと、誤ったアノテーションをimportしてしまう可能性がある。 そのような問題をデバッグするのはフラストレーションになるだろう。

スコープの実装

The scope interface ensures there's at most one type instance for each scope instance. SimpleScopeがスレッド毎実装として適当なスタートだろう。 このクラスをコピーして、要望に応じて変更すること。

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;

/**
 * 一つのコードブロックの実行をスコープする
 * このスコープをtry/finallyブロックに適用すること: <pre>   {@code
 *
 *   scope.enter();
 *   try {
 *     // 明示的に「種」オブジェクトを撒く
 *     scope.seed(Key.get(SomeObject.class), someObject);
 *     // create and access scoped objects
 *   } finally {
 *     scope.exit();
 *   }
 * }</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(), it's good practice
 * (since you have to provide <i>some</i> binding anyhow) to include a
 * corresponding binding that will throw an exception if Guice is asked to
 * provide for that key if it was not yet seeded: <pre>   {@code
 *
 *   bind(key)
 *       .toProvider(SimpleScope.<KeyClass>seededKeyProvider())
 *       .in(ScopeAnnotation.class);
 * }</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();
          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;
  }
}

アノテーションを実装にバインドする

スコーピングアノテーションは関係するスコープ実装にアタッチされなければならない。 ちょうどバインディングのように、これをモジュールのconfigure()メソッドの中で行うことができる。 通常は、スコープそのものもバインドすることができるので、インターセプタやフィルタもそれを使うことができる。

public class BatchScopeModule {
  public void configure() {
    SimpleScope batchScope = new SimpleScope();

    // Guiceにスコープのことを伝える。
    bindScope(BatchScoped.class, batchScope);

    // スコープを注入可能にする
    bind(SimpleScope.class)
        .annotatedWith(Names.named("batchScope"))
        .toInstance(batchScope);
  }
}

スコープをトリガリングする

SimpleScopeは手作業で入りと出を制御する必要がある。 通常これは、フィルタやインターセプタと言った低レベルなインフラストラクチャコードの中で行われる。 finallyでexit()を呼び出すことを忘れないこと。さもないと、例外発生時にまでスコープは開いたままになってしまう。

  @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();
    }
  }