ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

【Retrofit を読む】 利用者が定義したインターフェイスに実装を提供する Java ライブラリの作り方 【リフクレション】

この記事は、はてなエンジニアアドベントカレンダー 2014 の 15 日目のエントリです。 昨日は id:chris4403 による 「開発合宿で何を考えてどう作ったか」 でした。

このエントリでは、Android アプリおよび Java アプリケーション用の REST クライアントライブラリである Retrofit のコードを参照しながら、利用者が定義したインターフェイスの実装を提供するようなライブラリの実装方法について説明します。 主に Java のリフレクションの話になります。

注意点など

  • 本記事中に掲載されている Retrofit のコードは、Apache License, Version 2.0 のもとで公開されているものです。
  • 記事執筆時点の master ブランチの最新のコミットを参照しています。
  • Android アプリ開発者で Retrofit のコードを読みたい人は Android Studio で Retrofit のプロジェクトを開くのが楽で良いと思います。

Retrofit の紹介

Retrofit について簡単に紹介します。 詳しい話は公式サイトの方を見てください。

Retrofit は Square によって開発されている REST クライアントライブラリです。 HTTP リクエストを実行するためのメソッドを持ったインターフェイスを定義すると、Retrofit がそのインターフェイスの実装を提供してくれます。 アノテーションにより、HTTP メソッドの指定やどの引数がなんのパラメータであるか、あるいはリクエストボディであるかといったことを指定できます。

下記は Groovy スクリプトはてなハイクの公開タイムラインを取得する REST クライアントのサンプルコード *1 です。 HTTP メソッドの種類とリクエスト先のパスを GET アノテーション で指定しています。 そして、リクエストごとにクエリパラメータを変更できるように、クエリパラメータの値を引数として受け取るようになっています。

@Grab('com.squareup.retrofit:retrofit:1.8.0')

import retrofit.RestAdapter
import retrofit.http.GET
import retrofit.http.Query

// HTTP リクエストを投げるメソッドを持つインターフェイスを定義する。
interface HaikuService {
    /** はてなハイクの公開タイムラインを取得する */
    @GET("/api/statuses/public_timeline.json")
    List<?> getPublicTimeline(@Query("page") int page, @Query("count") int count)
}

// 定義したインターフェイスの実装を取得。
RestAdapter restAdapter = new RestAdapter.Builder()
    .setEndpoint("http://h.hatena.ne.jp")
    .build()
HaikuService haiku = restAdapter.create(HaikuService.class)

// 取得した実装を使用してハイクの公開タイムラインの内容を表示。
def statuses = haiku.getPublicTimeline(1, 10)
for (def status : statuses) {
    println '---'
    println status.user.name
    println status.text
}

利用者側がインターフェイスを定義して、そのインターフェイスの実装をライブラリが提供する、というのが特徴的ですね。

なお、HTTP 通信の実装や、リクエストボディおよびレスポンスボディと Java オブジェクトの変換機能の実装については Retrofit 自体が持っているわけではありません。 (デフォルトで HTTP 通信の実装はプラットフォームに応じたものが使われ、変換機能には Gson が使われますが、好みに応じてそれらは変更できます。)

利用者が定義したインターフェイスに対して実装を提供するために必要な技術

さて、Retrofit のように利用者が定義したインターフェイスに対して実装を提供するようなライブラリを書くことを考えましょう。 Java では、リフレクションを使用することで実現可能です。

任意のインターフェイスに対して実装を提供するために、Proxy クラス を使用することができます。 そして、実行されたメソッドの情報を取得するために、リフレクションのための各種メソッドを使用できます。 この 2 つについて順番に説明します。

Proxy クラス

Proxy クラスは、動的プロキシクラス (実行時に指定されたインターフェイスを実装するクラス) を生成するためのクラスであり、また、動的クラスのスーパークラスにもなるものです。 実際のメソッド呼び出しの処理は InvocationHandler インターフェイス を実装したクラスのインスタンスが担います。 Proxy クラスを使うことで、実行時に任意のインターフェイスに実装を提供することができます。

