retrofit

2015-06-15T17:45:49+09:00 Java RxJava Ruby Rails

公式: http://square.github.io/retrofit/

以前に

っていうように単純なデータのやりとりだけなのであればそういうのがあれば良いんじゃねって思ってたんですが、ソースとかを生成したりはしない(はずだ)けど、retrofitっていうのがあるみたいで使ってみた

サーバー側

とりあえずRailsで適当にResource Controller辺りを使って

class EntryController < ApplicationController

  def index
    sort = params[:sortBy] || "id"
    entries = Entry.order(sort + " DESC").all
    render :json => entries
  end

  def show
    entry = Entry.find(params[:id])
    render :json => entry
  end

  def create
    entry = Entry.new(params[:entry].permit(:name))
    if entry.save
      render :json => entry
    else
      head :internal_server_error
    end
  end
end

単純に

  • データベースのデータを取得してJSONで出力(その際にORDERを指定出来る)
  • 指定したIDのデータを取得してJSONで出力
  • 送られたデータを利用して格納

っていうようなのを作っておく。よってルーティング定義は

     Prefix Verb   URI Pattern     Controller#Action
entry_index GET    /entry          entry#index
            POST   /entry          entry#create
      entry GET    /entry/:id      entry#show

というような感じになる。これをretrofitで使う

retrofitの概要

retrofitでは基本的にインターフェースを定義して、メソッドに適切なアノテーションを付与する事によって利用する事が出来る

でretrofitで使用されるHTTPクライアントの実装はデフォルト(retrofitの依存性だけ追加した場合)で使用されるのはソースまでちょっと追いかけれてないので分からないのだけど

Retrofit will automatically use OkHttp (version 2.0 or newer) when it is present.

というようにokhttp及びokhttp-urlconnectionの依存性を追加していると自動でokhttpを使うようになる模様

あと後述するが、RxJavaを使ったObservableを使う事も出来るので

んまぁ概要はこんなもんで

全件を取得する

package sample;

import java.util.List;

import retrofit.http.GET;
import retrofit.http.Query;

// import rx.Observable;

// 接続エラー時はretrofit.RetrofitErrorの例外が出る
public interface SampleService {

    // 引数とかに「@Header("Authorization") String authorization」とかで引数の値をヘッダーに打ち込む事が可能らしい
    @Headers("User-Agent: retrofit")
    @GET("/entry")
    List<Entry> listEntries();

    // @Queryじゃなくて@QueryMapを使えばMap<String, String>で引数持てる
    @GET("/entry")
    List<Entry> listEntries(@Query("sortBy") String sortBy);

    // RxJavaなObservableを使って取得する事も可能
    // @GET("/entry")
    // Observable<List<Entry>> listEntries();

}

っていうように

  • @GET等のHTTPメソッドに対応したアノテーションを付与
  • HTTPヘッダーに特有のヘッダーを設定したい場合には@Headerや@Headers等を利用する事が出来る
  • パラメーター等が必要な場合には@Queryや@QueryMap等を利用してクエリーストリングを設定する事も可能
  • 書いてる通り、RxJavaを使った結果取得方法を利用する事も出来る

っていう感じでやって

package sample;

import java.util.List;

import org.junit.Before;
import org.junit.Test;

import retrofit.RestAdapter;

import rx.Observable;

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

public class SampleServiceTest {

    SampleService service;

    @Before
    public void setUp() {
        RestAdapter restAdapter =
            new RestAdapter.Builder()
                .setLogLevel(RestAdapter.LogLevel.FULL)
                .setEndpoint("http://localhost:3000")
                .build();

        service = restAdapter.create(SampleService.class);
    }

    // 引数無しのlistEntriesをコールして結果をテスト
    @Test
    public void test1() {
        List<Entry> entries = service.listEntries();
        assertThat(entries.size(), is(3));
        assertThat(entries.get(0).getName(), is("foobar"));
        assertThat(entries.get(1).getName(), is("fuga"));
        assertThat(entries.get(2).getName(), is("hoge"));
    }

    // 引数に指定されたsortByをクエリーストリングに設定して結果をテスト
    @Test
    public void test2() {
        List<Entry> entries = service.listEntries("name");
        assertThat(entries.size(), is(3));
        assertThat(entries.get(0).getName(), is("hoge"));
        assertThat(entries.get(1).getName(), is("fuga"));
        assertThat(entries.get(2).getName(), is("foobar"));
    }

    // RxJavaのObservableを使っての結果取得をテスト
    @Test
    public void test3() {
        Observable<List<Entry>> o = service.listEntries();
        List<Entry> entries = o.toBlocking().first();
        assertThat(entries.size(), is(3));
    }
}

RestAdapterのインスタンスでエンドポイントを設定してcreateメソッドで作成したインターフェースを指定してインスタンスを取得。あとはインターフェースのメソッドを呼ぶだけでHTTPリクエストを行い結果を取得してくれる模様(JSONの実装は多分GSON)

URLパスに埋め込んでリクエスト

package sample;

import java.util.List;

