Locked History Actions

guice/Wicket/GuiceFilter

GuiceFilterの利用

サーブレットフィルタとしてGuiceFilterを用いると、@SessionScopedや@RequestScopedのアノテーションが有効になる。 それらのスコープの制御は、Guiceがサーブレットからのリクエストをフックして行うからである。

Wicketでこの機能を利用するには、基本的には「サーブレット」に記述した通りのことを行えばよい。 ただし、これをやっても「インジェクションの方法」で取り上げた問題が解決するわけではない。 相変わらず以下の問題があるのでまとめておく。

  • コンストラクタ注入されるオブジェクト以外はコンストラクタ中で使用することはできないが、一般にWicketのページクラスのコンストラクタにはそのような特別なパラメータを指定できる余地がない。
  • フィールド注入したとしても、Wicketのページは直列化されるため、それらのオブジェクトはSerizlizableでなければならず、大きいと直列化領域を圧迫する。なおかつ、直列化復帰後に(DB接続等の)機能を復旧できなければならない。

したがって、少なくともWicketのページ及びその中のコンポーネント等では、DIではなくサービスロケータを使用するのがベストであると結論づけた(「Injectorをサービスロケータとして使う」も参照のこと)。

その上で、@SessionScoped/@RequestScopedの恩恵にあずかることはできる。 サービスロケータからこれらのオブジェクトを取得すると、それぞれセッションで一意なオブジェクト、リクエストで一意なオブジェクトを取得することができるのである。

そしてむしろ、これらのスコープされたオブジェクトを使う場合には、オブジェクトそのものの注入は避けるべきである。 なぜなら、注入先のオブジェクトがリクエストやセッションを越えて生き残る場合には、誤ってスコープ外でスコープ付オブジェクトを使ってしまうというバグになりかねないからである。

したがって、これらのスコープされたオブジェクト使う場合は、その都度サービスロケータから取得するのがベストと思われる。

※プロバイダも避けた方が無難。プロバイダはSerializableではない。

Guice, Wicket, Jettyを使う例

以下では、Guice 2.0, Wicket 1.4.3, Jetty 7.0.1を利用している。 特にJettyはバージョンによって仕様がよく変更されているので注意・

Guiceは極力XMLを排除するように設計されている。ウェブとサーブレットでのGuiceFilterの利用例でもこれが貫かれている。 それでも他のサーブレットコンテナでは最低限のweb.xml記述が必要と思われるが、 Jettyでは完全にweb.xmlを排除することができる。

ほとんどのセットアップはInjectorの作成時に行えばよい。 仮にInjectorが得られたものとして、以下のコードでJettyを起動する。 以下のコードは、ほぼどんな場合でも同じになると思われる。

import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.bio.*;
import org.eclipse.jetty.servlet.*;

import com.google.inject.*;
import com.google.inject.servlet.*;

.................................

    Server server = new Server();
    Connector connector = new SocketConnector();
    connector.setPort(8080);
    server.addConnector(connector);
    ServletContextHandler context = 
      new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS);
    context.addFilter(GuiceFilter.class, "/*", 0);
    context.addEventListener(new GuiceServletContextListener() {
      @Override protected Injector getInjector() { return injector; }
    });
    context.addServlet(DefaultServlet.class, "/");
    server.start();
    server.join(); 

次に、Injectorの生成について見てみるが、その前にWicketFilterのサブクラスを作成し、 @Singletonを指定しておく。

import org.apache.wicket.protocol.http.*;

import com.google.inject.*;

@Singleton
public class MyWicketFilter extends WicketFilter {
}

Injectorに渡すモジュールとして、特別なServletModuleを使う。ここでは、拡張しやすいようにサブクラス化しておく。 他のフィルターがもしあればそれを先に指定できるように、configureFilters()を下位クラスでオーバライドできるようにする。

import java.util.*;

import com.cm55.clman.web.*;
import com.cm55.clman.web.lic.*;
import com.cm55.third.wicket.*;
import com.google.inject.servlet.*;

public class MyServletModule extends ServletModule {
  protected final void configureServlets() {    
    // 他のフィルタ
    configureFilters();

    // WicketFilterの設定
    Map<String, String> params = new HashMap<String, String>();
    params.put("applicationClassName", MyApplication.class.getCanonicalName());
    filter("/*").through(MyWicketFilter.class, params);
  }
  protected void configureFilters() {
  }
}

そしてインジェクタを作成するのだが、通常のモジュールと共にMyServletModuleを指定する。

    Injector injector = Guice.createInjector(
        new AbstractModule() {
          @Override protected void configure() {
            // 通常のバインドがあればここで指定
          }
        },
        new MyServletModule() {
          protected void configureFilters() {
            // 他のフィルタが必要であればここで指定
          }
        }
    );    

ServletModuleは、通常のモジュールと同様にいくつ指定しても構わないようである。 しかし、フィルタの順序は重要なので、できれば一つのServletModuleの中に適切な順番でフィルタを並べた方がよいだろう。

@SessionScoped, @RequestScopedの利用

これらのスコープアノテーションをWikcetの中でどのように便利に使えるのだろうか? 一つ思いつくことは、例えばWicketとは無関係なレイヤーで、データベースにリクエスト側のIPアドレスを記録するといったことが考えられる。 通常なら、Wicketを介して取得したHttpServletRequestからIPアドレスを抽出して、それを下のレイヤーに渡さなければならないが、Guiceを介することによって、これが必要なくなる。

public interface HttpRequestInfo {
 public String getRemoteAddr();
}

@SessionScoped
public class HttpRequestInfoImpl implements HttpRequestInfo {
  @Inject HttpServletRequest request;
  @Inject HttpServletResponse response;  
  public String getRemoteAddr() {
    return request.getRemoteAddr();
  }
}

としておき、利用側では、

  String ipAddress = ServiceLocator.getInstance(HttpRequestInfo.class).getRemoteAddr();

などとすればよい(サービスロケータを使う場合)。

ただし、テストのことを十分考えてから使うことが必要である。 通常、テストではサーブレットコンテナも動作させないし、HttpServletRequest,HttpServletResponse, HttpSessionが存在せず、なおかつSessionScopedの実装(スコープ自体の実装)も存在しないのである。 したがって、これらがテストに関わってくるとテストが失敗してしまう。

これらについてもテスト用モックを用意することもできるが、いかにも面倒であろう。 テストでは、上のようにHttpRequestInfoなどというインターフェースのみを扱うようにした方がよいと思われる。