JAX-RSをやってみる (1)

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

Jerseyドキュメント: https://jersey.java.net/documentation/latest/index.html

参考: http://backpaper0.github.io/2013/05/02/jaxrs.html

JavaでRESTfulなWebアプリケーションを作るっていう場合とかだとJAX-RSを使えば何かと便利っぽそうなのでいろいろ勉強してみたのでちょっとだけまとめる

※JAX-RSのバージョンは2.x系をターゲットにしてます。又、以下で書くのはあくまでサーバーを起動して利用する前提ではなくテストを書いて実行しているだけなのでサーバー関係に伴ってくる部分に関しては今後に書く予定

セットアップ

// javaプラグインじゃなくてwarプラグインを利用するべきかも
apply plugin: "java"
apply plugin: "jetty"
apply plugin: "eclipse"

repositories {
    mavenCentral()
}

dependencies {
    compile "org.glassfish.jersey.containers:jersey-container-servlet:2.+"
    runtime "org.glassfish.jersey.media:jersey-media-moxy:+"

    testCompile "org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2:+"
    testCompile "org.hamcrest:hamcrest-all:+"
}

バージョン指定周りは適当ですけど、実際にこっちで使ってるバージョンは2.9.x系な模様なので

まぁこれでデプロイ出来るwar作ってアプリケーションサーバーにデプロイしたらなんらかの問題出そうだけど

web.xml

https://jersey.java.net/documentation/latest/user-guide.html#deployment.servletにも書いてあるけど、javax.ws.rs.Applicationに該当するクラスを作るか、アノテーションを探索するパッケージを指定するかの2つを指定出来る

例えば前者のjavax.ws.rs.Applicationに該当するようなクラスを作る場合とかだと

package sample;

import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.server.ResourceConfig;

@ApplicationPath("/aaa")
public class SampleApplication extends ResourceConfig {

    public SampleApplication() {
        packages("sample");
    }
}

のようにしてコンストラクタでpackagesメソッドを用いてアノテーションを探索するパッケージを指定する。で後者のパッケージ名をweb.xml自体に記述する事でも可能

<?xml version="1.0" encoding="UTF-8"?>
<web-app
    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_3_0.xsd"
    version="3.0">

    <servlet>
        <servlet-name>jersey</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>jersey.config.server.provider.packages</param-name>
            <param-value>sample</param-value>
        </init-param>

        <!-- javax.ws.rs.Applicationを使う場合はこっちで指定
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>sample.SampleApplication</param-value>
        </init-param>
        -->

        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>jersey</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

</web-app>

んまぁここまでが開発環境構築的な所。ちなみに上記のjavax.ws.rs.Applicationに該当するクラスにある@ApplicationPathはJAX-RSに準拠しているアプリケーションサーバー以外では作用しないのではないかと(あくまで<servlet-mapping>によるパスをオーバーライドする目的な模様。詳しくないのでggrks)

コントローラーを作成

JAX-RSアプリケーションのコントローラーは特に何かを継承したりはしない。ほとんどがアノテーションを利用してマッピングなどを利用する為アノテーションを使ったコードを書けば良いだけ

package sample.controller;

import java.util.ArrayList;
import java.util.List;

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

import sample.bean.SampleBean;

import static javax.ws.rs.core.MediaType.*;

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

    @GET
    @Produces({ TEXT_PLAIN, TEXT_HTML })
    public Response index() {
        return Response.ok("hoge").build();
    }

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

        return beans;
    }
}

んまぁ@Pathだとか@GETは単純に名称だけでどういう役割を持つかわかると思うので省略するが、@Producesやここには書いてないけど@Consumesっていうのもある(以降で記述しているのでここでは避ける)

@Producesや@ConsumesはSpring WebMVCにもあるけど受け手や出し手に対するメディアタイプを指定するような物だと思うので、上記の場合indexListToJsonで返されたListはJSONとしてレンダリングするように出来る。だけど、それのメディアタイプのコンバーターが無いと返す値によってはレンダリングは出来ない。Listやオブジェクトなどの場合においては上記のbuild.gradleで書いてるjersey-media-moxyっていうのを使えば出来る模様

