金曜日 4 18, 2008

Tips for accessing Java objects from JRuby (Part 3)

前回前々回に引き続いて、JRubyからJavaクラスAPIへアクセスする場合のTIPSの紹介です。

3回目の今回は、JRubyにおけるJavaインタフェースの実装に関して、もう少し深く堀下げてみたいと思います。

Javaインタフェース実装をJRuby側で定義する

JRubyでは、Javaのクラスを親クラスとするサブクラスをRubyクラスとして定義することができますが、Javaのインタフェース実装をRuby側で定義することもできます。

前回のエントリで述べたように、JavaのインタフェースはJRuby上ではModule型にマッピングされますので、通常のRubyにおけるmixed-inクラスと同様にinclude文によってRubyクラスに取り込みます。ただし、一般的なmixed-inクラスと異なりJavaインタフェースはメソッドの実装がありませんので、includeしたJavaインタフェースのメソッド実装をクラスに記述します。以下の例は、Swing JButtonクラスのaddActionListener(java.awt.event.ActionListener)メソッドにRubyクラスとして定義したMyActionListenerクラスオブジェクトを与えてみた場合の例です。

>> class MyActionListener
>>   include java.awt.event.ActionListener
>>   def actionPerformed(ev)
>>     p ev
>>   end
>> end
=> nil
>> button = javax.swing.JButton.new
=> ...
>> button.add_action_listener(MyActionListener.new)

上記のinclude宣言はJRuby 1.0.xでは必須でしたが、JRuby 1.1では実装するJavaインタフェースをincludeする必要がなくなりました。

# JRuby 1.1の場合

>> class MyActionListener
>>   def actionPerformed(ev)
>>     p ev
>>   end
>> end
=> nil
>> button = javax.swing.JButton.new
=> ...
>> button.add_action_listener(MyActionListener.new)

これは強い型づけの言語に慣れてたJava開発者から見ると、とても不思議な現象に思えるかも知れません。しかし、JRubyはRubyの動的な性質を備えているため、こんな芸当があっさりできてしまいます。具体的に何が起こっているかというと、Javaのメソッドが実行された時、JRubyランタイムがメソッド引数の型と与えられた引数の型をチェックし、不足しているインタフェースを実装したプロキシーで引数のオブジェクトをラップし、その後でJavaのメソッドを実行しているのです。

この現象を理解するために、もう一度上記スクリプトの動作をチェックしてみましょう。MyActionListenerクラスのオブジェクトを生成した時点では、そのオブジェクトは純粋なRubyクラスオブジェクトであり、java_class()メソッドにも応答できません。すなわち、Javaのプロキシーオブジェクトも関連付けられていません。

>> class MyActionListener
>>   def actionPerformed(ev)
>>     p ev
>>   end
>> end
=> nil
>> listener = MyActionListener.new
=> #<MyActionListener:0x65394b>
>> listener.respond_to? :java_class
=> false                                 # java_class()メソッドがない

そして、このオブジェクトをSwing JButtonのaddActionListener(ActionListener)メソッドに与えた後、もう一度このオブジェクトを調べてみると、今度はjava_class()メソッドに反応してプロキシーオブジェクトが設定されており、必要なActionListenerインタフェースも勝手に組み込まれていることが分かります。

>> button = javax.swing.JButton.new 'Button'
=> ...
>> button.add_action_listener listener   # リスナを追加
=> nil
>> listener.respond_to? :java_class
=> true                                  # java_class()メソッドが存在する
>> listener.java_class
=> $Proxy7                               # プロキシーが作られている
>> listener.java_class.interfaces
=> [java.awt.event.ActionListener]       # インタフェースが組み込まれている

なお、この動的プロキシーの生成はクラスに対して行なわれるのではなく、オブジェクト毎に行なわれます。そのため、もう一つ別のMyActionListenerオブジェクトを生成してみると、それは純粋なRubyクラスオブジェクトとして生成されることが分かります。

>> another_listener = MyActionListener.new
=> #<MyActionListener:0x1289e48>
>> another_listener.respond_to? :java_class
=> false

