-GroovyスクリプトAdvice

AdviceをJavaでハードコードするだけでなく、スクリプトでかけるとテストやデバッグ時にちょっと楽になることもあるかと思い、作ってみました。はじめはBSFを検討していましたが、Groovyが流行っているみたいでもありますので、Groovyをスクリプトホストとして作りました。以下です。

ShellAdvice.java
package org.seasar.groovy;
import java.io.*;
import java.util.*;
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import org.seasar.framework.aop.AroundAdvice;
import org.seasar.framework.aop.Joinpoint;
import org.seasar.framework.util.ResourceUtil;
public class ShellAdvice implements AroundAdvice {
  // GroovyShellをプールするための内部クラス
  private class ShellHolder {
    private Binding binding;
    private GroovyShell shell;
    private Set variables;
    ShellHolder() {
      variables = new HashSet();
      binding = new Binding();
      // 標準出力オブジェクトはデフォルトでホスト
      binding.setVariable("out", System.out);
      shell = new GroovyShell(binding);
    }
    void addVariable(String name, Object variable) {
      // スクリプト変数追加
      binding.setVariable(name, variable);
      variables.add(name);
    }
    void clear() {
      Map bindMap = binding.getVariables();
      for(Iterator it = variables.iterator(); it.hasNext();) {
        // 追加スクリプト変数を削除する
        bindMap.remove(it.next());
      }
    }
    Object evaluate(String script) throws Throwable {
      if( (script == null) || (script.length() == 0) ) {
        // FIXME: create new exception type.
        throw new Exception("script is null/0");
      }
      // スクリプト実行!
      return shell.evaluate(script);
    }
  }
  private int poolsize;
  private String script;
  private Stack pool = new Stack();
  public ShellAdvice() {
    this(10);
  }
  public ShellAdvice(int poolsize) {
    this.poolsize = poolsize;  
  }
  public void addScript(String script) {
    this.script += script;    
  }
  public void setScriptFile(String path) throws IOException {
    // 外部ファイルを読み込む
    InputStream scriptStream =
      ResourceUtil.getResourceAsStream(path, "txt");
    BufferedReader reader =
      new BufferedReader(new InputStreamReader(scriptStream));
    StringBuffer buffer = new StringBuffer();
    for(String line = reader.readLine(); line != null;) {
       buffer.append(line);
       line = reader.readLine();
    }
    script = buffer.toString();
  }
  private ShellHolder popShell() {
    // プールのハンドラ
    ShellHolder shell = null;
    try {
      shell = (ShellHolder)pool.pop();
    } catch(EmptyStackException e) {
      shell = new ShellHolder();
    } finally {
      return shell;
    }
  }
  private void pushShell(ShellHolder shell) {
    // プールのハンドラ
    int size = pool.size();
    if(size < poolsize) {
      shell.clear();
      pool.push(shell);
    }
  }
  // これが大事なメソッド
  public Object invoke(Joinpoint joinpoint) throws Throwable {
    ShellHolder shell = popShell();
    // Joinpointをスクリプト変数にする
    shell.addVariable("joinpoint", joinpoint);
    // スクリプト実行!
    Object obj = shell.evaluate(script);
    pushShell(shell);
    return obj;
  }
}

ソースはほとんどがGroovyShellをプールする仕組みで、大事なところはinvoke()だけ。ちょびっとです。設定はこんな感じです。

car.xml
<?xml version="1.0" encoding="UTF-8"?>
<components>
    <component class="tutorial.org.seasar.console.HelloCar">
    <aspect pointcut="run">
      <component class="org.seasar.groovy.ShellAdvice">
        <property name="scriptFile">
          'sample/org/seasar/script.txt'
        </property>
      </component>
    </aspect>
  </component>
</components>

スクリプトはscriptFileプロパティに指定したパスの外部ファイル、もしくはaddScriptで直接書きます。ファイルにはこんな感じでスクリプトを書けます。

script.txt
out.println("before fire");
Object obj = joinpoint.proceed();
out.println("after fire");
return obj;

Groovyって初めて触りましたが、結構手軽に使えますね。次は、スクリプト設定ファイルをXMLで書いて、メソッドをオーバーロードするような仕組み作ろうかと思います。そうなるとBSFのほうがJavaScriptJythonもOKだからいいかも。