AST変換の無駄使い

G* Advent Calendar 2012の8日目です。

[twitter:@uehaj]さんとジャンルが被ってしまいましたが、AST変換ネタです。

AST変換とは?

「AST変換ってナニ?」ですが、AST変換を参照してください。簡単に言ってしまえば、ソースから作られた抽象構文木バイトコードに変換する前に都合がいいように弄ってしまおう、ということになります。

今回のお題

Groovyを使っていると、printlnを使って各種情報を出力することがあり、それはそれで有意義なことではあります。
普通に出力するだけではつまらないので、何かできないかと考えてみました。
そう言えば、Macではsayというコマンドがあり、色々と喋らせることができます。LionやMountain Lionでは、Kyokoを入れることで、日本語を喋らせることができます。
これらをうまく組み合わせられないかと考え、AST変換を使って、printlnしてる内容をsayコマンドで喋らせてしまえばいいんだ、と思い立ちました。

と、いうことで、今回のお題は、printlnしてる箇所をAST変換してsayコマンドで喋らせるようにするアノテーションを作る、というものです。

Sayableアノテーション

今回作成するのは、Sayableアノテーションという名前のアノテーションです。このアノテーションをパッケージ、クラス、メソッド、コンストラクタに付けることで、printlnメソッドの引数で指定された内容をsayコマンドで喋らせることができます。その代わり、標準出力には出力しません。
Sayableアノテーションを付ける位置で喋らせる範囲を変えることできます。
変換する対象は、printlnメソッドだけでなく、System.out.printlnメソッド、System.err.printlnメソッドも対象となります。
Sayableアノテーションはフルアノテーションであり、sayコマンドで指定できるオプションを全てではないですが指定することができます。

Sayableアノテーションの例

Sayableアノテーションの例です。

ファイル:Example01.groovy

def say(message) {
    println message
}

say 'Hello, World!'

これを実行すると、

$ groovy Test.groovy
Hello, World!
$

とprintlnメソッドの引数の内容が標準出力に出力されますが、Sayableアノテーションを付け

ファイル:Example02.groovy

import org.jggug.dojo.sayable.transform.Sayable

@Sayable
def say(message) {
    println message
}

say 'Hello, World!'

て実行すると、

$ groovy Test.groovy
$

と、標準出力には出力されず、デフォルトの声で'Hello, World!'としゃべります。
声を指定したい場合は、voice変数で指定することができます。Example03.groovyでは、Kyokoで喋らせています。

ファイル:Example03.groovy

import org.jggug.dojo.sayable.transform.Sayable

@Sayable(voice = 'Kyoko')
def say(message) {
    println message
}

say 'Hello, World!'

Sayableアノテーションのコード

Sayableアノテーションのコードです。まずは、アノテーションの定義です。

ファイル:Sayable.java

package org.jggug.dojo.sayable.transform;

import org.codehaus.groovy.transform.GroovyASTTransformationClass;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@java.lang.annotation.Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PACKAGE, ElementType.TYPE})
@GroovyASTTransformationClass("org.jggug.dojo.sayable.transform.SayableASTTransformation")
public @interface Sayable {
//    String inputFile() default "";
//    boolean progress() default false;
    String voice() default "";
    int rate() default -1;
    String outputFile() default "";
    String networkSend() default "";
    String audioDevice() default "";
    String fileFormat() default "";
    String dataFormat() default "";
    int channels() default -1;
    int bitRate() default -1;
    int quality() default -1;
}

次がAST変換している部分です。

ファイル:SayableASTTransformation.java

package org.jggug.dojo.sayable.transform;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.PackageNode;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.syntax.Token;
import org.codehaus.groovy.syntax.Types;
import org.codehaus.groovy.transform.ASTTransformation;
import org.codehaus.groovy.transform.GroovyASTTransformation;
import org.objectweb.asm.Opcodes;

@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
public class SayableASTTransformation extends ClassCodeExpressionTransformer implements ASTTransformation, Opcodes {

    static final Class MY_CLASS = Sayable.class;
    static final ClassNode MY_TYPE = ClassHelper.make(MY_CLASS);
    static final String MY_TYPE_NAME = "@" + MY_TYPE.getNameWithoutPackage();

    private SourceUnit sourceUnit;

    @Override
    public SourceUnit getSourceUnit() {
        return sourceUnit;
    }

    private AnnotationNode annotationNode;

