「Java SE 6完全攻略」第3回でjconsoleの機能向上を取り上げたのを覚えているでしょうか。この回ではjconsoleの改良点としてGUIを取り上げましたが,拡張されたのはGUI部分だけではありません。jconsoleに独自のビューを追加することも可能になったのです。

そこで,今週はjconsoleをカスタマイズして新しいビューを追加する機能を紹介します。

jconsoleには「メモリ」「スレッド」などのタブがあり,それぞれグラフなどの形式で情報を表示します。表で数値を表示するより,グラフで表示する方が傾向を一目で見られるので便利です。

メモリーやスレッドにはそれぞれ対応するMXBeanがあり,jconsoleはMXBeanが保持する情報を専用のビューで見やすく表示します。

しかし,自作のMBeanやMXBeanなどを専用のビューで表示することはできません。もちろん,MBeanタブの属性の表示でグラフ表示はできますが,決して見やすいものではありませんでした。

標準で提供されるMXBeanの情報参照用に限ってしまえば,jconsoleは使いやすいのですが,汎用のJMXクライアントとしては少し見劣りします。

そこで,Java SE 6ではjconsoleをカスタマイズができるようになりました。標準で提供されるMXBean以外のMBeanやMXBeanに対応したビューを追加できるようになったのです。

jconsoleにタブを追加する

jconsoleをカスタマイズするために提供されているのがJConsole APIです。こういう名前が付いていますが,中身はインタフェース一つ,クラス一つというシンプルなAPIです。

ただし,jconsoleにビューを追加するにはSwingとMBeanも必要です。

それでは,さっそく簡単なサンプルを作ってみましょう。

新たにMBeanを作るのは大げさなので,MemoryMXBeanを利用して,ヒープの使用状況を表示するタブを追加することにします。

サンプルはこちらからダウンロードできます: MemoryUsagePlugin.java, MemoryUsageIndicator.java

プラグインの実装

jconsoleのカスタマイズはプラグインという形式で行います。プラグインはcom.sun.tools.jconsole.JConsolePluginクラスの派生クラスを作成することで実現できます。JconsolePluginはアブストラクト・クラスで,これを派生したクラスは次の二つのメソッドを実装する必要があります。

  • Map<String, JPanel> getTabs()
  • SwingWorker<?, ?> newSwingWorker()

getTabsメソッドは,追加するタブを返すメソッドです。一つのプラグインで複数のタブを作成することができるため,戻り値はMapオブジェクトになっています。Mapオブジェクトのキーがタブに表示する文字列,値がタブで表示を行うJPanelオブジェクトになります。

newSwingWorkerメソッドは,表示の更新を行うSwingWorkerオブジェクトを返すメソッドです。SwingWorkerクラスもJava SE 6で導入されたクラスで,非同期にSwingの描画処理を行うためのユーティリティ・クラスです(SwingWorkerクラスも今後,この連載で取り上げる予定です)。

これらのメソッドを実装する以外に,jconsoleとJava VMとの接続,切断などのイベント処理を行う必要があります。

サンプルは二つのクラスで構成されています。MemoryUsagePluginクラスがJConsolePluginクラスを派生させたクラスで,プラグインのメインになるクラスです。もう一つのMemoryUsageIndicatorクラスはJPanelの派生クラスで,表示を行うクラスになります。

まずはMemoryUsagePluginクラスから見ていきましょう。

MemoryUsagePluginクラスのコンストラクタを以下に示します。

    public MemoryUsagePlugin() {
        addContextPropertyChangeListener(new PropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent ev) {
                // 変更されたプロパティを取得
                String prop = ev.getPropertyName();
                if (prop == JConsoleContext.CONNECTION_STATE_PROPERTY) {
 
                    // JavaVMに接続したらMBeanServerConnectionを取得し
                    // MemoryUsageIndicatorにセットする
                    ConnectionState state = (ConnectionState)ev.getNewValue();
                    if (state == ConnectionState.CONNECTED 
                                                 && indicator != null) {
                        MBeanServerConnection connection 
                            = getContext().getMBeanServerConnection(); 
                        indicator.setMBeanServerConnection(connection);
                    }
                }
            }
        });
    }