なお、Rubyでは、クラスが変更される前に生成されたインスタンスでも、クラスの変更後の変更内容が即座に反映されます。上記の例ではactionPerfomed()メソッドの実装を変えてMyActionListenerクラスを再定義すると、JButtonオブジェクトに追加済みのリスナオブジェクトを入れ換えることなく、直ちにactionPerformed()メソッドの変更が反映できることが確認できます。これは、もう一つのスクリプト言語Groovyでは得られない動的な性質です。

Javaインタフェースの実装にクロージャを使用する

JRuby 1.0.xでは利用できませんが、JRuby 1.1からは、Javaインタフェースの実装にクロージャを使うこともできるようになりました。先ほどのActionListenerの例をクロージャを利用して書き換えると以下のように非常にシンプルな記述になります。

>> button.add_action_listener do |ev|
?>   p ev
>> end
=> nil

クロージャなのでインタフェースどころか、実装すべきメソッド名さえ明示する必要がない点に注意して下さい。追加したクロージャベースのActionListenerを取り出して調べてみると、きちんとActionListenerインタフェースを実装したプロキシーでラップされていることが分かります。

>> button.action_listeners[0]
=> #<#<Class:01x39471b>:0x6b3fc7 @java_object=$Proxy7>
>> button.action_listeners[0].java_class.interfaces
=> [java.awt.event.ActionListener]

上記の場合はActionListenerのインタフェースメソッドがactionPerformed(ActionEvent)メソッドの1つだけでしたが、インタフェースに複数のメソッドがある場合でもクロージャによるインタフェース実装を行なうことができます。例えば、java.awt.event.MouseListenerインタフェースは以下のように5つのイベントメソッドが定義されています。

public interface MouseListener extends EventListener {
  public void mouseClicked(MouseEvent e);
  public void mouseEntered(MouseEvent e);
  public void mouseExited(MouseEvent e);
  public void mousePressed(MouseEvent e);
  public void mouseReleased(MouseEvent e);
}

このような場合でも先ほどと同じようにクロージャでリスナを代用することができます。

>> button.add_mouse_listener do |ev|
?>   p ev
>> end
=> nil

>> button.mouse_listeners.to_ary
=> [#<Java::JavaxSwingPlafBasic::BasicButtonListener:0x3d5149 @java_
object=javax.swing.plaf.basic.BasicButtonListener@bf9a12>, #<#<Class
:01x1a3bff5>:0x18f73cb @java_object=$Proxy8>]     # 2つ目が追加したもの
>> button.mouse_listeners[1].java_class.interfaces
=> [java.awt.event.MouseListener]

この場合、インタフェースに定義されている全てのメソッドが同じクロージャの定義で実装されたことになります。このようなイベントをダンプする簡単なアダプタを追加して動作を確認したい場合、JRubyはとても簡単な方法を提供してくれます。

ただし、クロージャでリスナを代用した場合、実行途中でリスナの動作を変えたい場合は、Javaの場合と同様に一旦コンポーネントからリスナを外し、リスナの実装を変えてから再度リスナの登録が必要になります。クロージャは定義が手軽ですがクロージャ定義はProcクラスのインスタンスそのものですので、この点は仕方がありません。JavaインタフェースをRubyで実装する場合に、Rubyクラスを使うか、クロージャを使うかは目的に応じて選択して下さい。

木曜日 4 17, 2008

Tips for accessing Java objects from JRuby (Part 2)

前回のエントリに続いて、JRubyからJavaクラスAPIへアクセスする際のTIPSを紹介します。

今回のテーマは、JRubyランタイムにおけるJavaクラスとJavaインタフェースクラスの扱いの基本的な概念についてです。

RubyクラスとJavaクラス

JRubyランタイム上でJavaのオブジェクトを生成した場合、そのJavaオブジェクトは必ずそのJavaクラスに対応するプロキシーオブジェクトでラップされます。例えば、java.lang.Integerのオブジェクトを生成するとそのプロキシークラスとなるJava::JavaLang::Integerクラスのオブジェクトが生成されます。通常、Rubyオブジェクトのクラスを取得する場合は、class()メソッドを使用しますが、JRuby上でJavaクラスオブジェクトの参照に対して、class()メソッドを実行すると、Javaオブジェクトをラップしたプロキシーオブジェクトのクラスが取得されます。

