option(update:2014/11/11)


[ return ]

実行中にクラスを更新する方法のメモ

サンプルプログラム

参考URL

1.初めに

 プログラム実行中に機能の追加や変更が行えれば、柔軟なシステムが作れます。エクセルの様にマクロ機能を持たせるのも一つの方法ですが、新たにマクロを作るのは言語仕様の設計やマクロ処理系の実装など多くの課題を解決する必要が有りj実現は容易ではありません。

 ところで、javaのプログラムを実行するjavaVMには動的にクラスを読み込む機能が用意されています。実際にアプリケーションサーバーではサーブレットの更新時にサーバーの再起動を必要としないものがあります。実行中に新しいクラスを作って読み込むことが出来るはずなので、これを利用できないかと考えました。javaの既存の言語仕様とコンパイラを利用できるので、容易にマクロよりも柔軟なシステム拡張が実現できるはずです。おおよその手順は次のようになるでしょう。

  1. システムプログラムから子プロセスでエディターを起動しソースコードを記述
  2. 同じく子プロセスでjavaコンパイラを起動しクラスファイルを作成
  3. このクラスをロードしてインスタンスを生成し既存のインスタンスと入れ替える
  4. 更新されたインスタンスで新しい機能を実行する

 サンプル・システムとして関数のグラフ表示システムを作ります。関数部分をクラスとしてシステム実行中に関数を書き換えて様々なグラフ表示が出来ることが目標です。

2.それでも実現には知識が必要

 実際にプログラムを作ろうとすると、それでも色々知識が必要です。自分がjavaについて知らないことを思い知らされることばかりでした。

2−1.子プロセスの実行方法

 子プロセスの窓口となるProcessのインスタンスを実行中のRuntimeからexeメソッドで作成して取得します。子プロセスは並列実行されますがwaitFor()で終了まで待機することも出来ます。必要なら子プロセスの標準入出力を取得したり、戻り値を取得するメソッドがProcessに在りますから詳しくはjavaのAPIを見てください。以下の例は../reloadディレクトリをカレントディレクトリとしてメモ帳を起動しF1.javaファイルを開き、メモ帳を閉じるまでwaitFor()で待機します。

Runtime rt=Runtime.getRuntime();
Process pro=rt.exec("notepad.exe F1.java",null,new File("../reload"));
pro.waitFor();//子プロセスの終了まで待機します。
※例外処理が必要ですが以下も含めて省略します。あしからず。

2−2.新しいクラスの読み込みとインスタンスの生成

 新しいクラスを読み込むのにクラスローダーを使い、コンストラクタでインスタンスを生成するのにリフレクションの機能を利用します。手順は次のようになります

2−3.クラスの更新

1)既存クラスの破棄

 同じ名前のクラスを読み込んで既存のものを更新するには。先ず既存のクラスを廃棄しなければなりません。このためにはクラスのインスタンスとクラス自体、さらに対象のクラスを読み込んだクラスローダーを全てガベージコレクションにより廃棄する必要が有ります。これらへの参照にnullを代入するなどして全ての参照を外しSystem.gc()を呼び出して廃棄を行います。
  しかし、問題なのはクラスローダーです。システムのアプリケーションを読み込んだクラスローダーはjavaVM実行中に止めることは出来ません。従って更新予定のクラスは破棄が可能な専用のクラスローダーを用意して読み込んでおく必要があります。
 
 ※クラス名が毎回異なればクラスの読み込みは問題なく行えましたが、クラスを同じ名前で更新しようとした辺りから壁にぶつかりました^^。特にクラスローダーについて知る必要が出てきたのです。

