GuiceのRequestScopedでDBコネクションプールをハンドリング
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() {
... リクエスト処理終了時
}
}
ディスカッション
コメント一覧
まだ、コメントがありません