んまぁコントローラーはざっくりとこんなもんで。他のアノテーションも以降で記述するので(ry

コントローラーのテストを書く

んまぁJerseyTestを使えば簡単に出来る

package sample.controller;

import java.util.List;

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

import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;

import sample.bean.SampleBean;

import static javax.ws.rs.core.MediaType.*;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;

public class HomeTest extends JerseyTest {

    @Override
    protected Application configure() {
        // org.glassfish.jersey.server.ResourceConfig を継承したクラスを定義してjavax.ws.rs.Applicationとしてweb.xmlで設定しているような場合にはそのインスタンスをぶん投げれば良い模様
        // return new SampleApplication();

        return new ResourceConfig(Home.class);
    }

    @Test
    public void test_index() {
        String data = target("/sample").request(TEXT_PLAIN).get(String.class);
        assertThat(data, notNullValue());
        assertThat(data, is("hoge"));

        // Response型と利用してテストするケース
        Response response = target("/sample").request(TEXT_PLAIN).get();
        assertThat(response, notNullValue());
        assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode()));

        String readData = response.readEntity(String.class);
        assertThat(readData, notNullValue());
        assertThat(readData, is("hoge"));
    }

    @Test
    public void test_indexListToJson() {
        List<SampleBean> beans = target("/sample/list")
            .request()
            .accept(APPLICATION_JSON)
            .get(new GenericType<List<SampleBean>>() {});

        assertThat(beans, hasSize(2));
    }
}

てな感じでテスト書いて実行

んまぁそんな感じでJAX-RSを使ってアノテーションベースでRESTful Webアプリケーションを作る事が出来るっていう感じで。んまぁ今後もドキュメントなり読みつつ勉強する予定なのでその都度色々ネタ書く予定って事で、おさわり的な所はこんなもんで

※以降がその他のアノテーションの紹介

@QueryParamアノテーション

@QueryParamを使えばクエリパラメータをメソッドの引数等(フィールド等に定義したりする事も可能)に指定する事で値を引数にマッピングしたり出来る。又、@DefualtValueを使えばデフォルト値の設定なども出来る

package sample.controller;

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

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

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

    @Path("get")
    @GET
    @Produces(APPLICATION_JSON)
    public SampleBean get(@QueryParam("name") @DefaultValue("hoge") SampleBean bean) {
        return bean;
    }
}

でこの場合引数の型がSampleBeanっていうのになっているけど、引数にオブジェクト型を指定するような場合だとそのクラスのvalueOfかfromStringメソッドを呼び出してオブジェクトのインスタンス化出来るようにするので

package sample.bean;

import java.io.Serializable;

public class SampleBean implements Serializable {

    private String name;

    public SampleBean() {
    }

    public SampleBean(String name) {
        setName(name);
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public static SampleBean fromString(String value) {
        return new SampleBean(value);
    }
}

っていうようにfromStringメソッドなりを実装しておけば引数にオブジェクト型を指定した場合には型変換の仕組みを利用する事が出来る模様

でこの場合テストは

package sample.controller;

import javax.ws.rs.core.Application;

import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;

import sample.bean.SampleBean;

import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;

public class HomeTest extends JerseyTest {

    @Override
    protected Application configure() {
        return new ResourceConfig(Home.class);
    }

    @Test
    public void test_get() {
        SampleBean bean = target("/sample/get")
            .queryParam("name", "test_get")
            .request()
            .get(SampleBean.class);

        assertThat(bean, notNullValue());
        assertThat(bean.getName(), is("test_get"));
    }

    @Test
    public void test_get_default() {
        SampleBean bean = target("/sample/get").request().get(SampleBean.class);
        assertThat(bean, notNullValue());
        assertThat(bean.getName(), is("hoge"));
    }
}

又、引数にInteger等を指定した場合にInteger型ではないデータを指定しているような場合だと、javax.ws.rs.NotFoundExceptionになる模様

@PathParamアノテーション

package sample.controller;

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

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

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

    @Path("show/{name}")
    @GET
    @Produces(APPLICATION_JSON)
    public SampleBean show(@PathParam("name") SampleBean bean) {
        return bean;
    }
}

URLパスの一部をマッピングするアノテーションで、正規表現によるパターンマッチングも可能な模様。テストする場合には普通にパス指定すれば良いだけなのでこの件に関しては省略

@MatrixParamアノテーション

参考サイトによるとセミコロンで区切った形式の値をマッピング出来る模様

package sample.controller;

import java.util.ArrayList;
import java.util.List;

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

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

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

    @Path("pair")
    @GET
    @Produces(APPLICATION_JSON)
    public List<SampleBean> pair(
        @MatrixParam("first") SampleBean first,
        @MatrixParam("second") SampleBean second
    ) {
        List<SampleBean> beans = new ArrayList<SampleBean>(2);
        beans.add(first);
        beans.add(second);

        return beans;
    }
}

っていう風に定義すると/sample/pair;first=first;second=secondのようなリクエストを利用出来る

テストは

package sample.controller;

import java.util.List;

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

import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;

import sample.bean.SampleBean;

import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;

public class HomeTest extends JerseyTest {

    @Override
    protected Application configure() {
        return new ResourceConfig(Home.class);
    }

