第2回 実践!RESTful Webサービス構築

第2回 実践!RESTful Webサービス構築

連載第2回目は、ショッピングサイトの注文機能を題材にして簡単なWebサービスを構築してみましょう。必要な機能を決め、実装とテストを行い、最終的にはデータベースと連携させたいと思います。

なお、紙面の都合がありますので、データベース連携については次回以降に取り上げたいと思います。



HTTPメソッド

まずは、改めてHTTPメソッドについておさらいしておきましょう。JAX-RSでは、以下のHTTPメソッドを取り扱うことが可能です。

表 1 Wikipedia「Hypertext Transfer Protocol」の引用

GET指定されたURIのリソースを取り出す。
POSTクライアントがサーバにデータを送信する。
PUT指定したURIにリソースを保存する。
DELETE指定したURIのリソースを削除する。
HEADGETと似ているが、サーバはHTTPヘッダのみ返す。
OPTIONSサーバを調査する。サーバがサポートしているHTTPバージョンなどを知ることができる。

これらは、それぞれアノテーションとして提供されています。

以下に、それぞれのアノテーションを利用したメソッドの定義例を載せておきます。提示したコードは、前回作成したプロジェクトを流用していますので、この回から読み始めた方は、前回の記事を参考にプロジェクトを作成してください。

@Path("/sample")
public interface SampleResource {

    @GET
    @Path("/hello/{message}")
    String sayHello(@PathParam("message") String message);

    @POST
    @Path("/hello/{id}/{name}")
    @Consumes(MediaType.TEXT_PLAIN)
    String insert(@PathParam("id") int id, @PathParam("name") String name);

    @PUT
    @Path("/hello/{id}/{name}")
    @Consumes(MediaType.TEXT_PLAIN)
    String update(@PathParam("id") int id, @PathParam("name") String name);

    @DELETE
    @Path("/hello/{id}")
    String delete(@PathParam("id") int id);
}
@Component
public class HelloResource implements SampleResource {

    @Override
    public String sayHello(String message) {
        return String.format("Hello, %s", message);
    }

    @Override
    public String insert(int id, String name) {
        return String.format("insert id=%d and name=%s", id, name);
    }

    @Override
    public String update(int id, String name) {
        return String.format("update id=%d and name=%s", id, name);
    }

    @Override
    public String delete(int id) {
        return String.format("delete id=%d", id);
    }
}

(※ コードの全文は、SampleResource.javaHelloResource.javaを参照)

修正したコードの動作を確認するため、WTPとTomcatを使ってプロジェクトを起動します。動作の確認には、「REST Client」というツールが便利です。このツールは、画面上から簡単にHTTPメソッドを変更して、リクエストを送ることができます。
下記に、REST Clientを使った各HTTPメソッドの実行結果を載せておきます。実行結果を見ていただくとわかると思いますが、例えばPOSTメソッドとPUTメソッドは同じURLにも関わらず、HTTP ResponseのBodyに表示されている結果は異なっています。これは想定した結果で、リクエストに使用したHTTPメソッドをJAX-RSが判断して、実行するHelloResourceのメソッドを切り替えているからです。


図 1 GETメソッドの実行結果


図 2 POSTメソッドの実行結果


図 3 PUTメソッドの実行結果


図 4 DELETEメソッドの実行結果



注文機能の構築

それではいよいよ本題に入りましょう。簡単ではありますが、ショッピングサイトの注文機能を構築していきたいと思います。なお、「注文機能とはちょっと違うのでは?」といった機能や、「機能が足りなくない?」というようなことがあっても、あくまでも主題はRESTful Webサービスの構築なので、大目に見ていただければ幸いです。プロジェクトは、前回の記事で作成したプロジェクトを使用します。

ユースケースを考える

まずは、注文機能のユースケースを考えてみましょう。アクターは、ショッピングサイトを利用する消費者だと思ってください。商品を注文する、注文した商品を取り消す、注文した商品の履歴を見るなどがあると思います。以下に、想定されるユースケースを挙げます。

  • 商品を注文する
  • 注文した商品を確認する
  • 注文した商品を更新する
  • 注文した商品を取り消す
  • 注文した商品の履歴を見る

この段階では、まだデータモデルについては考えません。おおよそ必要なデータモデルは想像できると思いますが、実装を進めながら考えていきます。

注文処理の実装とテスト

ユースケースの中から「注文する」機能を取り上げて、実装とテストを繰り返しながら、ひとつのインターフェースを完成させましょう。テスト駆動開発で実装を進めますので、テストエラーとエラー解消の繰り返しになります。
まずはテストケースを作成します。プロジェクトにsrc/test/javaフォルダを作成してソースフォルダに指定し、配下に「jp.sample.jaxrs.service」パッケージを作成します。

(※ コードや設定ファイルの全文は、記事の最後に掲載します)


図 5 テストソースフォルダの設定

そのパッケージに、「OrderResourceTest」クラスを作成します。クラスの中身は、以下のように記述します。

public class OrderResourceTest {