    @Override
    public void visit(ASTNode[] nodes, SourceUnit source) {
        sourceUnit = source;

        if (nodes.length != 2 || !(nodes[0] instanceof AnnotationNode) || !(nodes[1] instanceof AnnotatedNode)) {
            throw new GroovyBugError("Internal error: expecting [AnnotationNode, AnnotatedNode] but got: " + Arrays.asList(nodes));
        }

        AnnotatedNode parent = (AnnotatedNode)nodes[1];
        AnnotationNode anno = (AnnotationNode)nodes[0];
        if (!MY_TYPE.equals(anno.getClassNode())) {
        	return;
        }
        annotationNode = anno;
        if (parent instanceof PackageNode) {
            visitPackage((PackageNode)parent);
        } else if (parent instanceof ClassNode) {
            visitClass((ClassNode)parent);
        } else if (parent instanceof MethodNode) {
            visitMethod((MethodNode)parent);
        }
    }

    @Override
    public Expression transform(Expression exp) {
        if (exp == null) {
        	return null;
        }
        if (exp instanceof MethodCallExpression) {
            MethodCallExpression methodCallExpression = (MethodCallExpression)exp;
            Expression objectExpression = methodCallExpression.getObjectExpression();

            if (objectExpression instanceof VariableExpression) {
            	VariableExpression variableExpression = (VariableExpression)objectExpression;
            	if ("this".equals(variableExpression.getName())) {
                    ConstantExpression methodConstant = (ConstantExpression)methodCallExpression.getMethod();
            		if ("println".equals(methodConstant.getText())) {
                        return createExcuteExpression(methodCallExpression);
            		}
            	}
            } else if (objectExpression instanceof PropertyExpression) {
                PropertyExpression propertyExpression = (PropertyExpression)objectExpression;
                ClassExpression object = (ClassExpression) propertyExpression.getObjectExpression();
                ConstantExpression property = (ConstantExpression)propertyExpression.getProperty();
                if ("java.lang.System".equals(object.getText())) {
                    if ("out".equals(property.getText()) || "err".equals(property.getText())) {
                        return createExcuteExpression(methodCallExpression);
                    }
                }
            }

            Expression object = transform(methodCallExpression.getObjectExpression());
            Expression method = transform(methodCallExpression.getMethod());
            Expression args = transform(methodCallExpression.getArguments());
            return new MethodCallExpression(object, method, args);
        } else if (exp instanceof ArgumentListExpression) {
        	ArgumentListExpression argumentListExpression = (ArgumentListExpression)exp;
            List<Expression> argumentList = new ArrayList<Expression>();
        	for (Expression argumentExpression : argumentListExpression.getExpressions()) {
        		argumentList.add(transform(argumentExpression));
        	}
        	return new ArgumentListExpression(argumentList);
        }
        return exp.transformExpression(this);
    }

    protected Expression createExcuteExpression(MethodCallExpression methodCallExpression) {
        Expression objectExpression = methodCallExpression.getObjectExpression();
        ConstantExpression methodConstant = (ConstantExpression)methodCallExpression.getMethod();

        StringBuilder sb = new StringBuilder("say ");
        setStringOption(sb, "voice", "--voice");
        setIntegerOption(sb, "rate", "--rate");
        setStringOption(sb, "outputFile", "--output-file");
        setStringOption(sb, "networkSend", "--network-send");
        setStringOption(sb, "audioDevice", "--audio-device");
        setStringOption(sb, "fileFormat", "--file-format");
        setStringOption(sb, "dataFormat", "--data-format");
        setIntegerOption(sb, "channels", "--channels");
        setIntegerOption(sb, "bitRate", "--bit-rate");
        setIntegerOption(sb, "quality", "--quality");

        Expression object = new BinaryExpression(
            new ConstantExpression(sb.toString()),
            Token.newSymbol("+", methodConstant.getLineNumber(), methodConstant.getColumnNumber()),
            ((ArgumentListExpression)(methodCallExpression.getArguments())).getExpressions().get(0)
        );
        Expression method = new ConstantExpression("execute");
        Expression args = new ArgumentListExpression();
        return new MethodCallExpression(object, method, args);
    }

    protected Object getMemberValue(AnnotationNode node, String name) {
        final Expression member = node.getMember(name);
        if (member != null && member instanceof ConstantExpression) {
            return ((ConstantExpression)member).getValue();
        }
        return null;
    }

    protected StringBuilder setStringOption(StringBuilder sb, String name, String option) {
        String value = (String)getMemberValue(annotationNode, name);
        if (value != null && !value.isEmpty()) {
            sb.append(option).append("=").append(value).append(" ");
        }
        return sb;
    }

    protected StringBuilder setIntegerOption(StringBuilder sb, String name, String option) {
        Integer value = (Integer)getMemberValue(annotationNode, name);
        if (value != null && value != -1) {
            sb.append(option).append("=").append(value).append(" ");
        }
        return sb;
    }
}

テストケースもない、取りあえず動くコードなので、いろいろとアレですが...

AST変換でやっていること

