Java
AOP

アノテーションを使用してAOPをする

More than 3 years have passed since last update.


要約

実業務では、エラー発生時にログ解析ができるように、ログにメソッド名、引数を出力するというめんどっちい、非道いのになるとif文などの分岐ごとにログを出力する仕様が多々ある。

各クラス・メソッドごとにログを出力するソースを書くのではなく、アスペクト指向プログラミング(AOP)の考えに沿って、ログ出力のソースを一カ所にまとめようと思います。

クラス単位ではプロキシをそのまま使えばよいので、今回はアノテーションを使用して、メソッドごとにログを出力するかしないかを指定できるようにしたいと思います。

AOP箇所は「最もシンプルにJavaのAOPを書いてみる→そしてJavaScriptへ」を参考にしました。

また作成したソースはこちら


ログ出力の必要性を示すアノテーション

アノテーションの実践です。

ログを出力することを示すアノテーションとして、public @interface MethodAnnotationを定義しています。

@Retention(RetentionPolicy.RUNTIME)で実行時に使用されるアノテーションを示します。

@Target(ElementType.METHOD)でメソッドに使用されるアノテーションを示します。


AnnotationTest.java


import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;

public class AnnotationTest {

/**
* アノテーションの定義
* ・RetentionPolicy.RUNTIME: 実行時に有効になる
* ・ElementType.METHOD:メソッドに使用するアノテーション
*/

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodAnnotation{
}

/**
* アノテーション付きメソッド
*/

@MethodAnnotation
public void method(){
}

public static void main(String[] args)
         throws NoSuchMethodException, SecurityException {
Class<AnnotationTest> clazz = AnnotationTest.class;
Method method = clazz.getMethod("method");
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation: annotations){
System.out.println(annotation.toString());
}
}
}



アノテーションで機能を追加するクラスを定義する

機能を追加するクラスは二つ定義します。

・インターフェース

・実装クラス


Command.java

interface Command{

/**
* アノテーション、引数ありのメソッド
*/

@MethodAnnotation
void execute(String message);

/**
* アノテーション、戻り値ありのメソッド
*/

@MethodAnnotation
boolean booleanMethod();

/**
* アノテーションなし
*/

void voidMethod();
}


アノテーションはインターフェースにつけます。ここでは、

1. 引数があり、戻り値がないメソッド

2. 引数がなく、戻り値があるメソッド

3. アノテーションのないメソッド

の3種類定義しています。

引数が出力されること、戻り値が出力されること、アノテーションがないメソッドには機能が追加されないことを確認します。

上記インターフェースの実装クラスは以下のようです。


CommandImpl.java


class CommandImpl implements Command{

@Override
public void execute(String message) {
System.out.println("CommandImpl.execute呼び出し");
}

@Override
@MethodAnnotation
public boolean booleanMethod() {
System.out.println("CommandImpl.booleanMethod呼び出し");
return false;
}

@Override
@MethodAnnotation
public void voidMethod() {
System.out.println("CommandImpl.voidMethod呼び出し");
}
}


3つのメソッドを実装していますが、メッセージを標準出力に出すだけです。実際はここで、実処理を実装します。

voidMethod()にアノテーションをつけて、実装クラスにアノテーションをつけても影響がないことを確認します。


インターセプタの実装

処理を横取りして、処理を追加するクラスを作成します。

処理を追加したいインスタンスにプロキシ経由でアクセスするようにし、そのプロキシに追加したい処理を定義します。該当インスタンスのメソッドが呼ばれたときに、そのメソッドにアノテーションがついていた場合、本来の処理の前後に、追加処理を実施します。ついていなかった場合、通常の処理のみ実施します。

処理を追加したいインスタンスは、コンストラクタにて受け取りtargetに保存しています。


Intercepter

class Intercepter implements InvocationHandler{

// 処理を追加したいインスタンス
private Object target;
public Intercepter(Object target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// MethodAnnotationアノテーションがついていないメソッドは、追加処理せず、終了。
if (! Arrays.stream(method.getAnnotations()).anyMatch(p-> p instanceof MethodAnnotation)){
return method.invoke(target, args);
}
// 以下、MethodAnnotationアノテーションがついているメソッドは、処理を追加する
System.out.println("AOP処理開始");
// 引数の一覧を作成
StringBuilder sb = new StringBuilder();
if(args != null){
Arrays.stream(args).forEach(arg -> sb.append(arg.toString()).append(" "));
}
// メソッド名と引数を出力
System.out.println("呼び出しメソッド:" + method.getName() + " 引数:" + sb.toString());
// 実際に実施し、結果を保存する
Object result = method.invoke(target, args);
// 結果がnullでなければ、結果を出力する
if (result != null){
System.out.println("結果:" + result.toString());
}

// AOPの処理が完了したことを出力。空行も出力。
System.out.println("AOP処理完了");
System.out.println();
// 実施した結果を返し、通常の実施を同じにする
return result;
}
}



メイン文

上記のインターセプタを使用して、実行するメイン文です。


メイン文

public static void main(String[] args) {

AOP aop = new AOP();
Command commandImpl = aop.new CommandImpl();
Command proxy = (Command)Proxy.newProxyInstance(
Command.class.getClassLoader()
, commandImpl.getClass().getInterfaces()
, aop.new Intercepter(commandImpl));

proxy.execute("引数テスト");
proxy.booleanMethod();
proxy.voidMethod();
}


Proxy.newProxyInstanceでプロキシクラスを作成しています。引数にクラスローダ、インターフェースの一覧、インターセプタ(InvocationHandlerの実装)を指定します。

ちなみに、2つめの引数は、new Class[]{Command.class}でも可能です。この場合、指定できるのはインターフェースのみで、クラスを指定するとエラーが発生します。

プロキシを定義後、呼び出しています。

実行すると、以下の通り。アノテーションをインターフェースにつけたexecute,booleanMethodの前後にメッセージが追加されている。voidMethodにはついていない。


実行結果

AOP処理開始

呼び出しメソッド:execute 引数:引数テスト
CommandImpl.execute呼び出し
AOP処理完了

AOP処理開始
呼び出しメソッド:booleanMethod 引数:
CommandImpl.booleanMethod呼び出し
結果:false
AOP処理完了

CommandImpl.voidMethod呼び出し



考察

メリット

1. ログ出力のソースを一カ所にまとめられる

2. ログ出力の手段を各メソッドに書かなくて良い(ロジック内にログ出力のコードを交ぜなくて、ロジックに専念できる)

デメリット

1. 常にインターフェースとクラスの2種類作らなければならない

2. デバッグ実行したとき、色々とソースが飛ぶため、ソースを追い辛い

あと、プロキシを作成するのをうまくまとめられれば、良い手段になると思います。実業務で使用したときは1クラスにしか適用しなかったので、多くのクラスに適応させるとき、どのように実装するのが効率的なのかは考察しておりません。


まとめ

AOPは10年以上前から知っておりましたが、実際に使用するのは初めてでした。うまくやれば、非常にスマートに「機能」を実装できる、はず。