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を使えることを確認できたのでよしとしよう。