import retrofit.http.GET;
import retrofit.http.Path;

public interface SampleService {

    @GET("/entry/{id}")
    Entry getEntry(@Path("id") int id);

}

っていうようにリクエストメソッドに対応するアノテーションにメソッドの引数で指定させた値を埋め込む事も出来る。さきほどのでは言ってないが@Queryにも@EncodedQueryがあったり、この@Pathにも@EncodedPathっていうのがあるのでURLエンコードされてリクエストされるべきデータに応じてはそのアノテーションを付与する必要

ちなみに結果を取得する際にサーバーがエラーを起こした場合にはRetrofitErrorの例外が送出される模様

package sample;

import org.junit.Before;
import org.junit.Test;

import retrofit.RestAdapter;
import retrofit.RetrofitError;

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

public class SampleServiceTest {

    SampleService service;

    @Before
    public void setUp() {
        RestAdapter restAdapter =
            new RestAdapter.Builder()
                .setLogLevel(RestAdapter.LogLevel.FULL)
                .setEndpoint("http://localhost:3000")
                .build();

        service = restAdapter.create(SampleService.class);
    }

    @Test
    public void test1() {
        Entry entry = service.getEntry(1);
        assertThat(entry, notNullValue());
        assertThat(entry.getName(), is("hoge"));
    }

    @Test(expected=RetrofitError.class)
    public void test2() {
        service.getEntry(9);
    }
}

特に言う事無いので次

POSTでデータを送信する場合 (@Bodyを使用)

package sample;

import retrofit.Callback;
import retrofit.http.POST;
import retrofit.http.Body;

public interface SampleService {

    @POST("/entry")
    void createEntry(@Body Entry entry, Callback<Entry> callback);

}

っていうように@POSTで定義して@Bodyを持つ引数を定義する事で可能。但し

  • @Bodyをつけた場合はapplication/jsonとして送られるので他の方式の@FormUrlEncoded等と共用する事は出来ない
  • 返り値か引数の最後にCallbackを持つ必要がある

2つ目のCallbackを持つ件に関しては

Asynchronous execution requires the last parameter of the method be a Callback.

と明記されており、Callbackを引数の最後に持つ場合には非同期処理によって実行される模様

package sample;

import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.Rule;
import org.junit.rules.ExpectedException;

import retrofit.Callback;
import retrofit.RestAdapter;
import retrofit.RetrofitError;
import retrofit.client.Response;

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

public class SampleServiceTest {

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    ExecutorService executor;
    SampleService service;

    @Before
    public void setUp() {
        executor = Executors.newSingleThreadExecutor();

        RestAdapter restAdapter =
            new RestAdapter.Builder()
                .setExecutors(executor, executor)
                .setLogLevel(RestAdapter.LogLevel.FULL)
                .setEndpoint("http://localhost:3000")
                .build();

        service = restAdapter.create(SampleService.class);
    }

    @After
    public void tearDown() {
        executor.shutdownNow();
    }

    @Test(timeout = 5000)
    public void test4() throws Exception {
        Entry entry = new Entry();
        entry.setName("abc");

        service.createEntry(entry, new Callback<Entry>() {
            @Override
            public void success(Entry entry, Response response) {
            }

            @Override
            public void failure(RetrofitError error) {
                thrown.expect(error.getClass());
                thrown.expectMessage(error.getMessage());
            }
        });

        executor.awaitTermination(3, TimeUnit.SECONDS);
    }
}

というようにテストする際にはExecutorで一定の待機処理等を行わないと正常にテスト実行されないはずなので

という感じで@Bodyを持った引数を持った場合にはapplication/jsonとしてリクエストされてデータを送信できる

POSTでデータを送信する場合 (@FormUrlEncodedを使用)

package sample;

import retrofit.Callback;
import retrofit.http.FormUrlEncoded;
import retrofit.http.Field;
import retrofit.http.FieldMap;
import retrofit.http.POST;
import retrofit.http.Body;

public interface SampleService {

    @FormUrlEncoded
    @POST("/entry")
    void createEntry(@Field("name") String name, Callback<Entry> callback);

}

上記で書いたように@Bodyを使う場合にはapplication/jsonで送られるので、application/x-www-form-urlencoded?で送りたいような事案の場合には@FormUrlEncodedを使えば良い

但し、@Bodyが使えないので送るデータはメソッドに一つづつ持つようになる。それか@Fieldじゃなくて@FieldMapで持つ事で可能な模様。又、TypedString(クエリーのようにid=1&name=hogeのようにフォームデータとしてエンコードしたデータ)等を使ってなんとかする方法もあるらしい

というような感じで使用する事が出来る

余談: その他のアノテーションやAPI

  • @Multipart及び@Part、TypedFile等を利用する事でマルチパートリクエスト等も出来る
  • RequestInterceptorを使う事でリクエストを行う前に共通する設定等を仕込む事も可能
  • ErrorHandlerによるエラー処理を定義する事も可能

他にも色々ありそうですが、検証次第追記なりネタ書くかも

ECMAScript6でangular.js retrolambda