AST変換でやっていることですが、簡単に言うと、抽象構文木から

  • "this.println"というメッソッド呼び出し式
  • "System.out.println”というメソッド呼び出し式
  • "System.err.println”というメソッド呼び出し式

を探し、「("say " + "メソッドの引数").execute()」というメソッド呼び出し式に変換する、ということをやっています。

使い道

...ないな(-_-;)

ソースの公開とか

githubで公開予定ですが、まだ準備ができていません...orz 公開を希望している人はそうそういないと思いますが、準備ができましたら、twitterやブログなどでお知らせします。

今後の予定

テストケース作ったりとか、テストケースやgradleのビルドファイルからでも動かせるようにするとか、まぁ、それなりに。

さ〜て、次回のG* Advent Calendarさんは?

G* Advent Calendar 2012の9日目は、[twitter:@kyon_mm]さんです。「Groovyのマニアックな話を書きます」ということなので、非常に楽しみです。

GroovyでVert.x - 2. HTTPリクエストハンドラ その1 -

さて

今回もHTTPサーバの続きで、HTTPリクエストハンドラを掘り下げたいと思います。

実行環境

今回は、以下の環境で実行します。

なお、1.0.1 finalにはJavaScript用のjarファイルが含まれていないため、

vertx run HelloWorld.groovy

を実行すると、

Exception in thread "main" java.util.ServiceConfigurationError: org.vertx.java.deploy.VerticleFactory: Provider org.vertx.java.deploy.impl.rhino.RhinoVerticleFactory could not be instantiated: java.lang.NoClassDefFoundError: org/mozilla/javascript/ContextFactory

という例外が発生し起動しません。
これに対処するためには、1.0 finalのtarボール、あるいはソースからjs.jarを取得し、${VERTX_HOME}/lib/jarsにコピーしてください。

前回のおさらい

前回は"Hello, World!"を返すHTTPサーバを実行しました。

vertx.createHttpServer().requestHandler { request ->
    request.response.end('<html><body><h1>Hello, World!</h1></body></html>')
}.listen(8080, 'localhost')

このコードでは、どのようなリクエストでも一律"Hello, World!"を返すことになり、リクエストに応じた内容を返すことができません。また、返す内容もハードコーディングされています。

今回は

今回と次回でリクエストハンドラの引数でありリクエストをあらわすHttpServerRequestと、レスポンスをあらわすHttpServerResponseを紹介し、前回のサンプルコードを改良したいと思います。

HttpServerRequest

HTTPサーバのリクエストハンドラは、リクエストをあらわすHttpServerRequestクラスのオブジェクトを引数に取ります。このHttpServerRequestクラスのオブジェクトから様々なリクエスト情報を取得することができます。

リクエストのHTTPメソッドの取得

リクエストのHTTPメソッドは、HttpServerRequest#getMethod()で取得できます。

vertx.createHttpServer().requestHandler { request ->
    request.response.end("<html><body><h1>Your method is ${request.method}</h1></body></html>")
}.listen(8080, 'localhost')

例えば、Webブラウザhttp://localhost:8080/aaa/bbb/ccc/index.html?xxx=1&yyy=2&zzz=3にアクセスした場合、

Your method is GET

となります。

リクエストのURIの取得

リクエストのURIは、HttpServerRequest#getUri()で取得できます。

vertx.createHttpServer().requestHandler { request ->
    request.response.end("<html><body><h1>Your URI is ${request.uri}</h1></body></html>")
}.listen(8080, 'localhost')

例えば、Webブラウザhttp://localhost:8080/aaa/bbb/ccc/index.html?xxx=1&yyy=2&zzz=3にアクセスした場合、

Your URI is /aaa/bbb/ccc/index.html?xxx=1&yyy=2&zzz=3

となります。

リクエストのパスの取得

リクエストのパスは、HttpServerRequest#getPath()で取得できます。

vertx.createHttpServer().requestHandler { request ->
    request.response.end("<html><body><h1>Your path is ${request.path}</h1></body></html>")
}.listen(8080, 'localhost')

例えば、Webブラウザhttp://localhost:8080/aaa/bbb/ccc/index.html?xxx=1&yyy=2&zzz=3にアクセスした場合、

Your path is /aaa/bbb/ccc/index.html

となります。

リクエストのクエリの取得

リクエストのクエリは、HttpServerRequest#getQuery()でString型で取得できます。

vertx.createHttpServer().requestHandler { request ->
    request.response.end("<html><body><h1>Your query is ${request.query}</h1></body></html>")
}.listen(8080, 'localhost')

例えば、Webブラウザhttp://localhost:8080/aaa/bbb/ccc/index.html?xxx=1&yyy=2&zzz=3にアクセスした場合、

Your query is xxx=1&yyy=2&zzz=3

となります。

リクエストヘッダの取得

