JAX-RS Jerseyを超強引に拡張してみる
SAStrutsがActionにトランザクションをかけていたことからヒントを得て、JAX-RS実装のJerseyを拡張して、Resourceクラスの実行時に直接トランザクションをかけるようにしてみたら面白いんじゃないかと思いました。そうすれば、コンテナ管理のトランザクションスコープEntityManagerが簡単に使えるようになるので、JPAのEntityを簡単にURLに紐づけることができます。
JerseyのServletを拡張してEntityManagerをDIできるようにし、Resourceが呼ばれる単位でトランザクションが自動でかかるように拡張してみました。やりかたが超強引なので、参考にもならないレベルですが・・・
まずはServletクラスの拡張から
package mapsample.rest.container.servlet; import java.lang.reflect.Proxy; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.servlet.ServletConfig; import com.sun.ws.rest.api.core.ResourceConfig; import com.sun.ws.rest.impl.container.servlet.ServletAdaptor; import com.sun.ws.rest.impl.container.servlet.ThreadLocalNamedInvoker; import com.sun.ws.rest.spi.container.WebApplication; import com.sun.ws.rest.spi.resource.Injectable; public class TransactionalServletAdaptor extends ServletAdaptor { /** * */ private static final long serialVersionUID = 1L; @Override protected void configure(ServletConfig servletConfig, ResourceConfig rc, WebApplication wa) { super.configure(servletConfig, rc, wa); wa.addInjectable(EntityManager.class, new Injectable<PersistenceContext, EntityManager>() { public Class<PersistenceContext> getAnnotationClass() { return PersistenceContext.class; } public EntityManager getInjectableValue(PersistenceContext pc) { String jndiName = pc.name(); if ("".equals(jndiName)) { jndiName = "persistence/" + pc.unitName(); } ThreadLocalNamedInvoker<EntityManager> emHandler = new ThreadLocalNamedInvoker<EntityManager>("java:comp/env/" + jndiName); EntityManager em = (EntityManager) Proxy.newProxyInstance( EntityManager.class.getClassLoader(), new Class[] {EntityManager.class }, emHandler); return em; } } ); } }
JSF実装のDI機能提供クラスあたりと互換性を取ってくれたら、もっと使いやすくなりそうな気がするのですが・・・
次はトランザクション・・・といきたかったのですが、JerseyのServletのserviceメソッドにはfinal宣言がされてあって拡張できません。また、処理実行を受け持つWebApplicationインターフェイスのJersey実装もまたfinalクラスなので、これもまた拡張できません・・・
仕方ないので、独自のWebApplication実装クラスから処理を委譲させることにしました。
(2008/04/29追記:またまた例外処理がダメダメだったので修正)
package mapsample.rest.application; import java.lang.reflect.Type; import javax.naming.Context; import javax.naming.InitialContext; import javax.transaction.UserTransaction; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.sun.ws.rest.api.container.ContainerException; import com.sun.ws.rest.api.core.HttpContext; import com.sun.ws.rest.api.core.ResourceConfig; import com.sun.ws.rest.impl.application.WebApplicationImpl; import com.sun.ws.rest.spi.container.ContainerRequest; import com.sun.ws.rest.spi.container.ContainerResponse; import com.sun.ws.rest.spi.container.MessageBodyContext; import com.sun.ws.rest.spi.container.WebApplication; import com.sun.ws.rest.spi.resource.Injectable; import com.sun.ws.rest.spi.service.ComponentProvider; public class TransactionalWebApplicationImpl implements WebApplication { private static Log log = LogFactory.getLog(TransactionalWebApplicationImpl.class); private WebApplication delegate = new WebApplicationImpl(); public void addInjectable(Type fieldType, Injectable injectable) { delegate.addInjectable(fieldType, injectable); } public ComponentProvider getComponentProvider() { return delegate.getComponentProvider(); } public MessageBodyContext getMessageBodyContext() { return delegate.getMessageBodyContext(); } public HttpContext getThreadLocalHttpContext() { return delegate.getThreadLocalHttpContext(); } public void handleRequest(ContainerRequest request, ContainerResponse response) throws ContainerException { try { Context ctx = new InitialContext(); try { UserTransaction ut = UserTransaction.class.cast(ctx.lookup("java:comp/UserTransaction")); ut.begin(); log.debug("トランザクションを開始しました"); try { delegate.handleRequest(request, response); if (response.getStatus() >= 400) { ut.rollback(); log.debug("トランザクションをロールバックしました"); } else { ut.commit(); log.debug("トランザクションをコミットしました"); } } catch (Exception e) { ut.rollback(); log.debug("トランザクションをロールバックしました"); throw e; } } finally { ctx.close(); } } catch (Exception e) { if (e instanceof RuntimeException) { throw RuntimeException.class.cast(e); } else if (e instanceof ContainerException) { throw ContainerException.class.cast(e); } throw new ContainerException(e); } } public void initiate(ResourceConfig resourceConfig) throws IllegalArgumentException, ContainerException { delegate.initiate(resourceConfig); } public void initiate(ResourceConfig resourceConfig, ComponentProvider provider) throws IllegalArgumentException, ContainerException { delegate.initiate(resourceConfig, provider); } public WebApplication clone() { TransactionalWebApplicationImpl clone = new TransactionalWebApplicationImpl(); clone.delegate = this.delegate.clone(); return clone; } }
かなり強引すぎるトランザクション定義ですが、それは置いといて・・・後は、このクラスインスタンスを作成するProvider実装を作成して、META-INF/servicesで登録。これでとりあえず、JAX-RSのクラスにPersistnceContextがDIされるようになり、実行前後にトランザクションがかかるようになりました。
とりあえず目的は達成したので、次はAjaxクライアントとこの拡張Jerseyで何かやり取りをさせてみたいと思います。