    @Test
    public void testOrderItem01() {
        WebClient client =
                WebClient
                        .create("http://localhost:8080/jax-rs/order/create")
                        .type(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON);
        Response actual = client.post(new Order());

        try {
            Assert.assertEquals(Status.OK.getStatusCode(), actual.getStatus());
            // Macは\マークをバックスラッシュに変更してください
            Assert.assertEquals("{\"order\":\"\"}",
                    IOUtils.readStringFromStream((InputStream) actual.getEntity()));
        } catch (IOException e) {
            Assert.fail();
        }
    }
}

JUnitを使用するので、pom.xmlに以下の依存関係を追記しておいてください。

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.10</version>
  <scope>test</scope>
</dependency>

さて、WTPとTomcatを使ってプロジェクトを起動し、テストケースを実行してみます。しかし、当然のことながら作成したテストケースはエラーになります。
では、エラーを解消するために、必要なクラスを追加しましょう。まずは、src/main/java配下のjp.sample.jaxrs.serviceにOrderResourceを作成します。

@Path("/order")
public interface OrderResource {

    @POST
    @Path("/create")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    Order create(Order order);
}

そして、OrderResourceの具象クラスであるItemOrderResourceを作成します。処理の中身は、引数で受け取った値を返却するだけです。

@Component
public class ItemOrderResource implements OrderResource {

    @Override
    public Order create(Order order) {
        return order;
    }

}

さらに、Orderを作成します。Orderは注文情報を格納するJavaBeanです。

@XmlRootElement
public class Order {

}

最後に、applicationContext.xmlにItemOrderResourceをサービスとして定義します。

<jaxrs:server id="itemorderresource" address="/">
    <jaxrs:serviceBeans>
        <ref bean="itemOrderResource" />
    </jaxrs:serviceBeans>
</jaxrs:server>

再度、テストを実行してみてください。今度はテストが成功します。

注文情報の作成

今は注文情報であるOrderクラスには、何も定義されていせん。では、Orderクラスに持たせるプロパティを決めましょう。

  • 注文番号
  • 注文者
  • 注文商品
  • 注文日

ひとまずはこの程度にしておきます。では、Orderクラスに、上記の4項目を記述します。

@XmlRootElement
@XmlType(propOrder = { "orderNo", "userName", "itemName", "orderDate" })
public class Order {

    private Long orderNo;

    private String userName;

    private String itemName;

    private Date orderDate;

...getter/setter...

}

テストケースで、Orderクラスのオブジェクトを作成し、WebClient#postメソッドに渡しましょう。そして、レスポンスを検証します。

public class OrderResourceTest {

    @Test
    public void testOrderItem01() {
        Order order = new Order();
        order.setOrderNo(1L);
        order.setUserName("Tarou");
        order.setItemName("item01");

        WebClient client =
                WebClient
                        .create("http://localhost:8080/jax-rs/order/create")
                        .type(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON);
        Response actual = client.post(order);

        try {
            Assert.assertEquals(Status.OK.getStatusCode(), actual.getStatus());
            Assert.assertEquals(
                    "{\"order\":{\"orderNo\":1,\"userName\":\"Tarou\",\"itemName\":\"item01\"}}",
                    IOUtils.readStringFromStream((InputStream) actual.getEntity()));
        } catch (IOException e) {
            Assert.fail();
        }
    }
}

テストを実行してみてください。成功するはずです。単純ではありますが、ひとつのインターフェースが完成しました。このインターフェースに対して、データベース連携などを追加して、徐々に肉付けを行なっていきます。

その他のユースケースの作成

いったん、「注文する」機能の実装は終わりにして、その他のユースケースの機能を実装してみましょう。最初に、各ユースケースのテストケースを作成します。

@Test
public void testGetOrderItem01() {
    WebClient client =
            WebClient
                    .create("http://localhost:8080/jax-rs/order/get/1")
                    .type(MediaType.APPLICATION_JSON);
    Response actual = client.get();

    Assert.assertEquals(Status.OK.getStatusCode(), actual.getStatus());
}

@Test
public void testUpdateOrderItem01() {
    WebClient client =
            WebClient
                    .create("http://localhost:8080/jax-rs/order/update")
                    .type(MediaType.APPLICATION_JSON);
    Response actual = client.put(new Order());

    Assert.assertEquals(Status.OK.getStatusCode(), actual.getStatus());
}

@Test
public void testCancelOrderItem01() {
    WebClient client =
            WebClient
                    .create("http://localhost:8080/jax-rs/order/cancel")
                    .type(MediaType.APPLICATION_JSON);
    Response actual = client.delete();

    Assert.assertEquals(Status.OK.getStatusCode(), actual.getStatus());
}

@Test
public void testShowOrderHistory() {
    WebClient client =
            WebClient
                    .create("http://localhost:8080/jax-rs/order/history")
                    .type(MediaType.APPLICATION_JSON);
    Response actual = client.get();

    Assert.assertEquals(Status.OK.getStatusCode(), actual.getStatus());
}

テストケースを実行すると、もちろんエラーになります。アクセス対象となるメソッドが、OrderResourceに定義されていないからです。では、OrderResourceとItemOrderResourceにメソッドを定義しましょう。