>> i = java.lang.Integer.new 0
=> #<Java::JavaLang::Integer:0xfd8f9c @java_object=0>
>> i.class
=> Java::JavaLang::Integer      # java.lang.IntegerのRubyクラス(プロキシークラス)
>> i.class.class
=> Class                        # Rubyクラスのクラス

class()メソッドは元々Ruby Objectに備わったメソッドです。java.lang.Object#getClass()メソッドに対応するRubyスタイルのプロパティメソッドではありません。前のエントリで、Javaクラスのビーンプロパティの全てのgetterメソッドに対して、"get"プリフィックスを取り除いたRubyスタイルのプロパティメソッドが追加されることを述べましたが、純粋なRubyとしての互換性を考慮した結果、java.lang.Object#getClass()のようにRubyスタイルのプロパティメソッドが追加されない例外が存在することに注意してください。

Ruby上のJavaオブジェクトから、それがラップしている本来のJavaクラス参照を取得するためには、java_class()メソッドを使用します。

>> i.java_class
=> java.lang.Integer            # java.lang.IntegerのJavaクラス
>> i.java_class.java_class
=> java.lang.Class              # Javaクラスのクラス

また、java_class()メソッドはJavaクラスのプロキシークラスにも用意されていますので、特定のJavaクラスの参照は以下のようにしても取得できます。

>> java.lang.Integer
=> Java::JavaLang::Integer      # java.lang.IntegerのRubyクラス(プロキシークラス)
>> java.lang.Integer.java_class
=> java.lang.Integer            # java.lang.IntegerのJavaクラス

このようにJavaのオブジェクトを扱う場合、RubyのクラスとJavaのクラスを区別して考えなければいけない場合があります。しかし、Javaクラスの参照をJavaのメソッドの引数に与える場合、これら2つのクラスの違いを意識する必要はありません。JavaのプロキシークラスはJavaクラスに自動的に変換されます。例えば以下のようなJavaのクラスがあるとします。

public class Foo {
  public static void print(Class c) {
    System.out.println("The class is: " + c);
  }
}

この場合、Foo.print(Class)メソッドに与える引数が本来のJavaクラスの参照であっても、プロキシークラスの参照であっても正しく動作します。

>> include_class 'Foo'
=> ["Foo"]
>> Foo.print java.lang.Integer           
The class is: class java.lang.Integer       # プロキシークラスはJavaクラスに自動変換される
=> nil
>> Foo.print java.lang.Integer.java_class
The class is: class java.lang.Integer
=> nil

一見複雑そうに見えるJRubyにおけるJavaクラスの二重生活("secret double-life")ですが、この二重生活があるおかげで静的なJavaクラスに対してRubyの柔軟性のメリットもたらしており、Javaのクラスであっても継承を使うことなくメソッドの追加や再定義を行うことができるようになっています。例えば、Javaのクラスにはデフォルトで"get"プリフィックスなしのgetterメソッドや、アンダースコア(_)スタイルのメソッドが追加されていますが、これらの追加メソッドはこのプロキシークラスに対して追加されているわけです。

JRubyにおけるJavaインタフェースクラスの扱い

先ほどのJavaメソッドFoo.print(Class)に対して、Javaのインタフェースクラスを適用して見たらどうなるでしょうか。

>> Foo.print java.lang.Runnable.java_class
The class is: interface java.lang.Runnable
=> nil
>> Foo.print java.lang.Runnable           
TypeError: expected [java.lang.Class]; got: [org.jruby.RubyModule]; error: argument type mismatch
        from ...

java_class()メソッドでJavaのRunnableインタフェースクラスを抽出して引数に渡した場合は正しく動作しましたが、java_class()メソッドを省略した場合は失敗してしまいました。これはなぜでしょうか。