リクエストヘッダは、HttpServerRequest#getHeaders()でjava.util.Map型で取得できます。

vertx.createHttpServer().requestHandler { request ->
    def sb = new StringBuilder()
    for (def h : request.headers) {
        sb << "<li>${h.key} : ${h.value}</li>"
    }
    request.response.end("<html><body><h1>Your headers are <ul>${sb.toString()}</ul></h1></body></html>")
}.listen(8080, 'localhost')

例えば、Webブラウザhttp://localhost:8080/aaa/bbb/ccc/index.html?xxx=1&yyy=2&zzz=3にアクセスした場合、

Your headers are
* Accept-Language : ja,en-US;q=0.8,en;q=0.6
* Host : localhost:8080
* Accept-Charset : Shift_JIS,utf-8;q=0.7,*;q=0.3
* Accept-Encoding : gzip,deflate,sdch
* User-Agent : Mozilla/5.0 (X11; Linux i686) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.52 Safari/536.5
* Accept : text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
* Connection : keep-alive
* Cache-Control : max-age=0

となります。

リクエストパラメータの取得

リクエストパラメータは、HttpServerRequest#getQuery()でString型で取得できますが、HttpServerRequest#getParams()でjava.util.Map型で取得できます。

vertx.createHttpServer().requestHandler { request ->
    def sb = new StringBuilder()
    for (def h : request.params) {
        sb << "<li>${h.key} : ${h.value}</li>"
    }
    request.response.end("<html><body><h1>Your params are <ul>${sb.toString()}</ul></h1></body></html>")
}.listen(8080, 'localhost')

例えば、Webブラウザhttp://localhost:8080/aaa/bbb/ccc/index.html?xxx=1&yyy=2&zzz=3にアクセスした場合、

Your params are
* xxx : 1
* yyy : 2
* zzz : 3

となります。
ただし、http://localhost:8080/aaa/bbb/ccc/index.html?xxx=1&yyy=2&zzz=3&xxx=4&yyy=5&zzz=6と、同じパラメータ名を複数指定した場合、

Your params are
* xxx : 1
* yyy : 2
* zzz : 3

となり、クエリの最初に指定された値が採用されるようです。

リクエストボディの取得 - dataHandler

POSTでデータを送信するような場合、送信されたデータはHttpServerRequest#getQuery()メソッドあるいはHttpServerRequest#getParams()メソッドでは取得できません。
どうやって取得するかというと、リクエストハンドラのHttpServerRequestオブジェクトに対してリクエストボディを対処するハンドラ(クロージャ)を設定し、そのハンドラ内で取得します。

vertx.createHttpServer().requestHandler { request ->
    request.dataHandler { buffer ->
        println "buffer = ${buffer.toString()}"
    }
    request.response.end("<html><body><h1>You send data.</h1></body></html>")
}.listen(8080, 'localhost')

リクエストボディを対処するデータハンドラをHttpServerRequest#dataHandler(groovy.lang.Closure)メソッドで設定します。
HttpServerRequest#dataHandlerに設定するデータハンドラは、リクエストボディ用のバッファクラスであるorg.vertx.groovy.core.buffer.Bufferクラスのオブジェクトを1つ引数にとります。
HttpServerRequest#dataHandlerに設定するデータハンドラは、リクエストボディのチャンクごとに呼び出されます。

リクエストボディの取得 - endHandler

1リクエストのボディが1チャンクであれば上記のコードで良いですが、複数のチャンクに分かれている場合は、チャンクごとにリクエストボディの内容を保持するようにします。

import org.vertx.groovy.core.buffer.Buffer

vertx.createHttpServer().requestHandler { request ->
    def body = new Buffer(0)
    request.dataHandler { buffer ->
        body << buffer
    }
    request.endHandler {
        request.response.end("<html><body><h1>You send data - ${body.toString()}</body></html>")
    }
}.listen(8080, 'localhost')

リクエストハンドラ内でBufferクラスのインスタンスを生成し、データハンドラ内で、引数で渡されたボディチャンクの内容をBufferクラスのインスタンスに追加します。
データハンドラはチャンクごとに呼ばれるため、チャンクの終わり(= ボディの受信の終わり)を知る必要があります。Vert.xのHTTPサーバの場合、チャンクの受信が完了した時に専用のイベントを発生させ、対応するイベントハンドラを実行します。この場合、エンドハンドラになります。
エンドハンドラをHttpServerRequest#endHandler(groovy.lang.Closure)メソッドで設定します。エンドハンドラに渡される引数はありません。エンドハンドラ内で、Bufferクラスのインスタンスに対応します。

リクエストボディの取得 - bodyHandler

HTTPチャンクごとではなく、全てのリクエストボディを受信した後で処理をしたい場合もあると思います。その場合は、ボディハンドラを設定することで対応できます。

