groovy-macro-methodsについて(後編)
はじめに
これはG*アドベントカレンダーの最終日、第25日目の記事です。
今回は後編ということで、前編でご紹介したgroovy-macro-methodsを使用したマクロのサンプルをいくつか作成しましたので、これらのサンプルをご紹介することで「groovy-macro-methodsを使うとこんなことができるんだ」と思っていただけると幸いです。
なお、本記事は半年くらい前のワークショップで発表する予定だったネタですが、時間の都合上発表できなかったので、今回もその時のネタを使い回すことにしました。
サンプルについて
今回サンプルとして、「常に使いたくなるようなマクロ」ではなく「四年に一度くらい使いたいと思うようなマクロ」を目標として、5つ、マクロを作ってみました。 マクロ自体の内容は割と微妙ですが、「マクロのサンプルとしては手頃な内容なのでは?」と思っています。
サンプルのコードの場所とビルド
今回のgroovy-macro-methodsの サンプルは、githubでコードを公開しています。
$ git clone https://github.com/touchez-du-bois/akatsuki.git
でコードをクローンし、
$ cd akatsuki $ ./gradlew build
で、サンプルのクラスのみのjarファイルbuild/libs/akatsuki-x.y.z.jarが作成され、
$ cd akatsuki $ ./gradlew shadowJar
で、サンプルのクラスとgroovy-macro-methodsのクラスが入ったjarファイルbuild/libs/akatsuki-x.y.z-all.jarが作成されます。
ちなみに、"akatsuki"は、この娘です。
動作確認環境
動作確認は次の環境で行いました。
サンプルの概要
サンプルとして作成したマクロですが、次のとおりです。
- nullSafeマクロ
- doWhileマクロ
- newTraitマクロ
- matchマクロ
- doWithDataマクロ
以降、実行結果を踏まえマクロの説明をしていきたいと思います。
nullSafeマクロ
JavaにはなくてGroovyにある便利な機能と言えば、いくつかありますが、その一つにnull safe operator "?"があります。
def result = a.b.c.d.e.f()
といったコードで、aからeのいずれかがnullの場合、NullPointerExceptionがスローされます。この場合、
def result = a?.b?.c?.d?.e?.f()
とnull safe operatorを明示すれば、途中がnullであってもNullPointerExceptionはスローされず、resultはnullとなります。
それでも、四年に一度くらい「自動でnull safe operatorを付けてくれないかなぁ…」と思ったことはないでしょうか?
そこで、nullSafeマクロです。
nullSafeマクロでは、メソッド呼び出しやプロパティ参照の対象となるオブジェクトに遡ってnull safe operatorを付けるようにASTを変換します。さらに、メソッド呼び出しの場合、引数のメソッド呼び出しやプロパティ参照に対してもnull safe operatorを付けるようにASTを変換します。
以下はnullSafeマクロの使用例です。
class A { String hello() { "Hello!" } } class B { A a } class C { B b } C obj = new C() assert nullSafe(obj.b.a.hello()) == null
上の例の場合、nullSafeマクロに渡されたメソッド呼び出し「obj.b.a.hello()」が次のようにnull safe operatorを付けたASTに変換され実行されます。
assert obj?.b?.a?.hello() == null
doWhileマクロ
JavaにあってGroovyにないもの、 と言えば、do〜while文ですが、四年に一度くらい「do〜while文使いたいなぁ」と思ったことはないでしょうか?
そこでdoWhileマクロです。
厳密には「do〜while文」ではなく「do〜while式」なのですが、条件式とClosureを受け取って 「do〜while文」っぽいことをします。
def x = 0 dowhile ({ x++ }, x == 0) assert x == 1
または
def x = 0 dowhile (x == 0) { x++ } assert x == 1
のように使います。
上の例の場合、doWhileマクロに渡された条件とClosureを、次のようなコードのASTに変換し実行します。
{ { x++ }.call() while (x == 0) { { x++ }.call() } }.call()
newTraitマクロ
Groovyの2.3から追加された機能 と言えば、traitですが、traitのインスタンスを直接生成することはできず、
- クラス定義時にimplementsでtraitを指定
- オブジェクトにasキーワードまたはwithTraitsメソッドでtraitを指定
する必要があります。例えば、
trait FlyingAbility { String fly() { "I'm flying!" } }
というtraitがあった場合、
class Bird implements FlyingAbility { } def b = new Bird() assert b.fly() == "I'm flying!"
あるいは
class Bird { } def b = new Bird() as FlyingAbility assert b.fly() == "I'm flying!"
というように使用します。
ですが、四年に一度くらい「traitのインスタンスが簡単に欲しいなぁ」と思ったことはないでしょうか?
そこで、newTraitマクロです。厳密には、Objectクラスのインスタンスにtraitを組み込んだプロキシオブジェクトを生成していますので、traitのインスタンスを生成、というわけではないのですが。
別の例になりますが、
trait Sample { String name int age String hello() { "Hello, my name is ${name}, and my age is ${age}" } } def x = newTrait(Sample, name: "aaa", age: 10) assert x.hello() == "Hello, my name is aaa, and my age is 10"
というコードは、コンパイル時に、
{ def obj = new Object().withTraits(Sample) obj['name'] = 'aaa' obj['age'] = 10 return obj }.call()
と、Objectクラスのインスタンスにtraitを組み込んだプロキシオブジェクトを生成、かつ初期化パラメータがある場合は初期化する、というASTに変換され実行されます。
matchマクロ
Groovyにあるといいなと思う機能 と言えば、match式ですが、四年に一度くらい「match式が使いたいなぁ」と思ったことはないでしょうか?
そこで、matchマクロです。Sergei Egorovさんのスライドに例として載っていたのですが、コードはなかったので実装してみました。
例えば、階乗を求めるメソッドで、matchマクロを使って実装してみたのが次のコードです。
def fact(num) { return match(num) { when String then fact(num.toInteger()) // (1) when (0|1) then 1 // (2) when 2 then 2 // (3) orElse num * fact(num - 1) // (4) } } assert fact("5") == 120
matchマクロの引数を使って、その後のクロージャ内で分岐をしています。(1)ではnumがStringクラスの場合、(2)ではnumが0か1の場合、(3)ではnumが2の場合、(4)ではそれ以外の場合、といった判断分岐をしています。
上記のコードは、コンパイル時に
def fact(num) { return { it -> if ( it instanceof java.lang.String) { return this.fact(num.toInteger()) } if ( it == 0 || it == 1) { return 1 } if ( it == 2) { return 2 } return num * this.fact( num - 1) }.call( num ) }
と、if文とreturn文のASTに変換され実行されます。
doWithDataマクロ
Spockにある便利な機能 と言えば、データテーブルですが、四年に一度くらい「テストコードじゃないところでデータテーブルを使いたいなぁ」と思ったことはないでしょうか?
def "データテーブルの例"() { expect: a + b == c where: a | b || c 1 | 2 || 3 4 | 5 || 9 7 | 8 || 15 }
そこで、doWithDataマクロです。普通のGroovyコードで、Spockのようなデータテーブルを使うことができます。
doWithData { dowith: assert a + b == c where: a | b || c 1 | 2 || 3 4 | 5 || 9 7 | 8 || 15 }
whereラベルの1行目で変数を宣言し、2行目以降に変数に設定する値を定義します。whereラベルで宣言した値は、行毎にdowithラベルで記述したコード中に変数に設定され実行されます。
上記のコードは、
{ { def a = 1 ; def b = 2 ; def c = 3 assert a + b == c } { def a = 4 ; def b = 5 ; def c = 9 assert a + b == c } { def a = 7 ; def b = 8 ; def c = 15 assert a + b == c } }.call()
と、現状ベタに展開するASTに変換され実行されます。
最後に
前回と今回の2回にわたりgroovy-macro-methodsと、groovy-macro-methodsを使ったマクロのサンプルを紹介しました。マクロ自体、作るのがちょっと大変ですが、工夫次第で面白いことができることを実感していただけたら幸いです。
groovy-macro-methodsについて(前編)
はじめに
これはG*アドベントカレンダーの第14日目の記事です。
今回は、groovy-macro-methodsをご紹介します。
なお、本記事は半年くらい前のワークショップで発表する予定だったネタですが、時間の都合上発表できなかったので、今回その時のネタを使い回すことにしました。
groovy-macro-methodsとは
簡単に言いますと、groovy-macro-methodsは、AST(Abstract Syntax Tree : 抽象構文木)を引数に受け取り変換したASTを返すメソッドを、マクロ(コンパイル時にASTを変換する機能)としてサポートするためのライブラリです。
詳細は、次のspeakerdeckを参照してください。
groovy-macro-methodsの例
例えば、
def nullObject = null assert null == nullObject.hashCode()
といったコードがある場合、NullPointerExceptionで落ちます。これを、
def nullObject = null assert null == safe(nullObject.hashCode())
という風にsafeマクロで「コンパイル時にASTを変換」して、NullPointerExceptionで落ちないようにしたいと思います。つまり、「nullObject.hashCode()」といったメソッド呼び出しのASTを引数に受け取り、
def nullObject = null assert null == (nullObject != null ? nullObject.hashCode() : null)
といった三項演算子のASTに変換してコンパイル時に置き換える、というマクロを定義します。この場合のsafeマクロの定義は次のようになります。
package foo.bar; import org.codehaus.groovy.ast.expr.*; import static org.codehaus.groovy.ast.tools.GeneralUtils.*; import ru.trylogic.groovy.macro.runtime.*; public class TestMacroMethods { @Macro public static Expression safe(MacroContext macroContext, MethodCallExpression callExpression) { return ternaryX(notNullX(callExpression.getObjectExpression()), callExpression, constX(null)); } }
引数に「メソッド呼び出しの結果」ではなく「メソッド呼び出しのAST」を受け取り、三項演算子のASTに変換した結果(のAST)を戻り値として返します。 notNullX()が「引数がnullかどうかを判定するAST」を生成するメソッド(三項演算子の「nullObject != null」の部分)、constX()が「引数を定数として表すAST」を生成するメソッド、ternaryX()が「三項演算子のAST」を生成するメソッド、となります。
groovy-macro-methodsを使用したマクロの作り方
groovy-macro-methodsを使用してマクロを作る際、次の点に気をつけてください。
- public staticメソッドを用意する。
- メソッドに@Macroを付ける。
- メソッドの第一引数はMacroContextとする。
- メソッドの第二引数以降は変換したいASTのクラスとする。
- 戻り値はExpressionかExpressionの派生クラスとする。
- メソッド定義とは別に、拡張モジュールの定義を行う。
public staticメソッドを用意する。
マクロは、public staticのメソッドとして定義します。public staticでないとマクロとして認識してくれません。
メソッドに@Macroを付ける。
このメソッドがマクロであると宣言します。
メソッドの第一引数はMacroContextとする。
メソッドの第一引数はMacroContext型の値です。MacroContext自体は、次のクラスのオブジェクトを持ちます。
- CompilationUnit
- SourceUnit
- マクロ自体のMethodCallExpression
先の例のように、必ずしも使用しないといけないわけではありません。
メソッドの第二引数以降は変換したいASTのクラスとする。
メソッドの第二引数以降は変換したいASTのクラスを指定します。 ただし、指定できるASTのクラスは、Expression(式)かその派生クラスのみとなります。なので、Statement(文)やその派生クラスをマクロに渡して変換することはできません。 これをなんとかしたい場合、ClosureExpression(クロージャ式)を引数とするマクロを定義し、ClosureExpression内のBlockStatement内のStatementを変換、といったことならできます。
戻り値はExpressionかExpressionの派生クラスとする。
戻り値はExpression(式)かExpressionの派生クラスとなります。Statement(文)やその派生クラスを戻り値として返すことはできません。 これをなんとかしたい場合、Closureを定義してcallメソッドを呼び出す、つまりClosureExpression(クロージャ式)のMethodCallExpression(メソッド呼び出し)を戻り値として返すマクロを定義し、そのClosureExpressionの中でStatementやその派生クラスを定義する、といったことならできます。
メソッド定義とは別に、拡張モジュールの定義を行う。
メソッドを定義するだけではマクロとして機能せず、groovyの拡張モジュールの定義を行う必要があります。
ファイル「META-INF/services/org.codehaus.groovy.runtime.ExtensionModule」を作成し、extensionClassesにマクロの定義を持つクラスのFQCN(完全修飾クラス名)を指定します。例えば先のsafeマクロの拡張モジュールの定義は次のとおりです。
moduleName=macro-module moduleVersion=1.0 extensionClasses=foo.bar.TestMacroMethods
最終的には
マクロを使用した場合、最終的には、
def nullObject = null assert null == safe(nullObject.hashCode())
というコードは、
def nullObject = null assert null == MacroStub.INSTANCE.macroMethod (nullObject != null ? nullObject.hashCode() : null)
というコードにコンパイル時に変換され実行されます。macroMethodメソッド自体は、
public enum MacroStub { INSTANCE; public <T> T macroMethod(T obj) { return obj; } }
のように引数を返すだけのメソッド呼び出しとなっています。
マクロの使い所
普通のメソッド呼び出しで事足りるのであれば、マクロを使う必要はないと思います。普通のメソッド呼び出しでは実現できない場合にマクロ(= AST変換)が使えないか検討してみてください。例えば、DSL(Domain Specification Language)、糖衣構文(シンタックスシュガー)、等です。
マクロ開発の問題点
マクロを開発する上で問題となることと言えば、マクロがどういうASTを返しているか、想定したASTを返しているか、といったことがわかりづらい、ということでしょう。
これを解決するために、GroovyConsoleを使用します。GroovyConsoleで「Inspect Ast」を実行すると、「Semantic Analysis」以降であれば、マクロを呼び出した際のAST変換後のコードを見ることができます。
後編に続く
さて、前編はここまで、groovy-macro-methodsとは何か、groovy-macro-methodsを使ったマクロの作り方をご紹介しました。
後編は、groovy-macro-methodsを使った「あまり実用的ではないのですが、サンプルとしてはまぁ何とか...」というマクロをいくつか作ってみましたので、それをご紹介することで、groovy-macro-methodsでどんなことができるかご紹介したいと思います。
RoboVMはGroovyの夢を見るか
はじめに
これはG*アドベントカレンダーの最終日、第25日目の記事です。
巷では「AndroidはGroovyの夢を見るか」という話がありますが、もう一つの雄である、iOSでGroovyは何とかならんのか、というのが今回のネタです。
もう少し具体的に言うと、JavaでiOS用アプリを開発するソフトウェアとしてRoboVMがありますが、Groovyで使えないか?を探ります。
結論から言うと、力及ばず、夢は夢のままでした... (¦3[___]
#私の環境だけかもしれませんが...
この記事では、次の環境で動作をさせています。
OS: OSX 10.9.5 Java: 1.8.0_25 Groovy: 2.4.0-rc-1 Gradle: 2.2.1 XCode: 6.1.1 RoboVM: 1.0.0-beta-01
ここからは、私が何をして、何ができて、何ができなかったのか、第一歩でコケた話をお楽しみください。
RoboVMのインストール
RoboVMのインストール自体は簡単です。RoboVMの「Download the latest RoboVM release」からtar+gzファイルをダウンロードし、適当なディレクトリに展開します。
tar zxvf robovm-1.0.0-beta-01.tar.gz
あとは、展開してできるディレクトリを環境変数ROBOVM_HOMEに、${ROBOVM_HOME}/binを環境変数PATHに、それぞれ設定します。
先人の知恵を参考にする
既に「RoboVMをGroovyで」という先人が何人かいらっしゃるようで、今回はここから一式クローンして試してみます。
git clone https://github.com/msgilligan/GradleGroovyRobot.git
名前からわかるように、Gradleのプロジェクトになっています。build.gradleにちょっと手を入れ、次のように変更しました。
apply plugin: 'groovy' repositories { mavenCentral() flatDir { dirs 'lib' } } def robovmver = "1.0.0-beta-01" project.ext.set("robovmver", robovmver) dependencies { compile 'org.codehaus.groovy:groovy-all:2.4.0-rc-1' compile "org.robovm:robovm-rt:$robovmver" compile "org.robovm:robovm-objc:$robovmver" compile "org.robovm:robovm-cocoatouch:$robovmver" } project.ext.set("robovm_home", "/Users/rhapsody/Tools/RoboVM/robovm-$robovmver") // main class String mainClass = "IOSDemo" def groovyJar = "" task findGroovyJar << { groovyJar = project.configurations.compile.find { it.name.startsWith("groovy-all-") } } // compile bytecode to llvm and run in the ios simulator task run (dependsOn: [build, findGroovyJar]){ doFirst { println(">> Running RoboVM") String cmd = "$project.robovm_home/bin/robovm -verbose -arch x86 -os ios -cp $project.robovm_home/lib/robovm-objc.jar:$project.robovm_home/lib/robovm-cocoatouch.jar:$projectDir/build/classes/main/:$groovyJar -run $mainClass" def proc = cmd.execute() proc.in.eachLine {line -> println line} proc.err.eachLine {line -> System.err.println( 'ERROR: ' + line)} proc.waitFor() } }
サンプルのコード
サンプルのコードですが、クローンしたプロジェクトに含まれていますが、ここに載っているJavaのコードをGroovyで書きなおしてみました。ポイントは、@CompileStaticを使うことです。
import org.robovm.apple.coregraphics.CGRect import org.robovm.apple.foundation.NSAutoreleasePool import org.robovm.apple.uikit.UIApplication import org.robovm.apple.uikit.UIApplicationDelegateAdapter import org.robovm.apple.uikit.UIApplicationLaunchOptions import org.robovm.apple.uikit.UIButton import org.robovm.apple.uikit.UIButtonType import org.robovm.apple.uikit.UIColor import org.robovm.apple.uikit.UIControl import org.robovm.apple.uikit.UIControlState import org.robovm.apple.uikit.UIEvent import org.robovm.apple.uikit.UIScreen import org.robovm.apple.uikit.UIWindow import groovy.transform.CompileStatic @CompileStatic class IOSDemo extends UIApplicationDelegateAdapter { UIWindow window = null int clickCount = 0 @Override boolean didFinishLaunching(UIApplication application, UIApplicationLaunchOptions launchOptions) { final UIButton button = UIButton.create(UIButtonType.RoundedRect) button.frame = new CGRect(115.0f, 121.0f, 91.0f, 37.0f) button.setTitle('Click me!', UIControlState.Normal) /* button.addOnTouchUpInsideListener(new UIControl.OnTouchUpInsideListener() { @Override public void onTouchUpInside(UIControl control, UIEvent event) { button.setTitle('Click #' + (++clickCount), UIControlState.Normal) } }) */ window = new UIWindow(UIScreen.mainScreen.bounds) window.backgroundColor = UIColor.lightGray() window.addSubview(button) window.makeKeyAndVisible() true } static void main(String[] args) { NSAutoreleasePool pool = new NSAutoreleasePool() UIApplication.main(args, null, IOSDemo) pool.close() } }
これで実行の準備が整いました。では実行してみましょう。
gradle run
初回はバイトコードをネイティブなコードに変換しているようで時間がかかり、また私の環境だけかもしれませんが、エラーでiOSシミュレータが起動しなかったりする場合があります。そのような場合は、再度実行すると、次のようにiOSシミュレータが起動し、アプリが実行されます。
ここまではできるのですが、ここから先には進めませんでした...orz
サンプルのコード、その2
先程のサンプルコードですが、コメントアウトしている箇所があります。
button.addOnTouchUpInsideListener(new UIControl.OnTouchUpInsideListener() { @Override public void onTouchUpInside(UIControl control, UIEvent event) { button.setTitle('Click #' + (++clickCount), UIControlState.Normal) } })
ボタンにリスナーを登録して、ボタンがクリックされた場合に、ボタンのタイトルを再設定することをしています。
では、このコメントアウトしている箇所を有効にして実行してみましょう。iOSシミュレータが起動しますが、先程のようにアプリは実行されず、次のようなメッセージが出力されてしまいます。
#ちなみに、クローンしたプロジェクトのサンプルコードも基本的に同じで、リスナーを登録するコードはありませんでした。
[WARN] android.System: A resource was acquired at attached stack trace but never released. See java.io.Closeable for information on avoiding resource leaks. [WARN] android.System: java.lang.Throwable: Explicit termination method 'close' not called at dalvik.system.CloseGuard.open(CloseGuard.java) at java.io.RandomAccessFile.<init>(RandomAccessFile.java) at java.io.RandomAccessFile.<init>(RandomAccessFile.java) at java.util.zip.ZipFile.<init>(ZipFile.java) at java.util.zip.ZipFile.<init>(ZipFile.java) at java.lang.PathClassLoader.init(PathClassLoader.java) at java.lang.PathClassLoader.findResource(PathClassLoader.java) at java.lang.ClassLoader.getResource(ClassLoader.java) at java.lang.ClassLoader.getResourceAsStream(ClassLoader.java) at org.codehaus.groovy.reflection.GeneratedMetaMethod$DgmMethodRecord.loadDgmInfo(GeneratedMetaMethod.java) at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.registerMethods(MetaClassRegistryImpl.java) at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.<init>(MetaClassRegistryImpl.java) at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.<init>(MetaClassRegistryImpl.java) at groovy.lang.GroovySystem.<clinit>(GroovySystem.java) at org.codehaus.groovy.runtime.InvokerHelper.<clinit>(InvokerHelper.java) at groovy.lang.GroovyObjectSupport.<init>(GroovyObjectSupport.java) at groovy.lang.Reference.<init>(Reference.java) at IOSDemo.didFinishLaunching(IOSDemo.groovy) at org.robovm.apple.uikit.UIApplicationDelegate$ObjCProxy.$cb$application$didFinishLaunchingWithOptions$(Unknown Source) at org.robovm.apple.uikit.UIApplication.main(Native Method) at org.robovm.apple.uikit.UIApplication.main(UIApplication.java) at IOSDemo.main(IOSDemo.groovy) [WARN] java.lang.Class: Class.forName() failed to load 'java.lang.ClassValue'. Use the -forcelinkclasses command line option or add <forceLinkClasses><pattern>java.lang.ClassValue</pattern></forceLinkClasses> to your robovm.xml file to link it in. Loading class 'java.util.logging.ConsoleHandler' failed java.lang.ClassNotFoundException: java.util.logging.ConsoleHandler java.lang.ExceptionInInitializerError at org.codehaus.groovy.runtime.InvokerHelper.<clinit>(InvokerHelper.java) at groovy.lang.GroovyObjectSupport.<init>(GroovyObjectSupport.java) at groovy.lang.Reference.<init>(Reference.java) at IOSDemo.didFinishLaunching(IOSDemo.groovy) at org.robovm.apple.uikit.UIApplicationDelegate$ObjCProxy.$cb$application$didFinishLaunchingWithOptions$(Unknown Source) at org.robovm.apple.uikit.UIApplication.main(Native Method) at org.robovm.apple.uikit.UIApplication.main(UIApplication.java) at IOSDemo.main(IOSDemo.groovy) Caused by: java.lang.NullPointerException at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.<init>(MetaClassRegistryImpl.java) at org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.<init>(MetaClassRegistryImpl.java) at groovy.lang.GroovySystem.<clinit>(GroovySystem.java) ... 8 more BUILD SUCCESSFUL Total time: 34.537 secs
リスナーが登録できなければ、先に進めません...
時間もないので、今回はここまでです...
最後に
今後、やる気があれば、できたバイトコードをデコンパイルしたり、Groovyのソースを追っかけてみるつもりです。
Android用のコードが入っているので、同じようにRoboVM用のコードを追加してなんとかなるといいのですが。
GParsでリモート処理
はじめに
これはG*アドベントカレンダーの第21日目の記事です。
今回は、GPars 1.3.0から追加される(であろう)機能の一つ、リモート処理(remoting)をご紹介します。
この記事では、次の環境で動作をさせています。
OS: OSX 10.9.5 Java: 1.8.0_25 Groovy: 2.4.0-beta-4
GParsとは
GParsとは、JavaやGroovyで並行処理を行うためのフレームワークです。12月21日時点では1.2.1が安定版のバージョンで、1.3.0はスナップショット版となっています。
GPars自体はGroovyに同梱されており、2.3.9、2.4.0-beta-4とも安定版の1.2.1が同梱されています。
GParsのリモート処理
GParsが元々持っている
- Actor
- Agent
- Dataflow
は同じJVM上で動作させることになりますが、GParsのリモート処理とはこれらを別JVM上で動作せることが可能となります。これにより、別JVM上のActorにメッセージを送ったり、応答を受け取ったりすることができるようになります。
同じホスト上の別JVM上だけでなく、別ホスト上のJVM上でも動作させることが可能となっています。
なお、メッセージのやりとりについては、ベースにNettyを使って実現しているようです。
GPars 1.3.0のjarのビルド
GPars 1.3.0はまだ開発中なので、スナップショット版のjarファイルをリポジトリから取得するか、自分でビルドする必要があります。
公式ページからたどると、スナップショット版のjarファイルをリポジトリから取得できる記載がありますが、リポジトリに上がっているjarファイルにはリモート処理用のclassファイルが含まれていないようでしたので、今回は自分でビルドすることにします。
GitHubからソースを落とし、
git clone https://github.com/GPars/GPars.git
ソースディレクトリ内のgradlewコマンドでビルドします。
cd GPars ./gradlew
私の環境ではtestの途中で先に進まなかったので、control+Cで中断しjarファイルだけコピーすることにしました。
cloneしたディレクトリの下に「build/libs/gpars-1.3-SNAPSHOT.jar」ができていますので、${GROOVY_HOME}/lib/gpars-1.2.1.jarを削除して、代わりに「build/libs/gpars-1.3-SNAPSHOT.jar」を${GROOVY_HOME}/libにコピーします。
あと、先程も述べたとおりNettyを使っていますので、Mavenリポジトリからnetty-all-4.0.24.Final.jarを取得し${GROOVY_HOME}/libにコピーします。
リモート処理のサンプルコード
cloneしたディレクトリの下に「src/test/groovy/groovyx/gpars/samples/remote/」というディレクトリがあり、このディレクトリ配下のソースがリモート処理のサンプルコードとなっています。
ここからは、actor/calculatorのサンプルコードを使って、通常のActorとリモート処理のActorがどのように違うのか、を見てみたいと思います。
calculatorのサンプルコードについて
calculatorのサンプルコードですが、次のような内容になっています。
- 計算を依頼し結果を表示するActor(A)と、実際に計算をするActor(B)の、2つのActorを用意。
- AからBへは、2つの数値をメッセージとして順に送信する。
- Bは受け取った2つのメッセージを加算し、計算結果としてAに返す。
- 計算結果を受け取ったAは、標準出力に計算結果を出力する。
従来のActorでのコード
従来のActorを使って実現すると、こんな感じのコードになります。
// LocalCalculator.groovy package groovyx.gpars.samples.remote.actor.calculator import groovyx.gpars.actor.Actors def answerActor = Actors.actor { println "Calculator - Answer" react { a-> // (*a) react { b-> // (*b) reply a + b // (*c) } } } def queryActor = Actors.actor { println "Calculator - Query" answerActor << 1 // (*d) answerActor << 2 // (*e) react { println it } // (*f) } [answerActor, queryActor]*.join()
計算をするanswerActorと、計算を依頼するqueryActorの、2つのActorのオブジェクトを生成し、動作させます。
上記のコードでは、queryActorから1, 2という数値をメッセージとして送信し(*d, *e)、2つのメッセージ受け取った(*a, *b)answerActorではそれらのメッセージを数値として加算し、その結果を返します(*c)。結果を受け取ったqueryActorは、その結果を標準出力に出力します(*f)。
これら2つのActorは、同じJVM上で動作することになります。
従来のActorの実行結果
上記のコードを実行してみます。
groovy LocalCalculator.groovy
実行すると、次の値が標準出力に出力されます。
Calculator - Query Calculator - Answer 3
リモート処理のActorのコード : 計算をする側
リモート処理のActorのコードですが、計算をする側と計算を依頼する側にコードがわかれます。
計算をする側はこんな感じのコードになります。
// RemoteCalculatorAnswer.groovy package groovyx.gpars.samples.remote.actor.calculator import groovyx.gpars.actor.Actors import groovyx.gpars.actor.remote.RemoteActors def HOST = "localhost" def PORT = 9000 def remoteActors = RemoteActors.create() // (*a) remoteActors.startServer HOST, PORT // (*b) def answerActor = Actors.actor { println "Remote Calculator - Answer" remoteActors.publish delegate, "remote-calculator" // (*c) react { a-> react { b-> reply a + b } } } answerActor.join() remoteActors.stopServer() // (*d)
まず、リモート処理用のコンテキストを作成します(*a)。次に、localhostの9000ポートでメッセージ受信を開始します(*b)。
実際にメッセージを受信し処理をするコードは従来のActorと基本的には同じですが、"remote-calculator"という名前でメッセージを受信できるようにパブリッシュ処理を別途行います(*c)。
処理の最後に、コンテキストを終了させます(*d)。
リモート処理のActorのコード : 計算を依頼する側
計算を依頼する側はこんな感じのコードになります。
// RemoteCalculatorQuery.groovy package groovyx.gpars.samples.remote.actor.calculator import groovyx.gpars.actor.Actors import groovyx.gpars.actor.remote.RemoteActors def HOST = "localhost" def PORT = 9000 def remoteActors = RemoteActors.create() // (*a) def queryActor = Actors.actor { println "Remote Calculator - Query" def remoteCalculator = remoteActors.get HOST, PORT, "remote-calculator" get() // (*b) remoteCalculator << 1 // (*c) remoteCalculator << 2 // (*d) react { println it } // (*e) } queryActor.join()
まず、リモート処理用のコンテキストを作成します(*a)。次に、localhostの9000ポート、"remote-calculator"という名前のActorに対してメッセージを送信できるよう、Proxyオブジェクトを取得します(*b)。
取得したProxyオブジェクトに対しメッセージを送信することで、Proxyオブジェクトを介して別JVM上のActorにメッセージを送信することができます(*c, *d)。
最後に、別JVM上のActorからメッセージを受信し標準出力に出力します(*e)。
リモート処理のActorの実行結果
上記のコードを実行してみます。まず、計算する側のコードを実行します。
groovy RemoteCalculatorQuery.groovy
実行すると、次の値が標準出力に出力されます。
Remote Calculator - Answer
次に、計算を依頼する側のコードを先程とは別のターミナルで実行します。
groovy RemoteCalculatorQuery.groovy
実行すると、次の値が標準出力に出力されます。一見すると同じJVM上で動作しているようにも見えますが、別のJVM上で計算した結果を受け取って表示しています。
Remote Calculator - Query 3
なお、私の環境では、次の警告メッセージとスタックトレースが標準エラー出力に出力されました。
12 22, 2014 1:00:11 午前 io.netty.channel.DefaultChannelPipeline$TailContext exceptionCaught 警告: An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception. java.io.IOException: Connection reset by peer at sun.nio.ch.FileDispatcherImpl.read0(Native Method) at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:39) at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223) at sun.nio.ch.IOUtil.read(IOUtil.java:192) at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:379) at io.netty.buffer.UnpooledUnsafeDirectByteBuf.setBytes(UnpooledUnsafeDirectByteBuf.java:446) at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:881) at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:225) at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:119) at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:511) at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:468) at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:382) at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:354) at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:116) at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:137) at java.lang.Thread.run(Thread.java:745)
最後に
GParsのリモート処理を使うことで、他サーバのActorと割とお手軽に連携する可能になります。また、今回は紹介できませんでしたが、AgentもDataflowについても、Actorと基本的に同じように使うことができると思います。
他サーバと処理を連携させるコードを書く場合などに使ってみるのはいかがでしょうか。
早く1.3.0がリリースされることを希望します。
GroovyでElasticSearchしてみた
はじめに
これはG*アドベントカレンダーの第24日目の記事です。
今回は、GroovyでElasticSearchサーバにアクセスするための、クライアントプログラムを書くお話です。
ElasticSearchとは
ElasticSearchは、オープンソースの検索エンジンです。詳細はこちらを参照してください。
ElasticSearchサーバへのアクセス
ElasticSearch自体、RESTfulな構造になっていますので、curlなどのツールを使ってアクセスすることができます。
また、Javaを始めとするプログラミング言語からアクセスするためのAPIも提供されています。APIが提供されている言語は次のとおりです。
今回はGroovy用のAPIを使って、ElasticSearchにドキュメントを登録するところまでをやってみます。時間の都合上、今回はこれが限界です...orz
Groovy用APIの詳細は、こちらのガイドを参照してください。
今回、Groovyはバージョン2.2.1を使用します。
実行に必要なライブラリの指定
実行に必要なライブラリは、@Grabアノテーションで指定します。
@Grab(group = 'org.elasticsearch', module = 'elasticsearch-lang-groovy', version = '1.5.0')
moduleの指定がガイドと違いますが、こちらの方が新しかったもので...
クライアント用ノードの設定
次にクライアント用ノードの設定を行います。Groovy用のノードビルダークラスが有りますので、このクラスのインスタンスを取得し、各種設定を行います。
GNodeBuilder nodeBuilder = nodeBuilder(); nodeBuilder.settings.put("node.client", true) nodeBuilder.settings.put("cluster.name", "jggug")
なお、ガイドでは、クロージャで設定ができるようなことが書いてありましたが、今回試したバージョンでは上手く行かなかったので、クロージャを使用しないで設定を行っています。
クライアントのインスタンスの生成
次にクライアントのインスタンスを生成します。
GNode node = nodeBuilder.node() GClient client = node.client
ドキュメントの登録
ドキュメントの登録は、indexメソッドに、インデックス名、タイプ名、ID、登録したいソースを指定します。
def indexR = client.index { index "test" type "type1" id "1" source { test = "value" complex { value1 = "value1" value2 = "value2" } } }
ガイドのサンプルはこのとおりなのですが、これだと"complex"という空のオブジェクトが登録されるだけです...orz いろいろ調べてはみたのですが、上手く行かず...orz
終了処理
最後にクライアントの終了処理です。
node.stop().close()
全コード
これまでのコードを合わせると次のとおりです。
@Grab(group = 'org.elasticsearch', module = 'elasticsearch-lang-groovy', version = '1.5.0') import org.elasticsearch.groovy.client.GClient import org.elasticsearch.groovy.node.GNode import org.elasticsearch.groovy.node.GNodeBuilder import static org.elasticsearch.groovy.node.GNodeBuilder.* GNodeBuilder nodeBuilder = nodeBuilder(); nodeBuilder.settings.put("node.client", true) nodeBuilder.settings.put("cluster.name", "jggug")GNode node = nodeBuilder.node() GClient client = node.client def indexR = client.index { index "index_test" type "type1" id "1" source { test = "value" complex { value1 = "value1" value2 = "value2" } } } println "Indexed $indexR.response.id into $indexR.response.index/$indexR.response.type" node.stop().close()
で?
使用したバージョンが悪いのか、ガイドのサンプルが悪いのか不明ですが、結局うまく登録できませんでした...orz
時間があれば、再チャレンジといきたいところです。
GrailsでElasticSearch
GroovyではなくGrailsでElasticSearchしたい場合ですが、現状次の2つのプラグインがあるようです。
どちらかのプラグイン(おそらく後者)を次のG*マガジンの「Plugin探訪」で紹介をしたいと思います。
次は
中途半端な記事になってしまいましたが、明日25日も私が担当します。 ネタは今から探します...orz