JavaOne2010 9/21 -- Code Generation on the Java Virtual Machine: Writing Code That Writes Code

Groovyベースのデスクトップ系GUIアプリケーションのフルスタックフレームワーク(長い)であるGriffonのPJリーダのAndres Almirayのセッションです。ホントは、Canooの同僚であるHamlet D'arcyの担当セッションだったんですが、なにやらの事情でAndresが担当することになったようです。ちなみに彼は、9/22のPolyglot Programmingというセッションが本来のメイン担当。そっちも聞いたので別途まとめます。


今回は、Twitterに実況ならぬ感想戦(後でメモから起こしたダイジェストのツイート)してたので、それをはりつけながらちょっと補足していきます。

from twitter

03:09 8:00のセッションはCode Generationに行ってきた。GriffonコミッタのAalmiray氏。Project Rombokの紹介をちょっとだけした後は、Groovy自慢のターンでした。内容はよくまとまっていたけど、個人的には復習的な話だった。 #javaonejp

Project Lombok(RじゃなくてLでした)は、唐辛子がトレードマークのOSSプロジェクトです。

http://projectlombok.org/

Javaプログラミングで、おなじみのgetter/setterを書くのがメンドイけど、@Getter/@Setterアノテーションをフィールドにつけるとコンパイル時にバイトコードを生成してくれて、ソースコードにgetter/setterが最初から書いてあったかのようなclassファイルができあがる、という寸法。
他にもいくつか品揃えがあります。

あまり知らないのですが、ぱっとみでは割と小規模なコードジェネレーションによる開発サポートツール、のようです。

はい。Lombokの紹介終わり。あとはずっとGroovyのターン!

from twitter

03:14 [Code Generation] GroovyのASTTransformationは(使うのは)簡単かつ強力。@Delegateをフィールドにつけると、そのフィールドのクラスが持つ全メソッドに対する委譲メソッド群をコンパイル時に自動生成&展開してくれる。 #javaonejp

Groovyでは、1.6からAST変換による黒魔術が導入されました。

ASTというのは、Abstract Syntax Treeの略で、日本語では抽象構文木ですね。Wikipediaによれば、

抽象構文木(abstract syntax tree、AST)とは、計算機科学における有限なラベル付き有向木構造であり、節点(internal node)に演算子、葉(leaf node)にそのオペランドを対応させたものである。つまり、葉は変数や定数に対応する。抽象構文木構文解析構文木とデータ構造の中間的なものとして使用される。さらにコンパイラインタプリタでのプログラムの内部表現として使われ、コンパイラ最適化やコード生成はその上で行われる。
http://ja.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E6%A7%8B%E6%96%87%E6%9C%A8

ということになります。

AST変換を平たく言えば、とりあえずソースコードをパースしたけど内容を解釈する前の中間状態に対して、内容を自由に改ざんすることで、ソースコードで書いてなかったような実装を挿入したり、文法要素の意味を変更したりするような、まあ、いわゆる黒魔術です。


(参考) http://groovy.codehaus.org/Compile-time+Metaprogramming+-+AST+Transformations

で、Groovy標準として、いくつかAST変換をつかった便利実装が提供されているんですけど、AST変換系の便利アノテーションはGroovy1.7, 1.8でもじわじわと増加中です。


この後は、こんなのあるぜ!という自慢が続きます。はい。

from twitter

03:19 [CodeGeneration] @Lazyをフィールドにつけると、初めてアクセスするときに初期化するようなアクセサを生成してくれる。 #javaonejp
03:19 [CodeGeneration] @Lazyで展開される実装はdouble-checked-lockingっぽい。スレッドセーフか?というツッコミに、多分Yes、といってたけど、Java5以降はね、という条件が必要だと思った。 #javaonejp

class Event { 
   @Lazy ArrayList speakers 
} 

↑これが、コンパイルされたクラスの中では↓こうなります。

class Event { 
  ArrayList speakers 
  def getSpeakers() { 
    if (speakers != null) { 
      return speakers 
    } else { 
      synchronized(this) { 
        if (speakers == null) { 
          speakers = [] 
        } 
        return speakers 
      } 
    } 
  } 
} 

会場からの質問で、当然のように「それってスレッドセーフ?」というツッコミが入ったのですけど、ちょっとキョドったような感じで「Yes」と流してしまいました。
double-check lockingが実用になるのってJDK5からなんで、それくらい触れてもいいのになーと思いました。


が、しかし、


今、改めて見てみると、なんとspeakersにvolatileが付いてない・・・・。これじゃスレッドセーフじゃないじゃん。


ちょっと実際にコンパイル&逆コンパイルしてみましょう。

$ javap -private Event | grep speaker
Picked up _JAVA_OPTIONS: -Xmx512m -Dfile.encoding=UTF-8
    private java.util.ArrayList $speakers;
    public java.util.ArrayList get$speakers();
    public void set$speakers(java.util.ArrayList);

うーん。やはり、volatileついてない・・・。これじゃスレッドセーフじゃないですよねぇ。


LazyアノテーションクラスのJavadocを見てみます。

