GroovyのClosureでトランザクション

GroovyにはClosureを使ってJDBCを簡単に扱うためのクラス(groovy.sql.Sqlとgroovy.sql.DataSet)がありますが、トランザクションに関してはそれらしきものがなかったので考えてみました。

Tx.java : 実はSeasar2のRequiredInterceptorをほんのちょっぴり変えただけだったりします。AbstractTxもS2のAbstractTxInterceptorと殆ど同じ。

package nt.groovy;

import groovy.lang.Closure;
import javax.transaction.TransactionManager;

public class Tx extends AbstractTx{

    public Tx(TransactionManager transactionManager) {
        super(transactionManager);
    }
    
    public Object required(Closure closure) throws Throwable {
        boolean began = false;
        if (!hasTransaction()) {
            begin();
            began = true;
        } 
        Object ret = null;
        try {
            ret = closure.call();
            if (began) {
                commit();
            }
            return ret;
        } catch (Throwable t) {
            if (began) {
                rollback();
            }
            throw t;
        }
    }
}

Main.groovy : Closureの中にDBにアクセスする処理を書いてます。S2ContainerはDataSourceやTransactionManagerをもったTxを簡単に取得するために使っています。

package nt.groovy
import org.seasar.framework.container.factory.*
import groovy.sql.*

class Main {
    static void main(args) {
       SingletonS2ContainerFactory.init();
       container = SingletonS2ContainerFactory.container
       try {
           ds = container.getComponent("dataSource")
           tx = container.getComponent("tx")                 
           tx.required() { 
              dataSet = new Sql(ds).dataSet("dept")
              dataSet.add(["deptno":100, "dname":"ABC"])
           }
       } finally {
           SingletonS2ContainerFactory.destroy()
       }
    }
}

app.dicon : Txクラスを指定。TxにTransactionManagerが自動バインディングされる。



	
	

素直にAOPを使ったほうがいいかもしれませんがまあClosureの勉強ということで。

Groovyで定義したクラスのインスタンス化

なんだかとてもひさびさにgroovyを触ってみました。
クラスパスが通っていればGroovyから別のファイルに定義してあるGroovyのクラスがインスタンス化できるんですね。知りませんでした。Eclipseでgroovyファイルをclasspathが通るとこにおいて、GroovyShellのmainメソッドの引数にスクリプトファイルのlocationを渡して呼ぶとサクっと動きました。(コマンドラインから動かすとiPersonクラスを作るところでワーニングが出る...。何が違うんだろ)

Hoge.groovy : mainメソッドをもちPersonをインスタンス化する。

package nt.groovy
import java.net.*
class Hoge {
    static main(args) {
        p = new Person(name:"taedium", url:new URL("http://d.hatena.ne.jp/taedium/"))
        println p.speak()
    }
}

Person.groovy : 2つのプロパティと1つのメソッドをもつ。

package nt.groovy
class Person {
    property name
    property url 
    String speak() {
        "My name is ${name}. My diary's ulr is '${url}'."
    }
}

結果

My name is taedium. My diary's ulr is 'http://d.hatena.ne.jp/taedium/'

中学生の英語みたいでちょっと例がかっこ悪いかも。

XmlS2ContainerBuilderにGroovyClassLoaderを渡す

Seasarコンポーネントにgroovyで定義したインスタンスを使いたい場合はS2GroovySeasarを使えばできますが、diconファイルを使ってもできそうです。S2ContainerBuilderのbuildメソッドにClassLoaderを引数にとるものがあるのでここにGroovyClassLoaderを渡してみます。

person.dicon : 通常のdiconファイルです。コンポーネントにnt.groovy.Personというクラス指定していますがこれは下のエントリで使ったgroovyスクリプトのクラスです。



  
    "taedium"
    new java.net.URL("http://d.hatena.ne.jp/taedium/")
  

Client.groovy : XmlS2ContainerBuilder#build(String, ClassLoader)の引数にdaiconファイルのpathとGroovyClassLoaderを渡しています。containerからgoovyのクラスを受け取ってメソッドを実行しています。

package nt.groovy
import org.seasar.framework.container.factory.XmlS2ContainerBuilder;

class Client {  
    private static final PATH = "nt/groovy/person.dicon"
    static void main(args) {
        builder = new XmlS2ContainerBuilder(); 
        container = builder.build(PATH, new GroovyClassLoader());
        person = container.getComponent("person");
        println person.speak()
    }
}

