かとじゅんの技術日誌

技術の話をするところ

AOPの使い方

AOPとは何か?

http://s2container.seasar.org/ja/aop.html

ここにはこういう説明があります。

AOPとは、Aspect Oriented Programming (アスペクト指向プログラミング) の略です。
プログラム本来の目的とは異なる処理を内部に埋め込まず、外から織り込むように作ることです。

開発者はプログラム本来の目的だけに集中したいのですが、異なる処理、つまりロギングやトランザクション管理などにも気を配る必要があります。これはこれまでの開発の常識だったと思います。AOPでこの問題に対処できます。

またこのような説明もあります。

オブジェクト指向では顧客からの要求である機能(Core Concern)とロギング機能、宣言的トランザクション、DBコネクションの取得・解放、例外処理、セキュリティ機能や分散処理などの非機能要求 (Crosscutting Concern)が同じクラスの同じメソッドの中に実装されることがあります。

Core ConcernとCrosscutting Concernという概念を分離して考えましょうということですね。AOPではCore ConcernにCrosscutting Concernを織り込ませることが可能になり、ロギングやトランザクションを自動的に管理することが可能になります。

とにかくAOPを使ってみる

Seasar2にはS2AOPというAOP機能が実装されています。これを使ってみましょう。

S2AOPではIntercepterというクラスがいくつか用意されています。もちろん、自分で独自に実装もできますが、ここではまず使うところから。

まず、AOPがない場合のロギング処理を考えてみましょう。
public class Sample {
  private static Logger log = Logger.getLogger(Sample.class);
  public int doCalc(int a, int b){
    
    log.debug("Sample.doCalc Start"); // -> Crosscutting concern
    
    int result = a + b; // -> Core concern
    
    log.debug("Sample.doCalc End : result = " + result); // -> Crosscutting concern
    
    return result;
  }
}

このようにログ出力処理といのはいやっちゅうほど書いたことありますが、みなさんも経験あると思います。あちこちのメソッドに入れます。このようなコードを書いててかったりーなと思うことありませんでしたか?私はむちゃくちゃあります。
しかし、きつい作業ですがロギングはないと不具合の解析に不可欠なのでなかなか実装しないというわけにはいきません。これが泥臭いところですね。
これがAOPになると素晴らしく簡単に実装できます。

S2AOPを使えば、アプリ全体のロギング処理はさくっと適用できます

AOPを使うので、クラスにはCore concernだけの処理でよいことになります。ロギング処理はさっくり消します。

public class Sample {

  public int doCalc(int a, int b){
    
    int result = a + b; // -> Core concern
 
    return result;
  }
}

diconファイルに、Sampleを登録します。
その際にaspectタグを使いSeasar2標準で提供されているトレースインターセプタを適用します。(pointcutはアスペクト対象のメソッド名を正規表現で記述できます。指定しない場合はすべてのメソッドにAOPがかかります)

<component name="sample" class="Sample">
    <aspect pointcut="doCalc">
        <component class="org.seasar.framework.aop.interceptors.TraceInterceptor"/>
    </aspect>
</component>

コンテナからSampleを取り出しdoCalcを実行してみます。

public class AopTraceClient {
    public static void main(String[] args) {
        S2Container container = S2ContainerFactory.create(PATH);
        Sample sample = (Sample) container.getComponent(Sample.class);
        int result = sample.doCalc(100, 100);
        System.out.println(result);
    }
}

そうすると、ログには以下のように出力されます。

BEGIN Sample#doCalc()
END Sample#doCalc() : 200

このようにトレースインターセプタをSampleに割り当てることによってロギング処理を織り込ませることが可能になります。これをアプリでロギングが必要なクラスに設定すれば、アプリ全体としてのログイン処理の実装はものすごく簡単にできます。ここでもAOPの助けを借りることでコード量が激減することが分かってもらえたと思います。もう、ちまちまロギングコードを書いている時代ではないのです。

AOPトランザクションも自動管理できます

今回はロギング処理だったわけですが、トランザクションのBEGIN, COMMIT, ROLLBACKについても同じようにAOPで自動的に管理できます。メソッドの始まりでBEGIN, 終わりでCOMMIT, 例外がスローされたらROLLBACKです。
http://s2container.seasar.org/ja/tx.html
このあたりに説明がありますが、トランザクションをかけたいメソッドに、aspectタグでインターセプタを指定するだけでトランザクションは自動的に管理されるようになります。

