JavassistでAndroidメタプログラミングする
三行まとめ
詳細
Gradleはビルドプロセスのカスタマイズがしやすい。たとえば、クラスファイルを生成したあと、dexファイルにコンパイルする前にクラスファイルを編集するということも簡単にできる。これをJavassistでやってみた。
まず、project rootに buldSrc/ をつくり、それをビルドスクリプトのプロジェクトとする。
buildSrc/build.gradleで、buildSrc/ がGradle pluginであることと、javassistを使用することを宣言する。
apply plugin: 'groovy' repositories {     mavenCentral() } dependencies {     compile gradleApi()     compile localGroovy()     compile 'org.javassist:javassist:3.18.+' }
つぎに、buildSrc/src/main/groovyにgroovyでコードを書く。ちゃんとしたpluginにはあとでするとして、今はエントリポイントをひとつ書けば十分だ。
内容は、とりあえずMainActivity#onResume()の最後でログを吐くコードを注入するだけにした。このように、Javaのソースコードを注入するとJavassistはそれをバイトコードにコンパイルして、それを操作対象のバイトコードに注入する。それを再びクラスファイルに書き戻す。
package com.github.gfx.javassistexamp import javassist.ClassPool import javassist.CtClass import javassist.CtMethod public class JavassistExample { public static void process(String buildDir) { ClassPool classes = ClassPool.getDefault() classes.appendClassPath("/usr/local/opt/android-sdk/platforms/android-19/android.jar") classes.appendClassPath(buildDir) CtClass c = classes.getCtClass("com.github.gfx.javassistexample.app.MainActivity") CtMethod m = c.getDeclaredMethod("onCreate") m.insertAfter("android.util.Log.d(\"XXX\", \"hoge\");") // コードを注入する c.writeFile(buildDir) } }
このJavasistExample.process()を、app/build.gradleから呼び出す設定をして実行すると、注入したコードが実行される様子を観察できるはず。
task('processWithJavassist') << { //String path = file('build/classes/debug/com/github/gfx/javassistexample/app/MainActivity.class') String classPath = file('build/classes/debug') com.github.gfx.javassistexamp.JavassistExample.process(classPath) } android { // ... applicationVariants.all { variant -> variant.dex.dependsOn << processWithJavassist } }
実行可能なコードはgithubに置いた。
JavassitExampleはコードが雑だったりandroid.jarのパスをハードコードしていたりして課題はあるが、とりあえずクラスファイルの操作はうまく行って、AndroidでもJavassistを使えることを確認できたのでよしとしよう。