コンストラクタではjconsoleとJava VMの接続,切断などのイベント処理を行いました。このイベント処理にはjava.beans.PropertyChangeEventクラスを使用します。

リスナーはjava.beans.PropertyChangeListenerインタフェースですが,JConsolePluginクラスのリスナーを追加するメソッドがaddContextPropertyChangeListenerメソッドであることを注意してください。

プロパティが変更されたときにコールされるメソッドはpropertyChangeメソッドです。

propertyChangeメソッドでは,まず変化したプロパティを取得し,それがJConsoleContext.CONNECTION_STATE_PROPERTYかどうかをチェックしています。com.sun.tools.jconsole.JConsoleContextクラスはjconsoleとJava VMのやり取りを行うために使用するクラスです。

変更されたプロパティがCONNECTION_STATE_PROPERTYであった場合は,変更された値を取り出します。この値の型はJConnectionContext.ConnectionStateクラスで,JConsoleContextクラスの内部クラスです。

jconsoleとJava VMが接続されたときにはCONNECTED,接続中の場合はCONNECTING,切断されたらDISCONNECTEDになります。

CONNECTEDの場合,まずgetContextメソッドを使用してJConsoleContextオブジェクトを取得します。次に,getMBeanServerConnectoinメソッドをコールすることで,Java VMで動作しているMBeanServerとやり取りを行うjavax.management.MBeanServerConnectionオブジェクトを取り出します。そして,表示を行うMemoryUsageIndicatorオブジェクトに引き渡しています。

このサンプルは周期的にヒープの使用状況を更新するわけではないので,CONNECTEDだけを扱っています。

これでイベント処理は終了です。次がgetTabsメソッドです。

    @Override
    public synchronized Map<String, JPanel> getTabs() {
        if (tabs == null) {
            indicator = new MemoryUsageIndicator();
            indicator.setMBeanServerConnection(
                getContext().getMBeanServerConnection());
            
            tabs = new LinkedHashMap<String, JPanel>();
            tabs.put("HeapUsage", indicator);
        }
        
        return tabs;
    }

getTabsメソッドは,表示を行うクラスを保持するMapオブジェクトを返すためのメソッドです。親クラスで定義されたメソッドなので,@Overrideアノテーションで修飾しておきましょう。前述したように,マップのキーはタブに表示させる文字列,値がJPanelオブジェクトとなります。

このサンプルではJPanelクラスを派生させたMemoryUsageIndicatorクラスを使用しました。

tabsが存在しない場合はMemoryUsageIndicatorオブジェクトを生成し,イベント処理のときと同様にMBeanServerConnectionオブジェクトをセットします。

そして,Mapオブジェクトを生成します。

ここで,LinkedHashMapクラスを使用しているのは,Mapオブジェクトから値を取り出すときの順序を一意に決めることができるためです。この順序はLinkedHashMapオブジェクトに要素を挿入した順序となります。

このサンプルでは値は一つしかないのでほとんど意味はないのですが,タブの並び順にこだわるならばLinkedHashMapクラスを使用しましょう。

最後に残ったのが,newSwingWorkerメソッドです。SwingWorkerクラスは描画のためのクラスなので,SwingWorkerオブジェクトは描画を担当するMemoryUsageIndicatorクラスに委譲することにしました。

    @Override
    public SwingWorker<?,?> newSwingWorker() {
        return indicator.newSwingWorker();    
    }

表示部の実装

それでは表示を行うMemoryUsageIndicatorクラスを実装していきます。

MemoryUsageIndicatorクラスは前述したようにJPanelの派生クラスとして実装しました。

