JAX-RSをやってみる (3) - AutoDiscoverable -

2014-06-10T00:00:00+00:00 Java JAX-RS

JAX-RSで返される値の型に対してレスポンスを変換するような仕組みはMessageBodyWriter(MessageBodyReaderっていうのもある)で利用する事が出来る。@Providerアノテーションを使う事でJAX-RSアプリケーションプロジェクト内にクラスを定義すれば良いだけなのだけど、例えばそういう仕組みな所を外部のライブラリにした場合でアノテーションが処理されるパッケージフィルターに該当しないような場合にどうやってその仕組みをロードするのか調べてみるとAutoDiscoverableっていうのを使う事で外部ライブラリで定義されたクラスをコンポーネント的な形として登録して利用できる模様。

という事でやってみた

備考: glassfishのバージョンに関して

(記事投稿時の)glasshfish4では組み込まれているJAX-RS(Jersey2)のバージョンが古い模様なので、実装側とAPIのバージョンの整合性が合わないが為にエラーが起きる模様。なのでglassfishに同梱されているライブラリをアップデートするか、glassfish4.0.1(現時点では正式版は出ていない)を使う事で解決出来る模様

参考: GlassFish4.0でJAX-RSのクラスにCDIでインジェクションしようとしたらUnsatisfiedDependencyExceptionが発生するバグ?

概要

gsonを使って返された値をJSONにエンコードするだけのMessageBodyWriterをAutoDiscoverableを使って別パッケージにしてロード出来るようにする

SampleAutoDiscoverable.java

package sample.internal;

import javax.annotation.Priority;
import javax.ws.rs.Priorities;
import javax.ws.rs.core.FeatureContext;

import org.glassfish.jersey.internal.spi.AutoDiscoverable;

@Priority(Priorities.ENTITY_CODER)
public class SampleAutoDiscoverable implements AutoDiscoverable {
    @Override
    public void configure(FeatureContext context) {
        if (!context.getConfiguration().isRegistered(SampleFeature.class)) {
            context.register(SampleFeature.class);
        }
    }
}

SampleFeature.java

package sample.internal;

import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.Feature;
import javax.ws.rs.core.FeatureContext;
import javax.ws.rs.ext.MessageBodyWriter;

import org.glassfish.jersey.CommonProperties;
import org.glassfish.jersey.internal.util.PropertiesHelper;

import sample.provider.GsonProvider;

import static org.glassfish.jersey.internal.InternalProperties.JSON_FEATURE;

public class SampleFeature implements Feature {

    private static final String SAMPLE_JSON_FEATURE = SampleFeature.class.getSimpleName();

    @Override
    public boolean configure(final FeatureContext context) {
        final Configuration config = context.getConfiguration();

        // glasshfish4.0.0(リリース版)だとここでNoSuchMethodError出る
        final String jsonFeature = CommonProperties.getValue(
            config.getProperties(),
            config.getRuntimeType(),
            JSON_FEATURE,
            null,
            String.class
        );

        // このクラスとは違うのが入ってるのであれば、既にJSON_FEATUREがあるのでスキップする
        if (jsonFeature != null) {
            return false;
        }

        context.property(
            PropertiesHelper.getPropertyNameForRuntime(JSON_FEATURE, config.getRuntimeType()),
            SAMPLE_JSON_FEATURE
        );

        if (!config.isRegistered(GsonProvider.class)) {
            context.register(GsonProvider.class, MessageBodyWriter.class);
        }

        return true;
    }
}

GsonProvider.java

package sample.provider;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;

import javax.inject.Singleton;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyWriter;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

@Singleton
@Produces(MediaType.APPLICATION_JSON)
public class GsonProvider implements MessageBodyWriter<Object> {

    private final Gson gson = new GsonBuilder().create();

    @Override
    public boolean isWriteable(Class<?> type, Type genericType,
        Annotation[] annotations, MediaType mediaType) {

        // @Producesアノテーションがついてればそのメディアタイプに限定される模様
        // return mediaType == MediaType.APPLICATION_JSON_TYPE;

        return true;
    }

