GDKかSwingBuilderかよーわからんけど,Groovyのナゾがひとつ解けた

SwingBuilderにおける以下のようなコードに違和感を覚えつつも,お作法なんだろうからと深くは考えてなかったワケ。

button(text:"Click",
       actionPerformed: { /* ここにアクションを記述 */ })

でもね,JetGroovyで編集中にこんなコード補完を見つけてしまったのよ。それもSwingBuilder外で。


あれ?actionPerformedってJButtonのプロパティになってるやん。調子のって(↓)こんなことしたら,こっちもプロパティで出るし。


あれこれ調べてみた所,どうやらGroovyが気を利かせてJDKのクラスに独自の拡張を施しているみたい。一応,GDKの存在は知ってたつもりなんだけど,GDKのJavadocにSwingは見当たらなかったんだよなぁ。:-(
まあよい。JetGroovyを信じよう。今まで「無名クラスが作れないからSwingとGroovyの相性は悪い」と勝手に決めつけてたのは払拭しとこう。
ずーっとActionListener以外のイベントリスナ使うのは面倒なんだろうなと敬遠していたコードは,実はすんなり書けるハズ,ってことでちょうどxibbarさんとこに手頃な例があったので,Groovy Scriptで書いてみたよ。

import groovy.swing.SwingBuilder
import static javax.swing.WindowConstants.*

def x0, y0
SwingBuilder.build {
  frame(title: "paint", size: [350, 250], visible: true,
        defaultCloseOperation: EXIT_ON_CLOSE) {
    panel(id: 'p',
          mousePressed: {evt ->
            x0 = evt.x
            y0 = evt.y
          },
          mouseDragged: {evt ->
            g = p.graphics
            x = evt.x
            y = evt.y
            g.drawLine(x0, y0, x, y)
            x0 = x
            y0 = y
          })
  }
}

えー,これはちょっと目からウロコかもー。




一応,ここまでに至った経緯を残す。

  1. Griffon使ってみたい。
  2. SwingコンポーネントのactionPerformed属性にクロージャを与える例は見かけるけど,そもそもactionPerformedなんて属性は,JavaSwingコンポーネントに無い。
  3. もっかしてGDKで拡張してた?と調べるもその記述は見当たらない。
  4. じゃあ,SwingBuilderが気を利かしてる?と調べるもその記述は見当たらない。
  5. さらにactionPerformed以外のイベント系の属性も出てきた。

「なんでや?」と思い,やりたくなかったけどGroovyのコード読んだ。さすが言語処理系,見てもさっぱり分からないぞ。:-)
それでもIntelliJの超強力なコード検索機能のおかげでクサいところが分かったよ(あと,ここここのページが役に立ったよ)。
#あ,そうそう調べたのはGroovy 1.5.7。1.6だと,微妙に構造が変わってた。


すげー簡単に要約すると,こんな感じ。

  • Groovyに読み込まれたクラスはPOGO*1POJOに関わらずMetaClass*2という情報が追加される。
    • MetaClassは,MetaClassRegistory*3に登録される。
    • MetaClassRegistory自身はGroovySystem*4に管理されてる。
  • MetaClassは対象となるクラスを解析してGroovy的な味付け(プロパティの追加とか)をする。そこでイベントリスナ持てるクラスは,リスナをプロパティとして登録しちゃうみたい。
    • とりあえず,MetaClassImpl.addProperties()のコードみてよ(2513行付近)。
    • デバッガでみるとMetaClassのlistenersになんか登録されているのがわかる。


そんなワケで,SwingコンポーネントはGroovyで扱うとactionPerformedやmouseClickedなんかが属性として追加されるようだ。
こんなコードで実験してみると一目瞭然で,同じクラス(JButton)でもJavaのIntrospectorとGroovyのMetaClassとで認識するプロパティの数が異なるのが分かるよ(Groovyのほうが多い)。

JButton.metaClass
    .properties.eachWithIndex { it, idx -> println "$idx $it.name"}

Introspector.getBeanInfo(JButton.class)
    .propertyDescriptors.eachWithIndex { it, idx -> println "$idx $it.baseName"}

この仕組み,万能っていうわけではなく,例えば昔に書いたドラッグ&ドロップの例なんかは,あれ以外の書き方ができない。

理由は,DropTargetListenerがJListのEventSetDescriptorsに登録されてないからで,仮にそれが解決できたとしても,DropTargetが欲しているのはDropTargetListenerであって,クロージャではないってのが解決できないので,どーしようも無い(実は,なんか裏技あるのかも?)。
as演算子でなんとかできたよ


と,ここまで調べるのは本意じゃなかったけど,結果として理解が深まったから良しとするか。
そんなわけで,Java側のクラスも気ぃ利かせて拡張するGroovyは面白いなって思ったよ。:-)


ps.
JetGroovyのコード補完がウソついてるのかと思い,こんなコードで試してみると見事にエラーになる。

def button = new JButton()
button.actionperformed = { println "hello" }

--- error message ---
Exception in thread "main" groovy.lang.MissingPropertyException: No such property: actionperformed for class: javax.swing.JButton
  at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:50)
  at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.setProperty(ScriptBytecodeAdapter.java:508)

これでテキトーな属性指定すると怒られるっつう確信が出来たので,JetGroovyを信じる事にした。:-)
さらに,ちゃんと書いたコードをgroovycしてjad*5したみたが「Groovyは遅い」ってのが,なんとなく分かった気がしたよ。
#逆コンパイルすると,そう思わせるコードが出てくる。


ps2.
SwingBuilderにあまり触れなかったけど,SwingBuilderが追加している属性ってのも存在する。
constraintsとかidなんかはそう。


(追記)調べ直してみたら,ちゃんと書いてあった。

You can do that in Groovy with a closure:

// Add a closure for a particular method on the listener interface
deviceProc.controllerUpdate = { ce -> println "I was just called with event $ce" }

Notice how the closure is for a method on the listener interface (controllerUpdate), and not for the interface itself(ControllerListener). This technique means that Groovy's listener closures are used like a ListenerAdapter where only one method of interest is overridden. Beware: mistakenly misspelling the method name to override or using the interface name instead can be tricky to catch, because Groovy's parser may see this as a property assignment rather than a closure for an event listener.

This mechanism is heavily used in the Swing builder to define event listeners for various components and listeners. The JavaBeans introspector is used to make event listener methods available as properties which can be set with a closure.

The Java Beans introspector (java.beans.Introspector) which will look for a BeanInfo for your bean or create one using its own naming conventions. (See the Java Beans spec for details of the naming conventions it uses if you don't provide your own BeanInfo class). We're not performing any naming conventions ourselves - the standard Java Bean introspector does that for us.

Basically the BeanInfo is retrieved for a bean and its EventSetDescriptors are exposed as properties (assuming there is no clash with regular beans). It's actually the EventSetDescriptor.getListenerMethods() which is exposed as a writable property which can be assigned to a closure.

Groovy - Groovy Beans - Closures and listeners

*1:Plain Old Groovy Object

*2:groovy.lang.MetaClass, 実装は同じパッケージ下のMetaClassImpl

*3:groovy.lang.MetaClassRegistory, 実装はorg.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl

*4:groovy.lang.GroovySystem

*5:OSXの場合,Jar Inspectorのほうが便利