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

ソフトウェア開発に関する話を書きます。 最近は主に 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:サンプルコードなのでエラー処理などちゃんと行っていません。