    @Test
    public void test_pair() {
        List<SampleBean> beans = target("/sample/pair")
            .matrixParam("first", "sample_first")
            .matrixParam("second", "sample_second")
            .request()
            .get(new GenericType<List<SampleBean>>() {});

        assertThat(beans, hasSize(2));
        assertThat(beans.get(0).getName(), is("sample_first"));
        assertThat(beans.get(1).getName(), is("sample_second"));
    }
}

っていう感じで、martixParamを用いてパラメーターを設定してリクエストしてテスト出来る

@FormParam

@FormParamを用いるとフォーム送信(application/x-www-form-urlencoded?)によるパラメーターの取得が可能になる。又、@BeanParamを使う事でフォームから送信されたデータをオブジェクトにマッピングしたりも出来る。その場合はオブジェクトに@FormParamアノテーションを付与する

package sample.bean;

import java.io.Serializable;
import javax.ws.rs.FormParam;

public class SampleBean implements Serializable {

    @FormParam("name")
    private String name;

    public SampleBean() {
    }

    public SampleBean(String name) {
        setName(name);
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

っていうように@FormParamアノテーションを引数にマッピングするオブジェクトに付与しておく

package sample.controller;

import javax.ws.rs.BeanParam;
import javax.ws.rs.POST;
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("save")
    @POST
    // 明示的に@Consumesをつけておいた方が良いのかも
    @Produces(APPLICATION_JSON)
    public SampleBean save(@BeanParam SampleBean bean) {
        return bean;
    }
}

っていうように@BeanParamを付与したパラメーターを指定する事で@FormParamで参照されるパラメーターがマッピングされたオブジェクトを利用出来る。でフォームテストをする場合には

package sample.controller;

import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Form;

import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;

import sample.bean.SampleBean;

import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;

public class HomeTest extends JerseyTest {

    @Override
    protected Application configure() {
        return new ResourceConfig(Home.class);
    }

    @Test
    public void test_save_form() {
        Form form = new Form();
        form.param("name", "test_save_form");

        SampleBean bean = target("/sample/save")
            .request()
            .post(Entity.form(form), SampleBean.class);

        assertThat(bean, notNullValue());
        assertThat(bean.getName(), is("test_save_form"));
    }
}

っていう感じでpostメソッドの引数に指定すりゃ良い。ちなみにリクエストメソッドがサポートされていないような場合とかだとjavax.ws.rs.NotAllowedExceptionがスローされる

余談としてJSONをPOSTしてぶん投げるような場合のテストは

package sample.controller;

import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Application;

import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;

import sample.bean.SampleBean;

import static javax.ws.rs.core.MediaType.*;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;

public class HomeTest extends JerseyTest {

    @Override
    protected Application configure() {
        return new ResourceConfig(Home.class);
    }

    @Test
    public void test_save_json() {
        SampleBean bean = target("/sample/save")
            .request()
            .post(Entity.entity(new SampleBean("test_save"), APPLICATION_JSON), SampleBean.class);

        assertThat(bean, notNullValue());
        assertThat(bean.getName(), is("test_save"));
    }
}

っていうようにEntity.entityでやれば良い模様

以上でアノテーションの紹介は終わりだけど、まだまだアノテーションたくさんあると思われるので状況次第でネタを書く

追記: @Encodedアノテーションについて

package sample.controller;

import javax.ws.rs.Encoded;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;

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

    @GET
    @Path("save")
    public String saveEncode(@Encoded @QueryParam("q") String query) {
        return query;
    }

    @GET
    @Path("save/noenc")
    public String saveNonEncode(@QueryParam("q") String query) {
        return query;
    }
}

っていう感じで@Encodedがついてるメソッドとついてないメソッドを定義しておく

package sample.controller;

import javax.ws.rs.core.Application;

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

import sample.SampleApplication;

import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;

public class HomeTest extends JerseyTest {

    @Override
    protected Application configure() {
        return new SampleApplication();
    }

    @Test
    public void test_save() {
        String response = target("/sample/save")
            .queryParam("q", "<script>alert("hoge");</script>")
            .request()
            .get(String.class);
        assertThat(response, is("%3Cscript%3Ealert("hoge");%3C/script%3E"));
    }

    @Test
    public void test_saveNonEncode() {
        String response = target("/sample/save/noenc")
            .queryParam("q", "<script>alert("hoge");</script>")
            .request()
            .get(String.class);
        assertThat(response, is("<script>alert("hoge");</script>"));
    }
}

っていう事になる。JAX-RSではクエリー等を受け取る際にエンコードされたデータを自動でデコードしてメソッド等にマッピングするが、@Encodedをついてる場合に限っては生データが直接渡される模様

ロクな検証してないけど、@Encodedをついてないでデータベースに登録してテンプレートエンジン等を用いてそのデータをレンダリングした場合等にはXSSの可能性が出てくるのはないかと

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