表示に関してはサンプルということもあり,かなり簡略化しています。MemoryUsageクラスのused,committed,maxの表示にはJLabelクラスを使用しています。

    public MemoryUsageIndicator() {
        JPanel panel = new JPanel();
        panel.setLayout(new GridLayout(3, 2, 5, 5));
  
        JLabel usedLabel = new JLabel("Used");
        panel.add(usedLabel);
        usedValue = new JLabel("0");
        panel.add(usedValue);
  
        JLabel committedLabel = new JLabel("Committed");
        panel.add(committedLabel);
        committedValue = new JLabel("0");
        panel.add(committedValue);
  
        JLabel maxLabel = new JLabel("Max");
        panel.add(maxLabel);
        maxValue = new JLabel("0");
        panel.add(maxValue);
  
        add(panel);
    }

ヒープの使用状況を表示するにはMemoryMXBeanから情報を取得しなければなりません。MBeanServerConnectionクラスを使用してMemoryMXBeanのプロパティを取得することもできるのですが,ここではもっと簡単な方法を使用します。

MXBeanのプロキシを使用する方法です。プロキシを使うと,MXBeanの実体はリモートにあるにもかかわらず,それを意識することなく,通常のオブジェクトのように使うことができます。

    public void setMBeanServerConnection(
                MBeanServerConnection connection) {
        try {
            memoryMXBean 
                = ManagementFactory.newPlatformMXBeanProxy(
                    connection,
                    ManagementFactory.MEMORY_MXBEAN_NAME,
                    MemoryMXBean.class);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

MXBeanのプロキシを生成するには,java.lang.management.ManagementFactoryクラスのnewPlatformMXBeanメソッドを使用します。このメソッドの第1引数はMBeanServerConnectionオブジェクト,第2引数がMXBeanの名前になります。MXBeanの名前はManagementFactoryクラスが定数として定義しているものを使用します。最後の引数がプロキシを作成するMXBeanのClassオブジェクトになります。

たいていの場合,このようなメソッドにはダウンキャストが必要なのですが,このメソッドはダウンキャストが必要ありません。というのも,ジェネリクスを使用しているからです。

MXBeanでないMBeanの場合はjavax.management.JMXクラスのnewMBeanProxyメソッドを使用します。

さて,MemoryMXBeanが取得できたので,あとはGUIの表示更新の部分です。表示更新には前述したようにSwingWorkerクラスを使用します。

    class Worker extends SwingWorker<MemoryUsage,Object> {
        @Override
        public MemoryUsage doInBackground() {
            return getMemoryUsage();
        }

        @Override
        protected void done() {
            try {
                // doInBackgroundで取得したMemoryUsageを取得
                MemoryUsage usage = get();
 
                usedValue.setText(
                    String.format("%d", usage.getUsed()));
                committedValue.setText(
                    String.format("%d", usage.getCommitted()));
                maxValue.setText(
                    String.format("%d", usage.getMax()));
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            } catch (ExecutionException ex) {
                ex.printStackTrace();
            }
        }
    }
 
    private MemoryUsage getMemoryUsage() {
        return memoryMXBean.getHeapMemoryUsage();
    }
 
    public SwingWorker<?,?> newSwingWorker() {
        return new Worker();
    }

SwingWorkerクラスはabstractクラスで,doInBackgroundメソッドとdoneメソッドをオーバライドする必要があります。

SwingWorkerクラスの定義では二つの型をジェネリクスで指定しています。前者はdoInBackgroundメソッドとgetメソッドの戻り値の型になります。後者はここでは使用しません。今後,SwingWorkerの解説を予定しているので,詳しい説明はそのときに行います。

doInBackgroundメソッドでMemoryMXBeanからヒープの使用状況を表すMemoryUsageオブジェクトを取得します。このMemoryUsageオブジェクトはgetメソッドで取得できます。

実際に描画処理を記述するのがdoneメソッドです。まず,getメソッドを使用して,doInBackgroundメソッドで取得したMemoryUsageオブジェクトを取得します。

あとは,MemoryUsageオブジェクトからused,committed,maxの値を取得して,JLabelオブジェクトにセットするだけです。ここでは簡易フォーマットのためにString#formatメソッドを使用しました。String#formatメソッドはC言語のprintfと同様のフォーマットが可能です。

このSwingWorkerクラスを派生したWorkerクラスを生成して,MemoryUsagePluginオブジェクトに引き渡すための,newSwingWorkerメソッドを記述します。

プラグインのコンパイル,実行

プログラムができたので,コンパイルしましょう。ここで使用したcom.sun.tools.jconsole.JConsolePluginなどのクラスは[JAVA_HOME]\lib\jconsole.jarで定義されています。したがって,クラスパスにはこのJARファイルを指定します。

Windows XPでの例を示します。JAVA_HOME環境変数はC:\Program Files\Java\jdk1.6.0を示しています。

C:\temp>java -cp "%JAVA_HOME%\lib\jconsole.jar";. MemoryUsagePlaugin.java

コンパイルできたので,次にJARファイルにまとめます。ただ,単にJARファイルにまとめただけでは,どのクラスがプラグインのメインクラスなのかわかりません。そこで,com.sun.tools.jconsole.JConsolePluginというファイルを用意します。すごい長ったらしいファイル名ですが,これで一つのファイル名です。

com.sun.tools.jconsole.JConsolePluginファイルにはプラグインのメインクラスを記述します。

MemoryUsagePlugin

そして,com.sun.tools.jconsole.JConsolePluginファイルをMETA_INF\servicesディレクトリの配置します。ここではc:\tempにソースなどが置いてあるので,c:\temp\META_INF\services\com.sun.tools.jconsole.JConsolePluginになります。

JARファイルにはこのファイルも一緒に含めるようにします。

C:\temp>jar cvf memoryusageplugin.jar MemoryUsage*.class
 META_INF\service\com.sun.tools.jconsole.JConsolePlugin
(実際には1行)

このようにして特定のクラスを指定する方法は,java.util.ServiceLoaderクラスを使用したものです。このクラスもJava SE 6から導入されています。

やっとJARファイルを作ることができました。最後にこれをjconsoleに組み込みましょう。JARファイルを指定するには-pluginpathオプションで指定します。

C:\temp>jconsole -pluginpath memoryusageplugin.jar

何か問題があった場合は問題個所を示したダイアログが表示されるので,それを見て修正を加えてください。プラグインが正しくできていれば,[HeapUsage]というタブが表示されるはずです(図1)。

Heap Usageタブ
図1 Heap Usageタブ

サンプルということで,表示はかなりしょぼくなっています。もっとも表示はSwingで記述できるので,ターゲットになるMBean/MXBeanに応じていろいろと見せ方を工夫できるはずです。

今回は既存のMemoryMXBeanを例に取りましたが,自作のMBean/MXBeanを扱うことも可能です。アプリケーションに応じてプラグインを作成すれば,あたかもそのアプリケーション専用の管理ツールができあがることになります。

複数のMBeanの情報をまとめて表示することももちろん可能です。ぜひ,自分なりの管理ツールを作り上げてみてください。

著者紹介 櫻庭祐一

横河電機 ネットワーク開発センタ所属。Java in the Box 主筆

今月の櫻庭

秋も深まりつつある今日この頃。櫻庭の住んでいる東京でもそろそろ紅葉の時期になってきました。

カエデ,ナナカマド,ツタ,サクラなど,それぞれの色で秋の景色を彩っています。櫻庭が好きなのはニシキギ。名前のとおり錦のように赤い葉っぱが特徴です。

家のそばを通っているイチョウ並木もだんだんと黄色くなり始めてきました。日当たりのいい場所ほど,黄葉がはやくはじまるようです。イチョウが黄色くなる頃といえば,ギンナン。おいしいですよね。

と,今月も最後は食べ物の話題になってしまうのでした。