GroovyはJavaバーチャルマシン向けの優れた動的言語です。Javaとシームレスに統合でき、その構文やAPIなどはJavaを基にしています。また同時に、動的言語としてはSmalltalk、Python、Rubyといった他言語を倣っています。
GroovyはGrails、Spring、JBoss Seamなど多くのオープンソースプロジェクトで用いられています。また多くの商用製品やFortune 500(Fortune 誌による全米上位500社)のミッションクリティカルなアプリケーションでも用いらていて、Groovyのスクリプティング性能によってそれらアプリケーションのメカニズムに大きな拡張性をもたらしています。その他にもGroovyは、ビジネスコンセプトを読み易く、かつ保守し易い方法で表現するドメイン固有言語(Domain-Specific Language)を組み込む手段やアイディアをその方面の専門家やディベロッパに提供しています。
このArticleでは、SpringSource社におけるGroovy開発トップのGroovyプロジェクトマネージャであるGuillaume Laforge氏が新しくリリースされたGroovy 1.6の注目の新機能について概要説明をおこなってくれます。
Groovy 1.6の概要
この記事で見ていくことになるGroovy 1.6での注目機能を最初にあげておきましょう。
- コンパイル時間の短縮と実行時のパフォーマンスの改善
- 多重代入
if
/else
ブロックやtry
/catch
ブロックでの任意の戻り値- Java 5のアノテーションによる定義
- AST(抽象構文木)変換およびアノテーション(
@Singleton
,@Lazy
,@Immutable
,@Delegate
とその類)による変換 - Grapeというモジュールおよび依存性についてのシステムと、
@Grab
変換 - Swing Builderの改善(Swing/Griffonチームによる賜物)およびSwingコンソールの改善
- JMX builderの統合
- メタプログラミング性の拡張(EMC DSL、POJOでのインスタンスごとのメタクラス、実行時ミックスイン)
- JSR-223で策定されたスクリプトエンジンの内蔵
- OSGiへの準拠
これらの改善および新機能はすべて一つのゴールに向けたものです。すなわち、以下のことによりディベロッパがより生産性を上げられ、よりアジャイルになることを助けるのです。
- 技術的なコードではなく目の前のタスクによりフォーカスをあてる
- 車輪の再発明をする(既存技術を一から作る)のでなく既存のエンタープライズAPIを活用する
- 全般的にパフォーマンスを改善し、言語としての質を向上させる
- 自分たちのドメイン固有言語を構築できるよう、ディベロッパが言語を好きにカスタマイズできるようにする
これらはGroovyの重要な側面です。しかしより重要なのは、Groovyが単に言語であるだけでなく、ひとつのエコシステムであるということです。
Groovyのバイトコードに含まれる情報を増やしたことは、Coberturaのような優れたカバレッジツールがGroovyをサポートする助けとなり、CodeNarcのような新らしい静的コード解析ツールにとっては利用しやすい情報が手に入ることになります。
また構文の柔軟性とメタプログラミングの特性は、ビヘイビア駆動開発のためのEasybやモックライブラリであるGMock、テスト/テスト仕様化のフレームワークSpockといった先進的なテストツールを生み出しています。
さらに、Groovyの柔軟性および表現可能性、そしてスクリプティングの特性によって、継続的インテグレーション(ビルドやテストを頻繁に行う方法論)を実践するための先進的なスクリプトシステムや基盤へのドアを開き、GantやGraddleのようなソリューションを生み出しています。
ツールレベルでもGroovyは進化しています。たとえばgroovydoc
のAntタスクを使うと、GroovyとJavaが混在するプロジェクトのソースファイルでも、GroovyもJavaもカバーして相互にリンクするJavaDocを生成してドキュメント化できます。
一方でIDEのベンダもGroovyのサポートを強化していて、両言語をまたぐコードリファクタリング、動的言語のイディオムのより的確な認識、コード補完、といった強力な機能が提供されています。そのおかげでGroovyをプロジェクトで使う際の生産性が向上します。
Groovyの世界について予備知識もついたところで、これからGroovy 1.6の新機能を見ていくことにしましょう。
パフォーマンスの改善
以前のバージョンに比べると、多くの点でコンパイル時間と実行時のパフォーマンス向上が図られています。
コンパイラは以前のものより3倍から5倍速くなりました。この改良は1.5系にも反映されているので、保守ブランチのバージョンでも現行の安定バージョンでも恩恵が受けられます。このコンパイル時間短縮は、クラス参照キャッシュのおかげで大きいプロジェクトほど効果が現れるでしょう。
しかしより特筆すべきなのは実行時パフォーマンスの全体的な改善の方でしょう。Great Language Shootoutのいくつものベンチマークを使ってその改善具合を測ったところ、Groovy 1.5系に比べて150%から460%のパフォーマンス向上が認められました。みなさんのプロジェクトでも局所的なベンチマークではあまり明快な改善を確認するのは難しいかもしれませんが、全体的なパフォーマンスは大幅に向上しているはずです。
多重代入
Groovy 1.6で唯一追加された構文は、複数の変数について一度に宣言・代入をおこなうためのものです。
def (a, b) = [1, 2] assert a == 1 assert b == 2
これより経度と緯度のセットを返すメソッドを使った次の例の方が分かりやすいかもしれません。そのセットが2つの要素をもつリストであるなら、それぞれの要素を次のように簡単に取得することができます。
def geocode(String location) { // implementation returns [48.824068, 2.531733] for Paris, France } def (lat, long) = geocode("Paris, France") assert lat == 48.824068 assert long == 2.531733
複数の変数の型の宣言も次のように一度におこなえます。
def (int i, String s) = [1, 'Groovy'] assert i == 1 assert s == 'Groovy'
(既に型が宣言されている変数への)代入をおこなうにはdef
キーワードを取り除くだけです。
def firstname, lastname (firstname, lastname) = "Guillaume Laforge".tokenize() assert firstname == "Guillaume" assert lastname == "Laforge"
右辺の要素の数が左辺より多い場合は、先にある要素の方が変数に代入されます。逆に右辺の方が少ない場合は、nullが追加で代入されます。
そのため代入先の変数の数が代入元のリスト要素数より多い以下の例の場合、c
はnull
となります。
def elements = [1, 2] def (a, b, c) = elements assert a == 1 assert b == 2 assert c == null
代入先の変数の方が少ない場合では、次のような結果になります。
def elements = [1, 2, 3, 4] def (a, b, c) = elements assert a == 1 assert b == 2 assert c == 3
好奇心旺盛な方には次のような例もお見せしておきましょう。ここでは値の入れ替えを多重代入を使って1行でおこなっています。
// given those two variables def a = 1, b = 2 // swap variables with a list (a, b) = [b, a] assert a == 2 assert b == 1
アノテーション定義
さきほど、追加された構文は多重代入だけだと書きましたが、実は100%正確ではありません。GroovyはGroovy 1.5でもアノテーション定義の構文をサポートしていましたが、その機能を完璧には実装していませんでした。嬉しいことに、1.6ではこれが修正され、そのことでJava 5の全ての機能がGroovyでカバーできるようになりました。たとえば静的インポート、ジェネリック、アノテーション、列挙型クラスがそうで、これによりGroovyはJava 5の機能を全てサポートするJVM向けの完全な動的言語となりました。これはJavaとのシームレスな統合という目標において重要なことですし、アノテーションやジェネリックに依存するJPA、EJB3、Spring、TestNGなどのエンタープライズフレームワークを用いる上でも決定的な意味を持つことです。
if/elseブロックやtry/catch/finallyブロックでの任意の戻り値
if/else
ブロックやtry/catch/finally
ブロックがメソッドやクロージャ内での最後の式である場合に、それらのブロックで戻り値を返せるようになりました。ブロック内の最後の式が戻り値の式である場合は、明示的にreturn
キーワードを用いる必要はありません。
例えば、次のメソッドにはreturn
キーワードがありませんが、戻り値として1
を返します。
def method() { if (true) 1 else 0 } assert method() == 1
try/catch/finally
ブロックでは、最後に評価された式が戻り値の式となります。もしtry
ブロックで例外が起きた場合、catch
ブロック内の最後の式が返ることになります。その場合finally
ブロックは値を返さないことに注意してください。
def method(bool) { try { if (bool) throw new Exception("foo") 1 } catch(e) { 2 } finally { 3 } } assert method(false) == 1 assert method(true) == 2
AST変換
時々、新しい機能を実装するにはGroovyの構文を拡張するのがいい(その例が今回の多重代入です)という声もありますが、ほとんどの場合、単に文法へ新しいキーワードを追加したり、新しい概念を表現する新しい構文構造を作ったりするだけで対応できるものではありません。しかしAST(抽象構文木)変換については、文法を変えることなく、新しく画期的なアイディアに対応することができました。
Groovy のコンパイラがGroovyスクリプトやクラスをコンパイルする過程で、ソースコードは具象構文木の形でメモリに格納され、それから抽象構文木(AST)に変換されます。そのAST変換の目的は、JVMで実行することになるバイトコードになる前にASTに手を加えられるよう、コンパイルプロセスをフックできるようにすることです。
AST変換によって、コンパイルの段階でメタプログラミングができ、Groovyに言語レベルの強力な柔軟性をもたらします。コンパイル時間は1.6で改善されていますし、実行時パフォーマンスを損なうこともありません。
この変換にはグローバル変換とローカル変換の2種類があります。
- グローバル変換は、コンパイラによってコンパイル中のコードに適用されます。これはAST変換を適用する場合に必ずおこなわれます。コンパイラのクラスパスにあるJARファイルには
META-INF/services/org.codehaus.groovy.transform.ASTTransformation
にあるサービスロケータファイルが格納され、そこにはグローバル変換をおこなうクラスの名前が記された行が含まれることになります。グローバル変換クラスは引数無しのコンストラクタを持ち、org.codehaus.groovy.transform.ASTTransformation
インターフェースを実装します。グローバル変換はコンパイル対象の全てのソースコードに対しておこなわれるため、リソースや時間を大量に消費するような方法で全ASTを走査するような変換クラスを作ってしまうとコンパイルが遅くなってしまいます。 - ローカル変換は、変換をおこないたい箇所に付けられたアノテーションによって局所的に適用される変換です。この変換にGroovyのアノテーション表記法が使えるようにし、そのためのアノテーションは implement
org.codehaus.groovy.transform.ASTTransformation
を実装することになります。コンパイラはローカル変換のアノテーションを見つけると、コードの対象となる箇所に対して変換をおこないます。
Groovy 1.6にはいくつかのローカル変換アノテーションがあります。Groovy Swing Builderでデータバインドをおこなう@Bindable
および@Vetoable
、Grapeモジュールシステムでスクリプトライブラリの依存性を示す@Grab
、それ以外には、構文の変更なしでも使えるGroovy言語標準のアノテーションとして@Singleton
、@Immutable
、@Delegate
、@Lazy
、@Newify
、@Category
、@Mixin
、@PackageScope
があります。これら標準のアノテーションのうちいくつかについてこれから見ていきましょう(@Bindable
および@Vetoable
はSwing拡張の項で、@Grab
はGrapeの項で触れます)。
@Singleton
シングルトンがデザインパターンであるにしてもアンチパターンであるにしても、シングルトンを作らないといけないケースはあるものです。その場合によく用いられるのはprivateなコンストラクタとstaticフィールドあるいは初期化済みpublic static final
フィールドを取得するためのgetInstance
メソッドを作る方法です。これをJavaで書くと次のようになります。
public class T { public static final T instance = new T(); private T() {} }
これは@Singleton
アノテーションを使うと次のように型宣言するだけですみます。
@Singleton class T {}
シングルトンのインスタンスはT.instance
(publicなフィールドを直接参照しています)と書くだけで参照できます。
また次のようにパラメータをアノテーションに加えるとレイジーローディング(遅延読み込み)をおこなうこともできます。
@Singleton(lazy = true) class T {}
これは次のGroovyクラスとほぼ同等なものになります。
class T { private static volatile T instance private T() {} static T getInstance () { if (instance) { instance } else { synchronized(T) { if (instance) { instance } else { instance = new T () } } } } }
レイジーローディングをおこなう/おこなわないのいずれにしろ、ここでもインスタンスを参照するにはT.instance
と書くだけ(プロパティ参照の形で、T.getInstance()
の省略形)です。
@Immutable
イミュータブルなオブジェクトは一度生成したら変更ができないオブジェクトのことです。このようなオブジェクトが必要とされることはよくあることで、それはマルチスレッドにおいてもシンプルかつ安全に共有することが可能だからです。このことは関数型の処理や並列処理で大きな効力を発揮します。そしてこのようなオブジェクトを作る際にはお決まりのルールがあります。
- 内部の状態を変えるメソッドを持たない
- クラスはfinalで
- フィールドはprivateかつfinalで
- 外部で変更可能なコンポーネントのディフェンシブコピー(そのコンポーネント用の変数には参照でなくクローンをセットする)
- オブジェクトを比較したりMapなどのキーとしてフィールドを利用したい場合、フィールドに関する
equals()
、hashCode()
、toString()
を実装する
GroovyではJavaやGroovyのクラスをイミュータブルにするためにこのようなルールをコーディングしなくても、次のように書くだけですみます
@Immutable final class Coordinates { Double latitude, longitude } def c1 = new Coordinates(latitude: 48.824068, longitude: 2.531733) def c2 = new Coordinates(48.824068, 2.531733) assert c1 == c2
お決まりのコードはコンパイル時に生成されます。この変換時には2つのコンストラクタが作られます。上の例から分かるように、一方ではキーと値を指定して対応するフィールドに値をセットし、もう一方では値をパラメータで指定します。またassert
式から分かるように、上記のイミュータブルオブジェクトを適切に比較するためのequals()
が実装されています。
この@Immutable変換の実装の詳細を確認することもできます。実際の所、上の@Immutable
を使った例では50行強に相当するJavaコードが生成されます。
@Lazy
変換には他にも@Lazy
があります。フィールドの初期化時に初期値を求めるのが時間とメモリの大きな消費になる場合、その初期化を実際に最初に利用する時におこないたいことがあります。そのような場合、そのフィールドに対するゲッターをカスタマイズして、初めてそのゲッターが呼ばれた時に初期化を行うのが常套手段です。しかしGroovy 1.6では@Lazy
アノテーションが使えます。
class Person { @Lazy pets = ['Cat', 'Dog', 'Bird'] } def p = new Person() assert !(p.dump().contains('Cat')) assert p.pets.size() == 3 assert p.dump().contains('Cat')
この例のようにではなく、フィールドの初期化で複雑な計算をおこなうためにメソッドをいくつか呼び出したいケースもあるでしょう。そのような場合は次のように遅延評価をクロージャの呼び出しでおこなうことができます。
class Person { @Lazy List pets = { /* complex computation here */ }() }
また巨大なデータを格納するフィールドについてガベージコレクションを効率的におこなうために、そのフィールドへのソフト参照(参照しているオブジェクトをなるべくガベージコレクションの対象にしないようにする参照)を@Lazy
で指定することもできます。
class Person { @Lazy(soft = true) List pets = ['Cat', 'Dog', 'Bird'] } def p = new Person() assert p.pets.contains('Cat')
コンパイラで生成されるpets
フィールドは内部的にはソフト参照になるのですが、このクラスのユーザはp.pets
でソフト参照自体でなく直接その値(つまりpetsリスト)にアクセスすることができます。これによりソフト参照を透過的に使うことができるようになっています。
@Delegate
Java はそれ自体にデリゲート(委譲)の仕組みがなく、Groovyも今のところ同様です。しかしクラスのフィールドやプロパティに@Delegate
を付けることでメソッド呼び出しをデリゲートすることが可能になります。次の例では、Event
クラスのDate
型フィールドへのデリゲートがされていて、コンパイラはEvent
クラス上で呼び出されるDate
クラスメソッドをこのDate
型フィールドへデリゲートします。最後のassert
式からEvent
クラスがbefore(Date)
メソッドを持っていることが分かりますが、実際にEvent
クラスはDate
クラスのメソッドを全て持つようになっています。
import java.text.SimpleDateFormat class Event { @Delegate Date when String title, url } def df = new SimpleDateFormat("yyyy/MM/dd") def gr8conf = new Event(title: "GR8 Conference", url: "http://www.gr8conf.org", when: df.parse("2009/05/18")) def javaOne = new Event(title: "JavaOne", url: "http://java.sun.com/javaone/", when: df.parse("2009/06/02")) assert gr8conf.before(javaOne.when)
GroovyコンパイラによってDate
クラスの全メソッドがEvent
クラスに追加されます。Event
クラスのそれらのメソッドの呼び出しはDate
型のフィールドにデリゲートされます。もしデリゲート先(この場合Date)がfinalクラスでない場合、以下のようにEvent
クラスをDate
クラスのサブクラスとして拡張することもできます。デリゲートの実装をするために自分たちでDate
クラスの各メソッドをEvent
クラスに追加する必要はありません。これもGroovyコンパイラがとても使いやすいように作業をおこなってくれる恩恵です。
class Event extends Date { @Delegate Date when String title, url }
デリゲート先がインターフェースの場合も、明示的にそのインターフェースを実装する必要はありません。@Delegate
変換がそのことを認識してインターフェースの実装をおこなってくれるのです。それによって、そのクラスのインスタンスが自動的にDelegate先であるインターフェースの instanceof
(この場合インターフェースの実装クラスのインスタンスであるという意)となります。
import java.util.concurrent.locks.* class LockableList { @Delegate private List list = [] @Delegate private Lock lock = new ReentrantLock() } def list = new LockableList() list.lock() try { list << 'Groovy' list << 'Grails' list << 'Griffon' } finally { list.unlock() } assert list.size() == 3 assert list instanceof Lock assert list instanceof List
この例ではLockableList
がListインスタンスとLockインスタンスからなるコンポジット(複合物)となり、List
とLock
の instanceof
となります。もしインターフェースの実装クラスとしたくない場合も、次のようにアノテーションのパラメータを指定することで対応できます。
@Delegate(interfaces = false) private List list = []
@Newify
@Newify
変換はクラスのインスタンス化を2つの方法でおこなうためのものです。1つはRuby風にnew()
クラスメソッドを使ってインスタンスを生成する方法です。
@Newify rubyLikeNew() { assert Integer.new(42) == 42 } rubyLikeNew()
2つめはPythonのようにnew
キーワードなしでインスタンス化する方法です。次のようなクラスによるツリー生成を考えてみてください。
class Tree { def elements Tree(Object... elements) { this.elements = elements as List } } class Leaf { def value Leaf(value) { this.value = value } } def buildTree() { new Tree(new Tree(new Leaf(1), new Leaf(2)), new Leaf(3)) } buildTree()
このツリー生成では全てのnew
キーワードが1行に何カ所も出てくるため、読みやすいコードとはいえません。Ruby風の方法でも各要素を生成するのにnew()
メソッドを呼び出す必要があるので変わりはありません。しかし@Newify
を使うと、これを少し読みやすいコードにすることができます。
@Newify([Tree, Leaf]) buildTree() { Tree(Tree(Leaf(1), Leaf(2)), Leaf(3)) }
ここではTree
とLeaf
でだけnewできていることにもお気づきでしょう。デフォルトではアノテーションを付けられたスコープ内の全てのインスタンス化がこの形でnewされます。特定のクラスにだけこのnewを適用させるようにすることも可能です。またこの例でいえば、@Newify
を使うのが一番適切であると思われます。なぜなら変換の目的がそもそも階層構造/木構造を作るためのものだからです。
ここでさきほどの座標(coordinates)の例をまた持ち出すと、@Immutable
と@Newifyを使えば簡潔かつタイプセーフな方法で座標を結ぶ経路(path)を作成することができそうです。
@Immutable final class Coordinates { Double latitude, longitude } @Immutable final class Path { Coordinates[] coordinates } @Newify([Coordinates, Path]) def build() { Path( Coordinates(48.824068, 2.531733), Coordinates(48.857840, 2.347212), Coordinates(48.858429, 2.342622) ) } assert build().coordinates.size() == 3
ここで特筆しておくことは、コンパイラによって生成されるPath(Coordinates[] coordinates)
のコンストラクタを使えば、Path(Coordinates... coordinates)
のコンストラクタが定義されているかのようにGroovyでも可変引数が実現できることです。
@Categoryと@Mixin
Groovy を少し使ったことがある方でしたらカテゴリ(Category)はお馴染みの概念でしょう。これは既存の型を拡張して(JDKやサードパーティの finalクラスも含めて)新しいメソッドを追加するためのメカニズムです。またこれはドメイン固有言語を書く場合に使うテクニックでもあります。次の例について見てみましょう。
final class Distance { def number String toString() { "${number}m" } } class NumberCategory { static Distance getMeters(Number self) { new Distance(number: self) } } use(NumberCategory) { def dist = 300.meters assert dist instanceof Distance assert dist.toString() == "300m" }
単純なDistance
クラスがありますが、これはサードパーティから提供されたものであると想定しましょう。そしてなぜかfinal
クラスとして作られてしまったせいで拡張することができないものになっているとします。このような時でもGroovyのカテゴリの仕組みのおかげでDistance
型にメソッドを加えることができるのです。ここではgetMeters()
メソッドをNumber
型のインスタンスに修飾できるようにしています。このようにNumber
型にgetterを追加することで、Groovyのプロパティ構文を使ってNumber型のインスタンスのプロパティを参照することができるようになります。つまり300.getMeters()
と書く代わりに300.meters
と書くことができるのです。
このカテゴリの仕組み・記述法のマイナス面は、他の型にインスタンスメソッドを追加するためにstatic
メソッドを作って、さらに対象となるクラスのインスタンスをその最初の引数としないといけないことです。その他の引数はメソッドのパラメータに通常の引数として渡されます。そのためDistance
クラスに普通にメソッドを追加するのに比べるとやや分かりづらくなります。かといってDistanceクラスを拡張するにはソースコードへ直接アクセスできないといけません。ここで@Category
アノテーションの登場です。このアノテーションによってインスタンスメソッドをもつクラスをGroovyカテゴリに変換してくれます。
@Category(Number) class NumberCategory { Distance getMeters() { new Distance(number: this) } }
メソッドをstatic
宣言する必要はありません。またここで使われる this
は作成しないといけないカテゴリインスタンスを表すthis
ではなく、カテゴリが適用されるnumberを指します。そのため use(Category) {}
ブロックはそのまま使えます。ただし、本来のカテゴリは複数の型に対して適用できるのに対し、この方法によるカテゴリは一度に一つの型にしか適用できないので注意してください。
今度は@Mixin
と複数の@Category
を組み合わせて、多重継承のようにしてクラスへさまざまな振る舞いを追加してみましょう。
@Category(Vehicle) class FlyingAbility { def fly() { "I'm the ${name} and I fly!" } } @Category(Vehicle) class DivingAbility { def dive() { "I'm the ${name} and I dive!" } } interface Vehicle { String getName() } @Mixin(DivingAbility) class Submarine implements Vehicle { String getName() { "Yellow Submarine" } } @Mixin(FlyingAbility) class Plane implements Vehicle { String getName() { "Concorde" } } @Mixin([DivingAbility, FlyingAbility]) class JamesBondVehicle implements Vehicle { String getName() { "James Bond's vehicle" } } assert new Plane().fly() == "I'm the Concorde and I fly!" assert new Submarine().dive() == "I'm the Yellow Submarine and I dive!" assert new JamesBondVehicle().fly() == "I'm the James Bond's vehicle and I fly!" assert new JamesBondVehicle().dive() == "I'm the James Bond's vehicle and I dive!"
この方法は、いくつものインターフェースを継承したり各サブクラスで同じ振る舞いを記述する代わりに、複数のカテゴリをクラスにミックスインするものです。この例では JamesBondVehicle(vehicleは乗り物の意)がミックスインによって飛行能力(flying ability)と潜水能力(diving ability)を備えることに成功しています。
ここで特筆すべきは、それが宣言されているクラスにインターフェースを実装させる@Delegate
と違い、@Mixin
は実行時にミックスインをおこなうことです。@Mixin
についてはこの記事で後述する「拡張されたメタプログラミング性」の項もご覧ください。
@PackageScope
プロパティに関するGroovyの決まりでは、アクセス修飾子のないフィールドはプロパティとして認識され、自動的にgetterとsetterが作成されます。たとえば次のPerson
クラスではname
フィールドに関するgetName()
とsetName()
が作成されます。
class Person { String name }
これは以下のJavaクラスと同等です。
public class Person { private String name; public String getName() { return name; } public void setName(name) { this.name = name; } }
そのため、この決まりにはパッケージ全体から見ることのできるフィールドを定義することができないという欠点があります。しかし新しい@PackageScope
アノテーションをフィールドに付けることでそれが可能になりました。
Grape (the Groovy Adaptable / Advanced Packaging Engine)
AST 変換についての概要を見てきたので、Grapeについて理解する準備が整いました。GrapeはGroovyスクリプトに依存性を加えたり、その依存性を解決するための仕組みです。@Grab変換あるいはGrape.grab()メソッド呼び出しを使ってライブラリの読込をGroovyスクリプトで明示的に指定することで、ランタイムがJARファイルを自動的に探してくれます。Grapeを使うと依存するJARなどを一緒にしないでもスクリプトを配布でき、必要なJARはスクリプトを最初に起動した時にダウンロードされてキャッシュされます。内部的には、スクリプトに必要なライブラリを保持するIvyおよびMavenのレポジトリを利用しています。
たとえばJava 5のドキュメントが参照している全てのPDF文書のリンクがほしいとします。そのためにGroovy XmlParser
を使ってXML準拠文書としてその(XML準拠してない)HTMLページをパースしたいと考えた時、TagSoup SAX準拠パーサを使ってHTMLを整形式(well-formed)のXMLに変換することができます。このSAXパーサを使うには通常スクリプト実行時にクラスパスを指定する必要がありますが、そのようにコードを乱雑にしなくてもGrapeを通してTagSoupをgrabすることができます。
import org.ccil.cowan.tagsoup.Parser // find the PDF links in the Java 1.5.0 documentation @Grab(group='org.ccil.cowan.tagsoup', module='tagsoup', version='0.9.7') def getHtml() { def tagsoupParser = new Parser() def parser = new XmlParser(tagsoupParser) parser.parse("http://java.sun.com/j2se/1.5.0/download-pdf.html") } html.body.'**'.a.@href.grep(~/.*\.pdf/).each{ println it }
Grapeの素晴らしさを別の表す例として、JettyサーブレットコンテナでGroovyテンプレートを使えるようにするたった数行のコードをお見せしましょう。
import org.mortbay.jetty.Server import org.mortbay.jetty.servlet.* import groovy.servlet.* @Grab(group = 'org.mortbay.jetty', module = 'jetty-embedded', version = '6.1.0') def runServer(duration) { def server = new Server(8080) def context = new Context(server, "/", Context.SESSIONS); context.resourceBase = "." context.addServlet(TemplateServlet, "*.gsp") server.start() sleep duration server.stop() } runServer(10000)
Grape はこのスクリプトの初回起動時にJettyとそれが依存するコンポーネントをダウンロードしてキャッシュします。スクリプトの最初ではJettyの Server
を8080ポート上に生成し、そのContext上にGroovyのTemplateServlet
をのせています。これによりGroovy は強力なテンプレートエンジンを手に入れることになります。次の処理ではサーバを起動して一定の時間(duration)動かしています。もし誰かが http://localhost:8080/somepage.gsp
にアクセスすれば、スクリプトと同ディレクトリにあるsomepage.gsp
テンプレートが表示されることになります。
Grapeはアノテーションの代わりのようにメソッド呼び出しをおこなうこともできます。またGrape
のコマンドラインを使えば依存するコンポーネントをインストール、列挙、準備することができます。Grapeについてのより詳しい情報はドキュメントをご覧下さい。
改善されたSwing Builder
最後のAST変換は@Bindable
と@Vetoable
です。この2種類の変換はSwingディベロッパにとって便利なものです。SwingのUIを作る時、あるUI要素の値の変更をモニタリングしたいと思うことがよくあります。これを実現するのに通常採られるのがJavaBeanのPropertyChangeListener
を使ってクラスのフィールドの値が変更されたことを知らせる方法です。そのためにJava Beansに書くお決まりのコードは次のようなものです。
import java.beans.PropertyChangeSupport; import java.beans.PropertyChangeListener; public class MyBean { private String prop; PropertyChangeSupport pcs = new PropertyChangeSupport(this); public void addPropertyChangeListener(PropertyChangeListener l) { pcs.add(l); } public void removePropertyChangeListener(PropertyChangeListener l) { pcs.remove(l); } public String getProp() { return prop; } public void setProp(String prop) { pcs.firePropertyChanged("prop", this.prop, this.prop = prop); } }
幸いなことに、Groovyと@Bindable
アノテーションを使うとこのコードは次のように劇的にシンプルなものにできます。
class MyBean { @Bindable String prop }
そしてこれをGroovyのSwing Builderの新しいbind()
メソッドと組み合わせると、次のようにテキストフィールドを定義することでその値をデータモデルのプロパティにバインドすることができます。
textField text: bind(source: myBeanInstance, sourceProperty: 'prop')
または
textField text: bind { myBeanInstance.prop }
このバインドではクロージャの括弧内でシンプルな式が使えるので、次のようなことが可能です。
bean location: bind { pos.x + ', ' + pos.y }
GroovyにはMapやListについても同じような仕組みがあります。興味のある方はObservableMapとObservableListについて調べてみてください。
@Bindable
に続いて、@Vetoable
変換について見てみましょう。これはプロパティの変更を拒否(veto)できるようにする時に使います。例として、次のようにTrompetist
クラスを書くと、zが含まれる演奏者の名前が許可されなくなります。
import java.beans.* import groovy.beans.Vetoable class Trumpetist { @Vetoable String name } def me = new Trumpetist() me.vetoableChange = { PropertyChangeEvent pce -> if (pce.newValue.contains('z')) throw new PropertyVetoException("The letter 'z' is not allowed in a name", pce) } me.name = "Louis Armstrong" try { me.name = "Dizzy Gillespie" assert false: "You should not be able to set a name with letter 'z' in it." } catch (PropertyVetoException pve) { assert true }
Swing Builderでバインドを使う例をもう少し見てみましょう。
import groovy.swing.SwingBuilder import groovy.beans.Bindable import static javax.swing.JFrame.EXIT_ON_CLOSE class TextModel { @Bindable String text } def textModel = new TextModel() SwingBuilder.build { frame( title: 'Binding Example (Groovy)', size: [240,100], show: true, locationRelativeTo: null, defaultCloseOperation: EXIT_ON_CLOSE ) { gridLayout cols: 1, rows: 2 textField id: 'textField' bean textModel, text: bind{ textField.text } label text: bind{ textModel.text } } }
このスクリプトを実行すると次の図のようにテキストフィールドとラベルを持つフレームが表示されます。このラベルの表示文字はテキストフィールドの内容に連動します。
Swing Builderはこの一年で大きく進化したので、GroovyのSwingチームはSwing BuilderとGrailsをベースに新しいプロジェクトGriffonを立ち上げることにしました。GriffonはGrailsの「設定より規約」という考え方を持ち、プロジェクトの構造、プラグインシステム、Gantスクリプトの利用などもGrailsを踏襲しています。
Swingリッチクライアントを作っている方は、ぜひGriffonを見てみて下さい。
改善されたSwingコンソール
SwingのUIに関しては、Swingコンソールも進化しました。
- コンソールをアプレット(
groovy.ui.ConsoleApplet
)として実行可能 - 構文ハイライトだけでなく、強調表示も可能になったエディタ
- ドラッグ・アンド・ドロップでGroovyスクリプトファイルを開く
- コンソールで実行しようとしているスクリプトのクラスパスに新しいJARやディレクトリが追加可能
- メニューのView項目に2つのオプションが追加。ひとつは実行するスクリプトを出力エリアに表示し、もうひとつは実行結果を視覚化する(後述)。
- スクリプトで例外が発生すると、スタックトレースの情報のうちスクリプトの行に関する箇所がクリック可能に表示され、エラーが起きた箇所までジャンプしてくれる。
- スクリプトでコンパイルエラーが起きた時も、同様にクリック可能に表示される。
「出力エリアの実行結果を視覚化」に関してですが、1.6では実行結果をどのように描画するかをカスタマイズできるシステムが追加されています。たとえば、ジャズミュージシャンのマップを返すスクリプトを実行した時に、その結果がコンソールに次のように表示されるとします。
ここではMap
を単にテキスト化したものが表示されています。これを視覚化するにはどうするかというと、それはSwingコンソールがおこなってくれるのです。まず、視覚化オプション(メニューのView -> Visualize Script Results
)が有効になっているのを確認します(ちなみに、Groovyコンソールの全ての設定はPreference APIによって保存・読込されています)。標準では、java.awt.Image
、javax.swing.Icon
、親のないjava.awt.Component
をスクリプトが返した時に、そのオブジェクトをtoString()
ではなく視覚的に表示します。それ以外のオブジェクトはそのままではテキストで表示されるだけです。では、次のGroovyスクリプトを~/.groovy/OutputTransforms.groovy
に作ってみましょう。
import javax.swing.* transforms << { result -> if (result instanceof Map) { def table = new JTable( result.collect{ k, v -< [k, v?.inspect()] as Object[] } as Object[][], ['Key', 'Value'] as Object[]) table.preferredViewportSize = table.preferredSize return new JScrollPane(table) } }
Groovy のSwingコンソールは起動時にこのスクリプトを実行し、スクリプトバインディングにtransforms
リストを挿入します。それにより独自のスクリプト結果の視覚化ができるのです。このスクリプトでは、Map
を見た目のいいJTable
に変換し、これによりMapをユーザフレンドリで魅力的に視覚化できる(下図)ようにしています。
Swingコンソールは完璧なIDEといえるものではありませんが、日々のスクリプトを使った作業においては便利なツールになります。
拡張されたメタプログラミング性
Groovyの動的言語たる由縁はMeta-Object Protocolと、クラスやインスタンスの実行時における振る舞いを表すメタクラスの概念です。Groovy 1.6ではこの動的実行環境の改善も続けられていて、新しい機能がいくつも追加されています。
POJOでもインスタンスごとのメタクラスを持つことが可能に
これまではGroovyのPOGO(Plain Old Groovy Obnect)はインスタンスごとにメタクラスを持つことができましたが、POJOは全てのインスタンスに対して一つのメタクラス(つまりクラスごとのメタクラス)しか持つことができませんでした。それが今回、POJOでもインスタンスごとにメタクラスを持てるようになりました。また、メタクラスのプロパティをnullにセットするとデフォルトのメタクラスが復元するようになりました。
ExpandoMetaClass DSL
元々はGrailsで作られ、その後Groovy 1.5に統合されたExpandoMetaClassは、オブジェクトおよびクラスの実行時の振る舞いを変えるのにMetaClass
クラスを書かなくてもいいようにしてくれる便利なものです。既存の型のプロパティあるいはメソッドを追加/変更する場合、それごとにType.metaClass.xxx
を書くことになります。例として、Unit manipulation DSLから演算子のオーバーローディングをおこなうコードを引用しましょう。
Number.metaClass.multiply = { Amount amount -> amount.times(delegate) } Number.metaClass.div = { Amount amount -> amount.inverse().times(delegate) } Amount.metaClass.div = { Number factor -> delegate.divide(factor) } Amount.metaClass.div = { Amount factor -> delegate.divide(factor) } Amount.metaClass.multiply = { Number factor -> delegate.times(factor) } Amount.metaClass.power = { Number factor -> delegate.pow(factor) } Amount.metaClass.negative = { -> delegate.opposite() }
ごらんの通り同じこと(Number.metaClassやAmount.metaClass)が何度も書かれています。しかしExpandoMetaClass DSLを使うと、型ごとに再グループ化をして無駄をなくすことができます。
Number.metaClass { multiply { Amount amount -> amount.times(delegate) } div { Amount amount -> amount.inverse().times(delegate) } } Amount.metaClass { div << { Number factor -> delegate.divide(factor) } div << { Amount factor -> delegate.divide(factor) } multiply { Number factor -> delegate.times(factor) } power { Number factor -> delegate.pow(factor) } negative { -> delegate.opposite() } }
metaClass()
メソッドは引数を一つだけ受け取ります。その引数はクロージャで、メソッドやプロパティについての定義を含みます。これによりType.metaClass
を各行で繰り返す必要がなくなります。この時、もし一つのメソッドだけを渡すなら「methodName { /* closure */ }
」と書くことができますが、複数の場合は結合演算子を使った「methodName << { /* closure */ }
」の形にする必要があります。この方法によりstaticメソッドも渡すことができ、従来の方法を書き換えることができます。
// クラスの完全修飾名を取得する(つまり単純にClass#getNameを呼ぶ)メソッドfqn()をClassに追加する Class.metaClass.static.fqn = { delegate.name } assert String.fqn() == "java.lang.String"
これを次のようにすることができます。
Class.metaClass { 'static' { fqn { delegate.name } } }
注意点として、static初期化子と間違われないようにstatic
キーワードを引用符で囲む必要があります。一回きりのメソッド追加であれば従来の方法が分かりやすいかもしれませんが、いくつものメソッドを追加するのであればExpandoMetaClass DSLの方が良いでしょう。
今度はプロパティについてですが、ExpandoMetaClassでプロパティを既存クラスに追加するにはgetterメソッドとsetterメソッドを追加するのが通常のやり方になります。たとえば、テキストファイル中の単語数をカウントするメソッドを追加しようとしているのであれば、プロパティを追加する次の方法を試してみましょう。
File.metaClass.getWordCount = { delegate.text.split(/\w/).size() } new File('myFile.txt').wordCount
getterの中に複数行のロジックがあるならこの方法が一番いいはずです。ただし、単純に値を保持するプロパティを作りたいだけであれば、次のようにExpandoMetaClass DSLを使って定義することが可能です。この例では、lastAccessed
プロパティをCarクラスに追加し、Car
クラスの各インスタンスがこのプロパティを持つようにしています。Car
インスタンスのメソッドのいづれかが呼ばれると、lastAccessedは新しいタイムスタンプで更新されます。
class Car { void turnOn() {} void drive() {} void turnOff() {} } Car.metaClass { lastAccessed = null invokeMethod = { String name, args -> def metaMethod = delegate.metaClass.getMetaMethod(name, args) if (metaMethod) { delegate.lastAccessed = new Date() metaMethod.doMethodInvoke(delegate, args) } else { throw new MissingMethodException(name, delegate.class, args) } } } def car = new Car() println "Last accessed: ${car.lastAccessed ?: 'Never'}" car.turnOn() println "Last accessed: ${car.lastAccessed ?: 'Never'}" car.drive() sleep 1000 println "Last accessed: ${car.lastAccessed ?: 'Never'}" sleep 1000 car.turnOff() println "Last accessed: ${car.lastAccessed ?: 'Never'}"
ここでは、DSLで、クロージャのdelegate
を通してlastAccessedプロパティにアクセスしています(delegate.lastAccessed = new Date()
の部分)。そしてinvokeMethod()
を使ってメソッド呼び出しをインターセプトし、元々呼ばれたメソッドを呼び出したり、そのメソッドが存在しない場合は例外を投げたりしています。このスクリプトを実行してみると、Carインスタンスのメソッドを呼び出すごとに lastAccessed
が更新されている結果を見ることができます。
実行時ミックスイン
今回ご紹介する最後のメタプログラミング機能が実行時ミックスインです。さきほど出てきたように@Mixin
を使うとクラスに新しい振る舞いをミックスインすることができます。しかし自分で作ったクラスは問題ないとして、自分が変更できない型については本来手を加えることができないはずです。実行時ミックスインは、そのギャップを埋めるためにどんな型へも実行時にミックスインできるようにする仕組みです。例として、さきほどのVehicleにいくつかの機能をミックスインさせる例をもう一度使いましょう。もしJamesBondVehicleがサードパーティ製の型だったとすると、次のようにして JamesBondVehicleにdivingの機能を追加できます。
// provided by a third-party interface Vehicle { String getName() } // provided by a third-party class JamesBondVehicle implements Vehicle { String getName() { "James Bond's vehicle" } } JamesBondVehicle.mixin DivingAbility, FlyingAbility assert new JamesBondVehicle().fly() == "I'm the James Bond's vehicle and I fly!" assert new JamesBondVehicle().dive() == "I'm the James Bond's vehicle and I dive!"
staticメソッドであるmixin()
に一つあるいは複数の引数が渡された時、GroovyがClass
にそのメソッドを追加してくれます。
JSR-223 Groovy Scripting Engine
Groovy 1.6以前、JSR-223 = javax.script.*
によってGroovyを自分たちのJavaプロジェクト内で使えるようにするには、java.netからGroovy Scripting Engine実装をダウンロードして、そのJARをクラスパスに追加しないといけませんでした。この作業をおこなうことはディベロッパにとってあまり親切なものではありせんでしたが、何しろGroovyのパッケージにすらこのJARが含まれていなかったのでせざるをえませんでした。しかし1.6ではjavax.script.*
のAPIが含まれるようになりました。
Groovy Scripting Engineを使うと、次のコードのようにGroovyの式がJavaでも使えます(コードはGroovyですが自動的にJavaコードに変換されます)。
import javax.script.* def manager = new ScriptEngineManager() def engine = manager.getEngineByName("groovy") assert engine.evaluate("2 + 3") == 5
ただし、javax.script.*
のAPIはJava 6だけで使えることに注意してください。
JMX Builder
Groovy 1.6ではGoogle Code上のオープンソースプロジェクトから生まれたJMX Builderが統合されました。これによりJMXサービスとやり取りしたりJMXサービスを使えるようにする必要のあるディベロッパの苦労が軽減されます。JMX Builderの機能には次のものが含まれます。
- Builderパターンを使ったJMX API用のドメイン固有言語(DSL)
- JMX APIを使ったプログラミングの簡素化
- 宣言型の記述でJava/GroovyオブジェクトをMBeansで管理されたJMXとして使えるようにする
- Descriptorをクラスに埋め込んだり、明示的に生成することが可能
- JMXのイベントモデルを標準でサポート
- JMXのイベントブロードキャスタをGroovyからシームレスに作成可能にする
- インラインのクロージャをイベントリスナとしてアタッチ可能にする
- JMXのイベント通知に対して処理を簡単におこなうためにGroovyの動的な性質を利用する
- MBeanのフレキシブルな登録ポリシーを提供
- 特別なインターフェースがなく、クラスパスの制限もない
- JMX APIの複雑さからディベロッパを護る
- 属性、コンストラクタ、演算子、パラメータ、通知を利用可能にする
- コネクタサーバやコネクタクライアントの作成を簡素化
- JMXのタイマサービスをGroovyで利用可能にする
リンク先(more information on JMX Builder)ではJMX Builderの他の情報を得ることができ、JMX BuilderがJMXシステムを広範囲に渡りカバーしていることが分かります。また多くの例によって、JMXコネクタサーバ/クライアントを作成する方法やPOGOをJMXのマネージドビーンにエクスポートする方法、JMXイベントを受ける方法、その他多くのことを説明しています。
改善したOSGiサポート
Groovy のJARファイルは正規のOSGiメタデータを含んでいるため、Eclipse EquinoxやApache FelixのようなOSGi準拠のコンテナであればこれらJARファイルをバンドル(OSGiのコンポーネント)として読み込むことができます。 GroovyとOSGiの使い方についての詳細はGroovyプロジェクトのウェブサイトに載っています。そこには次のような方法についてチュートリアルがあります。
- GroovyをOSGiサービスとしてロードする
- GroovyのOSGiサービスを書く
- GroovyのJARをバンドル内に含める
- Groovyで書かれたサービスを作成する
- GroovyからOSGiサービスを利用する
- 問題があった時のトラブルシューティング
他にも異なるバージョンのGroovyを自分たちのアプリケーションで使う方法など、OSGiによってできることに興味を持たれることでしょう。
まとめ
複雑なディベロッパの仕事を簡潔にするというゴールに向けて、Groovyはさまざまな新機能や改善を今回の新しいリリースでおこないました。それにはお決まりのパターンや横断的機能を実装するためのコードを劇的に減らしGroovyを言語的に拡張しディベロッパに開かれたものにするAST変換、コードの無駄をなくしビジネスルールを的確に表現する記述ができる拡張されたメタプログラミング性、Java 6のスクリプティングAPIやJMX管理システムやOSGiのプログラミングモデルといった業界標準のエンタープライズAPIのサポートがあります。これらは全てJavaとシームレスかつ妥協なしに統合でき、それでいながら以前のバージョンより高いパフォーマンスを実現しています。
この記事も終わりになりましたが、Groovyのユーザでない方にはみなさんのプロジェクトでGroovyが何をできるかを理解する助けに、Groovyを知っていて既に使っている方には全ての新しい機能に理解していただけたことを望んでいます。親愛なる読者のみなさん、これを読んだらGroovy 1.6をダウンロードしましょう)。そしてもしGroovy、Grails、Griffonについてより深く知りたいと思われた方はGR8 Conferenceにお越しください。このGroovy、Grails、Griffonに特化したカンファレンスはデンマークのコペンハーゲンで開かれ、これらのテクノロジの専門家や制作者が経験に裏打ちされたプレゼンテーションや実践的テクニックを披露してくれます。
筆者について
SpringSource社におけるGroovy開発トップのGroovyプロジェクトマネージャであるGuillaume Laforge氏は、オフィシャルなGroovyプロジェクトマネージャであり、動的言語Groovyの標準化をおこなうJSR-241のスペックリードを務めました。氏はJavaOne、SpringOne、QCon、Sun TechDays、JavaPolis/Devoxxにおいて幾度もGroovyおよびGrailsについてのスピーチをおこなっています。氏は Dierk König氏とともにGroovy in Actionの共著者でもあります。SpringSource社が2008年に買収したGroovy/Grails企業G2Oneを設立して技術担当バイスプレシデントを務めていた以前は、アーキテクチャや方法論を専門とするコンサルタント企業OCTO Technologyに勤めていました。OCTO時代に顧客へ提供するために開発されたのがGroovyおよびGrailsの始まりでした。