実はRubyの世界では、メソッド実装を持たないインタフェースという概念がありません。Javaの世界では実装を持つことができるクラスと実装を持たないインタフェースが概念として分離されていますが、クラスもインタフェースもJava上ではjava.lang.Class型として表現されています。JRubyでは、JavaのクラスはRubyのClass型にマッピングされますが、JavaのインタフェースはModule型にマッピングされるという違いがあります。

>> java.lang.Integer.class
=> Class
>> java.lang.Runnable.class
=> Module

ここで少しModule型について説明しておきます。RubyにおいてClass型とModule型の機能的な違いは、それ自身からインスタンスを生成できるかどうかだけです。RubyにおけるModule型の代表的な使われ方は、クラスや変数をグループ化してネームスペースを区別するための箱として使われる場合と、mixed-in(あるいは、mix-in)クラスとして使われる場合の2通りがあります。

1つめの使われ方ですが、Module型はネームスペース空間として用いられるため、JRubyではJavaのパッケージをRubyのModule型にマッピングしています。

>> java.lang.class
=> Module

2つ目の使われ方のmixed-inクラスというのは、オブジェクト指向における多重継承の問題を回避しつつメソッドの実装を複数のクラスで共有するための代表的な手法で、クラス継承ツリーの制約を受けずにメソッドの実装を任意のクラスに付与することができる特別なクラスのことを言います。Javaではmixed-inの概念がないため、似たような実装のメソッドを複数のクラスに重複して実装せざるを得ない場合があります。例えば、SwingコンポーネントのJComboBoxクラスとAbstractButtonクラスは共にjava.awt.ItemSelectableインタフェースを実装していますが、クラス継承のパスが同一ではないため、ItemSelectableインタフェースのメソッドはJComboBoxクラスとAbstractButtonクラスに重複して実装せざるを得なくなっています。このようにJavaのインタフェースは実装を持つことができないという欠点がありますが、それ自身ではインスタンス化することができず、多重継承が可能であるという性質であることから、比較的性質の近いModule型にマッピングされる仕様になっています。

>> java.lang.Runnable.class
=> Module

JavaインタフェースがなぜClass型ではなくModule型にマッピングされたかの説明が長くなりましたが、JRubyからJavaオブジェクトのメソッド引数にJavaインタフェースクラスの参照を渡す必要がある場合は、class()メソッドではなくjava_class()メソッドを使ってJavaのインタフェースクラスを取り出す必要があります。例えばjavax.swing.JComponent#getListeners(Class)メソッドを呼び出す場合は以下のようなスクリプトになります。

>> label = javax.swing.JLabel.new 'label'
=> ...
>> label.get_listeners java.awt.event.MouseListener.java_class
=> #<#<Class:01xc3362f>:0x1a5770 @java_object=[Ljava.awt.event.MouseListener;@13f348b>
 
# さらにto_ary()メソッドを適用するとEventListener配列の中身を表示できる
 
>> label.get_listeners(java.awt.event.MouseListener.java_class).to_ary
=> []

金曜日 4 11, 2008

Tips for accessing Java objects from JRuby

先日、JRubyの最新バージョン1.1の正式版がリリースされました。JRuby 1.1の目玉はjrubycコマンドによるAOT(Ahead Of Time)コンパイルが利用できるようになったことですが、それ以外にも本家MRI(Matz Ruby Implementation)に対する互換性の改善やRubyランタイムからJavaオブジェクトへアクセス方法の改善が非常に細かいレベルで行われています。

このエントリでは、純粋なRubyとしてJRubyを使う場合ではなく、RubyからJavaクラスAPIを使用する場合のTIPSをいくつか紹介します。

JavaプログラマのツールとしてのJRuby

純粋にJavaだけを使用した開発プロジェクトに参加しているプログラマや設計者にとっても、JRubyは簡単な動作検証のための道具としてとても役に立ちます。プログラマは時々既存のJavaクラスライブラリのAPI仕様書からは読み取れない動作仕様を確認するため、プロダクション用のソースコードを書く前に(あるいは書いている途中で)、これらのAPIを呼び出す小さなテストプログラムを書いて、実際の振る舞いを確認することがあります。

