はじめに
例えば、メソッドの開始と終了時にログを出したいとして、それを全てのメソッドに書くことは現実的でしょうか。
パフォーマンス測定のために処理時間を計測するために特定のメソッドにその処理を埋め込むとして、そのためにコードを修正したいでしょうか。
どちらもNoですよね。
アスペクト指向プログラミングはそれらを「比較的簡単に」解決する手段を提供するものです。
これを組み込むことで、関心ごとに集中できるわけです。
いろいろ記事を探しましたが、日本語でいい感じに書かれている記事が見つけられなかったので、こちら をもとに記事を書きます。
基本的には参考ページに則ったコードになります。
アスペクト指向プログラミング
アスペクト指向プラグラミングとは何か、ということについては割愛させていただきます。
学問的な内容や用語などは他の記事や文献を参考にしてください。
導入手順
ここから、具体的に動くものを作るための手順を解説します。
その前に、この記事ではプロジェクト内にソースコードを含める方法をとります。
別のプロジェクトとして切り出して、ライブラリとしてプロジェクトに追加する場合は適宜読み替えてください。
読み換える必要があるのは build.gradle を書き換えるところだけだと思います。
そのための方法を次の2種類ご説明します。
- プラグインを使用する方法
- AspectJを生で使用する方法
前者もAspectJを使用しますが、面倒な設定をプラグインがしてくれます。
そのため、設定は簡単です。
後者はAspectJをそのまま使用するので、ビルドするためにapp/build.gradleにビルドするための処理を書く必要があります。
目印作成
まず、処理を割り込ませるための目印を作ります。
目印はアノテーションです。
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.METHOD })
public @interface DebugTraceBefore {}
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.METHOD })
public @interface DebugTraceAround {}
アノテーションのRetentionやTargetについては別の記事を参照ください。
目印を2種類用意しました。
DebugTraceBefore
はこのアノテーションをセットしたメソッドの前に、
DebugTraceAround
はこのアノテーションをセットしたメソッドの前後に処理を割り込ませるようにします。
アノテーションの名前は任意です。
割り込み処理実装
次に、どんな処理を割り込ませるのかを書きます。
今回はログを出力する処理を書きます。
@Aspect
はアスペクト指向プログラミングによって割り込ませる処理はここに書かれていますよ、という目印です。
@Pointcut
では処理を割り込ませる目印をセットします。
仮にこのアノテーションをセットしたメソッドを ポイントカットメソッド とでも呼びましょう。
@Before
や @After
、@Around
はどこに処理を割り込ませるかを示しており、どのポイントカットメソッドに対して割り込ませるのかを指定します。
メソッド名は任意ですが、わかりやすい名前をつけるといいと思います。
DebugTraceBefore
などはアノテーションなので、 execution(...)
の中で @
をつけています。
指定のクラスのメソッドであれば、execution(void android.app.Activity.onCreate(..))
のように指定することも可能です。
execution(...)
に指定するのはパッケージ名をフルで指定する必要があります。
@Aspect
public final class AspectDebugLog {
private static final String POINTCUT_BEFORE_METHOD =
"execution(@com.test.aspectorientationprogrammingsample.aspect.DebugTraceBefore * *(..))";
private static final String POINTCUT_AROUND_METHOD =
"execution(@com.test.aspectorientationprogrammingsample.aspect.DebugTraceAround * *(..))";
@Pointcut(POINTCUT_BEFORE_METHOD)
public void pointcutDebugTraceBefore() {}
@Pointcut(POINTCUT_AROUND_METHOD)
public void pointcutDebugTraceAround() {}
@Pointcut("execution(void android.app.Activity.onCreate(..))")
public void pointcutOnCreate() {}
@Before("pointcutOnCreate()")
public void weavePreOnCreate(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className = methodSignature.getDeclaringType().getSimpleName();
Log.d("Aspect", "### weavePreOnCreate: " + className);
}
@After("pointcutOnCreate()")
public void weavePostOnCreate(JoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className = methodSignature.getDeclaringType().getSimpleName();
Log.d("Aspect", "### weavePostOnCreate: " + className);
}
@Before("pointcutDebugTraceBefore()")
public void weaveDebugTraceBefore(JoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className = methodSignature.getDeclaringType().getSimpleName();
String methodName = methodSignature.getName();
Log.d("Aspect", "### weaveDebugTraceBefore: " + className + " " + methodName);
}
@Around("pointcutDebugTraceAround()")
public Object weaveDebugTraceAround(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className = methodSignature.getDeclaringType().getSimpleName();
String methodName = methodSignature.getName();
Log.d("Aspect", "### weaveDebugTraceAround - start: " + className + " " + methodName);
Object result = joinPoint.proceed();
Log.d("Aspect", "### weaveDebugTraceAround - end: " + className + " " + methodName);
return result;
}
}
画面作成
続いて画面を作成します。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.button1).setOnClickListener(
new View.OnClickListener() {
@Override
@DebugTraceAround
public void onClick(View view) {
Log.d("Log", "### onClick: button1");
}
});
findViewById(R.id.button2).setOnClickListener(
new View.OnClickListener() {
@Override
@DebugTraceBefore
public void onClick(View view) {
Log.d("Log", "### onClick: button2");
}
});
}
// @Override
// public void onResume() {
// super.onResume();
// }
@Override
@DebugTraceBefore
public void onPause() {
super.onPause();
}
}
ボタンが2個あるだけの画面です。
onCreate(...)
には今回作成したアノテーションを設定していません。
onPause()
の前にログ出力をするために DebugTraceBefore
を指定しています。
ボタンクリック時にもログを出すためにアノテーションを設定しました。
さて、Javaのコードを書くのはここまでです。
ここからは build.gradle を書き換えます。
プラグインを使用する方法
プラグインを使用する方法ではこちらを使用する例を紹介します。
// ここから追加
buildscript {
repositories {
jcenter()
mavenCentral()
maven { url "https://jitpack.io" }
}
dependencies {
classpath 'com.github.Archinamon:GradleAspectJ-Android:3.0.3'
}
}
// ここまで追加
apply plugin: 'com.android.application'
apply plugin: 'com.archinamon.aspectj'// 追加
android {
compileSdkVersion 27
buildToolsVersion "27.0.1"
defaultConfig {
applicationId "com.test.aspectorientationprogrammingsample"
minSdkVersion 21
targetSdkVersion 27
versionCode 1
versionName "1.0.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
multiDexEnabled true
minifyEnabled true // true: 難読化を実施する(難読化しないクラス、メソッド等の設定は適宜必要)、false: 難読化実施しない
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:27.+'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
}
AspectJを生で使用する方法
途中コメントアウトもありますが、それは削除しても大丈夫です。
// ここから追加
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
buildscript {
repositories {
jcenter()
mavenCentral()
}
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.1'
}
}
// ここまで追加
apply plugin: 'com.android.application'
android {
compileSdkVersion 27
buildToolsVersion "27.0.1"
defaultConfig {
applicationId "com.test.aspectorientationprogrammingsample"
minSdkVersion 21
targetSdkVersion 27
versionCode 1
versionName "1.0.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
multiDexEnabled true
minifyEnabled true // true: 難読化を実施する(難読化しないクラス、メソッド等の設定は適宜必要)、false: 難読化実施しない
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// ここから追加
applicationVariants.all { variant ->
// JavaCompile javaCompile = variant.javaCompile// use javaCompiler instead of javaCompile
// javaCompile.doLast {
// //
// }
variant.javaCompiler.doLast {
String[] args = ["-showWeaveInfo",
"-1.8",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(
File.pathSeparator)]
MessageHandler handler = new MessageHandler(true);
new Main().run(args, handler)
def log = project.logger
for (IMessage message : handler.getMessages(null, true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
break;
case IMessage.WARNING:
case IMessage.INFO:
log.info message.message, message.thrown
break;
case IMessage.DEBUG:
log.debug message.message, message.thrown
break;
}
}
}
}
// ここまで追加
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:27.+'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
compile 'org.aspectj:aspectjrt:1.8.1'// 追加
}
実行
これを実行すると、次のようにログが出力されます。
(読みづらくなるので時間などは削除してます。)
D/Aspect: ### weavePreOnCreate: MainActivity
D/Aspect: ### weavePostOnCreate: MainActivity
D/Aspect: ### weaveDebugTraceBefore: MainActivity onPause
D/Aspect: ### weaveDebugTraceAround - start: onClick
D/Log: ### onClick: button1
D/Aspect: ### weaveDebugTraceAround - end: onClick
D/Aspect: ### weaveDebugTraceBefore: onClick
D/Log: ### onClick: button2
さいごに
私の環境では動くことも確認したのですが、みなさんの環境ではいかがだったでしょうか。
動きましたでしょうか。
1点注意点としては、 execution(void android.app.Activity.onCreate(..))
を execution(void android.app.Activity.onResume())
としてもログは出力されません。
なぜなら、MyActivityはonResumeをオーバーライドしていないからです。
実装されていないメソッドのログは出力されない。
当たり前ですね。
これで余計なロジックを外に切り出せますね!
Appendix
環境
以下の環境で動作を確認しています。
環境 | バージョン |
---|---|
Mac OS | Sierra 10.12.6 |
Android Studio | 2.3.3 |
Java version | 8 |
Gradle version | 3.3 |
Android Plugin version | 2.3.3 |