Attach API (アプリケーションのインスタンス数を制御するには-その4)

同名のエントリではJavaアプリケーションのインスタンス数を管理、制御するための方法を紹介し、その後はファイルチャネルの部分ロッキング機能を利用した「システムセマフォもどき」により機能を実現することができた。
その後、前エントリのコメントでskさんにアドバイス頂いたがその中で聞いたことの無いAPIを紹介された。

Attach API - JSDK6 ドキュメント

まだ調べて間もない状況だが、どうやらこのAPIは現在動作中のJVMを列挙し、任意のJVMにアタッチしてエージェントを送りこむことができるらしいのである。(知らないのもそのはず、J2SE6で追加されたAPIであり、J2SE6に添付されているjconcoleもこの機能を利用している)

せっかくなのでこのAttah APIを使って同様にアプリケーションのインスタンスを管理してみることにする。

今までは書かなかったが、アプリケーションのインスタンス数を管理するために、以下のようなインタフェースを定義している。

public interface IAppInstanceCounter {
    int getInstanceCount();
}

これは単純に現在のプロセス(アプリケーション)のインスタンス数を返す操作であり、例えば先日紹介した「擬似セマフォ」を利用した実装では以下のように実装されている。

public class PseudoFileSemaphoreCounter implements IAppInstanceCounter {
    private PseudoFileSemaphore semaphore;
    private int launchLimit;
    public PseudoFileSemaphoreCounter(String appName, int launchLimit) {
        this.semaphore = new PseudoFileSemaphore(appName, launchLimit);
        this.launchLimit = launchLimit;
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable(){
            @Override
            public void run() {
                semaphore.release();
            }
        }));
    }
    @Override
    public int getInstanceCount() {
        int result = this.semaphore.tryAcquire();
        return ( result != 0 ) ? result : this.launchLimit + 1;
    }
}

今回はこれをAttach APIを利用して実装してみよう。

public class JVMDescriptorInstanceCounter implements IAppInstanceCounter {
    private final String mainclassName;
    public JVMDescriptorInstanceCounter() {
        StackTraceElement[] traces = Thread.currentThread().getStackTrace();
        this.mainclassName = traces[traces.length-1].getClassName();
    }
    public int getInstanceCount() {
        return FinderUtil.findAll(VirtualMachine.list()
              , new IPredicate(){
                    @Override
                    public boolean evaluate(VirtualMachineDescriptor input) {
                        return input.displayName().equals(mainclassName);
                    }
                }).size();        
    }
}

といっても、実際にJVMにアタッチする必要は無かったようだ。
Attach APIでは実際にJVMにアタッチするためには、その前にJVMのディスクリプタの一覧をVirtualMachineDescriptor.list()で取得するのだが、このディスクリプタのプロパティであるdisplayName()にはアプリケーションのエントリポイントとなっているメインクラスの名前が格納されているので、この名前が同じディスクリプタの数が、アプリケーションのインスタンス数として使えそうだ。
ここでふと「エントリポイントのメインクラス名」ってどうやって取得するのだろうと手が止まってしまった。
結局は上記のソースのようにスタックトレースの末尾から取得したが、

StackTraceElement[] traces = Thread.currentThread().getStackTrace();
this.mainclassName = traces[traces.length-1].getClassName();

これがベストなのかどうか正直自信が無い。


ソース中の"FinderUtil.findAll"だがこれは以下のようにIPredicateジェネリック述語論理によるコレクションの絞込みを行っている。

public interface IPredicate {
   boolean evaluate(T input);
}
public final class FinderUtil {
    public static final  List findAll(List list, IPredicate match) {
        ArrayList temp = new ArrayList();
        for (T t : list) {
            if ( match.evaluate(t) ){
                temp.add(t);
            }
        }
        return temp;
    }
}

C#のプログラマの方はお判りと思うがジェネリック述語論理とコレクションとの組合せは非常に便利であり、一度使うと癖になる。Javaの標準APIに無いのでこのように似た文法のユーティリティを用意して使っているのだ。

実際の使用はアプリケーションの初期化処理等から上記JVMDescriptorInstanceCounterクラスを実体化してメソッドを比較するだけである。

IAppInstanceCounter counter = new JVMDescriptorInstanceCounter();
if ( myapp.getLaunchLimit() <= counter.getInstanceCount() ) {
    //起動OK
} else {
    //起動NG
}

このようにAttach APIを利用したインスタンス数の制御はロックファイル等が一切不要であり、後処理も無いのでコードも含めて処理系が非常にすっきりする。がしかし、

  • JSDK外のjarパッケージ(tools.jarに包含)、かつ、Sunのパッケージ(com.sun.tools)を直接使用している
  • com.sun.tools.attach.VirtualMachineDescriptor#displayName()の戻り値が今後もメインクラス名であり続ける保障が無い

この二点において不安が残るアドホックな実際であることも確かなので、前回作成した擬似セマフォとこちらの、どちらを使うか迷う所だ。

なお、Attach APIだがSwingアプリケーションから見ても非常に興味深いAPIである。例えば.NET C#向けのコードには同様のインスタンス制御に加えて「許可された起動数を超えていたならば、他のどれかのインスタンスのウインドウをアクティブにした後、自身のアプリケーションは終了する」という機能を追加しているが、Javaでは無理だと思っていたこのような機能もこのAPIを使ってできそうである。