-S2GroovyAdvice

Groovyのスクリプト活用Adviceです。スクリプトでAdviceを書くのではなく、Adviceによってスクリプトで書いたコードをコンポーネントメソッドとするものです。インターフェイスの実装をAdviceでやるという、チュートリアルの次の回で説明する内容をどう説明しようか考えながら作ってみたものですが、もしかすると有用かもしれませんので、ご紹介します。まずAdvice本体です。

S2GroovyAdvice.java
package org.seasar.groovy;
import groovy.lang.Script;
import java.util.*;
import javax.xml.parsers.*;
import org.seasar.framework.aop.*;
import org.seasar.framework.util.SAXParserFactoryUtil;
import org.seasar.framework.xml.*;
import org.xml.sax.Attributes;

public class S2GroovyAdvice implements AroundAdvice {
  // XML中のscriptエレメントをパースするハンドラ
  private class ScriptHandler extends TagHandler {
    public void start(TagHandlerContext context, Attributes attributes) {
      String methodName = attributes.getValue("methodName");
      if(methodName != null) {
        methodName = methodName.trim();
        if(methodName.length() != 0) {
          ScriptHolder holder = new ScriptHolder();
          holderMap.put(methodName, holder);
          context.push(holder);
          return;
        }
      }
      throw new RuntimeException();
    }
    public void end(TagHandlerContext context, String body) {
      ScriptHolder holder = (ScriptHolder)context.pop();
      holder.setSource(body);
    }
  }

  // XML中のbindingエレメントをパースするハンドラ
  private class BindingHandler extends TagHandler {
    public void appendBody(TagHandlerContext context, String body) {
      if(body != null) {
        String arg = body.trim();
        if(arg.length() != 0) {
          ScriptHolder holder = (ScriptHolder)context.peek();
          holder.addArgName(body.trim());
          return;      
        }
      }
      throw new RuntimeException();
    }
  }

  private Map holderMap;
  private String path;

  public S2GroovyAdvice(String path) {
    holderMap = new HashMap();
    parseXML(path); 
  }

  // XMLをパースする
  private void parseXML(String path) {
    TagHandlerRule rule = new TagHandlerRule();
    rule.addTagHandler("/groovy/script", new ScriptHandler());
    rule.addTagHandler("/groovy/script/binding", new BindingHandler());
    SAXParserFactory factory = SAXParserFactoryUtil.newInstance();
    SAXParser saxParser = SAXParserFactoryUtil.newSAXParser(factory);
    SaxHandlerParser parser =
      new SaxHandlerParser(new SaxHandler(rule), saxParser);
    parser.parse(path);
  }

  // Adviceの実装
  public Object invoke(Joinpoint joinpoint) throws Throwable {
    ScriptHolder holder = 
      (ScriptHolder)holderMap.get(joinpoint.getMethod().getName());
    if(holder != null) {
      // XML中にメソッドに対するスクリプト定義があるとき
      Script script = holder.pop();
      holder.addVariable(script, "this", joinpoint.getTarget());
      holder.addVariable(script, "out", System.out);
      holder.addArgs(script, joinpoint.getArgs());
      Object obj = script.run();
      holder.push(script);
      return obj;
    } else {
      // スクリプト定義がないとき
      return joinpoint.proceed();
    }
  }
}

次に、Groovyスクリプトを実行するためのヘルパークラスです。しっかりパース済みのスクリプトをプールしています。

ScriptHolder .java
package org.seasar.groovy;
import groovy.lang.*;
import java.util.*;

public class ScriptHolder {
  private final int POOL_SIZE = 5;
  private List args = new ArrayList(); 
  private String source;
  private Stack stack = new Stack();

  public void addArgName(String arg) {
    args.add(arg);
  }

  public void setSource(String source) {
    this.source = source;
  }
  
  // プールからスクリプトを取り出す
  public Script pop() {
    Script script = null;
    try {
      script = (Script)stack.pop();
    } catch(EmptyStackException e) {
      script = new GroovyShell().parse(source);  
    } finally {
      return script;
    }
  }
  
  // プールへスクリプトを戻す
  public void push(Script script) {
    int size = stack.size();
    if(size < POOL_SIZE) {
      Binding binding = script.getBinding();
      Map bindMap = binding.getVariables();
      bindMap.clear();
      stack.push(script);
    }
  }
  
  // メソッド引数をバインドするヘルパーメソッド
  public void addArgs(Script script, Object[] variable) {
    Binding binding = script.getBinding();
    for(int i = 0; i < args.size(); i++) {
      binding.setVariable((String)args.get(i), variable[i]);
    }
  }
  
  // 個別に変数をバインドするヘルパーメソッド
  public void addVariable(Script script, String variable, Object object) {
    Binding binding = script.getBinding();
    binding.setVariable(variable, object);
  }
}

スクリプトはXMLで書きます。例です。<script>エレメントのmethodName属性にスクリプトでロジックを上書き実装するコンポーネントメソッド(あえて言うならJoinpoint)を記述します。エレメントボディにGroovyスクリプトを書きます。<binding&はコンポーネントメソッドの引数をスクリプト変数にバインドする際の名前です。引数の登場順に記述しなければなりません。

script.xml
<?xml version="1.0" encoding="UTF-8"?>
<groovy>
  <script methodName="say">
    <binding>name</binding>
    out.println("hello " + name);
  </script>
  <script methodName="cry">
    out.println("(;_;)");
  </script>
</groovy>

コンポーネントとして動かすための、インターフェイスの例です。

Speaker.java
package sample.org.seasar;
public interface Speaker {
  public String say(String name);
  public String cry();
}

最後に、設定XMLです。

<?xml version="1.0" encoding="UTF-8"?>
<components>
  <component class="sample.org.seasar.Speaker">
    <aspect>
      <component class="org.seasar.groovy.S2GroovyAdvice">
        <arg>'sample/org/seasar/script.xml'</arg>
      </component>
    </aspect>
  </component>
</components>

結果は、

hello masataka
(;_;)

<component>エレメントのclass属性には、インターフェイスである"Speaker"なのに、メソッドがコールでき、はたまたその内容が"S2GroovyAdvice"のコンストラクタ引数で渡した"script.xml"に書かれた内容というところを注意ください。チュートリアルでは、このメカニズムをもっと簡便なサンプルにて説明します。