ここでClient.groovyを実行してみると、実はorg.seasar.framework.util.ClassUtil.forName(String)でClassNotFoundExceptionが起きてしまいます。JavaDocによるとClass.forName("Foo")はClass.forName("Foo", true, this.getClass().getClassLoader())と同じらしいです。Classクラスと同じクラスローダーでgroovyスクリプトをロードしようとするためクラスが見つからないのです。2ContainerBuilderに渡したGroovyClassLoaderを使うためにClassUtil.forName(String)をちょっと変更してみました。
ClassUtilの変更

public static Class forName(String className)
  throws ClassNotFoundRuntimeException {
    ClassLoader loader = Thread.currentThread().getContextClassLoader();
  try {
    return Class.forName(className, true, loader);
  } catch (ClassNotFoundException ex) {
    throw new ClassNotFoundRuntimeException(ex);
  }
}

Client.groovyを実行する。
結果

My name is taedium. My diary's ulr is 'http://d.hatena.ne.jp/taedium/'.

上の例ではコンテナを使う側(Client.groovy)をgroovyで書きましたがJavaでもOKですが、Javaで扱うにはgroovyのクラスがinterfaceを実装していないと扱いにくいと思います。ただ、interfaceをつかっておけば、当然実装をJavaにしたりgroovyにしたりできて、さらに同じ名前でクラス作っておけばdiconファイルも直す必要なしです。xxx.javaとxxx.groovyでソースファイルはかぶらないし使えるかも?groovyとclassファイルで全く同じ名前のクラスがあった場合はclassファイルが優先されるようです。

ClassUtil.forName(String)を変更したらどこかに悪影響を及ぼすかな?

GroovyDelegateInterceptor

なんか昨日の日記の追記と矛盾するけどgroovyスクリプトにインタフェースを実装させるのは面倒くさい。
interfaceをimplementsする場合、メソッドのシグネチャはインタフェースと厳密に一致しなければいけない。アクセス修飾子、戻り値、メソッド名、引数の型と数、throws宣言など。それにインタフェースで宣言されているメソッドすべてを実装しなければいけない。でもそんなにいっぱい書くのめんどくさい、せっかくスクリプトなのに。そこでDelegateInterceptor!diconからお手軽にgroovyスクリプトを呼び出そう。

package study.seasar;

import groovy.lang.GroovyClassLoader;

import java.io.File;
import java.io.IOException;

import org.codehaus.groovy.control.CompilationFailedException;
import org.seasar.framework.aop.interceptors.DelegateInterceptor;
import org.seasar.framework.exception.IORuntimeException;
import org.seasar.framework.exception.IllegalAccessRuntimeException;
import org.seasar.framework.exception.InstantiationRuntimeException;
import org.seasar.framework.util.ResourceUtil;

public class GroovyDelegateInterceptor extends DelegateInterceptor {

    private String path_;
    
    public GroovyDelegateInterceptor() {
    }
    
    public GroovyDelegateInterceptor(String path) {
        setGroovyScriptPath(path);
    }
    
    public Object getGroovyScriptPath() {
        return path_;
    }

    public void setGroovyScriptPath(String path) {
        path_ = path;
        Class clazz = createClass(path_);
        Object obj;
        try {
            obj = clazz.newInstance();
        } catch (InstantiationException ex) {
            throw new InstantiationRuntimeException(clazz, ex);
        } catch (IllegalAccessException ex) {
            throw new IllegalAccessRuntimeException(clazz, ex);
        }
        setTarget(obj);
    }
    
    private Class createClass(String groovyPath) {
        GroovyClassLoader groovyLoader = 
            new GroovyClassLoader(getClass().getClassLoader());
        File file = ResourceUtil.getResourceAsFile(groovyPath);
        try {
            return groovyLoader.parseClass(file);
        } catch (CompilationFailedException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }    
}
  • Hoge.java インタフェース。クライアントからはこのインタフェース経由でgroovyスクリプトを実行。
public interface Hoge{
    Object hoge();
    String foo() throws IOException;
    int add(int a, int b);   
    
