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