Javaのリフレクションを使ったSDKテストアプリの開発




android-root


皆さん、こんにちは。AppDriverのAndroidSDKを担当しているチュンです。

最近、Javaのリフレクションを使ってSDKテストアプリを開発したので、今日はその内容について書きます。

AppDriverのSDK?


SDK (Software Development Kit)とは、ソフトウェア開発者のために開発に必要な機能をまとめた開発ツールのことです。
iOSAndroidアプリ開発者はAppDriverのSDKを通してAppDriverの機能を利用することになります。なのでAppDriverSDKはAppDriverの窓口みたいな感じですね。

SDKテストアプリ?


Q: テストアプリの目的?

A: 可能な限りAppDriver SDKの各機能が簡単にテストできることを目的とします。
また、ディレクター側は開発環境がなくてもテストアプリを使って、実機で動作確認テストができます。

Q: 簡単にテストできる? 

A: めっちゃ簡単です!基本の流れはこんな感じです。

  1. 必要なテストデータ(パラメータ)を入力
  2. テストしたい機能を選択
  3. テスト結果確認
テスト結果の確認は、logや画面の表示などですぐに確認ですますよ〜 
(アプリ内でも、Eclipse/Android Studioデバッグlogを表示します!)

現在のSDKテストアプリの問題点


現在、AppdriverSDKは様々なライブラリ、日本版、日本版(デバッグ機能付き)、アジア版、アジア版(デッバグ機能付き)・・などがあります。
今までのテストアプリの作り方だと、SDK毎にそれぞれテストアプリを作らないといけませんでした。

こんな感じですね。

mondai


めんどくさい! ですよね〜

テストするときもめんどくさいし、バージョン管理もめっちゃ複雑になっています〜

ひとつのテストアプリで、複数のSDKライブラリーをテストできたらいいのに・・・ と思って、SDKテストアプリの新しいバージョンを作成しました。

やりたいことは、こんな感じです。

kaiketsu


ひとつのアプリで、複数SDKライブラリーを動的に選択できるようにしたい!!!

ここで、Javaのリフレクションの出番です。
 

リフレクションとは?

日本語では自己言及と呼ばれる。通常リフレクションというと動的(実行時)リフレクションのことを指すが、静的(コンパイル時)リフレクションをサポートするプログラミング言語もある。リフレクションはSmalltalkJava.NET Frameworkのような仮想機械やインタプリタ上で実行されることを想定した言語でサポートされることが多く、C言語のような機械語として出力されることを想定した言語でサポートされることは少ない

 
 Javaでは、コンパイル時に決定するソースファイルに直接記述したクラス生成やメソッド呼び出しだけでなく、このリフレクションを使うと、プログラムの実行中にクラス名の文字列からクラスを生成したり、 メソッド名の文字列を使ってメソッドを呼び出したりすることが出来ます。

 素晴らしい技術ですね!

リフレクションの使い方 


Class cl = Class.forName("Foo");
Method method = cl.getMethod("hello");  
method.invoke(cl.newInstance());  

1行目では,Fooクラスをオブジェクトとして取得する。  
2行目では,Fooクラスのオブジェクトから、helloメソッドのオブジェクトを取得する。  
3行目では,Fooクラスのオブジェクトから、新たなFooクラスのインスタンスを生成し、helloメソッドを呼び出す。

お〜めっちゃシンプルじゃないですか!!!
 

実際のやり方


1 )
 まず、各ライブラリーを別のフォルダ(assets/)に入れておきます。
テストアプリをコンパイルするときに、一緒にコンパイルしないようにするためです。

reflection1



2 ) 各ライブラリの選択

Log.d(TAG, "Environment Mode ID : " + envId);

switch (envId) {
  case 0:
    JAR_FILE = "日本版.jar";
    break;
  case 1:
  case 2:
    JAR_FILE = "日本版_dev.jar";
    break;
  case 3:
    JAR_FILE = "アジア版.jar";
    break;
  case 4:
  case 5:
    JAR_FILE = "アジア版_dev.jar";
    break;
}

3 ) 選択したライブラリをassetsフォルダーから端末のローカルにコピーします。