2)専用のクラスローダー

 クラスローダーはjava.*の様なクラスを読み込む初期クラスローダー、拡張クラスを読み込むExtClassLoaderそしてアプリケーションのクラスを読み込むAppClassLoaderの順番に階層構造を作っています。AppClassLoaderの親がExtClassLoaderExtClassLoaderの親が初期クラスローダーです。クラスを読み込む時は親のクラスローダーに先ず委譲して、親が読み込みに失敗した場合のみ自分で読み込むという手順になっています。
 
 破棄できる専用のクラスローダーを作る場合も、クラス間の整合性を壊さないために委譲のルールは守る必要があります。親をAppClassLoaderにしますから、更新対象のクラスは既存のクラスローダーからは見えない場所に置かれたクラスを読むことになります。例えばクラスをjarファイルの中に入れたり、カレントディレクトリの外に置いたり工夫が必要です。
 今回はjarファイルを作る手間を避けて、クラスを外のディレクトリに置くことにします。

3)URLClassLoader

 専用のクラスローダーとしてURLClassLoaderが使えます。このクラスローダーはクラスを探す場所をURLの配列で渡してつくればいいだけでなので次のように作りました。
 
 URL url=new File("../reload").getCanonicalFile().toURI().toURL();
 URLClassLoader loader = new URLClassLoader(new URL[]{url});

 //外の../reloadディレクトリからクラスを探すクラスローダーとなります
 //指定しなかったので親は既存のAppClassLoaderになっています。
 //このクラスローダーからインスタンスを作る課程は2-2と同様です

4)インターフェースの利用で問題発生

 アプリケーションクラスがあるフォルダに用意したインターフェースFunctionを実装して更新用クラスを作り、読み込んだクラスのインスタンスをFunctionにキャストして使おうとしたのですがここで、クラスの読み込みで以下のエラーが出てしまいました。

※インターフェースを使わずにリフレクションでクラスの機能を利用するとこも可能です。しかし、そのプログラムは書いてみるとオブジェクトの使い捨てが多発する極めて効率の悪そうなものになってしまいました。

ファイルの配置は次のようになっています。
 
 test/ アプリケーションが置かれたディレクトリ
  Function.java インターフェースのソースコード
  Sample.java システムのソースコード
  
 reload/ 更新するクラスを置くディレクトリ
  F1.java 更新するクラスのソース
 ※コンパイルはclasspathでパスにtest/を追加

 F1のソースコード編集とコンパイルは問題なく実行できましたがクラスの読み込みで次のエラーが発生します。

F1 cannot access its superinterface Function

クラスの識別はクラス名 and クラスをロードしたクラスローダーで行われるらしいのですが、test/Function.classAppClassLoaderで読み込まれるはずです。ところがURLClassLoaderで読み込むときにF1からこれを参照できません??。

3.試行錯誤

 クラスローダーの具体的な仕組みは知らないので試行錯誤でした。何とか動くように出来ましたが、仕組みが理解できたとは言えません。

3−1 出発点

 testにFunction.javaを置いてコンパイル時点ではclasspathに../testを加えてF1をコンパイルすることは可能。しかし、これをクラスローダーで読み込む時点でFunctionインターフェイスへのアクセス違反エラーが発生。

3−2 インターフェースを外の共通の場所に置く

 commonにFunction.javaを置いてAppClassLoaderからもみえなくしましたが、これも動かない。ますます解らなくなるだけで改善しません。

3−3 import文の利用

試行錯誤の中でimportで別のパッケージを参照することを明示してみたら動くようになりました。これはどんな意味が在るのでしょう?
 F1を読み込むとき、実装の元インターフェースの読み込みで、親のクラスローダーに委譲せずに自分で読み込もうとして失敗していたのが、import文があると正しく親に委譲する結果うまく読めるようです。

1)import Function;

 これはコンパイルエラーとなる。既定パッケージのクラスをインポートするのは確かに不自然だし、インポートの単位はパッケージなのでこのような記述はコンパイルエラーとなります。

2)import common.Function;

パッケージ名を付けるためにFunction.javaを../test/commonに置き、さらに以下の様にpackageの記述を追加しました。

package common;

public interface Function{
    double value(double x);
}

3) Functhionを実装するF1側でimport分が使えるようになる

import common.Function;

public class F1 implements Function
{
    public double value(double x)
    {
        return Math.cos(10*(x-1.5)*(x-1.5)-1);
    }
}

※ここで、importするためにはパッケージ名が付いていることが必要になります。