読者です 読者をやめる 読者になる 読者になる

感謝のプログラミング 10000時間

たどり着いた結果(さき)は、感謝でした。

Spring frameworkでAOPを使うサンプルと、AOPの用語の紹介。

Spring
<スポンサーリンク>

AOPとは何か?

AOPAspect Oriented Programmingの略。アスペクト指向プログラミングという。
誤解されがちだが、オブジェクト指向と対をなすようなものではない。オブジェクト指向を補完し、さらにオブジェクト同士を疎結合にするものである。

アスペクト(Aspect)という英語は「側面」とか「様相」とかの意味がある。
アスペクト指向でいうアスペクトは、「ソフトウェアが持つ様々な特徴や性質」という意味。

で、アスペクト指向といえば、「横断的関心事の分離」である。英語でSeparation of Cross-Cutting Concernという。
1つのクラスが他の複数のクラスから参照されてしまうようなことを横断的であるという。
たとえば、例外の捕捉や、ロギングなどである。
オブジェクト指向によって、ロギングという機能を、ロギング機能を使うクラスから分離することに成功はしたものの、
ロギングメソッドの呼び出しをするにはやはり、Logger.logなどのコードを書く必要があった。
この辺は前回の記事を参照。

AOPでは分離した機能の「呼び出し」も「感心事」であると捉える。
このように各モジュールに散財する関心事を分離すること。つまり、各モジュールのコードから取り除くこと。
それがアスペクト指向でいう関心事の分離である。

AOPの用語

用語 説明
Aspect 横断的な関心事が持つ振る舞いと、その横断的な関心ごとを適用するソースコード上のポイントをまとめたもの。すなわち、AdviceとPointcutをまとめたもの。
Joinpoint 実行時の処理フローにおいて、Adviceを行う「時」すなわちポイント。Springではメソッドが呼び出された時と、呼び出し元に戻る時がAdviceを割りこませることが可能なJoinPointである。
Advice Joinpointで実行されるコード。アスペクトとして分離され、実行時にモジュールに差し込まれる具体的な処理のことをAOPではAdviceと呼ぶ。
Pointcut 1つ、もしくは複数のJoinpointを1つにまとめたものをPointcutと呼ぶ。Joinpointに達した時にAdviceを呼ぶかどうかをフィルタリングするのがpointcutである。

まず、AOPを使って、ロギング処理を分離した場合のコードのサンプルが、以下である。
前回の記事との差を出すために、わざとLoggerの処理をコメントアウトした。

package aop;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.FileSystemResource;

//import util.Logger;

public class MainAop {
    private final static String TARGET_XML = "aopbean.xml";
    public static void main(String[] args) {
        BeanFactory factory = new XmlBeanFactory(new FileSystemResource(TARGET_XML));
        GreetingService greetBean = (GreetingService) factory.getBean("proxy");
        //Logger logger = (Logger) factory.getBean("log");
        //logger.log("start!");
        greetBean.greet();
    }
}

前と違うところは、以下の部分だ。

GreetingService greetBean = (GreetingService) factory.getBean("proxy");

このように、GreetingServiceオブジェクトをproxyを介して参照しているところである。


では、proxyって何?というと、XMLに定義するものだ。
id="proxy"とある。
org.springframework.aop.framework.ProxyFactoryBeanのプロパティに、「こいつを実行するときにログを挿入したい!」と思っているオブジェクトのidを指定する。
で、id="advisor"のプロパティ「advice」に、ログの機能を実装しているオブジェクトのidを記入する。
patternというのは、「どんな名前のメソッドを実行した時に、このログを挿入するか、を設定する部分である。正規表現だ。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC
  "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
  <bean id="greet" class="aop.GreetingService">
    <property name="greeting">
        <value>Hello!everyone!</value>
    </property>
  </bean>
  <bean id="log" class="util.Logger">
    <property name="logFilePath">
        <value>C:\tmp</value>
    </property>
    <property name="logFileName">
        <value>log.txt</value>
    </property>
  </bean>
  
  <bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
    <!-- ID greetをターゲットとして参照するため -->
    <property name="target">
        <ref local="greet" />
    </property>
    <property name="interceptorNames">
        <list>
            <value>advisor</value>
        </list>
    </property>
  </bean>
  
  <bean id="advisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
    <!-- Advice(具体的な処理)として、id="log"を参照する -->
    <property name="advice">
        <ref local="log"></ref>
    </property>
    <property name="pointcut">
        <bean class="org.springframework.aop.support.JdkRegexpMethodPointcut">
            <property name="pattern">
                <value>.*greet.*</value>
            </property>
        </bean>
    </property>
  
  </bean>