    @Override
    public long getSize(Object t, Class<?> type, Type genericType,
        Annotation[] annotations, MediaType mediaType) {
        return -1;
    }

    @Override
    public void writeTo(Object t, Class<?> type, Type genericType,
        Annotation[] annotations, MediaType mediaType,
        MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
        throws IOException, WebApplicationException {

        try (OutputStreamWriter writer = new OutputStreamWriter(entityStream, "UTF-8")) {
            gson.toJson(t, genericType, writer);
        }
    }
}

META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverableを作成

AutoDiscoverableを使う場合にはServiceLoader周りを設定しとけば良い

sample.internal.SampleAutoDiscoverable

中身はAutoDiscoverableのパッケージ名を含むクラス名を指定するだけ

あとはこのプロジェクトをjar化してJAX-RSアプリケーションに含めるだけ

SampleApplication.java

package sample;

import javax.ws.rs.ApplicationPath;

import org.glassfish.jersey.CommonProperties;
import org.glassfish.jersey.server.ResourceConfig;

@ApplicationPath("resources")
public class SampleApplication extends ResourceConfig {

    public SampleApplication() {
        packages("sample");
        property(CommonProperties.MOXY_JSON_FEATURE_DISABLE, true);
    }
}

デフォでMOXyなJSONシリアライザ的なので処理されちゃって上記のFeatureでJSON_FEATUREによりGsonProviderでの処理がスキップされちゃうので、そこら辺の使えるようにするように設定しとかなあかんっぽい

Home.java

package sample.controller;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

import sample.bean.SampleBean;
import static javax.ws.rs.core.MediaType.*;

@Path("/sample")
public class Home {

    @Path("list")
    @GET
    @Produces(APPLICATION_JSON)
    public List<SampleBean> list() {
        List<SampleBean> beans = new ArrayList<SampleBean>(2);
        beans.add(new SampleBean("hoge"));
        beans.add(new SampleBean("fuga"));

        return beans;
    }

    @Path("map")
    @GET
    @Produces(APPLICATION_JSON)
    public Map<String, Object> map() {
        Map<String, Object> data = new HashMap<String, Object>(1);
        data.put("sample", new SampleBean("hoge"));

        return data;
    }
}

HomeTest.java

package sample.controller;

import java.util.List;
import java.util.Map;

import javax.ws.rs.core.Application;
import javax.ws.rs.core.GenericType;

import org.glassfish.jersey.test.JerseyTest;
import org.junit.Before;
import org.junit.Test;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.internal.LinkedTreeMap;

import sample.SampleApplication;
import sample.bean.SampleBean;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;

public class HomeTest extends JerseyTest {

    private Gson gson;

    @Override
    protected Application configure() {
        gson = new GsonBuilder().create();
        return new SampleApplication();
    }

    @Test
    public void test_list() {
        String response = target("/sample/list").request().get(String.class);
        assertThat(response, notNullValue());

        List<SampleBean> beans = gson.fromJson(
            response,
            new GenericType<List<SampleBean>>() {}.getType()
        );
        assertThat(beans, notNullValue());
        assertThat(beans, hasSize(2));
    }

    @Test
    public void test_map() {
        String response = target("/sample/map").request().get(String.class);
        assertThat(response, notNullValue());

        Map<String, Object> data = gson.fromJson(
            response,
            new GenericType<Map<String, Object>>() {}.getType()
        );
        assertThat(data, notNullValue());
        assertThat(data, hasKey("sample"));
    }
}

んまぁデプロイ先はglassfishを前提にして調査してたので他のアプリケーションサーバーとかの検証だとかしてませんが、glassfishだと何かとバージョンやらの問題が出てきちゃうので

とりまぁそういうように外部ライブラリに仕込んでやるような場合だとAutoDiscoverableのような仕組みを使う事で組み込む事が可能って事で

JAX-RSをやってみる (2) - glassfish deployment -