このような場合、JRubyのjirbコマンドを使って、目的のJavaクラスオブジェクトをJRubyランタイム上に生成し、インタラクティブに目的のメソッドを呼び出して実行結果を確認したり、オブジェクトの状態を簡単に確認することができます。

もちろん、このような目的であればJRuby以外のスクリプト言語でも同様の目的を果たすことができます。例えば、GroovyははじめからJavaランタイムを前提としたスクリプト言語であるため、Javaのシンタックスに近い形で、スクリプトからJavaのクラスオブジェクトにアクセスできます。どのスクリプト言語を選択するかは好みによるところが大きいと思いますが、JRubyにはGroovyにはない柔軟性があるため、個人的にはJRubyを好んで使用しています。その一方で、JRubyからJavaクラスAPIへのアクセスにはちょっとしたコツが必要になります。以下では、JRuby初心者が分かりにくいと思われるJavaクラスAPIアクセスに関するTIPSをいくつか紹介します。

Rubyらしいメソッド名を使う

Javaクラスのメソッド名はtoOctetString()のようにinterCap形式で定義するのが一般的ですが、Rubyの文化では単語の区切りにアンダースコア(_)を使う方が好まれているようです。JRubyでは全てのinterCap形式のメソッド名に対応してアンダースコアで区切られたメソッド名をランタイムに追加しています。例えば、java.lang.Integerクラスに定義されているtoXXXString()はto_xxx_string()としてもアクセスできるようになっています。

>> integer = java.lang.Integer.new 0
>> integer.methods.each {|m| p m if /to.\*[Ss]tring/ =~ m};''
"toBinaryString"
"toString"
"toOctalString"
"to_octal_string"
"to_string"
"toHexString"
"to_hex_string"
"to_binary_string"

>> java.lang.Integer.to_hex_string 255
=> "ff"

また、ビーンプロパティのアクセッサメソッドも、Rubyの文化に合わせgetterにはプリフィックス"get"をつけないものが追加され、setterの代わりに演算子"="を使用した代入文のためのメソッドも追加されています。

>> button = javax.swing.JButton.new
>> button.methods.each {|m| p m if /.\*[Ll]abel/ =~ m};''
"getLabel"
"setLabel"
"label"
"label="
"get_label"
"set_label"

>> button.label = 'Press Me!'
=> "Press Me!"

さらにbooleanタイプのプロパティの場合は、"is<プロパティ名>"に対して"<プロパティ名>?"という形式のメソッドも追加されます。

>> button = javax.swing.JButton.new
>> button.methods.each {|m| p m if /.\*[Oo]paque.\*/ =~ m};''
"set_opaque"
"opaque="
"opaque?"
"is_opaque"
"isOpaque"
"opaque"
"setOpaque"

>> button.opaque?
=> true

interCap形式のメソッドを使うか、Rubyらしいメソッドを使うかは好みの問題だと思いますので、どちらかお好きな方を使用してください。このように同じことをやるのに、たくさんの表現形式を用意してしまうところなどをみると、JRuby実装は本当にRubyらしさを追求しているなと思います。

staticメンバへのアクセス

これはJavaクラスのアクセスに特化した話ではないのですが、Rubyの言語仕様に合わせて、クラスのstaticなメンバ変数(定数)にアクセスする場合には、"::"を使用します。

>> javax.swing.JSlider::VERTICAL        
=> 1

# インタフェースクラスから直接参照することも可能

>> javax.swing.SwingConstants::HORIZONTAL
=> 0 

staticメソッドにアクセスする場合は、"::"または"."のどちらかを使用します。staticなメンバ変数のアクセスに"."は使用できません。

>> java.util.Calendar::getInstance()
=> #<Java::JavaUtil::GregorianCalendar:0x39826 @java_object=...

# または

>> java.util.Calendar.getInstance()
=> #<Java::JavaUtil::GregorianCalendar:0xc7ecd5 @java_object=...

Ruby配列をJavaの配列に変換する