    int sub(int a, int b);
}
  • hogeFake.groovy groovyスクリプト。インタフェースとメソッド名称と引数の数さえあっていれば呼び出せる。
import java.io.*
class hogeFake {
    hoge() {
        return "hoge method"
    }
    foo() {
        return new test().poo()
    }
    add(i, j) {
        return i + j
    }
    
}

class test {
    poo() {
        "poo"
    }
}


    
        
            
                "study/seasar/hogeFake.groovy"
            
        
        

  • Client.java クライアントはこんなカンジ。インタフェースさえ見とけばいい。
package study.seasar;


import java.io.IOException;

import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.factory.S2ContainerFactory;

public class Client {

    private static final String PATH = "study/seasar/delegate.dicon";

    public static void main(String[] args) {
        S2Container container = S2ContainerFactory.create(PATH);
        container.init();
        try {
            Hoge hoge = (Hoge)container.getComponent(Hoge.class);
            System.out.println(hoge.hoge());
            try {
                System.out.println(hoge.foo());
            } catch (IOException e) {
                System.out.println(e.getMessage());
            }
            System.out.println(hoge.add(40, 80));
        } finally {
            container.destroy();
        }
    }
}
  • 実行結果

hoge method
poo
120

表示した値があまりに面白みがないのは、まあ気にしない。

インタフェース経由で呼び出せば取替えきくし、とりあえず動かすというときには便利だと思う。あれ、でもこれじゃgroovyから生成したクラスにDIできなくなるのか?

GroovyだらけでSeasar

SeasarBuilderをちょびっと変更してgroovyスクリプトからつくったクラスをコンテナに登録しよう。

  • SeasarBuilderを変更したCustomizedSeasarBuilder groovyという属性が指定されたらgroovyファイルからClassを生成してコンテナに登録。
   // このメソッド変更
   private ComponentDef setupComponentDef(Map attributes) {
       ComponentDef def = null;
       
       String groovy = (String) attributes.get("groovy");
       Class cls;
       if (groovy != null) {
           cls = createClass(groovy + ".groovy");
       } else {
           cls = (Class) attributes.get("class");
       }
       
       // snip
   }

   // このメソッド追加
   private Class createClass(String groovyPath) {
        GroovyClassLoader groovyLoader =
            new GroovyClassLoader(getClass().getClassLoader());
        File file = ResourceUtil.getResourceAsFile(groovyPath);
        try {
            return groovyLoader.parseClass(file);
        } catch (CompilationFailedException e) {
            throw new SeasarBuilderException(
                    "can't create Class object from '" + groovyPath + "'", e);
        } catch (IOException e) {
            throw new SeasarBuilderException(
                    "can't create Class object from '" + groovyPath + "'", e);
        }
    }
<
  • app.groovy 定義ファイル。上記CustomizedSeasarBuilderを使う。componentのgroovyという属性にgroovyファイルのパスを指定。
import study.seasar.*
import org.seasar.framework.aop.interceptors.*

new CustomizedSeasarBuilder().components() {
    component(class: TraceInterceptor, name: "trace")
    component(groovy: "study/seasar/Calculator", name: "calc") {
        aspect(advice: "trace")
    }
}
class Calculator {
    int add(i, j) {
        return i + j
    }
}
  • Client.groovy コンテナを使うClientもgroovyにしちゃおう
package study.seasar;
import org.seasar.framework.container.factory.S2ContainerFactory;

public class Client {
    static void main(String[] args) {
        container = S2ContainerFactory.create("study/seasar/app.groovy")
        container.init()
        try {
            hoge = container.getComponent("calc")
            num = hoge.add(10,50)
            println num
        } finally {
            container.destroy()
        }
    }
}
  • 実行結果
BEGIN Calculator#invokeMethod(add, [Ljava.lang.Object;@17431b9) END Calculator#invokeMethod(add, [Ljava.lang.Object;@17431b9) : 60 60
本当はinvokeMethodが呼ばれているということがわかる。 Groovyを使うとなんだかちょっとうれしいカンジ。
  • 追記
groovyスクリプトからgroovyスクリプト呼び出すのってなんかいやだ。そんなときこそDIコンテナで依存性注入しとけばいいのでは。インタフェースもつかえるはずだし。

Groovy: A Dynamic OO Language for the Java Virtual Machine

http://www.openlogic.com/presentations
このドキュメントのURLって変わった?この資料見たかったんだけど以前のリンクから辿れなくてしばらく困ってた。サンプルが多くて好き。