vertx.createHttpServer().requestHandler { request ->
    request.bodyHandler { buffer ->
        request.response.end("<html><body><h1>You send data - ${buffer.toString()}</body></html>")
    }
}.listen(8080, 'localhost')

ボディハンドラは、すべてのリクエストボディを受信した後で一度だけ呼び出されます。

終わりに

今回は、受信したリクエストを処理するところを主に見てきました。次回は、レスポンス部分を見てみたいと思います。

GroovyでVert.x - 1. インストールとHello, World! -

あれから

前回からそれなりに時間が経ち、Vert.xのWebページができ、Vert.xでGroovyが正式採用となりました。
そこで、チュートリアルやサンプルを試しながら、何回かに分けてVert.xをご紹介したいと思います。
今回はVert.xのインストールと、HTTPサーバのサンプルです。

実行環境

今回は、以下の環境で実行します。

Vert.xのインストール

Vert.xのインストールは、ダウンロードページからダウンロードしたtar.gzファイルかzipファイルを展開して、展開したディレクトリ(以降、$VERTX_HOME)下のbinディレクトリを環境変数PATHに通すだけです。
ちゃんとインストールされたかどうかは、vertxコマンドで確認します。

$ vertx version
vert.x 1.0.beta10

HTTPサーバで"Hello, World!"

HTTPサーバのサンプルとして、お約束の、リクエストが来たら"Hello, World!"を返すHTTPサーバを作成してみましょう。

HTTPサーバの作成

HTTPサーバのコードは次のとおりです:

vertx.createHttpServer().requestHandler { request ->
    request.response.end('<html><body><h1>Hello, World!</h1></body></html>')
}.listen(8080, 'localhost')

HelloWorld.groovyに保存すれば、準備は完了です。

HTTPサーバの起動とアクセス

次に、HTTPサーバを起動します。起動は、vertxのrunコマンドで行います:

$ vertx run HelloWorld.groovy

メッセージは出力されませんが、エラーメッセージなど何も出力されなければ起動成功です。
その後、Webサーバでhttp://localhost:8080/にアクセスし、"Hello, World!"と表示されればOKです。

ソースの解説

では、先程のソースをざっと解説してみましょう。

verticle

Vert.xでは、Vert.xのライブラリを使って作成したサーバを"verticle"という名前で呼びます。前述のHTTPサーバも1つのverticleになります。

"vertx"って?

ソースの冒頭で"vertx"というオブジェクトが出てきますが、これは一体何でしょうか? import文でインポートしているわけでもないのに、エラーもなく使えています。
ここは、Vert.x自体のソースを漁る必要があります。ソースはここからgitで落とします。
$VERTX_HOME/bin/vertxを見ると、

java -Djava.util.logging.config.file=$DIRNAME/../conf/logging.properties -Djruby.home=$JRUBY_HOME \
-Dvertx.mods=$VERTX_MODS -Dvertx.install=$SCRIPTDIR/.. -cp $CLASSPATH org.vertx.java.deploy.impl.cli.VertxMgr "$@"

とありますので、src/main/java/org/vertx/java/deploy/impl/cli/VertxMgr.javaからソースをたどっていきます。
すると、src/main/java/org.vertx.java.deploy.impl.VerticleManager.javaのdoDeployメッソドの中に次のコードがありました(抜粋):

    VerticleType type = VerticleType.JAVA;
    if (main.endsWith(".js")) {
      type = VerticleType.JS;
    } else if (main.endsWith(".rb")) {
      type = VerticleType.RUBY;
    } else if (main.endsWith(".groovy")) {
      type = VerticleType.GROOVY;
    }

    final VerticleFactory verticleFactory;
      switch (type) {
        case JAVA:
          verticleFactory = new JavaVerticleFactory(this);
          break;
        case RUBY:
          verticleFactory = new JRubyVerticleFactory(this);
          break;
        case JS:
          verticleFactory = new RhinoVerticleFactory(this);
          break;
        case GROOVY:
          verticleFactory = new GroovyVerticleFactory(vertx, this);
          break;
        default:
          throw new IllegalArgumentException("Unsupported type: " + type);
      }

runコマンドの後に指定するファイルの拡張子でverticleのファクトリを切り替えているようです。
さらにsrc/main/java/org/vertx/java/deploy/impl/groovy/GroovyVerticleFactory.javaを見ると、

import groovy.lang.Binding;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyCodeSource;
import groovy.lang.Script;
import org.vertx.groovy.core.Vertx;
import org.vertx.groovy.deploy.Container;
import org.vertx.java.core.impl.VertxInternal;
import org.vertx.java.deploy.Verticle;
import org.vertx.java.deploy.impl.VerticleFactory;
import org.vertx.java.deploy.impl.VerticleManager;

