|
FAQ |
JavaTM 2 SDK, Standard Edition, v1.4 の目次 |
Naming.lookup
を呼び出すと、予期しないホスト名やポート番号に対する例外が発行されます。なぜですか。
CLASSPATH
に _Stub
ファイルをインストールする必要がありますか。ダウンロードできるのではないかと思いますが。
ClassNotFoundException
が返されるのはなぜですか。
java.lang.ClassMismatchError
が返されます。なぜでしょうか。
ArrayStoreException
が発行されます。どうなっているのですか。
ClassNotFoundException
が発行されます。どうなっているのですか。
java.net.UnknownHostException
を発行して失敗します。なぜですか。
UnknownHostException
が発行されます。なぜですか。
Naming.bind
と Naming.lookup
に非常に時間がかかるのはなぜですか。
java.net.SocketException: Address already in use
] が発行されるのですが、なぜですか。
System.exit
を使うのは賢明でしょうか。
unreferenced()
メソッドが呼び出されるまで 10 分かかります。この時間を短縮する方法を教えてください。
rmic
コマンドを実行させるにはどうすればよいでしょうか。
select()
呼び出しでブロック化ではなくポーリングしているようなのですが、レジストリはポーリングで実装されているのですか。
ObjectOutputStream
に書き込むために、クラスが Serializable
を実装しなければならないのはなぜですか。
ObjectOutputStream
から ObjectInputStream
を作成するには、どうすればよいでしょうか。
writeObject
メソッドを使ってネット上を送信し、readObject
メソッドを使って受信します。次に、オブジェクトのフィールドの値を変更してから同様に送信すると、readObject
メソッドから返されるオブジェクトは最初のものと同じで、フィールドの新しい値が反映されていないようです。これは正しい動作なのですか。
Serializable
を実装せず、そのサブクラス B が Serializable
を実装している場合、クラス B を直列化したとき、クラス A のフィールドは直列化されますか。
Naming.lookup
を呼び出すと、予期しないホスト名やポート番号に対する例外が発行されます。なぜですか。
サーバのホスト名や IP アドレスが誤っている (またはクライアントが解釈できないホスト名をサーバが持っている) 場合でも、サーバはその誤ったホスト名を使ってすべてのオブジェクトをエクスポートします。ただし、それらのオブジェクトを受け取ろうとするたびに例外が発生します。
レジストリの位置を示すために Naming.lookup
で指定したホスト名は、サーバへのリモート参照にすでに組み込まれているホスト名には効果がありません。
通常、不可解なホスト名はサーバの修飾されていないホスト名、つまりクライアントのネームサービスに知らされていない非公開な名前です。Microsoft Windows プラットフォームの場合、その名前はサーバの [ネットワーク] -> [ユーザー情報] -> [コンピュータ名] で設定されています。
対策としては、サーバの起動時にシステムプロパティ java.rmi.server.hostname
を設定します。
このプロパティの値は、外部からアクセス可能なサーバのホスト名 (または IP アドレス) でなければならず、これを Naming.lookup
のホスト部分に指定したときの動作は成功します。
詳細については、コールバックと完全指定のドメイン名に関する質問を参照してください。
CLASSPATH
に _Stub
ファイルをインストールする必要がありますか。ダウンロードできるのではないかと思いますが。
java.rmi.server.codebase
プロパティを注釈として付けている場合は、スタブクラスをダウンロードできます。
リモートオブジェクトをエクスポートするサーバ上の java.rmi.server.codebase
プロパティを設定する必要があります。
リモートクライアントがこのプロパティを設定できるようになると、指定されたコードベースだけからリモートオブジェクトを入手するように制限されます。
すべてのクライアント VM が、オブジェクトの場所を示すコードベースを指定しているわけではありません。
リモートオブジェクトが RMI によって整列化されるとき (リモート呼び出しの引数として、または戻り値として)、スタブクラスのコードベースが RMI によって取得され、直列化されたスタブの注釈付けに使用されます。
スタブが整列化解除されるときは、CLASSPATH
またはアプレットコードベースなど、オブジェクトを受け取るためのコンテクストクラスローダにすでにそのクラスが存在しない限り、RMIClassLoader
を使ってスタブのクラスファイルをロードするためにコードベースが使用されます。
_Stub
クラスが RMIClassLoader
によってロードされた場合は、RMI はすでに、注釈付けに使用するのはどのコードベースかを知っています。
_Stub
クラスが CLASSPATH
からロードされた場合は、明確なコードベースは存在しないので、RMI は java.rmi.server.codebase
システムプロパティを調べてコードベースを検索します。
このシステムプロパティが設定されていない場合は、スタブは null コードベースで整列化されます。つまり、クライアントがその CLASSPATH
内に _Stub
クラスファイルに一致するコピーを持っていない限り、このコードベースは使用できません。
コードベースプロパティを指定することは忘れがちです。
この間違いを検出する方法の 1 つに、rmiregistry
を個別に起動し、アプリケーションクラスにアクセスしないという方法があります。
こうすると、コードベースが省略された場合は、必ず Naming.rebind
が失敗します。
java.rmi.server.codebase
プロパティについての詳細は、チュートリアルの 「RMI の使用による動的なコードのダウンロード (java.rmi.server.codebase
プロパティを使用)」 を参照してください。
java.rmi.server.codebase
プロパティを、file
または ftp
など、任意の有効な URL プロトコルを使うように設定できます。HTTP サーバを使っても、クラスファイルの自動ダウンロード機構が提供されるだけです。HTTP サーバにアクセスしない場合、または HTTP サーバを設定しない場合は、ftp://ftp.java.sun.com/pub/jdk1.1/rmi/class-server.zip
にある小規模のクラスファイルサーバを使うことができます。
ClassNotFoundException
が返されるのはなぜですか。
java.rmi.server.codebase
プロパティが設定されていない (または正しく設定されていない) 可能性があります。チュートリアルの「RMI の使用による動的なコードのダウンロード (java.rmi.server.codebase
プロパティを使用)」を参照してください。
hashCode
メソッドおよび equals
メソッドを適切に実装する必要があります。クライアントソケットファクトリがこれらのメソッドを正しく実装しない場合は、同じリモートオブジェクトを参照する (クライアントソケットファクトリを使う) スタブが等しくないという結果になります。
RMI 実装は、サーバ側のポートも再利用しようとします。ただし、同等のソケットファクトリによって作成されたポートの既存のサーバソケットがある場合に限ります。サーバソケットファクトリクラスも hashCode
メソッドおよび equals
メソッドを実装する必要があります。
ソケットファクトリにインスタンス状態がない場合は、hashCode
メソッドと equals
メソッドを次のように実装します。
public int hashCode() { return 57; } public boolean equals(Object o) { return this.getClass() == o.getClass() }
javaw
コマンドは、stdout
と stderr
に出力を行うので、デバッグ目的では java
コマンドを別のウィンドウで実行してエラーの出力を表示できるようにする方が良いでしょう。
これを行うには、次のようにコマンドを実行します。
start java EchoImpl
開発時には、javaw
コマンドの使用はお勧めしません。
サーバの活動を観察するには、-Djava.rmi.server.logCalls=true
を指定してサーバを起動します。
java.lang.ClassMismatchError
が返されます。なぜでしょうか。
java.rmi.registry.RegistryImpl
を含む) を再起動してみてください。
これで問題がなくなります。
ArrayStoreException
が発行されます。どうなっているのですか。
FooRemote[] f = new FooRemote[10]; for (int i = 0; i < f.length; i++) { f[i] = new FooRemoteImpl(); }
このようにすれば、RMI が配列の各セルにスタブを入れても、リモート呼び出しでの例外は発生しません。
分散オブジェクトはローカルオブジェクトとは動作が異なります。ロックと障害を処理することなく、ローカルの実装を再利用するだけでは、予測不可能な結果が生じる可能性があります。
ClassNotFoundException
が発行されます。どうなっているのですか。
オブジェクトをバインドするためにレジストリへの呼び出しを行うと、レジストリは実際にはそのリモートオブジェクトのスタブへの参照をバインドします。スタブオブジェクトのインスタンスを生成するには、レジストリ VM からそのクラス定義がロード可能である必要があります。直列化形式のスタブをリモートメソッド呼び出しでレジストリに送信する VM (この場合は サーバ VM) は、そのクラスをどこからダウンロードできるかをスタブの注釈として付けます。スタブに適切な注釈が付けられていない場合、RMI はスタブのインスタンス生成を試みる際に、ClassNotFoundException
をスローします。
クラスに適切な注釈を付けるために、サーバは java.rmi.server.codebase
プロパティの値をスタブクラスの位置に設定する必要があります。RMI は、送出される直列化形式のオブジェクトインスタンスに、java.rmi.server.codebase
プロパティの値を自動的に注釈として付けます。
注: 関連するスタブクラスファイルすべてを rmiregistry の CLASSPATH に配置することにより、rmiregistry からスタブオブジェクトの非整列化を実行できます (特に少数の環境では適切な方法です)。ただし、rmiregistry は、必ずしもスタブクラスをダウンロードする必要はありません。スタブクラスがローカルで利用可能な場合には、それを使用します。スタブの配備に rmiregistry の CLASSPATH を使用する場合、レジストリから取得したスタブインスタンスを参照するすべての VM は、ローカルにインストールされたスタブのクラスファイルを (VM の CLASSPATH 内に) 保持する必要があります。
たとえば、レジストリが CLASSPATH からスタブクラスをロードする場合、直列化されたスタブオブジェクトをレジストリが他の VM に送信すると、直列化されたオブジェクトには、レジストリの java.rmi.server.codebase
プロパティの値 (ほぼ常に null) が注釈として付けられます。直列化されたスタブオブジェクトをレジストリから受け取る VM が、ローカルにインストールされたこれらスタブのクラスファイルを保持しない場合、VM から ClassNotFoundException
がスローされる場合があります。
一方、サーバ VM の java.rmi.server.codebase
注釈からクラスが動的にダウンロードされる場合、CLASSPATH 内のスタブクラスを保持する必要があるのは、サーバ VM だけです。この方法により、アプリケーション配備はよりシンプルになり、また稼動中の分散型システムへの新バージョンのスタブ導入が可能になります。
RMI でのコードの動的なダウンロードについては、チュートリアルの「RMI の使用による動的なコードのダウンロード (java.rmi.server.codebase
プロパティを使用)」を参照してください。
java -Djava.rmi.server.logCalls=true YourServerImplここで
YourServerImpl
はサーバ名です。
サーバがハングしたら、SolarisTM オペレーティング環境 (Solaris OE) では ctrl-¥、Microsoft Windows プラットフォームでは ctrl-break キーを押すことでモニタダンプとスレッドダンプが得られます。
java.rmi.
" で始まるプロパティは、公開された仕様の一部であり、Java RMI 仕様にドキュメント化されています。
"sun.rmi.
" で始まるプロパティは、Sun Microsystems が提供する JDKTM ソフトウェアまたは JavaTM 2 Platform, Standard Edition (J2SETM) の特定バージョンでのみサポートされています。これらの "sun.rmi.*
" プロパティは、実行時のデバッグおよびチューニングに便利ですが、public API の一部とは見なされていないこと、および将来の実装でその使用法が変更される (または完全に削除される) 可能性があることに注意してください。
RMI クライアントがリモート RMI サーバにコンタクトするためには、クライアントはまず最初にサーバへの参照を取得する必要があります。
クライアントが最初にリモートサーバへの参照を取得するもっとも一般的な方法は、Naming.lookup
メソッド呼び出しによる方法です。
リモート参照は他の方法でも取得することができます。たとえば、すべてのリモートメソッド呼び出しではリモート参照が返されます。
Naming.lookup
メソッドは、よく知られたスタブを使って rmiregistry
へのリモートメソッド呼び出しを行い、rmiregistry
は lookup
メソッドが要求したオブジェクトへのリモート参照を返します。
すべてのリモート参照にはサーバのホスト名とポート番号が含まれ、クライアントはそれを使って特定のリモートオブジェクトのサーバである VM の位置を知ることができます。 リモート参照の取得後に、RMI クライアントはその参照から得られるホスト名とポート番号を使ってリモートサーバへのソケット接続を開くことができます。
RMI では、「クライアント」と「サーバ」という語が同一のプログラムを指す場合があることに留意してください。 RMI サーバとして機能する Java プログラムには、エクスポートされたリモートオブジェクトが含まれます。 RMI クライアントは、別の仮想マシン内のリモートオブジェクト上の 1 つまたは複数のメソッドを呼び出すプログラムです。 1 つの VM が両方の機能を実行する場合、この VM は RMI クライアントと RMI サーバの両方となります。
java.net.UnknownHostException
を発行して失敗します。なぜですか。
UnknownHostException
を発行します。
実際に機能するリモート参照を作成するには、RMI サーバはすべての RMI クライアントが解釈可能な完全指定のホスト名または IP アドレスを提供できなければなりません (完全指定ホスト名の例: foo.bar.com)。
ある RMI プログラムがリモートコールバックを行う場合、このプログラムは RMI オブジェクトとして働きます。そのため、RMI クライアントに渡すリモート参照中のサーバホスト名として使用する、解釈可能なホスト名を決定できなければなりません。
リモートオブジェクトとして機能するアプレットを呼び出す VM は、アプレットが有効なサーバホスト名を提供できなかった場合には UnknownHostException
を発行することがあります。
RMI アプリケーションが UnknownHostException
を発行した場合は、スタックトレースの結果を調べて、クライアントがリモートサーバへのコンタクトに使用しているホスト名が正しいか、完全指定されているかどうかを確認してください。
必要であれば、サーバの java.rmi.server.hostname
プロパティを設定してサーバマシンの IP アドレスかホスト名を修正すると、RMI はこのプロパティの値を使ってサーバへのリモート参照を生成します。
UnknownHostException
が発行されます。なぜですか。
java.rmi.server.hostname
プロパティを、RMI サーバマシンの正しい IP アドレスに設定します。
このプロパティを設定して、サーバがネームサービスから取得した完全指定のホスト名を使用するように指定することもできます。
java.rmi.server.useLocalHostname=true
java.net.InetAddress.getLocalHost()
に依存して完全指定のドメイン名を返していました
InetAddress
オブジェクトは、コードの static ブロックでローカルホスト名を初期化して、ローカル IP アドレスを逆ルックアップしてローカルホスト名を取得しました。
しかし、ネットワークに接続されていないマシンでは、InetAddress
が探しているホスト名が見つからないために、この動作によりハングアップが起こりました。
InetAddress
を修正して、ネームサービスに問い合わせをしないネイティブのシステムコールから返される「修飾されていない可能性がある」ホスト名だけを取得するようにしました。
RMI はこの変更を補うように修正されませんでした。その理由は、ユーザが java.rmi.server.hostname
プロパティを設定することにより、InetAddress
から提供される正しくないホスト名を無視することができるからです。
RMI はネームサービスへの問い合わせをせず、デフォルトで非修飾ホスト名を使用することができます。
InetAddress
の機能上の変更により生じた多くの問題点を補うため、最近の SDK のバージョンには、次の動作が組み込まれています。
RMI はあるリモートオブジェクトのサーバのマシンを識別するために、IP アドレスまたは完全指定のドメイン名を使います。 サーバのホスト名は、次の動作によって取得した値に初期化されます。
java.rmi.server.hostname
プロパティが設定されている場合は、RMI はそのプロパティの値をサーバのホスト名として使用し、他の方法で完全指定のドメイン名を探すことはありません。
このプロパティは、RMI サーバ名を探す他のすべての方法に優先します。
java.rmi.server.useLocalHostname
プロパティが true
(デフォルトでは、このプロパティの値は false
) に設定されている場合、RMI は次の手順で RMI サーバのホスト名を取得します。
InetAddress.getLocalHost().getHostName()
メソッドから返された値に「.」文字が含まれている場合は、RMI はこの値をサーバの完全指定ドメイン名とみなし、サーバホスト名として使用します。
InetAddress.getLocalHost().getHostAddress()
により取得したサーバの IP アドレスを使用します。
sun.rmi.transport.tcp.localHostnameTimeOut
=timeOutMillis java -Dsun.rmi.transport.tcp.localHostnameTimeOut=2000 MyServerApp
java.rmi.server.useLocalHostname
プロパティを true
に設定することを推奨します。
一般的に、ホスト名は IP アドレスよりも永続性があります。
起動可能なリモートオブジェクトは、一時リモートオブジェクトよりも長く存在する傾向にあります (たとえば、リブート後も存在する)。
RMI クライアントは、明示的な IP アドレスを使うよりも修飾されたホスト名を使う方が、長期に渡ってリモートオブジェクトの場所を特定しやすくなります。
Naming.bind
と Naming.lookup
に非常に時間がかかるのはなぜですか。
java.net.InetAddress
を使って TCP/IP ホスト名をルックアップします (InetAddress
クラスは、セキュリティ上の理由から、ホストからアドレスへのマッピングとアドレスからホスト名へのマッピングの両方を行う)。
Microsoft Windows プラットフォームでは、ルックアップ機能はネイティブのソケットライブラリによって行われるので、遅れは RMI ではなくライブラリの中で起こります。
ホストが DNS を使用するよう設定されていて、DNS サーバが通信に関係するホストについての情報を持たず、DNS ルックアップタイムアウトが起こっている場合は問題です。
関連するすべてのホストのホスト名またはアドレスをローカルファイル ¥winnt¥system32¥drivers¥etc¥hosts
または ¥windows¥hosts
に書き込んでください。
ホストファイルの書式は一般的に、次のようなものです。 IPAddress Machine Nameたとえば:
208.2.84.61 homerこの処置により、最初のルックアップに要する時間を大幅に短縮できます。
192.168.1.1
) を設定します。
すると、これを DOS シェルから検出して ping を実行することができます (ping mymachine)。
これで、RMI のサンプルプログラムを実行できます。
java.net.SocketException: Address already in use
] が発行されるのですが、なぜですか。
RegistryImpl
が使用するポート (デフォルトは 1099) がすでに使用されていることを意味します。
マシン上に実行中の別のレジストリがあると思われるので、それを停止させてください。
RMI が目的のサーバへの通常の (SOCKS) 接続の作成に失敗し、HTTP プロキシサーバが設定されていると通知した場合は、そのプロキシサーバを通じて RMI 要求を 1 つずつトンネリングしようとします。
HTTP トンネリングには 2 つの形式があり、順番に試みられます。 1 つ目の形式は、http-to-port で、2 つ目は http-to-cgi です。
http-to-port トンネリングでは、RMI は目的のサーバの正確なホスト名とポート番号を指す http:
URL への HTTP POST 要求を試みます。
HTTP 要求には 1 つの RMI 要求が含まれます。
HTTP プロキシはこの URL を受け付けると、待機中の RMI サーバにこの POST 要求を転送します。RMI サーバは要求を認識してラップを解除します。
呼び出しの結果は HTTP 応答にラップされ、同じプロキシ経由で返送されます。
HTTP プロキシは、異常なポート番号に対する要求を拒否することがよくあります。
この場合、RMI は http-to-cgi トンネリングを実行しようとします。RMI 要求は前回同様 HTTP POST 要求内にカプセル化されますが、要求 URL の形式は http://hostname:80/cgi-bin/java-rmi.cgi?port=n
(hostname と n は目的のサーバのホスト名とポート番号) になります。
サーバホストのポート 80 で待機している HTTP サーバが必要で、これは java-rmi.cgi
スクリプト (SDK で提供) を実行します。次に、このスクリプトはポート n で待機中の RMI サーバに要求を転送します。
RMI は HTTP トンネリングされた要求を、HTTP サーバや CGI スクリプトなどの外部からの助けなしにラップ解除できます。
そのため、クライアントの HTTP プロキシがサーバのポートに直接接続できる場合は、java-rmi.cgi
スクリプトはまったく必要ありません。
HTTP トンネリングの使用をトリガするためには、標準システムプロパティ http.proxyHost
をローカル HTTP プロキシのホスト名に設定する必要があります。
報告によると、Navigator の一部のバージョンではこのプロパティが設定されません。
HTTP トンネリングの最大の短所は、内向きの呼び出しや多重接続を許可しない点です。 第 2 の短所は、http-to-cgi 方式ではサーバ側に深刻なセキュリティホールができてしまうという点です。その理由は、この方式では、修正しない限り、どんなポートへの内向きの要求もすべてリダイレクトされてしまうからです。
socksProxyHost
は、SOCKS サーバのホスト名に設定されている必要があります。SOCKS サーバのポート番号が 1080 でない場合は、socksProxyPort
プロパティで設定しなければなりません。
この方法が、最も一般的に使用できる方法と考えられます。現在のところ、ServerSockets
は SOCKS を使用しないので、内向きの呼び出しには別のメカニズムを使う必要があります。
この方法の短所は、ファイアウォールの通過を RMI サーバ側から提供されたコードを使って行わなければならない点です。RMI サーバ側では正しい通過方法が分かっているとは限らず、またファイアウォールを通過するための十分な特権を自動的に保持するとは限りません。
exportObject
メソッドには正確なポート番号を指定するための特別な引数があります。
JDK v1.1 では、サーバは RMISocketFactory
をサブクラス化して、createServerSocket(0)
への要求を横取りし、特定のポート番号に結びつける要求と置き換えます。
この方法の短所は、ローカルファイアウォールの責任者であるネットワーク管理者の助けが必要なことです。
エクスポートされたオブジェクトが別の場所で実行される (コードがそのサイトにダウンロードされたため) 場合、ローカルファイアウォールを管理するネットワーク管理者には、実行しようとしているユーザが誰かわかりません。
ここでの考え方は、「ファイアウォールの外側からそのオブジェクトのリモートメソッドを呼び出そうとする者は誰でも、かわりに (おそらく別のマシンの) 別のポートにコンタクトする」という方法でオブジェクトをエクスポートするということです。 この別のポートでは、実際のサーバへの二次的な接続を作成して両方向にバイトデータを送り出すプログラムが稼動しています。
この方法で難しいのは、ブリッジに接続することをクライアントに納得させることです。
ダウンロード可能なソケットファクトリ (Java 2 SDK, v1.2 以降) を使うと効率的にこれを行うことができます。ソケットファクトリを使わない場合は、java.rmi.server.hostname
プロパティを設定することにより、ブリッジホストに名前を付けてポート番号を同じにできます。
外部からプロキシに呼び出しがあると、プロキシは直ちにその呼び出しを内部サーバ上の元のオブジェクトに転送します。 プロキシの使い方は外部から見えますが、通信時に元の参照またはプロキシ参照のどちらを渡すかを決定する内部サーバからは見えません。
言うまでもなく、これには大量の設定とローカルネットワーク管理者間の協力が必要です。
最悪の場合は、クライアント側のファイアウォールが直接の TCP 接続を一切許可せず、そのファイアウォール内のクライアントが「Web サーフィン」するための HTTP プロキシサーバだけを持っているケースです。
この場合、サーバホストは、HTTP 要求に埋め込まれた RMI 要求を含むポート 80 での接続を受け取ります。
HTTP サーバで java-rmi.cgi
プログラムを使用するか、RMI サーバをポート 80 で直接実行できます。
どちらの方法でも、サーバはクライアントがエクスポートしたコールバックオブジェクトを使用することはできません。
それより良いケースに、クライアントがサーバへの直接接続を作成できるがサーバからの接続は受け取れないという場合があります。 この場合も、コールバックオブジェクトは普通には使用できません。
クライアントファイアウォールの管理者から協力を得られない場合、最も保守的な方法は、次のとおりです。
java-rmi.cgi
スクリプトを使ってポート 80 に置く
DeleGate
など) をポート 80 に置く。これは、接続を受け付けるとすぐに実際のサーバポートに接続し、バイトデータの受け渡しを行います。これは getClientHost()
が誤った情報を返す原因になるので、別のホスト上にない限り、このメソッドを使ってレジストリを利用可能にしないでください。
java-rmi.cgi
スクリプトをサーブレットを使って置換することは可能ですか。 java-rmi.cgi
スクリプトを実装する方法を示した例を参照してください。この例では、サーブレット VM でリモートオブジェクトを実行する方法についても説明しています。
注: HTTP を介してリモートメソッド呼び出しのトンネリングを行うときの、java-rmi.cgi
の動作については、RMI 内の HTTP トンネリングに関する FAQ を参照してください。
java.rmi.server.Unreferenced
インタフェースを (その他の必要なインタフェースに加えて) 実装する必要があります。
RMI はすべての接続が切り離されたとき、unreferenced
メソッドを呼び出して通知することができます。
unreferenced
メソッドの実装により、リモートオブジェクトが通知を受け取る方法が決められます。
ただし、レジストリに参照がある場合は、Unreferenced.unreferenced
メソッドは呼び出されません。
OutOfMemoryError
を発行して) 失敗します。
Java API ではコレクションの時期を指定しませんが、JDK v1.1 実装でリモートオブジェクトのコレクションが無期限に遅れるように見えるのには特別な理由があります。 RMI ランタイムは内部的に、エクスポートされたリモートオブジェクトへの「弱参照」をテーブルに保持しています (オブジェクトへのローカルとリモートの参照を追跡するため)。 JDK v1.1 で利用可能な弱参照の機構では、VM は攻撃的でないキャッシングコレクションポリシー (ブラウザに最適) を使います。そのため、「弱く参照されている」オブジェクトは、ローカル GC が次のメモリ割り当てで、そのメモリが本当に必要だと判断するまではコレクションされません。 これはアイドル状態のサーバには決して起こりませんが、 メモリが必要な場合には参照されていないサーバオブジェクトがコレクションされます。
Java 2 プラットフォームには RMI が使用する新しいインフラストラクチャが含まれ、この問題が発生する状況を大幅に減らすことができます。
System.exit
を使うのは賢明でしょうか。
System.exit()
が呼び出されると RMI ランタイムはサーバに適切な「参照なし」のメッセージを送ることができないので、これを呼び出したクライアントは異常終了とみなされます。
終了の前にクライアントで System.runFinalizersOnExit
を実行するだけでは不十分です。その理由は、ファイナライザでは必要な処理がすべて行われるわけではなく、「参照なし」のメッセージがサーバに送られないからです
(runFinalizersOnExit の使用は一般的に推奨できず、デッドロックを起こしやすい)。
System.exit()
を使ってクライアント VM を終了する必要がある場合は、VM 内に保持されているリモート参照がより早く確実に消去されるように、アクセス可能なリモート参照がすでに存在しないようにする必要があります。そのためには、実行中のスレッドからアクセスできなくするローカルリファレンスをすべて明示的に null にします。完全なガベージコレクションを実行し、ファイナライザを実行してから終了するという方法もあります。
System.gc(); System.runFinalization();
unreferenced()
メソッドが呼び出されます (レジストリはすべてのバインディングへの参照を保持しているため、レジストリもまた、この目的ではクライアントとなる)。
クライアントがリモート参照を保持している場合は、その参照のリースも保持しており、それを更新する (サーバにコンタクトして dirty()
呼び出しを行うことにより) 必要があります。
エクスポートされたオブジェクトに対する最後のリースが期限切れになるか閉じられるかすると、そのオブジェクトは参照されていないとみなされ、java.rmi.Unreferenced
を実装している場合は unreferenced()
メソッドが呼び出されます。
複数のクライアントが同一のリモートオブジェクトへの参照を持っている場合は、そのオブジェクトに対するすべてのクライアントのリースが期限切れにならない限り、unreferenced()
メソッドは呼び出されません。
したがって、この手法を使って個々のクライアントを追跡する場合は、個々のクライアントが Unreferenced
オブジェクトに対する参照を保持している必要があります。
unreferenced()
メソッドが呼び出されるまで 10 分かかります。この時間を短縮する方法を教えてください。
java.rmi.dgc.leaseValue
を使ってミリ単位で指定されます。
この時間を短くするには (例: 30 秒)、次のようにしてサーバを起動します。 java -Djava.rmi.dgc.leaseValue=30000 ServerMain
デフォルト値は 600000 ミリ秒 (10 分) です。
クライアントは期限の半分を過ぎると各自のリースを更新します。 リース期間が短すぎると、クライアントは無駄なリース更新のためにネットワーク帯域幅を浪費することになります。 リース期間が極端に短い場合にはクライアントのリース更新が間に合わなくなり、結果としてエクスポートされたオブジェクトが削除されることもあります。
RMI の将来のリリースでは、リースの更新が失敗するとリモート参照が無効になります (参照の完全性を維持するため)。失効したリモートオブジェクトへの参照の使用をあてにすべきではありません。
クライアントマシンがクラッシュした場合は、単にタイムアウトを待つだけでよいのです。
接続が切れたときにクライアントがまだ何らかの制御を維持している場合は、クライアントはすばやく DGC clean 呼び出しを行い、Unreferenced
をタイムリーに使用できます。
この処理をうまく進めるには、クライアントがリモートオブジェクトに対して保持している可能性のある参照をすべて null にしてから、System.gc()
を呼び出します
(v1.1.x では、ファイナライザを同期して実行させてから、もう一度 GC を実行することが必要)。
クラッシュしたクライアントがあとで再起動してサーバにコンタクトすれば、その時にサーバはクライアントがそれまでの間にクラッシュしたかどうかを推察することができます。 クライアントとサーバが対話している間、両者の間で TCP 接続がずっと開いているなら、あとでその接続への書き込み (1 時間ごとの TCP 維持パケットが有効な場合は、それを含む) が失敗したときに、サーバはクライアントが再起動したことを検出できます。 しかし、そのような永続的な接続はスケーラビリティを損なう上にあまり役に立たないので、RMI は永続的な接続を必要としないように設計されています。
ネットワークピアがいつクラッシュしたり利用できなくなったかを簡単に判断することは、まったく不可能なので、ピアが応答しなくなったときのアプリケーションの動作を決めておく必要があります。
この作業に使う主な手段はタイムアウトとリセットです。 タイムアウトが起きた場合、ピアが通信不可能になったと判断してもかまいません。しかし、そのピアがこちらへ通信しようとするのをやめるように、タイムアウトしたことがピアに分かる必要があります。リースのメカニズムは、これを半自動的に行うためのものです。
リセットとは、ピアのために保持されている状態を一掃することです。 たとえば、クライアントはサーバに最初に登録したときにリセットを行なって、サーバがそのクライアントのためにそれまで保持していた状態を破棄するようにします (クライアントがそれ以前の「死んだ」セッションの記憶を持たずに再起動したと考えて)。
多くの場合、目的は、サーバでクライアントの明確なリストを持ち、誤りや失敗なしにそのリストを最新の状態に維持することです。 ネットワークシステムでは故障や遅延はいつでも起こる可能性があるので、リストにはある程度の誤りがあることを予期する必要があります。 リースなどのメカニズムを使ってタイムアウトを強制すれば、リソースの漏れの問題は解決します。 失効したデータの問題がもっと深刻で、正常な動作を妨害するような場合があります。問題を取り除かなければ悪影響がある場合には、明示的に取り除く必要があります。
たとえば、ユーザが編集するため、あるビジネスオブジェクトがロックされているときにセッションが異常終了した場合には、何らかの方法でロックを解除する必要があります。 この場合、ロックにはタイムアウトが必要かもしれません。しかし、すぐに同一ユーザがログインし、そのユーザはタイムアウトまで待つ必要はないと思っているなら、新しいセッションではロックを引き継ぐか、ユーザがロックを保持していないと断定する (サーバが安全にロックを解除できるようにする) 必要があります。
rmic
コマンドを実行させるにはどうすればよいでしょうか。
call
コマンドを挿入する必要があります。
例を示します。 call rmic ClientHandler call rmic Server call rmic ServerHandler call rmic Client
java.rmi.RemoteServer.getClientHost
メソッドが、現在のスレッド上の現在の呼び出し元のクライアントホスト名を返します。
そのため、リモートオブジェクト参照をサーバからクライアントへ渡してから、サーバに送り返し、元の実装クラスにキャストバックすることはできません。 ただし、サーバ上のリモートオブジェクト参照を使ってそのオブジェクトへのリモート呼び出しを行うことはできます。
もう一度実装クラスを見つける必要がある場合は、リモート参照を実装クラスにマッピングするテーブルを保持する必要があります。
java.util.Observable
と java.util.Observer
を新しいインタフェースで「ラップ」することができます (それぞれを RemoteObservable
と RemoteObserver
と呼ぶ)。
これらの新しいインタフェースで、各メソッドが java.rmi.RemoteException
をスローするようにします。
次に、リモートオブジェクトでこれらのインタフェースを実装します。
リモートでないラップされたオブジェクトは java.rmi.server.UnicastRemoteObject
を継承していないので、そのオブジェクトを UnicastRemoteObject
の exportObject
メソッドを使って明示的にエクスポートする必要があります。
ただし、これを行うと、equals
、hashCode
、toString
の各メソッドの java.rmi.server.RemoteObject
実装を失います。
rmiregistry
への接続が作成されます。
一般に、リモート呼び出しのための新しい接続は、作成される場合と作成されない場合があります。
接続は、将来の利用に備えて、RMI トランスポートによってキャッシュされます。そのため、あるリモート呼び出しの正しい呼び出し先への接続が空いているときは、その接続が使用されます。
接続は RMI トランスポートのレベルで管理されているので、クライアントがサーバへの接続を明示的に閉じることはできません。
接続は、一定期間使われないとタイムアウトになります。
select()
呼び出しでブロック化ではなくポーリングしているようなのですが、レジストリはポーリングで実装されているのですか。
LocateRegistry.getRegistry(String host)
メソッドは、ホスト上のレジストリに直接アクセスするのではなく、レジストリが存在するかどうかを確認するためにホストをルックアップするだけです。
したがって、このメソッドが正常に終了しても、必ずしも指定されたホストでレジストリが実行中であるとは限りません。
その時点でレジストリにアクセス可能なスタブが返されるだけです。
RMI とオブジェクト直列化のどちらのユーザも、メーリングリスト rmi-users@java.sun.com
を通じて他のユーザと意見の交換ができます。
メーリングリストの購読を申し込むには、次のように書いたメールを listserv@java.sun.com にお送りください。
subscribe RMI-USERS購読を中止するには、次のように書いたメールをお送りください。
unsubscribe RMI-USERS
ObjectOutputStream
に書き込むために、クラスが Serializable
を実装しなければならないのはなぜですか。java.io.Serializable
インタフェースを実装すること」という条件は安易に決定されたわけではありません。予測可能で安全な機構を提供するため、設計には開発者からの要求とシステムの要求のバランスをとることが求められました。設計上のもっとも困難な制約は、Java プログラミング言語のクラスの安全性とセキュリティでした。
「クラスが直列化可能と明示されなければならない」という設計にすると、開発者が (その条件を忘れたり、無視するなどの理由で) クラスを Serializable
と宣言しなかった場合には、そのクラスが RMI で使用できなくなったり持続性を持たせるという目的に使用できなくなる恐れがあります。この条件によって、開発者に「あるクラスが将来、他人によってどのように使われるか」という本質的に予知不可能なことを考えなければならないという負担をかける恐れもあります。実際、予備設計では アルファ API に反映されているように、「デフォルトでは、クラス内のオブジェクトは直列化可能とする」と結論しました。その設計を変更したのは、セキュリティと正確さを考慮した結果、デフォルトではオブジェクトは直列化可能であってはならないと確信したからです。
オブジェクトを直列化可能にすると、このような制限を与えることはできなくなります。オブジェクトの直列化の結果であるバイトストリームは、そのストリームへアクセスできるどんなオブジェクトによっても読み出しと修正が可能です。これにより、直列化されたオブジェクトの状態にはどんなオブジェクトからもアクセスすることができるので、Java のユーザが期待する機密性の保証が破られます。さらに、ストリーム内のバイトを任意に変更できるので、Java プラットフォームによる保護下では不可能だったオブジェクトの再構築が可能になります。このようなオブジェクト再構成により、Java プラットフォームのユーザが期待する機密性保証だけでなく、プラットフォームそのものの完全性が脅かされる場合があります。
このような破壊行為を防ぐことはできません。その理由は、直列化の概念そのものが、オブジェクトを Java プラットフォームの外部 (つまり、Java 環境の機密性と安全性の保証の圏外) に持ち出すことのできる形に変換し、そのあとで Java プラットフォームに戻すことを可能にするためのものだからです。オブジェクトを直列化可能に宣言するという条件は、「クラスの設計者は、機密面や安全面でのそのような侵害の可能性を許容するという積極的な決断を行う必要がある」ことを意味します。直列化に関する知識のない開発者が、知識の欠如のせいで無防備になってはなりません。また、クラスを Serializable と宣言する場合は、その宣言の結果をよく考慮することが望ましいのです。
この種のセキュリティ問題は、セキュリティマネージャの機構によって解決できる問題ではありません。直列化の目的は、仮想マシン間のオブジェクトの移送 (RMI の場合のような空間上の移送、または、ファイルにストリームを保存する場合のような時間上の移送) を可能にすることです。したがって、セキュリティに使用される機構は特定の仮想マシンの実行時環境とは無関係であることが必要です。私たちは、可能な限り「ある仮想マシンでオブジェクトを直列化でき、別の仮想マシンではそのオブジェクトを直列化解除できない」という問題を避けようとしました。セキュリティマネージャは実行時環境の一部なので、もし直列化にセキュリティマネージャを使用すれば、この要求を達成できなかったでしょう。
実例は数多くあります。多くのクラスは、特定のオブジェクトが存在する実行時環境でだけ意味を持つ情報 (ファイルハンドル、オープンソケット接続、セキュリティ情報など) を処理します。このようなデータは、フィールドを transient
と宣言するだけで簡単に取り扱うことができますが、このような宣言が必要なのはオブジェクトが直列化される予定のときだけです。不慣れなプログラマの場合は、フィールドを transient
と宣言するのを怠るかも知れず、同様にクラスが Serializable
インタフェースを実装していることを宣言するのを怠るかもしれません。こうしたケースが不正な動作につながらないようにする必要があります。そのための方法が、Serializable
インタフェースの実装が宣言されていないオブジェクトは、直列化しないことなのです。
別の例として、多数のオブジェクトにまたがるグラフのルートとなっている「単純な」オブジェクトのことを考えてみましょう。直列化はグラフ全体に機能するので、このオブジェクトを直列化すると、他の多くのオブジェクトを直列化することになります。このような直列化は意識的に決定すべきことであり、デフォルトの動作で引き起こされるべきことではありません。
基本 Java API クラスライブラリの検討作業中に、私たちはこの問題を考慮する必要性を痛感しました。当初はライブラリのシステムクラスが適宜、直列化可能と宣言されているという設計でした。私たちは当初、この設計を実現するのは簡単だと考えました。「ほとんどのシステムクラスに Serializable
インタフェースの実装を宣言できるのだから、これをデフォルトの実装にすれば他に何も変更しなくてもそのまま使用できるだろう」と考えたのです。しかし、ほとんどの場合、予想通りにはなりませんでした。非常に多くのクラスで、フィールドを transient
と宣言すべきかどうか、またそもそもクラスを直列化可能にすることに意味があるのかどうかについて慎重に考える必要がありました。
もちろん、プログラマやクラスの設計者がクラスを直列化可能と宣言するときに、実際にこの問題について考えるという保証はありません。しかし、クラスに Serializable
インタフェースの実装を宣言することを要求することにより、プログラマにはなんらかの考慮を払うことが求められます。直列化をオブジェクトのデフォルト状態とすると、考慮の不足によって、プログラムになんらかの悪影響 (Java プラットフォーム全体の設計で防ごうとしたもの) を及ぼす可能性があります。
javadoc
ツールによって生成される API ドキュメントから入手できます。
対処法として、まずトップレベルのウィジェットをコンテナから削除してください (これで、ウィジェットは「アクティブ」ではなくなる)。この時点でピアは破棄され、AWT ウィジェットの状態だけを保存できます。あとでウィジェットを直列化解除して読み出すとき、フレームにトップレベルのウィジェットを追加して、AWT ウィジェットを表示させます。show
呼び出しを追加することが必要な場合があります。
JDK v1.1 以降の AWT ウィジェットは直列化可能です。java.awt.Component
クラスは Serializable
インタフェースを実装します。
RMI で直列化を使用する場合は、暗号化と暗号解読を下位のネットワークトランスポートに委ねます。安全なチャネルが必要な場合は、ネットワーク接続には SSL のようなものを使用することをお勧めします (「RMI での SSL の使用」を参照)。
ByteArrayInputStream
および ByteArrayOutputStream
オブジェクトを仲介する方法が利用できます。ByteArrayInputStream
および ByteArrayOutputStream
オブジェクトを使用してランダムアクセスファイルの読み書きを行い、バイトストリームから ObjectInputStream
および ObjectOutputStream
を作成してオブジェクトの転送を行います。バイトストリームにオブジェクト全体が入るように注意してください。そうしないと、オブジェクトへの読み書きに失敗します。
たとえば、java.io.ByteArrayOutputStream
を使って ObjectOutputStream
のバイトを受け取ります。バイト配列の形式で結果を 1 つ受け取ります。次に、このバイト配列を ByteArrayInputStream
で ObjectInput
ストリームへの入力として使用します。
ObjectOutputStream
には渡されません。ただし、そのオブジェクトのクラスがまだローカルで利用可能になっていない場合は、そのクラスを受信側でロードする必要があります。クラスファイルそのものではなく、クラス名だけが直列化されます。直列化を解除するときには、すべてのクラスは通常のクラスロード機構を使ってロード可能でなければなりません。アプレットの場合は AppletClassLoader
でロードされます。
ローカルオブジェクトがリモート VM に渡されるときは、オブジェクトの内容がコピーされて渡される (真の値渡し) ので、一貫性は保証されません。
ObjectOutputStream
から ObjectInputStream
を作成するには、どうすればよいでしょうか。ObjectOutputStream
と ObjectInputStream
は、どんなストリームオブジェクトに対しても機能します。ByteArrayOutputStream
を使用し、配列を取得して ByteArrayInputStream
に挿入することもできます。同様に piped stream クラスも使用できます。OutputStream
クラスと InputStream
クラスを継承するすべての java.io
クラスを使用できます。
writeObject
メソッドを使ってネット上を送信し、readObject
メソッドを使って受信します。次に、オブジェクトのフィールドの値を変更してから同様に送信すると、readObject
メソッドから返されるオブジェクトは最初のものと同じで、フィールドの新しい値が反映されていないようです。これは正しい動作なのですか。ObjectOutputStream
クラスは直列化した各オブジェクトを追跡し、それ以降そのオブジェクトがストリームに書き込まれる時は、ハンドルだけを送ります。これは、このクラスがオブジェクトのグラフを扱う方法です。対応する ObjectInputStream
は、生成したすべてのオブジェクトとハンドルを追跡し、もう一度ハンドルが参照されたときに、同じオブジェクトを返せるようにします。出力ストリームと入力ストリームは、どちらも開放されるまでこの状態を維持します。
また、ObjectOutputStream
クラスは reset
メソッドを実装しています。このメソッドを使うとオブジェクトを送信したという記録が破棄されるので、オブジェクトをもう一度送るとコピーが作成されます。
スレッドの直列化が難しいのは、スレッドは仮想マシンに複雑に結び付いた多くの状態を持っているために、別の場所にコンテキストを再確立することが困難または不可能だからです。たとえば、VM 呼び出しスタックを保存するだけでは不十分です。その理由は、多くのネイティブメソッドが C のプロシージャを呼び出し、そのプロシージャが Java プラットフォームのコードを呼び出している場合、Java プログラミング言語の構造と C のポインタが複雑に混合したものに対処する必要があるからです。また、スタックを直列化することは、任意のスタック変数からアクセス可能なすべてのオブジェクトを直列化することを意味します。
同じ VM 内でスレッドが再開されると、そのスレッドは多くの状態を元のスレッドと共有します。両方のスレッドが同時に実行されると、ちょうど 2 つの C のスレッドがスタックを共有しようとした場合のように、予測不可能な動作を起こします。また、別の VM で直列化解除した場合の結果は予想できません。
ObjectOutputStream
を作成する必要があります。
ObjectOutputStream
は OutputStream
を生成します。zip オブジェクトが OutputStream
クラスを継承していれば、圧縮しても問題ありません。
オブジェクトのツリーの直列化の例を示します。
import java.io.*; class tree implements java.io.Serializable { public tree left; public tree right; public int id; public int level; private static int count = 0; public tree(int depth) { id = count++; level = depth; if (depth > 0) { left = new tree(depth-1); right = new tree(depth-1); } } public void print(int levels) { for (int i = 0; i < level; i++) System.out.print(" "); System.out.println("node " + id); if (level <= levels && left != null) left.print(levels); if (level <= levels && right != null) right.print(levels); } public static void main(String argv[]) { try { /* Create a file to write the serialized tree to. */ FileOutputStream ostream = new FileOutputStream("tree.tmp"); /* Create the output stream */ ObjectOutputStream p = new ObjectOutputStream(ostream); /* Create a tree with three levels. */ tree base = new tree(3); p.writeObject(base); // Write the tree to the stream. p.flush(); ostream.close(); // close the file. /* Open the file and set to read objects from it. */ FileInputStream istream = new FileInputStream("tree.tmp"); ObjectInputStream q = new ObjectInputStream(istream); /* Read a tree object, and all the subtrees */ tree new_tree = (tree)q.readObject(); new_tree.print(3); // Print out the top 3 levels of the tree } catch (Exception ex) { ex.printStackTrace(); } } }
Serializable
を実装せず、そのサブクラス B が Serializable
を実装している場合、クラス B を直列化したとき、クラス A のフィールドは直列化されますか。Serializable
オブジェクトのフィールドだけが書き出されて復元されます。オブジェクトは、クラス A が引数のないコンストラクタを持っている場合にだけ復元されます。このコンストラクタは直列化可能でないスーパータイプのフィールドを初期化します。サブクラスがスーパークラスの状態にアクセスする場合は、その状態の保存と復元用に writeObject
と readObject
を実装できます。
RMI 開発者のメーリングリスト: RMI-USERS
購読の申し込み (「subscribe rmi-users」と記したメールをお送り下さい)listserv@javasoft.com
|