// Ref: http://groovy.codehaus.org/api/groovy/lang/Lazy.html

Example usage without any special modifiers just defers initialization until the first call but is not thread-safe:

 @Lazy T x
 
becomes
 private T $x

 T getX() {
    if ($x != null)
       return $x
    else {
       $x = new T()
       return $x
    }
 }
 
If the field is declared volatile then initialization will be synchronized using the double-checked locking pattern as shown here:
 @Lazy volatile T x
 
becomes
 private volatile T $x

 T getX() {
    T $x_local = $x
    if ($x_local != null)
       return $x_local
    else {
       synchronized(this) {
          if ($x == null) {
             $x = new T()
          }
          return $x
       }
    }
 }

っておい。

自前でvolatile追加しないとスレッドセーフじゃないと明言されてるじゃないですか。危ない危ない。


というわけで、@Lazyによる遅延初期化にスレッドセーフを期待する諸氏は、volatile修飾子をお忘れなきよう!!


さて、ツイートはしてないですが、他に紹介されてた主なAST変換系アノテーションとしては:

  • @Immutable
    • クラスに付けるとsetterが生成されない、classがfinalになる、Equals(), hashCode(), toString()が自動生成される等、色々がんばってきちんとImmutableにしてくれる。
  • @Delegate
    • フィールドに着けると、そのフィールドのオブジェクトの全メソッドに対する委譲メソッドを自動的に生成してくれる。
  • @Log
    • 1.8から導入。ロギングのためのlogインスタンス変数を自動生成してくれる。log.debug("HOGE")とかですぐにロギングできる。isXxxxでレベルチェックもきちんとされるので、レベルをきちんと指定しておけば文字列生成のオーバヘッドもありません。気が利いてますね。
      • これ、拙作のkobo-commonsの機能として実装しようかなーと思って調べたらちょうど本家で実装されてた、という裏話があります。どうでもいいですね。はい。
from twitter

03:21 [CodeGeneration]後はアノテーションによるAST変換の例をいくつか紹介してから、DbCのGroovy実装であるGContractsの紹介。アノテーションにクロージャで事前条件と事後条件を指定すると実行前後でチェックする実装をAST変換で生成。 #javaonejp

「Design by Contract(契約による設計)」を実現するためのGroovyベースのフレームワークである、GContractsの紹介です。

@Invariant({ elements != null })
class Stack {

    private List elements

    @Requires({ preElements?.size() > 0 })
    @Ensures({ !is_empty() })
    public Stack(List preElements)  {
        elements = preElements
    }

    // ...snip...

のように、事前条件を@Requires、事後条件を@Ensures、普遍条件をInvariantとしてクロージャで条件式を記述しておくと、AST変換によってチェックコードが生成されます。

会場からの質問:

  • Q. 実行時の性能面でのペナルティは?
    • A. AST変換でコード生成するので、余分な動的コストはかからない。条件評価程度。
from twitter

03:23 [CodeGeneration]次は、FindBugsのGroovy版的なCodeNarc。発見できるバグの種類は前者が200以上なのに対して、60いくつと少なめだけど、CodeNarcはAST変換でバグチェックを実現してるんだよ、と。 #javaonejp

まあ、そのままです。

from twitter

03:24 [CodeGeneration]Groovy標準付属のツールで、ASTツリーをGUI表示できる。その構造の見方の説明と、AST変換をするためのASTTransformationの実装の仕方の説明。 #javaonejp

[img:http://docs.codehaus.org/download/attachments/138379300/Image+2.png?version=1&modificationDate=1264085942922]
http://docs.codehaus.org/pages/viewpage.action?pageId=138379308 より

from twitter

03:25 [CodeGeneratin]AST変換といっても文法的には万能じゃなくて、Groovyとして正しい文法でなければいけない。文法さえ正しければ、その意味は自由に書き換えられる。文法NGならSyntaxErrorが発生してAST変換まで到達しない。 #javaonejp

そういうことです。


自前ASTTransformationを作ってみたくなった人は、↓などをご参照ください。

  • http://groovy.codehaus.org/Global+AST+Transformations
    • Groovyのシンタックスそのものに介入するAST変換はこっち。たとえばprivate修飾子をpublicに読み替える、とか(誰得)。
    • こっち側の場合ASTTranformationクラスのロード順序などの都合(うろおぼえ)で、groovyのjar自体を一度展開して自前クラスを追加する、などの手間が必要になってしまう。面倒。
  • http://groovy.codehaus.org/Local+AST+Transformations
    • 変換対象コードを独自アノテーションでマーキングしておいて、アノテーションを目印にコードをAST変換する。
    • 自前ASTTransformationがクラスパスに通っていればOkなので、比較的敷居が低い。だいたいはこっちを使えばOK.


ちなみに、kobo-commonsという拙作プロダクトにhashCode(), equals()を自動生成するEquivアノテーションというのがあるので*1、よければコードなどを参考にしてみて下さい。

というわけで、この辺で。

*1:1.8からは似たような機能が標準にはいるので、もうレガシーな機能になってしまいましたねぇ