JAX-RS Jerseyをもうちょっと真面目に拡張してみる
前回(id:da-yoshi:20080429:1209403828)のJersey拡張はあまりにも強引過ぎてお試し以外には使えそうも無いので、もうちょっと真面目にJerseyのAPIを調べてみました。
JAX-RS仕様に基づくResourceクラスのインスタンス化を受け持つインターフェイスとして、Jerseyにはcom.sun.ws.rest.spi.resource.ResourceProviderが定義されています。何の指定もしなければ、このインターフェイスの実装であるPerRequestProviderが使用されます。
com.sun.ws.rest.spi.resource.ResourceFactoryアノテーションをResoureceクラスに定義すれば、独自のResourceProviderを定義できます。でもいちいちクラス毎に設定するのは面倒なので、デフォルトの設定をJerseyのサーブレットのinitパラメータで指定出来ます。まずはこの設定を利用して、独自のResourceProviderを作成してみます。今回はSeasar2によってResourceクラスを作成することにします。ただし、Seasar2のEntityManagerのDIは色々設定が面倒なので、今回は使いません。代わりにJerseyのDI定義にEntityManagerの設定を追加します。
package mapsample.rest.resource; import com.sun.ws.rest.api.container.ContainerException; import com.sun.ws.rest.api.core.HttpContext; import com.sun.ws.rest.api.model.AbstractResource; import com.sun.ws.rest.api.model.AbstractResourceConstructor; import com.sun.ws.rest.impl.model.parameter.ParameterExtractor; import com.sun.ws.rest.impl.model.parameter.ParameterExtractorFactory; import com.sun.ws.rest.spi.resource.ResourceProvider; import com.sun.ws.rest.spi.service.ComponentProvider; import com.sun.ws.rest.spi.service.ComponentProvider.Scope; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import org.seasar.framework.container.factory.SingletonS2ContainerFactory; public final class S2ResourceProvider implements ResourceProvider { private Class<?> c; private Constructor<?> constructor; private ParameterExtractor[] extractors; public void init(ComponentProvider provider, AbstractResource abstractResource) { (省略) } public Object getInstance(ComponentProvider provider, HttpContext context) { try { if (constructor == null) { Object instance = SingletonS2ContainerFactory.getContainer().getComponent(c); provider.inject(instance); return instance; // return provider.getInstance(Scope.ApplicationDefined, c); } else { (省略) } } catch (InstantiationException ex) { throw new ContainerException("Unable to create resource", ex); } catch (IllegalAccessException ex) { throw new ContainerException("Unable to create resource", ex); } catch (InvocationTargetException ex) { (省略) } } }
本当はJerseyのPerRequestProviderを継承したかったのですが、これもまたfinalクラスなのでやむなく別クラスで定義しました(Jerseyってやけにfinalクラス使ってるけど、そんなに拡張してほしくないのだろうか?)。コンストラクタに引数を取らない場合、Seasar2でResourceインスタンスを生成し、ComponentProviderでEntityManager等をインジェクションして返します。その他はPerRequestProviderと同じ動作です。JAX-RSに関する様々なインスタンスのインジェクションはJerseyにお任せするという方針です。
作成したResourceProvider実装をweb.xmlに登録
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <listener> <listener-class>org.seasar.framework.container.servlet.S2ContainerListener</listener-class> </listener> <servlet> <servlet-name>Jersey Web Application</servlet-name> <servlet-class>mapsample.rest.container.servlet.ExtendServletAdaptor</servlet-class> <init-param> <param-name>com.sun.ws.rest.config.property.DefaultResourceProviderClass</param-name> <param-value>mapsample.rest.resource.S2ResourceProvider</param-value> </init-param> <load-on-startup>2</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Jersey Web Application</servlet-name> <url-pattern>/resources/*</url-pattern> </servlet-mapping> <resource-ref> <res-ref-name>jdbc/mapsample</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref> <persistence-context-ref> <persistence-context-ref-name> persistence/mapsample </persistence-context-ref-name> <persistence-unit-name>mapsample</persistence-unit-name> </persistence-context-ref> </web-app>
S2の初期化は今回Listenerでやってみました。
次はS2の設定・・・なんですけど、正直S2.4のCreator設定はよくわからないので、今回はServiceCreatorの設定をそのまま使うことにします。なので、Resourceクラスの名前をSeasar2の命名定義に合うように修正します(なんか本末転倒な気もしますが)
package mapsample.service; import java.util.ArrayList; import java.util.List; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.ProduceMime; import javax.ws.rs.WebApplicationException; import mapsample.entity.Map_contents; import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; @Path("/mapContents") public class MapContentsService { public MapContentsService() { System.out.println("new MapContentsResource()"); } @PersistenceContext(unitName = "mapsample") private EntityManager em; @SuppressWarnings("unchecked") @GET @ProduceMime("application/json") public JSONArray getList() throws JSONException { List<Map_contents> list = em.createNamedQuery("Map_contents.getMapContents") .getResultList(); List<JSONObject> jsons = new ArrayList<JSONObject>(); for (Map_contents mc : list) { jsons.add(mc.getJson()); } JSONArray array = new JSONArray(jsons); return array; } @GET @Path("{id}") @ProduceMime("application/json") public JSONObject find(@PathParam("id") String id) throws JSONException { Map_contents mc = em.find(Map_contents.class, Integer.valueOf(id)); if (mc == null) { throw new WebApplicationException(404); } return mc.getJson(); } }
このクラスが自動登録されるようにSeasar2の設定を行います。
s2container.dicon
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" "http://www.seasar.org/dtd/components24.dtd"> <components> <include condition="#ENV == 'ut'" path="warmdeploy.dicon"/> <include condition="#ENV == 'ct'" path="hotdeploy.dicon"/> <include condition="#ENV != 'ut' and #ENV != 'ct'" path="cooldeploy.dicon"/> <component class="org.seasar.framework.container.factory.SimplePathResolver"> <initMethod name="addRealPath"> <arg>"jta.dicon"</arg> <arg>"jta-sun9.dicon"</arg> </initMethod> </component> </components>
jta-sun9.diconはS2.4.25のjarに入っている内容では動かないので、Subversionのトランクから最新のファイルを取得しておきます。
app.dicon
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" "http://www.seasar.org/dtd/components24.dtd"> <components> <include path="convention.dicon"/> <include path="aop.dicon"/> <include path="j2ee.dicon"/> </components>
convention.dicon
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" "http://www.seasar.org/dtd/components24.dtd"> <components> <component class="org.seasar.framework.convention.impl.NamingConventionImpl"> <initMethod name="addRootPackageName"> <arg>"mapsample"</arg> </initMethod> </component> <component class="org.seasar.framework.convention.impl.PersistenceConventionImpl"/> </components>
creator.dicon
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" "http://www.seasar.org/dtd/components24.dtd"> <components> <include path="default-customizer.dicon"/> <component name="serviceCustomizer" class="org.seasar.framework.container.customizer.CustomizerChain"> <initMethod name="addCustomizer"> <arg>traceCustomizer</arg> </initMethod> <initMethod name="addCustomizer"> <arg>requiredTxCustomizer</arg> </initMethod> </component> </components>
jdbc.dicon
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE components PUBLIC "-//SEASAR2.1//DTD S2Container//EN" "http://www.seasar.org/dtd/components21.dtd"> <components namespace="jdbc"> <include path="jta.dicon"/> <include path="jdbc-extension.dicon"/> <component class="org.seasar.extension.jdbc.impl.BasicResultSetFactory"/> <component class="org.seasar.extension.jdbc.impl.ConfigurableStatementFactory"> <arg> <component class="org.seasar.extension.jdbc.impl.BasicStatementFactory"/> </arg> <property name="fetchSize">100</property> <!-- <property name="maxRows">100</property> --> </component> <!-- from JNDI --> <component name="dataSource" class="javax.sql.DataSource"> @org.seasar.extension.j2ee.JndiResourceLocator@lookup("java:comp/env/jdbc/mapsample") </component> </components>
jdbc.diconは本来要らない筈なんですけど、トランザクション定義をしているj2ee.diconが要求しているのでやむなく定義します。コピペでj2ee.diconを修正してもいいんでしょうけど、S2のjarに入ってるdiconを上書きすると思わぬところでエラーになることが多々あるので・・・
また、s2-tigerを入れてしまうとPersistenceContextを自動設定しようとしてエラーになるので、s2-tigerは外しておきます。ここら辺はSpringを経験した後だとかなりややこしく感じてしまいます。SpringもSeasar2を経験した後だとかなりストレスを感じる挙動をしたりするので、どっちもどっちなんですけど・・・
残りのファイルはS2のtest/resourcesあたりにあるファイルを適当にコピペして済ませます。これで動かしてみた・・・のですが、JerseyのDefaultResourceProviderパラメータにはClassオブジェクトを渡せというエラーが出ました。Servletのinitパラメータから取得してるのにどうやってClassオブジェクトを?・・・まぁ仕方ないのでServletContainerの継承クラスで変換させることにします。
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.ThreadLocalNamedInvoker; import com.sun.ws.rest.spi.container.WebApplication; import com.sun.ws.rest.spi.container.servlet.ServletContainer; import com.sun.ws.rest.spi.resource.Injectable; public class ExtendServletAdaptor extends ServletContainer { /** * */ private static final long serialVersionUID = 1L; 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) { StringBuilder jndiName = new StringBuilder("java:comp/env/"); if (!"".equals(pc.name())) { jndiName.append(pc.name()); } else if (!"".equals(pc.unitName())) { jndiName.append("persistence/" + pc.unitName()); } ThreadLocalNamedInvoker<EntityManager> emHandler = new ThreadLocalNamedInvoker<EntityManager>(jndiName.toString()); EntityManager em = (EntityManager) Proxy.newProxyInstance( EntityManager.class.getClassLoader(), new Class[] {EntityManager.class }, emHandler); return em; } } ); } @Override protected void initiate(ResourceConfig rc, WebApplication wa) { if (rc != null) { String rpName = (String) rc.getProperties().get(ResourceConfig.PROPERTY_DEFAULT_RESOURCE_PROVIDER_CLASS); if (rpName != null) { Class<?> clazz; try { clazz = Class.forName(rpName); rc.getProperties().put(ResourceConfig.PROPERTY_DEFAULT_RESOURCE_PROVIDER_CLASS, clazz); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } super.initiate(rc, wa); } }
EntityManagerのDI定義もこのクラスでやってます。JNDI名が「java:comp/env/persistence/${unitName}」となる前提です。前提が適用出来ない場合はPersistenceContextのname属性にJNDI名を記述します。
これで前回同様アプリを動かすことが出来ました。今回はGlassFishV3で動かしてみたのですが、特に問題なく動きました。Seasar2と連携出来るということは、別にJPAを使わなくともS2JDBCなりS2Daoなり自由に使ってJAX-RSを構築出来るということですね。個人的にはJAX-RSとJPAは相性が良いと思っているので、JPAを使うつもりですが。
今回は試していませんが、Springもほぼおなじパターンで連携出来ると思います。SpringのRequestContextListenerあたりを使えばWebApplicationContextを取得できる筈なので、後はSeasar2と同じですね。