Ruby Stringや数値(Fixnum、Float)、HashなどのオブジェクトがJava側のメソッドの引数に与えられた場合は適切なJavaのオブジェクトに自動変換されますが、Ruby配列はJavaの配列に自動的には変換されません。例えば、Ruby配列を引数にjava.util.Arrays.asList(Object[])メソッドを実行すると以下のようにタイプエラーとなります。

>> array = ["One", "Two", "Three", "Four"]
=> ["One", "Two", "Three", "Four"]
>> java.util.Arrays.as_list array
TypeError: expected [[Ljava.lang.Object;]; got: [$Proxy8]; error: argument type mismatch

Ruby配列をJavaの配列に変換するには、以下のようにto_java()メソッドを用います。

>> java.util.Arrays.as_list array.to_java
=> #<#<Class:01x12e7234>:0x4bd767 @java_object=[One, Two, Three, Four]>

to_java()メソッドには変換先Java配列のデータタイプを明示することもできます。引数を省略した場合は、Object[]型に変換されます。

>> [1,2,3].to_java
=> #<#<Class:01xc5575>:0x1be8bf1 @java_object=[Ljava.lang.Object;@d591a6>
>> [1,2,3].to_java :byte
=> #<#<Class:01x13244cd>:0x14300c8 @java_object=[B@1e867d6>
>> [1,2,3].to_java :int
=> #<#<Class:01x186dd93>:0x13d0fea @java_object=[I@1dffb78>
>> [1,2,3].to_java :string
=> #<#<Class:01xa6faa9>:0x928739 @java_object=[Ljava.lang.String;@1ebe8ec>

引数に指定できるデータタイプは、プリミティブ型とそのラッパークラス、String、BigDecimalが指定できるようです。指定可能な引数のリストはドキュメントに明示されていないようなので、JRubyに含まれる以下のソースを確認してみてください。

なお、to_java()メソッドは多次元配列に対しても有効です。例えば、2次元配列をモデルの初期データとしてSwingのJTableオブジェクトを生成するために、コンストラクタjavax.swing.JTable#JTable(Object[][], Object[])を呼び出す場合は以下のようなスクリプトになります。

>> table = javax.swing.JTable.new(
?> [['Thomas', 'Enebo'], ['Charles', 'Nutter']].to_java,
?> ['First name', 'Last name'].to_java)
=> #<Java::JavaxSwing::JTable:0x53b2c @java_object=...

Javaの配列をRuby配列に変換する

Javaの配列は配列要素を分かりやすく表示する専用のtoString()メソッドが用意されていないため、Javaのメソッドが返したJava配列の中身がそのままではよく分かりません。

>> javalist = java.util.Arrays.as_list array.to_java
=> #<#<Class:01x12e7234>:0x13577ca @java_object=[One, Two, Three, Four]>
>> javalist.to_array
=> #<#<Class:01xc5575>:0x1f854bd @java_object=[Ljava.lang.Object;@1f80c0e>

Javaの配列をRuby配列に変換するには、to_ary()メソッドを使用します。

>> javalist.to_array.to_ary
=> ["One", "Two", "Three", "Four"]

まだ、他にもありますが今日のところはここまでにしておきます。あまり、JRuby 1.1での拡張部分にふれることが出来なかったため、続きは次回のエントリで書きます。

土曜日 5 12, 2007

Custom JMX Client using JRuby (Part 2)

前回のエントリ(Part 1)では、JRubyを用いてアプリケーションサーバに含まれるJMXサーバに接続し、HTTPリスナMBeanからスレッド数を取得するまでの基本的な流れを示しました。

ここでの目的は、自分が興味のある監視項目を一定間隔で取得するツールに仕上げることですので、前回のサンプルに以下の2つの機能を追加することになります。

  • 興味のあるMBean名と属性名の一覧を定義し、定義された全ての属性値を取得する
  • 1.の処理をinterval秒間隔で定期的に実行する
  • MBean名と属性名の一覧定義には、簡略に定義できるように以下のような2次元配列(MBean:属性=1:nなので、実際には3次元配列)の表現を用いることにします。

    # 監視対象のMBean名と属性の定義
    
    mbeandefs = [
      # [<MBean名>, [<属性1>, <属性2>,...]]
      ["com.sun.appserv:name=http-listener-1," +
       "virtual-server=server,type=http-listener,category=monitor,server=server",
       ["currentthreadcount-count", "currentthreadbusy-count"]
      ]
    ].each { |def| def[0] = ObjectName.new(def[0]) }
    

    上記の例では、Sun Java System Application Server (glassfish)におけるhttp-listener-1のMBeanに対して、プール内のスレッド数("currentthreadcount-count")とその中で実際に処理中のスレッド数("currentthreadbusy-count")を取得することを定義しています。なお、ここではRuby配列に対してeachメソッドを適用し、配列内のMBean名を予めObjectNameに変換しておきます。

    後は、ループを定義して、mbeandefs配列に定義された全ての監視項目についての属性値の取得を、一定間隔毎に実行する定義をすればよいことになります。

    # 各MBeanの属性を取得・表示する
    
    while true
      mbeandefs.each do |mbeandef|
        name, attrs = mbeandef[0], mbeandef[1]
        values = con.getAttributes(name, attrs.to_java(:String))
        values.each do |data|
          print "#{data.value}¥t"
        end
      end
      puts
      sleep interval
    end
    

    getAttributes(ObjectName,String[])メソッドの戻り値はAttributeListオブジェクトですが、JRubyではJavaのCollectionクラスであってもeachメソッドを用いたループが定義できる点が嬉しいところです。なお、Rubyの配列はJavaのString[]に自動変換されないようなので、to_javaメソッドを用いて明示的にString[]に変換しています。

    後は、mbeandefs配列に監視したいMBeanの定義を追加していけばいいのですが、実際にはもう少し汎用性を高める必要があります。

    CompositeDataタイプの属性に対応する

    MBeanには、単純にgetAttribute(ObjectName,String)メソッドで単純な値を返してくれるものだけではありません。たとえば、JVMに標準で含まれるメモリ概要に関するMBean("java.lang:type=Memory")の属性"HeapMemoryUsage"を取得すると、複数のプロパティをもったCompositeDataとなっていることが分かります。

    irb(main):032:0> data = con.getAttribute(
    irb(main):033:1\*   ObjectName.new("java.lang:type=Memory"), "HeapMemoryUsage")
    => #<#<Class:01x3228a1>:0x10980e7 @java_object=javax.management.openmbean.Compos
    iteDataSupport(compositeType=javax.management.openmbean.CompositeType(name=java.
    lang.management.MemoryUsage,items=((itemName=committed,itemType=javax.management
    .openmbean.SimpleType(name=java.lang.Long)),(itemName=init,itemType=javax.manage
    ment.openmbean.SimpleType(name=java.lang.Long)),(itemName=max,itemType=javax.man
    agement.openmbean.SimpleType(name=java.lang.Long)),(itemName=used,itemType=javax
    .management.openmbean.SimpleType(name=java.lang.Long)))),contents={committed=415
    66208, init=0, max=518979584, used=33731368})>
    

    このようなCompositeDataオブジェクトに対しては、更にget(String)メソッドを用いてサブプロパティのキーを与えてあげる必要があります。

    irb(main):034:0> data.get("used")
    => 29729624
    

    そこで、監視項目の定義mbeandefs[]の仕様を見直し、CompositeDataの属性値を取得する場合は、属性値とサブプロパティのキーを"."で結合した"A.e"の形式で定義することにします。

    # 監視対象のMBean名と属性の定義
    
    mbeandefs = [
      # [<MBean名>, [<属性1>, <属性2>,...]]
      ["java.lang:type=Memory" ["HeapMemoryUsage.used"]
    
    ].each { |def| def[0] = ObjectName.new(def[0]) }
    

    属性値を取得する時には、属性名に"."が含まれているかどうかで以下のように条件分岐すれば、シンプルな属性とCompositeDataの値をまとめて取得することができます。MBean属性値を取得するループの部分をCompositeDataにも対応できるように改良すると以下のようになります。CompositeDataの場合であってもJavaのようなキャストは必要なく、そのままget(String)メソッドを呼び出せるのは嬉しいところです。

    # 各MBeanの属性を取得・表示する
    
    while true
      mbeandefs.each do |mbeandef|
        name = mbeandef[0]
        # "<属性>.<サブ属性>"を"."で分解し、それぞれattrs[]、subattrs[]にまとめる
        attrs, subattrs = [], []
        mbeandef[1].each do |attrdef|
          attr = attrdef.split(".")
          attrs << attr[0]
          subattrs << attr[1] # "."がなければnil
        end
        # MBeanの属性値をまとめて取得する
        values = con.getAttributes(name, attrs.to_java(:String))
        # 属性値のリストを表示する
        for i in 0..(values.size-1)
          if subattrs[i].nil? then
            print "#{values[i].value}¥t"
          else
            print "#{values[i].value.get(subattrs[i])}¥t"
          end
        end
      end
      puts
      sleep interval
    end
    

    状況によって名前が変わるMBeanに対応する

    MBeanの中には、環境が変わると名前が変わってしまうものがあります。例えば、JVMのメモリ概要が取得できるtype=MemoryPoolなMBeanは、デフォルトでは以下のような名前で取得されます。

    jmx-mem-normal

    もし、JVMでパラレルGCを有効にすると、"Eden Space"と"Survivor Space"は、それぞれ"Par Eden Space"と"Par Survivor Space"という名前に変わります。

    jmx-mem-parallelgc

    また、コンカレントGCを有効にすると、"Tenured Gen"と"Perm Gen"が"CMS Old Gen"と"CMS Perm Gen"に変わります。

    jmx-mem-concgc

    このように環境によって名前が変わるMBeanを監視対象にする場合、監視項目定義であるmbeandefs[]の管理が面倒になります。このような場合は、MBean名の中で変更される可能性のある部分をワイルドカード"\*"に置き換え、queryNames(ObjectName,QueryExp)メソッドを用いて実行時に目的のMBeanを見つけ出すようにするとよいでしょう。MemoryPoolを監視対象とする場合は、以下のようなコードになります。

    objnames = con.queryNames(ObjectName.new("java.lang:type=MemoryPool,\*"), nil)
    objnames.each do |name|
      mbeandefs << [name, ["Usage.used", "Usage.max"]]
    end
    

    また、アプリケーションを構成する各コンポーネント(Servlet、JSP、EJBなど)を監視対象にしたい場合では、全てのコンポーネントのMBean名をmbeandefs[]に列挙するのは面倒ですし、コンポーネントの名前が変われば、それに合わせてmbeandefs[]もメンテナンスしなければなりませんん。このような場合も同様にワイルドカードが役に立ちます。例えば、Sun Java System Application Server (glassfish)のEJBコンテナ上で動作している全てのEJBについて、EJBプールの使用状態を監視するには、以下のような定義をすればよいことになります。

    objnames = con.queryNames(
      ObjectName.new("com.sun.appserv:type=bean-pool,\*"), nil)
    objnames.each do |name|
      mbeandefs << [name, ["numbeansinpool-highwatermark",
                           "numbeansinpool-current"]]
    end
    

    いかがでしょうか。JRubyのようなスクリプト言語がJavaの実行環境で利用できるようになったことにより、多彩なJavaのAPIをスクリプト言語の簡潔なシンタックスを用いてアクセスできるようになりました。これにより、アプリケーション開発をサポートするためのカスタムツールを簡単に作成できるようになったメリットは大変大きいものがあります。JMXカスタムクライアントは、Javaにおけるスクリプト言語サポートを有効に利用する1つの代表的な例であるということができるでしょう。ここで紹介したサンプルのJRubyスクリプトを以下のリンクからダウンロードできるようにしました。

    上記スクリプトはまだまだ改善の余地が沢山ありますので、興味のある方は自由にダウンロードして、ご自身の開発プロジェクト用にカスタマイズして使ってみてください。

    About

    Takashi Nishigaya
    Principal Consultant
    Technology Solution Consulting
    Oracle Consulting Services

    Search

    Categories
    Archives
    « 4月 2014
      
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
       
           
    今日