JerseyとSeasar2を連携させる

前のエントリの続きです。
私はS2JDBCが使いやすいのでDIコンテナとしてSeasar2を使うことが多いです。Seasa2でRESTfulなサービスを提供する場合には、S2Axis2というプロダクトがあるようですが、将来性を考えJAX-RSを使ってみたいので、JerseyとSeasar2の連携を試してみます。
Jerseyには、もともとDIコンテナとの連携用にIoCComponentProviderFactoryというインタフェースが存在し、SpringとGuiceについてはjersey-springとjersey-guiceというライブラリがあります。このコードを参考にSeasarと連携させています。簡易的にですが。今回のソースはこちらにあります。

前提としてDolteng(Seasar2用のEclipseプラグイン)を使用してS2JDBCを使用するWebプロジェクトを使用します。DBは、H2データベースのスタンドアローンモードを使用します。

前回作成したContactクラスにS2JDBC用にJPAアノテーションを追加します。

package com.azuki3.sample.jaxrs.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.xml.bind.annotation.XmlRootElement;

@Entity
@XmlRootElement
public class Contact {

	@Id
	@GeneratedValue(strategy=GenerationType.IDENTITY)
	private Integer id;

	private String name;

	private String address;

	// 以下省略...
}

S2JDBC-Genでテーブルを生成しテストデータを追加しておきます。

次にリソースクラスを作成します。

package com.azuki3.sample.jaxrs.resource;

import java.util.List;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.seasar.extension.jdbc.service.S2AbstractService;

import com.azuki3.sample.jaxrs.entity.Contact;

@Path("/contacts2")
public class S2ContactResource extends S2AbstractService<Contact>{

	@GET
	@Produces({MediaType.APPLICATION_JSON})
	public List<Contact> getContacts() {
		List<Contact> list = select().getResultList();
		return list;
	}

	@Path("{id}")
	@GET
	@Produces({MediaType.APPLICATION_JSON})
	public Contact getContact(@PathParam("id")Integer id) {
		Contact contact = select().id(id).getSingleResult();
		if(contact == null){
			throw new NotFoundException("No such Contact.");
		}
		return contact;
	}
}

これは、S2JDBC-Genで生成されるサービスにJAX-RSアノテーションを付与する方が簡単かもしれません。
このS2ContactResourceクラスをSeasar2のコンテナで管理して(diconファイルに記述/自動登録/Creatorで)、Jerseyから取得します。

Jerseyではシングルトンとリクエスト(PerRequest)の二つのスコープがあり、DIコンテナインスタンスのライフサイクルまで管理する場合には、IoCManagedComponentProviderを継承したProviderを返すようです(他にもIoCInstanciatedComponentProviderなどインスタンスの管理方法に応じて何種類かあります)。Seasar2でプロトタイプやリクエストスコープで管理される場合には、Jerseyではリクエスト毎にインスタンスを取得しようとし、シングルトンの場合は一度取得したインスタンスを使い回すようです。

まず、IoCComponentProviderFactoryを実装したS2ComponentProviderFactoryというクラスを作成しました。

package com.azuki3.sample.jaxrs.container;

import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import org.seasar.framework.container.ComponentDef;
import org.seasar.framework.container.ComponentNotFoundRuntimeException;
import org.seasar.framework.container.InstanceDef;
import org.seasar.framework.container.SingletonS2Container;
import org.seasar.framework.container.factory.SingletonS2ContainerFactory;

import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.core.spi.component.ComponentScope;
import com.sun.jersey.core.spi.component.ioc.IoCComponentProvider;
import com.sun.jersey.core.spi.component.ioc.IoCComponentProviderFactory;
import com.sun.jersey.core.spi.component.ioc.IoCManagedComponentProvider;

/**
 * Seasar2ベースの{@link IoCComponentProviderFactory}
 *
 * <p>
 * Seasar2のコンテナに登録されたリソースとProviderクラスを取得するためのクラス。
 *
 * @author ktana
 *
 */
