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-RSJPAは相性が良いと思っているので、JPAを使うつもりですが。
今回は試していませんが、Springもほぼおなじパターンで連携出来ると思います。SpringのRequestContextListenerあたりを使えばWebApplicationContextを取得できる筈なので、後はSeasar2と同じですね。