</beans>

では、ログの処理を記載しているプログラムを見てみる。

package util;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class Logger implements MethodInterceptor {
    private String logFilePath;
    private String logFileName;
    
    public String getLogFilePath() {
        return logFilePath;
    }

    public void setLogFilePath(String logFilePath) {
        this.logFilePath = logFilePath;
    }

    public String getLogFileName() {
        return logFileName;
    }

    public void setLogFileName(String logFileName) {
        this.logFileName = logFileName;
    }

    public Object invoke(MethodInvocation invocation) throws Throwable {
        String methodName = invocation.getMethod().getName();
        log(methodName + "を実行します");
        
        log("ログの処理を挿入します");
        Object rtnObj = invocation.proceed();
        log(methodName + "を実行しました!");
        
        return rtnObj;
    }
    public void log(String message) {
        logFilePath = Utilities.checkSeparator(logFilePath);
        File file = new File(logFilePath + logFileName);
        PrintWriter pw = null;
        try {
            if (Utilities.fileCheck(file)) {
                pw = new PrintWriter(new BufferedWriter(new FileWriter(file,true)));
                pw.println(message);
                System.out.println(message + "というログを出力しています。");
                //ちゃんと明示的にcloseしないと、書き込みできないので注意。
                pw.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (pw != null) {
                pw.close();
            }
        }
    }
}

まず、MethodInterceptorをimplementsしている点が、前回と違う点だ。

public class Logger implements MethodInterceptor {

そして、以下のinvokeを書き足している。
mainメソッドが「greetBean.greet();」を実行する時に、invokeを呼び出している。
というか、greetBean.greetの処理は、invokeメソッドの中の、

Object rtnObj = invocation.proceed();

のときに実行される。
で、このinvokeメソッドの例では、proceedの前後にログの出力処理を挟み込んでみたわけだ。

    public Object invoke(MethodInvocation invocation) throws Throwable {
        String methodName = invocation.getMethod().getName();
        log(methodName + "を実行します");
        
        log("ログの処理を挿入します");
        Object rtnObj = invocation.proceed();
        log(methodName + "を実行しました!");
        
        return rtnObj;
    }

mainを実行すると、
標準出力にはこのように表示される。

greetを実行しますというログを出力しています。
ログの処理を挿入しますというログを出力しています。
Hello!everyone!
greetを実行しました!というログを出力しています。

log.txtには以下のようにしっかりメッセージが出力される。

greetを実行します
ログの処理を挿入します
greetを実行しました!

このように、使う側からlogging処理を分離することができたのである。
すなわち、mainにLogの処理を書かずに済んだ!
ということは、Loggerを変更しても、使う側には何の影響も無い!
ということだ。定義ファイルをいじるだけで良い。他のクラスを使う時も、ログの処理を書かずに済む。

ちなみにこんなエラーが出た時の解決策は・・・

Cannot proxy target class because CGLIB2 is not available. Add CGLIB to the class path or specify proxy interfaces.

1.Cglibのオフィシャルサイトに行く
http://cglib.sourceforge.net/

2.Downloadsで、cglib2を手に入れる。
(ここかな?↓)
http://sourceforge.net/projects/cglib/files/cglib2/2.2.3/

3.ダウンロードした「cglib-nodep-2.2.3.jar」をビルドパスに追加するとエラーは解決する!

「Could not find the main class」というメッセージが出たとき。
右上の「プロジェクト」→「クリーン」→「ワーキングセットのビルド」を実行。
プロジェクトを右クリックして「プロパティ」→「実行/デバッグ設定」で、実行したいmainクラスを指定する。
<参考>
http://sel.ist.osaka-u.ac.jp/~ishio/sd2004/note.html

勉強した本(参考文献)

SpringによるWebアプリケーションスーパーサンプル 第2版

SpringによるWebアプリケーションスーパーサンプル 第2版


基礎からちゃんと図解して解説してくれるため、AOPとかDIとか、Springを使う時に避けては通れない概念もしっかり頭に入る。
Spring3入門 ――Javaフレームワーク・より良い設計とアーキテクチャ

Spring3入門 ――Javaフレームワーク・より良い設計とアーキテクチャ


こちらも非常に良書で、口語調で頭に入ってきやすい。
アノテーションを使ったり、今のSpringにキャッチアップしている唯一の日本語書籍だと思う。
この2冊は必読だと思っているので、2冊とも常に手元に置いてプログラミングしている。

感謝のプログラミング

今回で感謝のプログラミングは【454時間目】
10000時間まで、あと【9546時間】