private void copyAssetToLocal(final String inAssetJarFileName, final String outJarFileName) {

  BufferedInputStream in = null;
  BufferedOutputStream out = null;
  try {
    in = new BufferedInputStream(getAssets().open(inAssetJarFileName));
    out = new BufferedOutputStream(openFileOutput(outJarFileName, MODE_PRIVATE));

    int buffer;
    while ((buffer = in.read()) != -1) {
      out.write(buffer);
    }
    Log.d(TAG, "copyAssetToLocal success: " + outJarFileName);

  } catch (IOException e) {
    Log.d(TAG, "copyAssetToLocal failed:", e);
  } finally {
    try {
      if (out != null) {
        out.close();
      }
      if (in != null) {
        in.close();
      }
    } catch (IOException e) {
      Log.d(TAG, "copyAssetToLocal failed:", e);
    }
  }
}

4 ) リフレクション実装しましよう!! まず、使いたいクラス(xClass)をロードします。

private Class loadXClass(String jarName) {
  Class loadclass = null;
  try {
    final ClassLoader classLoader = new DexClassLoader(getFilesDir().getAbsolutePath() + "/" + jarName,
        getFilesDir().getAbsolutePath(), null, getClassLoader());
    loadclass = classLoader.loadClass("net.adways.appdriver.sdk.X");
    Log.d(TAG, "load X class success!" + jarName);

  } catch (ClassNotFoundException e) {
    Log.d(TAG, "load X failed!", e);
  }
  return loadclass;
}

5 ) 次は、使いたいメソッドをロードし、呼び出します。

private void yMethod(Activity context, int parameter1, String parameter2) {
  // いろいろやっています。
}

yMethodをロードして、呼び出しましょう!

private void executeYmethod(Class xClass, String jarName, int parameter1, String parameter2) {
  try {
    Object[] setupArgs = new Object[] { this, parameter1, parameter2 };
    final Method setupMethod = xClass.getMethod("yMethod", Activity.class, int.class, String.class);
    final Object xClassInst = xClass.newInstance();
    setupMethod.invoke(xClass, setupArgs);

  } catch (NoSuchMethodException e) {
    Log.d(TAG, "Execute yMethod failed:", e);
  } catch (InstantiationException e) {
    Log.d(TAG, "Execute yMethod failed:", e);
  } catch (IllegalAccessException e) {
    Log.d(TAG, "Execute yMethod failed:", e);
  } catch (IllegalArgumentException e) {
    Log.d(TAG, "Execute yMethod failed:", e);
  } catch (InvocationTargetException e) {
    Log.d(TAG, "Execute yMethod failed:", e);
  }

}

6) これで、ひとつのアプリの中で動的に複数のSDKライブラリを選択して、各メソッドをテストする機能が実装できました!!!

リフレクションの危険性
 

クラス設計を破壊するかもしれない!
リフレクションは、クラス設計者の意図に反したことが出来てしまう強力な機能です。
クラスは、オブジェクト指向の基本概念である「カプセル化」に沿って作成されています。
そのため、オブジェクトの使用者が外部からクラス(インスタンス)を操作するには、必ずアクセッサを使用しなければならないというルールが設けられますが、リフレクションはこのルールを無視してクラス(インスタンス)を直接操作することを可能にしてしまいます。
つまり,リフレクションは既存のクラス設計を破壊してしまう可能性があります!

コンパイル時のエラー検出対象外である!
リフレクションを使用した部分のコードは、コンパイラリファクタリングツールのチェック機能の対象外となり、コンパイル時にエラーを検出することができません。

「技術の犬小屋 Javaにおけるリフレクションについて」 より


まとめ


リフレクションを使用することは,上記で説明したような危険性を伴いますが,適所で使用する分にはとても便利です。もし機会があれば、ぜひ使ってみてください〜

Javaのリフレクションについては以上です。

Androidの開発を始めてから半年しかたっていませんが、これからもっと深掘りして勉強します。

面白そうなネタがあればまた上げていきますので、よろしくお願いします〜

以上です。SDKチームのチュンでした!