GuiceのRequestScopedでDBコネクションプールをハンドリング

2019年7月30日

Guiceの@RequestScopedは非常に便利な機能で、同じ一つのリクエスト内であれば、どこでインジェクトしても同じオブジェクトが返される。この機能を利用してDBのコネクションプールをハンドリングできないかと考えた。

※おそらくJava EEコンテナにはこの機能があるのだろうが、無視して自前で行うことにする。

つまり、RequestScopedのオブジェクト生成時にコネクションプールからコネクションを一つ取り出して確保する。リクエスト処理中は、どこでも同じオブジェクト、同じコネクションが使われるというものだ。

しかし、困った問題がある。リクエスト終了時にコネクションをプールに戻す方法が無い。RequestScopedのオブジェクトにリクエスト終了を知らせる方法が無いのだ。

最初のこころみ

最初はGuiceFilterにおいて直接的にRequestScopedのdisposed()メソッドか何かを呼ばせる方法を考えた。

※もちろん、SomeGuiceFilterにはInjectorが与えられているものとする。したがって、web.xmlの記述によってサーブレットコンテナが生成したものの場合、staticな領域を使ってInjectorを受け渡す必要がある。web.xmlとは別の方法として、GuiceServletContextListener#contextInitialized内で作成して、ServletContext#addFilterで登録する方法もある。

public class SomeGuiceFilter extends com.google.inject.servlet.GuiceFilter {
  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
          throws IOException, ServletException {
    try {
      super.doFilter(servletRequest, servletResponse, filterChain);
    } finally {
      // RequestScopedオブジェクトのdisposedを呼び出す。
    }
  }
}

しかし、これは不可能ということがわかった。なぜなら、「super.doFilter(servletRequest, servletResponse, filterChain);」呼び出しの中でないと、RequestScoped、SessionScopedは有効ではないからだ。この処理を置き換える方法が存在しないため断念した。

うまく行く方法

うまくいった方法は以下だ。概要としては、

  • RequestScopedを管理するSingletonを用意する
  • RequestScopedオブジェクト生成時にそれを登録する
  • GuiceFilterでのリクエスト処理終了時に削除通知する

生成登録と削除のキーとしては、HttpServletRequestを用いるのだが、単純に同じにはならないため新たなキーを作成し、HttpServletRequestの属性として格納するようにしている。

以下コードを掲載する。

@Singleton
public class RequestScoper {

  /** リクエストマップ */
  private Map<String, List<RequestLifed>>requestMap = new HashMap<>();

  /** シリアル番号ジェネレータ */
  private long serialNumber = 0;

  public RequestScoper() {
  }

  /** 
   * {@link SomeGuiceFilter}からリクエスト処理開始通知。
   * {@link HttpServletRequest}の属性としてシリアル番号を保存しておく。
   * @param req リクエスト
   */
  void enter(HttpServletRequest req) {
    String sn;
    synchronized (this) {
      sn = "" + serialNumber++;
    }
    req.setAttribute(RequestScoper.class.getName(), sn);
  }

  /**
   * {@link RequestLifed#disposed()}通知対象オブジェクトを登録する。
   * @param target 対象オブジェクト
   * @param req リクエスト
   */
  void register(RequestLifed target, HttpServletRequest req) {
    String sn = (String)req.getAttribute(RequestScoper.class.getName());
    List<RequestLifed>list;
    synchronized(this) {
      list = requestMap.get(sn);
      if (list == null)
        requestMap.put(sn, list = new ArrayList<>());      
    }    
    list.add(target);
  }

  /**
   * リクエスト処理終了通知。このリクエストについて登録されたオブジェクトすべてに
   * {@link RequestLifed#disposed()}を通知する。
   * @param req リクエスト
   */
  void exit(HttpServletRequest req) {
    String sn = (String)req.getAttribute(RequestScoper.class.getName());
    List<RequestLifed>list;
    synchronized(this) {
      list = requestMap.remove(sn);
    }
    if (list == null) return;
    list.stream().forEach(RequestLifed::disposed);
  }  
}
public abstract class RequestLifed {

  private RequestScoper scoper;
  private HttpServletRequest request;

  /** 
   * {@link RequestScoper}、{@link HttpServletRequest}がインジェクトされ、
   * {@link RequestScoper}に登録される */
  @Inject
  private void injectRequest(RequestScoper scoper, HttpServletRequest request) {    
    (this.scoper = scoper).register(this, request);
  }

  protected abstract void disposed();
}
public class SomeGuiceFilter extends com.google.inject.servlet.GuiceFilter {
  /* 
    GuiceServletContextListener#contextInitialized中でインジェクタによって
    このオブジェクトが生成された時にだけ@Injectが可能なことに注意。
   */
  @Inject private RequestScoper requestScoper;

  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
          throws IOException, ServletException {    
    HttpServletRequest request = (HttpServletRequest)servletRequest;
    requestScoper.enter(request);
    try {
      super.doFilter(servletRequest, servletResponse, filterChain);
    } finally {
      requestScoper.exit(request);
    }
  }
}

あとは、単純に以下のように@RequestScopedオブジェクトを作成する。

@RequestScoped
public class DbConnection extends RequestLifed {
  @Override
  protected void disposed() {
    ... リクエスト処理終了時
  }
}