import java.lang.reflect.Method;
import java.net.URL;

public class GroovyVerticleFactory implements VerticleFactory {

  private final VerticleManager mgr;
  private final Vertx gVertx;

  public GroovyVerticleFactory(org.vertx.java.core.Vertx vertx, VerticleManager mgr) {
    this.mgr = mgr;
    this.gVertx = new Vertx((VertxInternal)vertx);
  }

  public Verticle createVerticle(String main, ClassLoader cl) throws Exception {

    URL url = cl.getResource(main);
    GroovyCodeSource gcs = new GroovyCodeSource(url);
    GroovyClassLoader gcl = new GroovyClassLoader(cl);
    Class clazz = gcl.parseClass(gcs);

    Method stop;
    try {
      stop = clazz.getMethod("vertxStop", (Class<?>[])null);
    } catch (NoSuchMethodException e) {
      stop = null;
    }
    final Method mstop = stop;

    Method run;
    try {
      run = clazz.getMethod("run", (Class<?>[])null);
    } catch (NoSuchMethodException e) {
      run = null;
    }
    final Method mrun = run;

    if (run == null) {
      throw new IllegalStateException("Groovy script must have run() method [whether implicit or not]");
    }

    final Script verticle = (Script)clazz.newInstance();

    // Inject vertx into the script binding
    Binding binding = new Binding();
    binding.setVariable("vertx", gVertx);
    binding.setVariable("container", new Container(new org.vertx.java.deploy.Container((mgr))));
    verticle.setBinding(binding);

    return new Verticle() {
      public void start() {
        try {
          mrun.invoke(verticle, (Object[])null);
        } catch (Throwable t) {
          reportException(t);
        }
      }

      public void stop() {
        if (mstop != null) {
          try {
            mstop.invoke(verticle, (Object[])null);
          } catch (Throwable t) {
            reportException(t);
          }
        }
      }
    };
}

と、お目当てのものがありました。
コメントに「Inject vertx into the script binding」にあるところがミソですね。バインド変数として"vertx","container"を定義し、verticle変数(引数で指定したGroovyファイルをインスタンス化したもの、型はgroovy.lang.Script)にバインディングすることで、引数で指定したGroovyファイルの中で"vertx","container"が宣言なしで使えるようです。
"vertx"変数の実体はorg.vertx.groovy.core.Vertxクラスのインスタンス、"container"変数の実体はorg.vertx.groovy.deploy.Containerクラスのインスタンスとなります。
それぞれのクラスの詳細は、GroovyDocを見て下さい。

HTTPサーバの作成

HTTPサーバは、vertxのcreateHttpServer()メソッドを呼び出して作成します。vertxには、この他にもcreateXxxx()系のメソッドが幾つかあり、目的のサーバの種類ごとに使い分けます。
createHttpServer()メソッドは戻り値として、org.vertx.groovy.core.http.HttpServerを継承したクラスのインスタンスを返します。

def server = vertx.createHttpServer()
リクエストハンドラの設定

リクエストの処理はハンドラで行います。org.vertx.groovy.core.http.HttpServer#requestHandler(groovy.lang.Closure hndlr)メソッドに、ハンドラとして引数が1つのクロージャを指定します。このハンドラが、リクエストごとに呼び出されます。
クロージャの引数はリクエストをあらわすオブジェクトで、org.vertx.groovy.core.http.HttpServerRequestクラスのインスタンスです。

def server = vertx.createHttpServer()
server.requestHandler { request ->
    request.response.end('<html><body><h1>Hello, World!</h1></body></html>')
}

requestHandler()メソッドも戻り値として、org.vertx.groovy.core.http.HttpServerを継承したクラスのインスタンスを返します。

サーバの起動・リクエストの受信準備

サーバの起動は、org.vertx.groovy.core.http.HttpServer#listen(int port)、またはorg.vertx.groovy.core.http.HttpServer#listen(int port, java.lang.String host)メソッドで、ポート番号のみ、あるいはポート番号とホスト名(またはIPアドレス)を指定して行います。

def server = vertx.createHttpServer()
server.requestHandler { request ->
    request.response.end('<html><body><h1>Hello, World!</h1></body></html>')
}
server.listen(8080, 'localhost')

終わりに

今回は、HTTPサーバで"Hello, World!"を表示するところまでやってみました。次回は、リクエストハンドラを少し掘り下げるのと、別の書き方で"Hello, World!"を表示するところをやってみたいと思います。

GroovyでVert.x

G* Advent Calendarの10日目です。

G*ネタ、とは言いがたいですが、他の方とネタがなるべく被らず、斜め下を行くよう、ロンドンで行われたGroovy & Grails eXchange 2011の初日のセッション、Peter Ledbrookさんの「Asynchronous IO on the JVM」からVert.xをご紹介したいと思います。

Vert.xとは?

「Vert.xってなに?」ということですが、READMEによると、

The next generation polyglot asynchronous application framework. (Formerly known as node.x)
次世代の多言語非同期アプリケーションフレームワーク。(以前はnode.xとして知られていた)

だそうです。特徴としては、