たとえば、以下ようにページからDaoを使ってDBにINSERTやUPDATEを行う際は、Seasar2によって自動的にトランザクションを管理できます。
doUpdateに対してトランザクション管理のインターセプタを割り当てますので、doUpdateが開始したらBEGIN, 終了したらCOMMIT, 例外が発生したらROLLBACKが自動的に実行されるようになります。
なのでトランザクション管理は手動でやらずAOPにお任せになります。

public class EditUserConfigPage {

  public void doUpdate() { // --> BEGIN

    userConfig = userConfigDxo.convert(this);
    userConfigDao.update(userConfig); // --> どこかで例外スローされるとROLLBACK

  } // --> COMMIT

}

独自にインターセプタを実装してみる

次は独自にインターセプタを実装する例を紹介します。
たとえば、ウェブアプリで必ず必要となる認証処理ですが、通常だと、ページを開く前に必ず未認証ならログインページに遷移して、認証済みならページを表示するという処理をコントローラのロジックに書く必要があります。(Servlet Filterという手もあります)
しかし、これもコントローラのロジックを扱うクラスにAOPで認証チェック用のインターセプタを割り当てるだけ済みます。
具体的には、これもLoginInterceptorというインターセプタを独自に定義して、それをクラスのメソッドに割り当てるだけです。

Teedaの場合で説明しますと、以下のようなコードになります。

// HTML top.htmlに対応するサーバサイドのコントローラのロジックです。
public class TopPage {

  // ページを開くとき一度だけ呼ばれるメソッド。
  // このメソッドにLoginInterceptorを割り当てて認証チェックさせます。
  @Aspect("LoginInterceptor")
  public String initialize() {
    return null;
  }

}

// ログイン中のユーザ情報を保持するDTO(Data Transfer Object)
// インスタンスはセッションで管理されます。
@Component(instance = InstanceType.SESSION)
public class CurrentUserDto {
  
  // ログイン中のユーザID
  private String userId;

  // trueは認証済み、falseは未認証
  private boolean authed;

  public String getUserId() {
    return userId;
  }

  public boolean isAuthed() {
    return authed;
  }

  public void setAuthed(boolean isAuthed) {
    this.authed = isAuthed;
  }

  public void setUserId(String userId) {
    this.userId = userId;
  }
}

public class LoginInterceptor implements MethodInterceptor {

  // TopPage#initializeが呼ばれる前にこちらのメソッドが呼ばれます
 // そしてログイン済みかどうかチェックし未認証ならログイン画面に遷移し、
  // それ以外ならinvocation.proceed()で本来の処理、ここではTopPage#initialize
  // を呼び出しページを表示させます。
  public Object invoke(MethodInvocation invocation) throws Throwable {
    if (currentUserDto != null) {
      if (currentUserDto.isAuthed() == true) {
        return invocation.proceed();
      }
    }
    return "userLogin";
  }

  private CurrentUserDto currentUserDto;
  
  // セッションからユーザ情報を受け取ります
  public void setCurrentUserDto(CurrentUserDto dto) {
    this.currentUserDto = dto;
  }

}

というように、認証かけたいページのメソッドに@Aspect("LoginInterceptor")を適用するだけで済みます。(これはTigerのアノテーションですが、diconファイルにaspectタグで指定もできます)

AOPの仕組み

この仕組みを見てえーそんなことなんでできるの?って不思議に感じた方多いと思いますが、S2AOPはリフレクション機能を使って実現しています。深い話になりますが、Javaで標準で提供されているリフレクション機能はクラスを外部から分析する機能しかないのですが、Javassistというライブラリを使うと既存のクラスにメソッドやプロパティを追加/修正がバイトコードレベルで可能になります。クラス構造自体を変更できるリフレクション機能ということで、「構造リフレクション」というそうです。

だから上記のようなことも簡単にできてしまうわけですね。しかも、バイトコードレベルで織り込むのでパフォーマンス的にも優れています。S2AOPを使えばその機能にさらに簡単に利用できるわけです。ほんと素晴らしい技術だと思います。

参考リンク:

Javassist
Seasar2以外に、JBOSS AOPにもAOPエンジンとして取り込まれています。

Javassist入門

OOエンジニアの輪! 〜 第 35 回 千葉 滋 さんの巻 〜

論文:Javaバイトコード変換による構造リフレクションの実現

というわけで、プログラマのみなさん、AOPを使って楽してください。