@Path("/order")
public interface OrderResource {

    @POST
    @Path("/create")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    Order create(Order order);

    @GET
    @Path("/get/{orderNo}")
    @Produces(MediaType.APPLICATION_JSON)
    Order getOrder(@PathParam("orderNo") Long orderNo);

    @PUT
    @Path("/update")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    Order update(Order order);

    @DELETE
    @Path("/cancel/{orderNo}")
    @Consumes(MediaType.APPLICATION_JSON)
    Order cancel(@PathParam("orderNo") Long orderNo);

    @GET
    @Path("/history")
    @Produces(MediaType.APPLICATION_JSON)
    List history();
}
@Component
public class ItemOrderResource implements OrderResource {

    @Override
    public Order create(Order order) {
        return order;
    }

    @Override
    public Order getOrder(Long orderNo) {
        Order order = new Order();
        order.setOrderNo(orderNo);
        order.setUserName("Tarou");
        order.setItemName("item01");
        return order;
    }

    @Override
    public Order update(Order order) {
        return order;
    }

    @Override
    public Order cancel(Long orderNo) {
        Order order = new Order();
        order.setOrderNo(orderNo);
        order.setUserName("Tarou");
        order.setItemName("item01");
        return order;
    }

    @Override
    public List history() {
        Order order1 = new Order();
        order1.setOrderNo(1L);
        order1.setUserName("Tarou");
        order1.setItemName("item01");

        Order order2 = new Order();
        order2.setOrderNo(2L);
        order2.setUserName("Tarou");
        order2.setItemName("item01");

        List orders = new ArrayList();
        orders.add(order1);
        orders.add(order2);
        return orders;
    }

}

再度テストケースを実行すると、正常終了するはずです。テストケースに、レスポンスのアサーションを追加しておきましょう。

public class OrderResourceTest {

    @Test
    public void testOrderItem01() {
        ・・・省略・・・
    }

    @Test
    public void testGetOrderItem01() {
        ・・・省略・・・
        try {
            Assert.assertEquals(Status.OK.getStatusCode(), actual.getStatus());
            Assert.assertEquals(
                    "{\"order\":{\"orderNo\":1,\"userName\":\"Tarou\",\"itemName\":\"item01\"}}",
                    IOUtils.readStringFromStream((InputStream) actual.getEntity()));
        } catch (IOException e) {
            Assert.fail();
        }
    }

    @Test
    public void testUpdateOrderItem01() {
        ・・・省略・・・
        try {
            Assert.assertEquals(Status.OK.getStatusCode(), actual.getStatus());
            Assert.assertEquals(
                    "{\"order\":{\"orderNo\":1,\"userName\":\"Tarou\",\"itemName\":\"item01\"}}",
                    IOUtils.readStringFromStream((InputStream) actual.getEntity()));
        } catch (IOException e) {
            Assert.fail();
        }
    }

    @Test
    public void testCancelOrderItem01() {
        ・・・省略・・・
        try {
            Assert.assertEquals(Status.OK.getStatusCode(), actual.getStatus());
            Assert.assertEquals(
                    "{\"order\":{\"orderNo\":1,\"userName\":\"Tarou\",\"itemName\":\"item01\"}}",
                    IOUtils.readStringFromStream((InputStream) actual.getEntity()));
        } catch (IOException e) {
            Assert.fail();
        }
    }

    @Test
    public void testShowOrderHistory() {
        ・・・省略・・・

        try {
            Assert.assertEquals(Status.OK.getStatusCode(), actual.getStatus());
            Assert.assertEquals(
                    "{\"order\":[{\"orderNo\":1,\"userName\":\"Tarou\",\"itemName\":\"item01\"}," +
                            "{\"orderNo\":2,\"userName\":\"Tarou\",\"itemName\":\"item01\"}]}",
                    IOUtils.readStringFromStream((InputStream) actual.getEntity()));
        } catch (IOException e) {
            Assert.fail();
        }
    }
}

(※ コードの全文は、OrderResourceTest.javaOrderResource.javaItemOrderResource.javaOrder.javaを参照)

(※ 設定ファイルの全文は、applicationContext.xmlpom.xmlを参照)

以上で、すべてのユースケースのインターフェースを作成することができました。まだまだ実装すべき処理や検討すべき事項がありますが、ひとまずはピザでも食べながら完成を喜びましょう!



おわりに

第2回はいかがだったでしょうか。とても単純な処理内容でしたが、RESTful Webサービスを作成することができました。そして、インターフェースの定義が意外と単純に思えたのではないでしょうか?OrderResourceやItemOrderResourceを見てみると、アノテーションが付いているだけで、他にリクエストやレスポンスを解析するような処理は書いていません。従って、とても見やすいメソッド定義になっていると思います。
次回は、データベース連携をおこなって、リクエストで受け取ったデータの登録や取得を実装してみたいと思います。やっとアプリケーションらしくなるはずです。お楽しみに!


第2回 おしまい



※.記載されているロゴ、システム名、製品名は各社及び商標権者の登録商標あるいは商標です。