10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

コピペでできる! アスペクト指向プログラミング(Android)

Last updated at Posted at 2018-02-05

はじめに

例えば、メソッドの開始と終了時にログを出したいとして、それを全てのメソッドに書くことは現実的でしょうか。
パフォーマンス測定のために処理時間を計測するために特定のメソッドにその処理を埋め込むとして、そのためにコードを修正したいでしょうか。
どちらもNoですよね。
アスペクト指向プログラミングはそれらを「比較的簡単に」解決する手段を提供するものです。
これを組み込むことで、関心ごとに集中できるわけです。

いろいろ記事を探しましたが、日本語でいい感じに書かれている記事が見つけられなかったので、こちら をもとに記事を書きます。
基本的には参考ページに則ったコードになります。

アスペクト指向プログラミング

アスペクト指向プラグラミングとは何か、ということについては割愛させていただきます。
学問的な内容や用語などは他の記事や文献を参考にしてください。

導入手順

ここから、具体的に動くものを作るための手順を解説します。

その前に、この記事ではプロジェクト内にソースコードを含める方法をとります。
別のプロジェクトとして切り出して、ライブラリとしてプロジェクトに追加する場合は適宜読み替えてください。
読み換える必要があるのは build.gradle を書き換えるところだけだと思います。

そのための方法を次の2種類ご説明します。

  1. プラグインを使用する方法
  2. AspectJを生で使用する方法

前者もAspectJを使用しますが、面倒な設定をプラグインがしてくれます。
そのため、設定は簡単です。
後者はAspectJをそのまま使用するので、ビルドするためにapp/build.gradleにビルドするための処理を書く必要があります。

目印作成

まず、処理を割り込ませるための目印を作ります。
目印はアノテーションです。

DebugTraceBefore.java
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.METHOD })
public @interface DebugTraceBefore {}
DebugTraceAround.java
@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(...) に指定するのはパッケージ名をフルで指定する必要があります。

AspectDebugLog.java
@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;
    }
}

画面作成

続いて画面を作成します。

MainActivity.java

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 を書き換えます。

プラグインを使用する方法

プラグインを使用する方法ではこちらを使用する例を紹介します。

GradleAspectJ-Android

app/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を生で使用する方法

途中コメントアウトもありますが、それは削除しても大丈夫です。

app/build.gradle
// ここから追加
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
10
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?