public class S2ComponentProviderFactory implements IoCComponentProviderFactory {
	private static final Logger LOGGER = Logger.getLogger(S2ComponentProviderFactory.class.getName());

	@Override
	public IoCComponentProvider getComponentProvider(Class<?> c) {
		return getComponentProvider(null, c);
	}

	@Override
	public IoCComponentProvider getComponentProvider(ComponentContext cc,
			Class<?> c) {
		ComponentDef def = null;
		try {
			def = SingletonS2ContainerFactory.getContainer().getComponentDef(c);
		} catch (ComponentNotFoundRuntimeException e) {
			return null;
		}
		return new S2ManagedComponentProvider(getComponentScope(def.getInstanceDef().getName()), c);
	}

    private ComponentScope getComponentScope(String scope) {
        ComponentScope cs = scopeMap.get(scope);
        return (cs != null) ? cs : ComponentScope.Undefined;
    }

    private final Map<String, ComponentScope> scopeMap = createScopeMap();

    private Map<String, ComponentScope> createScopeMap() {
        Map<String, ComponentScope> m = new HashMap<String, ComponentScope>();
        m.put(InstanceDef.SINGLETON_NAME, ComponentScope.Singleton);
        m.put(InstanceDef.PROTOTYPE_NAME, ComponentScope.PerRequest);
        m.put(InstanceDef.REQUEST_NAME, ComponentScope.PerRequest);
        return m;
    }

    private class S2ManagedComponentProvider implements IoCManagedComponentProvider{
        private final ComponentScope scope;

        @SuppressWarnings("rawtypes")
	private final Class c;

    	@SuppressWarnings("rawtypes")
	S2ManagedComponentProvider(ComponentScope scope,  Class c){
    		this.scope = scope;
    		this.c = c;
    	}

    	@Override
	public Object getInjectableInstance(Object obj) {
    		// TODO:
		return obj;
	}

	@SuppressWarnings("unchecked")
	@Override
	public Object getInstance() {
		return SingletonS2Container.getComponent(c);
	}

	@Override
	public ComponentScope getScope() {
		return scope;
	}
    }
}

さらにS2ComponentProviderFactoryを使うために、ServletContainerを継承したS2ServletContainerを作成しました。

package com.azuki3.sample.jaxrs.container.servlet;

import com.azuki3.sample.jaxrs.container.S2ComponentProviderFactory;
import com.sun.jersey.api.core.ResourceConfig;
import com.sun.jersey.spi.container.WebApplication;
import com.sun.jersey.spi.container.servlet.ServletContainer;

public class S2ServletContainer extends ServletContainer {

	private static final long serialVersionUID = -6048764957083284096L;

	@Override
	protected void initiate(ResourceConfig rc, WebApplication wa) {
		wa.initiate(rc, new S2ComponentProviderFactory());
	}}

ServletContainerのかわりに使用するようにweb.xmlを変更します。

	<servlet>
		<servlet-name>Jersey REST Service</servlet-name>
		<servlet-class>com.azuki3.sample.jaxrs.container.servlet.S2ServletContainer</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>

これで、リソースクラスをSeasar2のコンテナから取得できるようになりました。

課題

HOT deploy環境で以下のようなwarningが出力されます。

HOT deploy対象クラス(com.azuki3.sample.jaxrs.resource.S2ContactResource)が非対象クラスから参照されて通常のクラスローダにロードされています。

起動時にJerseyがリソースクラスをスキャンするので、その際にクラスがロードされてしまっていることが原因かもしれません。HOT deployはあまり使ったことがなく対応方法がわからないので、とりあえずHOT deploy非対象にするしかないですね。試しにリソース用のCreatorも作ってみたのですが。。

余談ですが、Java EE6の機能を使うんであればSeasar2(S2JDBC)にこだわらずに、Spring + Domaを考えたほうがよいかもしれません。Spring SecurityとかSpring Dynamic Moduleも使いたいので、今後はもう少しSpringに軸を移したいと思っています。