非同期 Web API アクセス
はじめに
Web API へのアクセスも、基本 URL を作ってアクセスし、出力結果を Stream として取り出すことはできます。
ただ、色々不便なこともあるので、便利なライブラリを使ってみましょう。利用するのはRetrofit というライブラリです。github 上で公開されています。
なにが便利かというと、こんな感じです。
- Web API の引数の処理を簡単に記述可能
- 問い合わせを同期的におこなったり、非同期におこなったりすることが簡単
- 取得結果を GSON や JAXB をつかってオブジェクトに変換することもできる
簡単に紹介します。詳しいことは、Retrofitのチュートリアルと API document をみてください。
利用プログラムは kobeU.cs.samplesNet.retrofit パッケージ以下にあり、起動するプログラムは SampleCallerです。
Web API を Java API に
以前お伝えしたように、 Web API 側は、URL を受け取って、結果を返す関数のようなものです。 それを、手軽に Java の関数のようにみせる手助けをしてくれます。
上記チュートリアルの例をそのまま使いますが(ユーザ名だけ octcat
から plham
に変更しています)、例えば、github Contents API の場合、
にアクセスすることで、plham
というユーザの repository 情報をリストの形で返してくれます。plham
の部分がパラメータですね。
[
{
"id": 54699962,
"node_id": "MDEwOlJlcG9zaXRvcnk1NDY5OTk2Mg==",
"name": "plham",
...
},
{
"id": 54700048,
"node_id": "MDEwOlJlcG9zaXRvcnk1NDcwMDA0OA==",
"name": "plham.github.io",
...
},
...
}
これを、Java から簡単にアクセスできるように、
Call<List<Repo>> repos = service.listRepos("plham");
のようなメソッド呼び出しで取得できるようにします。
そのためには、
- BaseURL が https://api.github.com であり、
- listRepos(String user) に応じて、
users/{user}/repos
という path にアクセスし({user}
がString user
に応じて置き換わる)、 - 結果を
List<Repo>
という形で受け取る (GSON を使って変換)Repo
というのは、結果の JSON オブジェクトの変換先である Java クラス
と利用者が宣言します。
インタフェイス GitHubService の宣言が下の二つを担当します。
public interface GitHubService {
@GET("users/{user}/repos") // path の作り方
Call<List<Repo>> listRepos(@Path("user") String user); // 引数が path に組み込まれることと、返り値型やメソッド名の宣言
}
BaseURL の指定は、Retrofit オブジェクト生成の段階で指定し、上記インタフェイスのクラスオブジェクト(GitHubService.class
)を引数に与えることで、サービスを生成しています。結果を GSON で List<Repo>
に変換するので、その指定も行っています(addConverterFactory
))。
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create()) // GSON 変換を行う際に利用
.build();
GitHubService service = retrofit.create(GitHubService.class);
あとは、以下で呼び出し準備完了です。
Call<List<Repo>> repos = service.listRepos("plham");
この段階では、まだ Web API にリクエストは送っておらず、このあと呼出しをおこないます。だから、Call
というクラス名なんでしょうね。
結果の取得
Web API 呼出しはネットを介した呼び出しです。失敗するかもしれませんし、時間がかかるかもしれません。例えば、GUI スレッドから呼び出すのには向かない処理です。
Retrofit は同期/非同期呼び出しの両方に対応しています。
- 同期呼出し(synchronous call): 結果が返るのを待つ
- 非同期呼出し(synchronous call): 結果が返るのを待たない
まずは同期呼出しの場合
Call<List<Repo>> repos = service.listRepos("plham");
Response<List<Repo>> response = repos.execute(); // 同期呼出し
if (response.isSuccessful()) {
List<Repo> result = response.body();
System.out.println("Success (Sync): " + result);
} else {
System.out.println("Failed (Sync): " + response.message());
}
呼出しが成功するとは限らないので、response.isSuccessful()
で成否を確認してから、結果を取り出しています。普通ですね。
GSON によって、ちゃんと JSON 文書がオブジェクトに変換されているのが偉いですね。
さて、もう一方の非同期呼び出しは、Callback という仕掛けを使っています。
System.out.println("Start Async Call");
Call<List<Repo>> repos2 = service.listRepos("plham");
repos2.enqueue(new Callback<List<Repo>>() { // callback はここから
@Override
public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) {
if (response.isSuccessful()) { // 結果が返ったときの処理
List<Repo> result = response.body();
System.out.println("Success (Async): " + result);
} else {
System.out.println("Failed (with Response, Async): " + response.message());
}
System.out.println("The program will shutdown...");
System.exit(0);
}
@Override
public void onFailure(Call<List<Repo>> call, Throwable t) {
System.out.println("Failed (without Response, Async.): " + t.getMessage());
// throw t; で例外発生させても良い
}
}); // callback はここまで。
System.out.println("Don't wait the completion!"); // (多分)callback 前に処理される
enqueue()
の段階でリクエストの発行(依頼)をおこなうが、結果は待たないで、代わりに結果が返ってきた際の処理をおこなう Callback を渡す。Callback
はインタフェイスで、結果が返ってきたときに呼ばれるonResponse
と、返ってこなかったときに呼ばれるonFailure
を提供結果が返ってきた
といっても、「失敗(例えば、対応するアカウントないよとか)」を意味する返事が返ることもある
enqueue()
の段階では結果を待たないので、callback の実行より、次の行のprintln("Dont..."
の方が(多分)先に実行される。onResponse()
が終わった段階でプログラムが完了なので、System.exit(0);
で終了させています。
プログラムの制御の流れが一見して見にくくなってしまいましたね。 まあでも、イベント駆動的にプログラムを処理している場合はあまり問題ないかと思います。 一方で、一連の外部問い合わせを処理を記述している場合は、callback の中で別のリクエストを発行して、その処理の callback を書いてと、結構なスパゲティになることもあります。 このあたりは、少しスレッド編で振り返ることにします。
さて、callback ですが、誰が実行しているかというと、callback などの処理用のスレッドが起動され、Executor として稼働しています。
ということで、例えば callback に応じて Model や View を修正したい場合も、GUI に処理を依頼する必要があります(参考)。
一方で、このスレッドはデーモン指定されていないようです。ですから、このプログラム例では、System.exit(0);
でプログラムを終了させています。普通の GUI アプリなどの場合、ユーザからの指示に応じてプログラムが終了するのが普通なので、あまり問題にならないと思います。