  • JVM上で動く。
  • Node.jsのようなイベント駆動型フレームワークのよいところを取り入れる。
  • すべてノンブロッキング
  • 多言語に対応。vert.xは、Ruby、Groovy、JavaJavaScriptPythonClojureScalaといった複数の言語から利用できるようになる予定。現在、JavaRubyをサポート。
  • Java7のInvokeDynamicを使って開発を進め、動的言語にとって有望な実行環境であるJVMに賭けている。
  • ネットワークサーバーやクライアントが簡単に作成できる。
  • 真のスケーラビリティ。他のよく知られているイベント駆動型フレームワークとは異なり、プロセスごとに複数のイベントループを持つことができる。
  • 非常にシンプルな並行性モデル。シングルスレッドでコードを書くと、複数のコア間でスケールするのがわかる。競合状態やロックを心配する必要はない。
  • TCPSSL、HTTP、HTTPS、Websocketsといった従来のプロトコルにとどまらず、複数のネットワークプロトコルを解釈する。
  • ユーザー空間をバイパスしてファイルシステムから静的ファイルを効率的に提供。
  • Sinatra/Expressスタイルの単純なリソース・ルーティング。
  • 分散イベントバス。分散イベントバスの提供のため、複数vert.xインスタンスがシームレスに連携。

だそうです。

Vert.xアーキテクチャ

Vert.xのコア部分はJavaで書かれており、サポート言語ごとにAPIが用意されています(現状はJavaRubyだけのようですが)。
内部的には、NettyとNIO 2が使われています。
また、Reactorパターンに基づいて実装されているようです。

動かしてみる

Vert.xを動かしてみます。動作するOSとしては、LinuxOSXJDKは7以上が必要なようです。
今回は、次の環境で動かしてみます。

Vert.xのビルド

Binary Releaseがあるのですが、Groovy用のソースが反映されていない気がするので、ソースを取得しビルドします。

ビルドの手順はここを参考にします。

githubからソースを取得します。今回は、Peter LedbrookさんとこからGroovy SupportのZIPで取得します。

次のアプリをインストールして、環境変数PATHに設定します。

  • Apache ant 1.8.2
  • JDK 1.7.0_01
  • JRuby 1.6.5
  • Yard ('jruby -S gem install yard'でインストール)
  • Groovy 1.8.4

取得したZIPを展開したソースディレクトリのルートで、antを実行し、配布・実行用のファイルを作成します。

$ ant dist

実行後、ソースディレクトリのルート/targetディレクトリに、vert.x-0.1.tar.gzができます。

Vert.xのインストール

先程作成したvert.x-0.1.tar.gzを展開し、展開して作成されるbinディレクトリを環境変数PATHに設定します。

$ tar zxvf vert.x-0.1.tar.gz

今後、展開したディレクトリをVERTX_HOMEとして話を進めます。

Vert.xのライフサイクル

Vert.xの起動から停止までのライフサイクルは、次のようです。

