retrofit
公式: http://square.github.io/retrofit/
以前に
っていうかインターフェース定義してアノテーションつけてみたいな事して、Webクライアントのクラスとなるソースを生成するの作れば良いんじゃないの
— kinjouj (@kinjou__j) 2015, 6月 12
っていうように単純なデータのやりとりだけなのであればそういうのがあれば良いんじゃねって思ってたんですが、ソースとかを生成したりはしない(はずだ)けど、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によるエラー処理を定義する事も可能
他にも色々ありそうですが、検証次第追記なりネタ書くかも