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種類のやり方がある。

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<Project> {

    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 についても、こんなに遅いわけもないと思うから、もうちょい頑張れば速くなるのかもしれないけど知見がまったくないです。