Retrofit では、retrofit.RestAdapter#create(Class) メソッド の中で動的プロキシクラスのインスタンス生成が行われます。

  /** Create an implementation of the API defined by the specified {@code service} interface. */
  @SuppressWarnings("unchecked")
  public <T> T create(Class<T> service) {
    Utils.validateServiceClass(service);
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new RestHandler(getMethodInfoCache(service)));
  }

retrofit.RestAdapter#create(Class<T>) メソッド

上記コードを見ればわかるように、指定されたインターフェイスの実際のメソッド呼び出しを処理するのは retrofit.RestAdapter.RestHandler クラスです。

リフレクションによるメソッド情報の取得

メソッドが呼ばれた際に実行される HTTP リクエストの内容は、各メソッドに付けられたアノテーションや返り値の型、仮引数の情報によって決まります。 すなわち、メソッドの情報を実行時に見る必要があります。 メソッドの情報を実行時に得るには、Method クラス に定義されている各種メソッドが使用できます。

Retrofit ではどのような処理になっているのか見ていきましょう。 retrofit.RestAdapter.RestHandler#invoke(Object, Method, Object[]) メソッドの実装は次のようになっています。

  @SuppressWarnings("unchecked") //
  @Override public Object invoke(Object proxy, Method method, final Object[] args)
  throws Throwable {
    /* (略) */
    // Load or create the details cache for the current method.
    final RestMethodInfo methodInfo = getMethodInfo(methodDetailsCache, method);
    if (methodInfo.isSynchronous) {
      try {
        return invokeRequest(requestInterceptor, methodInfo, args);
      } catch (RetrofitError error) {
    /* (略) */

retrofit.RestAdapter.RestHandler#invoke(Object, Method, Object[]) メソッド

まず、getMethodInfo(Method) メソッド呼び出しにより指定のメソッドの情報を RestMethodInfo オブジェクトとして受け取り、その後 invokeRequest メソッドを呼び出して実際の HTTP リクエスト処理に入ります。 実際には、同期実行か非同期実行か、あるいは RxJava を使っているかで処理が分かれたりしますが、ここではそこら辺の詳細には立ち入りません。

例えば、HTTP リクエストの結果の受け取り方は RestMethodInfo#parseResponseType() メソッドで解析されます。

  private ResponseType parseResponseType() {
    // Synchronous methods have a non-void return type.
    // Observable methods have a return type of Observable.
    Type returnType = method.getGenericReturnType();

    // Asynchronous methods should have a Callback type as the last argument.
    Type lastArgType = null;
    Class<?> lastArgClass = null;
    Type[] parameterTypes = method.getGenericParameterTypes();
    if (parameterTypes.length > 0) {
      Type typeToCheck = parameterTypes[parameterTypes.length - 1];
      lastArgType = typeToCheck;
      if (typeToCheck instanceof ParameterizedType) {
        typeToCheck = ((ParameterizedType) typeToCheck).getRawType();
      }
      if (typeToCheck instanceof Class) {
        lastArgClass = (Class<?>) typeToCheck;
      }
    }
    /* (略) */

retrofit.RestMethodInfo#parseResponseType() メソッド

同期実行の場合はメソッドの返り値として HTTP リクエストの結果が返され、非同期実行の場合はコールバックオブジェクトが引数に渡されるようになっているので、下記のメソッドを呼ぶことでメソッドの返り値と仮引数との両方を確認しています。

他にも、メソッドに付けられたアノテーション一覧を取得するために getAnnotations() メソッド が使われたり、仮引数につけられたアノテーションを取得するために getParameterAnnotations() メソッド が使われたりしています。

まとめと参考文献

  • Proxy クラスと InvocationHandler インターフェイスを使用することで、任意のインターフェイスに対して動的プロキシ・クラスを生成することができる。
  • リフレクションにより、メソッドの返り値の型などの情報を実行時に取得することができる。
  • これらの機能を使うことで、利用者が定義したインターフェイスに実装を提供するライブラリを記述できる。

リフレクションについては次のドキュメントを参考にすると良いでしょう。

おわりに

この記事では、Retrofit の実装を参照しながら Java のリフレクション機能について紹介しました。 バグの原因になりがちなのでアプリケーションコード中で直接リフレクションを使用するのはできるだけ避けた方がいいと思いますが、リフレクションにより Retrofit のような便利なライブラリを実現することも可能ですので、いい感じに使っていきたいですね。

*1:サンプルコードなのでエラー処理などちゃんと行っていません。

google-http-java-client 入門

Java で HTTP 通信するときのクライアントライブラリを何にするかいつも悩むのですが、最近 google-http-java-client が気になってたのでちょっと使ってみました。 汎用的に HTTP 通信ができればよい、というような用途にはちょうど良さそうです。

数年前からベータ版や RC 版としては存在していましたが、正式にリリースされたのは今年のようです。

google-http-java-client について

Google によって書かれた Java の HTTP クライアントライブラリです。

HTTP トランスポートの抽象化がされており、実際の HTTP 通信を行う低層のライブラリを選択できるのが特徴です。 例えば java.net.HttpURLConnection を使ったり、Apache HTTP Client を使ったりできます。

また、リクエストやレスポンスのコンテンツボディの XMLJSON のパースやシリアライズを行う機能も含まれており、便利です。

使用できる環境は、Java 5 以降の Java SE 環境や Java EE 環境、Android 1.5 以降などです。

ざっくりとした使い方

準備

使用するためには JAR ファイルをライブラリパスに追加するとか、Maven の依存関係管理に追加するとかする必要があります。 2014 年 11 月 8 日現在の最新バージョンは 1.19.0 で、Maven Central Repository に置かれています。

Gradle を使っているならば次のように依存関係を記述すればよいです。

repositories {
    mavenCentral()
}

dependencies {
    compile 'com.google.http-client:google-http-client:1.19.0'
}

HTTP リクエストを行う処理の全体の流れ

  1. HttpTransport オブジェクトを用意する。
    • これはアプリケーション全体で 1 つだけ存在すればよい。
  2. HttpTransportcreateRequestFactory メソッドを呼んで HttpRequestFactory オブジェクトを生成する。
  3. HttpRequestFactory から HttpRequest を生成する。
  4. HttpRequestexecute メソッドを呼び出してリクエスト実行、レスポンスとして HttpResponse を取得。
  5. レスポンスを処理したあと、終了処理。

サンプルコード

GET リクエストを投げてレスポンスボディを表示する例を Gist に置いてあります。 バージョン 1.19.0 を使用しています。

レスポンスのパース

HttpRequest#setParser メソッド を使って ObjectParser インターフェイスを実装したインスタンスをパーサーとして登録しておくと、レスポンスのパースを任せることができます。

ライブラリに含まれている ObjectParser の実装としては、JsonObjectParser クラスUrlEncodedParser クラス があります。

サンプルコード

/* 必要な import 文
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.UrlEncodedParser;
import com.google.api.client.util.GenericData;
*/

// HttpRequest オブジェクトにパーサーを設定しておく。
// (req は HttpRequest オブジェクト)
req.setParser(new UrlEncodedParser());
// リクエスト実行。
HttpResponse res = req.execute();
try {
    // レスポンスのパースを行う。 (上で設定した UrlEncodedParser が使われる。)
    GenericData d = res.parseAs(GenericData.class);
} finally {
    // (以下略)

レスポンスのパース後のクラスを作る

上の例ではパース後のクラスとして GenericData を使用しましたが、どういうパラメータが渡ってくるかわかっている場合、それを受け取るためのクラスを用意しておいて、フィールドに値を設定させることもできます。

例えば、OAuth 1.0 Protocol の Temporary Credentials を取得するためのリクエストのレスポンスをパースする場合を考えてみましょう。 レスポンスの形式は 「oauth_token=xxxxx&oauth_token_secret=xxxxx&oauth_callback_confirmed=true」 というものであることはわかっているので、次のようなクラスでレスポンスを受け取ることができます。 @Key アノテーション をフィールドに付けることで、パースした結果を受け取るフィールドであることを示しています。

/* 必要な import 文
import com.google.api.client.util.GenericData;
import com.google.api.client.util.Key;
*/

// 必ずしも GenericData を継承する必要はないが、継承しておけばフィールドで定義されていないパラメータも受け取ることができる。
public class OAuthTemporaryCredentialResponse extends GenericData {
    @Key("oauth_token")
    public String identifier;
    @Key("oauth_token_secret")
    public String sharedSecret;
    @Key("oauth_callback_confirmed")
    public String callbackConfirmed;
}

そして、パース時にこのクラスを指定することでパース結果を OAuthTemporaryCredentialResponse オブジェクトとして受け取ることができます。

    OAuthTemporaryCredentialResponse d = res.parseAs(OAuthTemporaryCredentialResponse.class);
    System.out.println("identifier: " + d.identifier); // フィールドアクセスによりレスポンスの値にアクセスできる。

関連

RecyclerView と view type について (Android アプリ開発)

このエントリでは、ListView の進化版とも言われる *1 RecyclerView の view type について簡単に紹介します。 RecyclerView 自体については次のページを参照してください。

View type とは

View type が何であるかの公式的な説明は見当たらなかったのですが、要は 1 つの RecyclerView の要素として複数種類の View を使い分けるための仕組みです。

例えば、RecyclerView の中でコンテンツと見出しの表示要素 *2 を混在させたい場合、コンテンツと見出しを別の view type として扱うことで出し分けが容易になります。

他にも、リストの最後の項目として 「続きを読む」 というような項目を表示したい場合も通常の view type とは別の view type の項目として扱えば良いですね。

各項目の view type の指定方法

各項目の view type を指定するには、RecyclerView.Adapter#getItemViewType(int) メソッドをサブクラスでオーバーライドします。 デフォルト実装は常に 0 を返すというものですので、RecyclerView の中で単一種類の表示項目しか扱わない場合はオーバーライドする必要はありません。

View type の値は int 型です。 ドキュメントには、衝突することを避けるために id リソースを使うことを検討するように、と書かれています。

View type ごとの View の生成

RecyclerView.Adapter#onCreateViewHolder(ViewGroup, int) メソッド の第 2 引数として view type の値が渡されてきます。 なので、このメソッドの中で生成する view を view type ごとに変化させ、ViewHolder にセットすることになります。

ViewHolder が保持している view の view type を知る方法

RecyclerView.ViewHolder#getItemViewType() メソッド が、ViewHolder が保持している view の view type を返してくれます。 なので、RecyclerView.Adapter#onBindViewHolder(VH, int) メソッドの中などで view type を知りたいときはこのメソッドを使いましょう。

サンプルコード

複数 view type を使う Adapter のサンプルコードです。 (とりあえず動くという程度の簡単な実装です。)

package info.vividcode.android.example;

import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class MyRecyclerViewAdapter extends RecyclerView.Adapter<MyRecyclerViewAdapter.ViewHolder> {

    public static class ViewHolder extends RecyclerView.ViewHolder {
        public ViewHolder(View itemView) {
            super(itemView);
        }
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // view type に応じて生成する view を変える。
        // 今回はサンプルコードなので手軽に両方とも TextView にしている。
        // 実際にはここで生成する View を違うものにする。
        View v;
        if (viewType == 0) {
            v = new TextView(parent.getContext());
        } else {
            v = new TextView(parent.getContext());
        }
        return new ViewHolder(v);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        // view type に応じて処理を分ける。
        // 今回はどちらも TextView なので単にセットする文字列を変えている。
        if (holder.getItemViewType() == 0) {
            ((TextView) holder.itemView).setText("Even: " + position);
        } else {
            ((TextView) holder.itemView).setText("Odd: " + position);
        }
    }

    @Override
    public int getItemViewType(int position) {
        // サンプルコードなので手軽に position が偶数の項目と奇数の項目で view type を分ける。
        return position % 2;
    }

    @Override
    public int getItemCount() {
        return 10;
    }

}

これを Activityメソッド内で以下のようにして使えばとりあえず動きます。

    // recyclerView はどこかで既に定義されているとする。
    LayoutManager layoutManager = new LinearLayoutManager(this);
    recyclerView.setLayoutManager(layoutManager);
    recyclerView.setAdapter(new MyRecyclerViewAdapter());

*1:「ListView2」 とか呼ばれてるのをたまに目にします。 実際には ListView の代わりとして以外にも使えます

*2:当然ながら View の構造は別物になるはずですね

Cookpad と Zaim のオフィスにお邪魔してきました

東京に行く機会があったので Cookpad と Zaim のオフィスにお邪魔してきました。

場所は恵比寿ガーデンプレイスです。 (最近オフィスの移転がありました。) Google Maps で調べたら恵比寿駅から徒歩 8 分ぐらいでちょっと遠いかと思っていたのですが、実際に行ってみると駅からガーデンプレイスまで歩く歩道の設置された通路 (恵比寿スカイウォーク) があって、さほど距離は感じません。

f:id:nobuoka:20141008134437j:plain

おしゃれなレンガ造りの建物がある公園という感じで恵比寿ガーデンプレイスはなかなかいい場所でした!

f:id:nobuoka:20141008102404j:plain

Cookpad のオフィスは @rejaspotaro さんに案内してもらいました。 Cookpad のオフィスといえばキッチンがあることで有名ですが、話には聞いていても本物を見ると 「オフィスにこんなキッチンがあるのすごいなー」 と思わざるを得ませんでした。 開放的な広々した空間に大きな冷蔵庫やキッチンがどーんとあるのを見ると羨ましい限り! 家にもああいうキッチンが欲しいですねー。

オフィス内にはシャワールームやトレーニング器具もあって、あとは洗濯さえできればオフィスに住めるやん、って感じです。 開発者のいる部屋は間仕切りなしの広々した空間になっていて、そこにデスクがばーっと並んでいて人の多さを実感しました。 他に気になった場所は本棚のあるライブラリで、そこで勉強会などができるようになっていました。 本棚が結構小さかったので本は全部入るのかなーと心配したり。

Cookpad の見学の後は @rejaspotaro さんと一緒に Zaim のオフィスにお邪魔して閑歳さんやスタッフの方とお話させて頂きました。 働きだした年に閑歳さんのインタビュー記事を読んで、「働きながら個人プロジェクトを成功させている人もいるんだ!」 と思ったのが印象に残っているので、今回お会いできてうれしかったです。

Cookpad の会社内にオフィスがあるとは聞いていたのですが、思った以上に Cookpad との行き来が容易で驚きました。 人によるとのことでしたが、Cookpad の方との交流も多いようです。

その後は Cookpad と Zaim の Android アプリエンジニアの方々と技術交流。 私が Android アプリ開発からちょっと遠ざかっているのでこちらからあまり面白い話ができなかったのは申し訳ないところですが、トラッキング周りの課題の話やら、オーケストレーション層を設けて複数の HTTP リクエストをまとめたいというような話ができて参考になりました。 あと 「いやー、新しくやりたいことはあるんですけど人が足りなくてねー」、「奇遇ですねー、うちもですよー」 みたいな話をしたり。 「ソフトウェアエンジニアの転職は人づてになりがちだからそういう方向が良さそう?」、「とはいえソーシャル活動してない人で優秀な人もいるし、そういう人に興味を持ってもらうためには Wantedly なども効果があるかもしれない?」 とかとか。 難しいところです。

最後にお昼ご飯をご一緒させて頂きました。 ガーデンプレイスには食べるところがいろいろあっていいですね。 今回は 「関西から来てお好み焼きなんw」 と突っ込まれつつタワー最上階近くの千房でお好み焼き。 美味しゅうございました。

お世話になった皆様ありがとうございました。

ソフトウェアエンジニアを募集しています!

文中にも書きましたが、ソフトウェアエンジニア募集中とのことです! お世話になったので紹介します! 恵比寿ガーデンプレイスはなかなか働きやすそうな場所でしたよー。

もちろんはてなもソフトウェアエンジニアを募集しています! 京都だけでなく東京開発センターもあります! 興味のある方は是非お気軽に私にご連絡ください!!

発表資料: Android アプリ開発における Gradle ビルドシステム (京都 Android 勉強会 2014.08)

去る 8 月 23 日に株式会社はてな主催で行われた Android アプリ開発の勉強会 「京都 Android 勉強会 2014.08」 にて、Android アプリ開発と Gradle について喋ってきました。

Android Studio ではビルドシステムとして Gradle が採用されていますので、今後 Gradle を使う人は増えていくと思います。 Android Studio でビルドをするだけであればそれほど Gradle に詳しくなくても問題ないわけですが、せっかくなのでいろいろ便利に使っていきましょう、という主旨の発表でした。

発表内容は下のような感じで、Gradle を使ったことがない人にも 「Gradle でこういうことができる」 というのが伝わるように喋ったつもりです。 また、後半の方は Gradle を使って Android アプリのビルドをした人でも触ったことがない人もいると思いますので、Gradle を使っている人にも参考になったかもしれません。

  • Gradle と Android アプリのビルド
  • Gradle や Android Gradle plugin の機能の一部を紹介
    • ここら辺は知らなくても問題なく開発できるはずではあります
  • AAR パッケージについて
  • Gradle プラグインについて

勉強会では、Cookpad の @rejaspotaro さんが 「debug ビルドの時だけデバッグに便利なメニューを表示するようにしている」 とか、「社内でいくつか Gradle プラグインを作って便利に使っている」 という話を先にされていたので、そこら辺とからめて Gradle の話ができたのも良かったところです。

質疑内容

会場で 2 つほど (確か 2 つ) 質問がありましたので、ここにも書いておきます。

Gradle wrapper の更新

Gradle wrapper を使う場合、Gradle のバージョン更新はした方がよいのか、という質問でした。

Android Studio のバージョンアップにともなって必要な Gradle のバージョンも変わってきますので、Android Studio のバージョンアップに合わせてプロジェクト内の Gradle (Gradle wrapper) のバージョンも更新する必要があります。 最近の Android Studio では、Gradle のバージョンが古い場合に警告を出してくれて、GUI 上でぽちぽち操作すると自動的にバージョンが更新されるはずです。

Gradle の欠点

発表資料の中で 「Groovy のことを理解するまでデバッグ等が大変」 と書いていますが、それ以外に不便な点などはあるか、という質問でした。

Gradle 自身の欠点というわけではないですが、Eclipse での Android アプリのビルドと比べて、一部を変更してビルドし直すというのが Android Studio でのビルド (あるいは直接コマンドライン上で Gradle を使った場合のビルド) では時間がかかってしまうというのがあります。 私は今のところあまり細かい変更でビルドし直すということをしないようにすることでなんとかしていますが、UI の変更などでは細かく調整したいということもあってなかなか不便ということでした。

Gradle をオフラインモードにすると速くなるという話もあります。 (ちゃんと試してません。)