tokuhirom's blog

aspectj の post compile weaving を gradle で行う方法

spring boot の起動が極めて遅くて辛いなと感じていたところ、どうやら spring aop が極めて多くの時間を浪費しているということが判明した。 spring aop をオフにすると起動時間が16秒なのに対し、オンにすると 26 秒に増える。しかも、利用しているエンドポイントの数に比例しているようで、これは今後さらに遅くなりそうだ。。

調べてみると spring aop は load time weaving を採用していることがわかった。 ref. http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html

AOP には3種類のやり方がある。

  • Compile-time weaving
    • ajc という eclipse java compiler を modified した版で .java を .class にコンパイルする際に AOP コードをねじ込む
    • lombok は ajc をサポートしていない。
  • Post compile weaving
    • ajc を利用するが、.class から .class への変換を行う。
  • load time weaving
    • 実行時に DI コンテナからの bean 取得時などに、proxy object を挟むことによって、AOP を実現する
    • JDK dynamic proxies を使う方法と CGLib 等によるバイトコード生成を利用する方法がある
    • JDK dynamic proxy は interface には適用可能だが class に対しては適用できない。
    • 実行時にオーバーヘッドがかかる。特に起動時にダミークラス挟まなきゃいけないし、ビーンのロード時にいちいちチェックしないといけない。

ref. https://eclipse.org/aspectj/doc/next/devguide/ltw.html

このうち compile-time weaving は、lombok を利用できないために却下。

そういうわけで、Post compile weaving を試してみた。 しかし、gradle で post compile weaving を利用する方法は、なかなか見つからない。 見つからないのでゴリゴリと書いた。

gradle では、プロジェクト内にプラグインを置くことができるので、aspectj プラグインを書く。 ディレクトリ構成は以下のようにする。

buildSrc
├── build.gradle
└──c
    └── main
        ├── groovy
        │   └── aspectj
        │       └── AspectJPlugin.groovy
        └── resour
            └── METAF
                └── gradle-plugins
                    └── aspectj.properties

resources/META-INF/gradle-plugins/aspectj.properties に、implementation-class=aspectj.AspectJPlugin と書くことで、aspectj plugin の実装がどこにあるのかを指定する。

src/main/groovy/aspetctj/AspectJPlugin.groovy は、class AspectJPlugin implements org.gradle.api.Plugin<org.gradle.api.Project> { } のように、プラグインクラスを継承した、 クラスを配置する。このPluginクラスの void apply(Project project) メソッドが、apply plugin: 'aspectj' された時に呼ばれるので、ここにコードを記載していく。

spring security に入っていた aspectj plugin をベースに書いたが、もはや原型はとどめていないコードがこちらになります。 ``` package aspectj

import org.aspectj.bridge.IMessage import org.aspectj.bridge.MessageHandler import org.aspectj.tools.ajc.Main import org.gradle.api.DefaultTask import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.plugins.JavaPlugin import org.gradle.api.tasks.TaskAction

import java.nio.file.Path

// taken from spring security. // https://github.com/spring-projects/spring-security/blob/master/buildSrc/src/main/groovy/aspectj/AspectJPlugin.groovy /** * * @author Luke Taylor */ class AspectJPlugin implements Plugin {

void apply(Project project) {
    project.plugins.apply(JavaPlugin)

    if (!project.hasProperty('aspectjVersion')) {
        throw new GradleException("You must set the property 'aspectjVersion' before applying the aspectj plugin")
    }

    if (project.configurations.findByName('ajtools') == null) {
        project.configurations.create('ajtools')
        project.dependencies {
            ajtools "org.aspectj:aspectjtools:${project.aspectjVersion}"
            compile "org.aspectj:aspectjrt:${project.aspectjVersion}"
        }
    }

    project.afterEvaluate {

        project.tasks.create(name: 'compileAspect', overwrite: true, description: 'Compiles AspectJ Source', type: Ajc) {
            dependsOn project.processResources, project.compileJava

            tmpDir = "${project.buildDir}/aspect/"
            args = [
                    "-inpath", project.sourceSets.main.output.classesDir.toPath(),
                    "-showWeaveInfo",
                    "-1.8",
                    "-d", tmpDir,
                    "-classpath", project.sourceSets.main.compileClasspath.asPath,
            ];
            dstDir = project.sourceSets.main.output.classesDir.toPath()
        }
        project.tasks.classes.dependsOn project.tasks.compileAspect

        project.tasks.create(name: 'compileTestAspect', overwrite: true, description: 'Compiles AspectJ Test Source', type: Ajc) {
            dependsOn project.processTestResources, project.compileTestJava

            tmpDir = "${project.buildDir}/test-aspect/"
            def classpath = project.sourceSets.test.compileClasspath.files.grep({ it.exists() }).join(":")
            args = [
                    "-inpath", project.sourceSets.test.output.classesDir.toPath(),
                    "-aspectpath", project.sourceSets.main.output.classesDir.toPath(),
                    "-aspectpath", project.sourceSets.test.output.classesDir.toPath(),
                    "-showWeaveInfo",
                    "-1.8",
                    "-d", tmpDir,
                    "-classpath", classpath
            ];
            dstDir = project.sourceSets.test.output.classesDir.toPath()
        }
        project.tasks.testClasses.dependsOn project.tasks.compileTestAspect
    }
}

}

class Ajc extends DefaultTask { String[] args String tmpDir Path dstDir

Ajc() {
    logging.captureStandardOutput(LogLevel.INFO)
}

//http://www.eclipse.org/aspectj/doc/released/devguide/ajc-ref.html

// http://stackoverflow.com/questions/3660547/apt-and-aop-in-the-same-project-using-maven
// https://github.com/uPhyca/gradle-android-aspectj-plugin/blob/8d580d8117932a23209421193da77f175d19d416/plugin/src/main/groovy/com/uphyca/gradle/android/AspectjCompile.groovy
@TaskAction
def compile() {
    logger.info("Running ajc ...")

    MessageHandler handler = new MessageHandler(false);
    logger.info("args: $args")
    new Main().run(args as String[], handler);
    for (IMessage message : handler.getMessages(null, true)) {
        switch (message.getKind()) {
            case IMessage.ABORT:
            case IMessage.ERROR:
            case IMessage.FAIL:
                logger.error message.message, message.thrown
                break;
            case IMessage.WARNING:
                logger.warn message.message, message.thrown
                break;
            case IMessage.INFO:
                logger.info message.message, message.thrown
                break;
            case IMessage.DEBUG:
                logger.debug message.message, message.thrown
                break;
        }
    }

    ant.move(file: tmpDir, tofile: dstDir)
}

} ```

ネットで情報を gradle+aspectj の例を探していると、aspectj の ant task を利用する方法がいくつか出てくるが、aspectj の ant plugin の記述方法を元に gradle で ant task を呼ぶのは、 デバッグしにくいので難しい。aspectj の Main を呼ぶ方法が便利だと思う。 (もちろん、Main を呼ぶのはdocumentedなインターフェースではないので、壊れる可能性があるが、壊れたら別の Main を呼べばいいだけ)

この Main を直接叩く方法は、gradle の android で aspectj するプラグインを参考にした。

まあそういう感じでできるようになったので良かったね、と。

で、load time weaving についても、こんなに遅いわけもないと思うから、もうちょい頑張れば速くなるのかもしれないけど知見がまったくないです。

Created: 2016-05-02 14:24:54 +0900
Updated: 2016-05-02 14:24:54 +0900