  1. Vert.xインスタンスの起動
  2. アプリケーションのデプロイ
  3. クライアントからアクセス
  4. アプリケーションのアンデプロイ
  5. Vert.xインスタンスの停止

Vert.xインスタンスの起動

Vert.xのインスタンスを起動します。

$ vertx start
vert.x server started

デフォルトで、ポート25571でリクエストをリッスンします。-portで、ポート番号を変更できます。

$ vertx start -p 35571
vert.x server started

また、-clusterで、クラスターモードで起動するようです。

$ vertx start -cluster
vert.x server started in clustered mode

アプリケーションのデプロイ

サーバアプリケーションをデプロイします。サーバアプリケーションは、org.vertx.java.core.app.VertxAppをクラスとなります。
展開したソースの中にGroovyで書かれたEchoサーバのサンプルソース(EchoServer.groovy)がありますので、今回はそれを動かしてみます。

$ cd $VERTX_HOME/examples/groovy
$ vertx deploy -groovy -name EchoServer -main echo/EchoServer.groovy -cp . -instances 1
Deploying application name: EchoServer instances: 1
OK
  • -groovyで、Groovyのアプリケーション(ソースファイル)をデプロイすることを指定します。
  • -nameで、アプリケーション名を指定します。
  • -mainで、アプリケーションのクラス、またはスクリプトファイル名を指定します。
  • -cpで、クラスパスを指定します。
  • -instancesで、アプリケーションのインスタンス番号を指定します。

Vert.x側では、次のログが出力されます。

[vert.x-core-thread-0] 18:17:03,339 INFO [org.vertx.java.core.app.AppManager]  Deploying application name : EchoServer type: GROOVY main class: echo/EchoServer.groovy instances: 1
[vert.x-core-thread-0] 18:17:04,305 INFO [org.vertx.java.core.app.AppManager]  Started 1 instances ok

クライアントからアクセス

サーバにアクセスしてみます。
展開したソースの中にGroovyで書かれたEchoクライアントのサンプルソース(echoclient.groovy)がありますので、サーバと同様、それを動かしてみます。

$ groovy -cp ../../lib/java/vert.x.jar:../../lib/java/netty.jar echoclient.groovy 
Press Ctrl-C to exit
Net client sending: hello 0
Net client sending: hello 1
Net client sending: hello 2
Net client sending: hello 3
Net client sending: hello 4
Net client sending: hello 5
Net client sending: hello 6
Net client sending: hello 7
Net client sending: hello 8
Net client sending: hello 9
Net client receiving: hello 3

Net client receiving: hello 2

Net client receiving: hello 5

Net client receiving: hello 1

Net client receiving: hello 7

Net client receiving: hello 8

Net client receiving: hello 0

Net client receiving: hello 4

Net client receiving: hello 6

Net client receiving: hello 9

アプリケーションのアンデプロイ

必要に応じて、アプリケーションをアンデプロイします。

$ vertx undeploy -name EchoServer
OK

アンデプロイするアプリケーション名を指定します。

Vert.x側では、次のログが出力されます。

[vert.x-core-thread-0] 18:30:47,893 INFO [org.vertx.java.core.app.AppManager]  Undeploying 1 instances of application: EchoServer
[vert.x-core-thread-0] 18:30:47,902 INFO [org.vertx.java.core.app.AppManager]  Undeployed ok

Vert.xインスタンスの停止

フォアグラウンドで実行している場合、Ctrl-Cで停止することができますが、バックグラウンドで実行している場合、stopでVert.xのインスタンスを停止します。

$ vertx stop
Stopped vert.x server

別のサンプルを実行してみる - その1

次に、別のサンプルを試してみます。Redisを使用した、ヒット数を記録しそれを返すサンプルアプリケーションです。このサンプルを実行するには、Redisサーバが起動されている必要があります。
アプリケーションをデプロイします。

$ cd $VERTX_HOME/examples/groovy
$ vertx deploy -groovy -name RedisServer -main redis/RedisExample.groovy -cp .
Deploying application name: RedisServer instances: -1
OK

クライアントを実行します。

$ groovy -cp ../../lib/java/vert.x.jar:../../lib/java/netty.jar redisclient.groovy 
Final output: <html><body><h1>Hit count is 5001</h1></body></html>
$ groovy -cp ../../lib/java/vert.x.jar:../../lib/java/netty.jar redisclient.groovy 
Final output: <html><body><h1>Hit count is 10002</h1></body></html>
$ groovy -cp ../../lib/java/vert.x.jar:../../lib/java/netty.jar redisclient.groovy 
Final output: <html><body><h1>Hit count is 15003</h1></body></html>
$ groovy -cp ../../lib/java/vert.x.jar:../../lib/java/netty.jar redisclient.groovy 
Final output: <html><body><h1>Hit count is 20004</h1></body></html>

クライアントの実行ごとに、Hit countが増加するのがわかります。

別のサンプルを実行してみる - その2

最後に、また別のサンプルを試してみます。WebSocketsを使用したサンプルアプリケーションです。
サンプルアプリケーションがあるディレクトリでVert.xを起動します。

$ cd $VERTX_HOME/examples/groovy
$ vertx start
vert.x server started

アプリケーションをデプロイします。

$ cd $VERTX_HOME/examples/groovy
$ vertx deploy -groovy -name WebSockets -main websockets/WebSocketsExample.groovy -cp .
Deploying application name: WebSockets instances: -1
OK

Chromehttp://localhost:8080/にアクセスします。

テキストボックスだけのページが表示されます。
テキストボックスに何か入力すると、入力した文字がどんどん追加されていくのがわかると思います。
#日本語は何故かダメみたいです...orz

まとめ

Vert.xをGroovyで動かすソースは、Peter Ledbrookさんとこのブランチだけのようですが、出来れば、本家に早めに取り込まれて欲しいものです。
サンプルのソースなど追わず、今回は紹介ベースなので、時間があればアプリケーションをどのように書くものなのか、などなど、いろいろと調べてみたいと思います。

さ〜て、次回のG* Advent Calendarさんは?

G* Advent Calendarの11日目は、帰国直前?直後?の[twitter:@bikisuke]さんです。