GeekFactory

int128.hatenablog.com

Apache CXFでRESTサービスをさくさく作る

Apache CXF で REST サービスをさくさく作ってみます。

自分で用意するもの。

Maven が用意してくれるもの。

新しいプロジェクトを作る

[New]-[Project...]-[Maven Project] で Archetype に cxf-jaxrs-service を選びます。すると、 Apache CXF ベースの新しいプロジェクトが生成されます。

とりあえず実行してみる

生成されたプロジェクトはすぐに実行可能です。私の環境では POM エディタでエラーが出ていましたが、Maven は問題なく動きました。

プロジェクトの [Debug As]-[Maven build...] で [Goals] に

tomcat:run

を指定すると Apache Tomcat が立ち上がります。なお、 [Debug As] ではなく [Run As] にするとクラスファイルの変更がすぐに反映されなくなります(Hot Deploy が無効)。

ここで http://localhost:13000/jaxrs-service/hello/echo/hoge を開いてみましょう。このURLを開くと HelloWorld#ping(String) が実行されます。HelloWorld クラスには以下のアノテーションが記述されています。

@Path("/hello")
public class HelloWorld {

    @GET
    @Path("/echo/{input}")
    @Produces("text/plain")
    public String ping(@PathParam("input") String input) {

結合テストを実行してみる

次は結合テストを実行してみましょう。プロジェクトの [Run As]-[Maven build...] で [Goals] に

integration-test

を指定すると、結合テストが実行されます。

結合テストは以下の流れで行われます。

  • Apache Tomcat を起動する。
  • JUnit を起動する。
  • JAX-RS クライアントがテスト対象 URL にリクエストを発行し、期待値が返ってくるか確認する。
  • Apache Tomcat を停止する。

HelloWorldIT クラスを見れば JAX-RS クライアントの使い方が何となくつかめると思います。

プロジェクトをカスタマイズする

気に入らない部分があればカスタマイズします。よく分からない人は飛ばしてもおkですが、★印はやった方がよいかと思います。

  • Java source/target version を1.6に変更(★)
  • POM エディタでエラーが出てるところを削除(())
  • Faceted Project に変換し、 Project Facets に以下を追加
    • Dynamic Web Module 2.5
    • JAX-RS 1.1
  • cxf-rt-frontend-jaxrs を 2.6.0 にバージョンアップ
  • spring-web を追加
  • 基準 URL を変更

色々いじったら https://github.com/int128/tokenizer-service/blob/master/pom.xml になりました。

新しいサービスを作る

ここでは kuromoji ライブラリによる形態素解析サービスを作ってみます。

デフォルトではすべての URL へのアクセスが CXF に流れてしまうので、 web.xml を修正します。

  <servlet-mapping>
    <servlet-name>CXFServlet</servlet-name>
    <url-pattern>/api/*</url-pattern>
  </servlet-mapping>

サンプルを参考にしてテストケースを書きます。最初から完璧なテストは書けないので、まずは assert that response is Status.OK とかから始めるといいと思います。

public class TokenizeIT {
  private static String endpointUrl;

  @BeforeClass
  public static void beforeClass() {
    endpointUrl = System.getProperty("service.url");
  }     

  @Test 
  public void testPost() throws Exception {
    WebClient client = WebClient.create(endpointUrl + "/api/tokenize"); 
    String input = "愛";
    Response r = client.accept("application/json").form(new Form().set("text", input));
    assertThat(r.getStatus(), is(Response.Status.OK.getStatusCode()));

    JsonFactory factory = new MappingJsonFactory();
    JsonParser parser = factory.createJsonParser((InputStream) r.getEntity());
    JsonNode node = parser.readValueAsTree();
    assertThat(node.isArray(), is(true));
    assertThat(node.size(), is(1));

    JsonNode token = node.get(0);
    assertThat(token.findValue("surfaceForm").asText(), is("愛"));
    assertThat(token.findValue("reading").asText(), is("アイ"));
    assertThat(token.findValue("position").asInt(), is(0));
  }     
}
https://github.com/int128/tokenizer-service/blob/master/src/test/java/org/hidetake/tokenizer/TokenizeIT.java

リソースクラスを実装します。

@Path("/tokenize")
public class Tokenize {
  private final Tokenizer tokenizer = Tokenizer.builder().build();

  @POST 
  @Produces("application/json")
  public Response post(@FormParam("text") String text) {
    List<Token> tokens = this.tokenizer.tokenize(text);
    return Response.ok().entity(tokens).build();
  }     
}
https://github.com/int128/tokenizer-service/blob/master/src/main/java/org/hidetake/tokenizer/Tokenize.java

フロントエンドを作ります。ここでは HTML+JavaScript で POST する画面を作りました。

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script src="http://cloud.github.com/downloads/SteveSanderson/knockout/knockout-2.0.0.js"></script>
$(function () {
  var ViewModel = function () {
    this.text = ko.observable('ほげはふがです。');
    this.loading = ko.observable();
    this.error = ko.observable();
    this.tokens = ko.observableArray();
    ko.computed(function () {
      $.ajax({          
        context: this,  
        type: 'POST',
        url: '/api/tokenize',
        data: {text: this.text()},
        dataType: 'json',       
        beforeSend: function () {
          this.loading(true);
          this.error(null);
          this.tokens(null);
        },      
        complete: function () {
          this.loading(false);
        },
        success: function (v) {
          this.tokens(v);
          this.error(null);
        },
        error: function (xhr, t, e) {
          this.tokens(null);
          this.error(xhr.responseText ? xhr.responseText : e);
        }
      });
    }, this);
  };
  ko.applyBindings(new ViewModel());
});
https://github.com/int128/tokenizer-service/blob/master/src/main/webapp/index.html

フロントエンドを REST サービスと同居させる必要はありません。他のドメインの Web ページだったり、スタンドアロンアプリだったりしても OK です。

デプロイする

開発環境で動くようになったらクラウドにデプロイしてみましょう。プロジェクトの [Run As]-[Maven build...] で [Goals] に

package

を指定すると WAR ファイルが生成されます。

ここでは CloudFoundry.com にデプロイしてみました。

$ cd target
$ vmc push
Would you like to deploy from the current directory? [Yn]: 
Application Name: tokenizer
Application Deployed URL [tokenizer.cloudfoundry.com]: 
Detected a Java SpringSource Spring Application, is this correct? [Yn]: 
Memory Reservation (64M, 128M, 256M, 512M, 1G) [512M]: 
Creating Application: OK
Would you like to bind any services to 'tokenizer'? [yN]: 
Uploading Application:
  Checking for available resources: OK
  Processing resources: OK
  Packing application: OK
  Uploading (2K): OK   
Push Status: OK
Staging Application: OK                                                         
Starting Application: OK                                                        

はい、終わりです。完成品は http://tokenizer.cloudfoundry.com/ に置いてあります。

ユニットテスト書けやゴルァと言われたら、、すいません。