FAQ
Java™ RMI とオブジェクト直列化


Java RMI

全般

Java RMI プログラムのデバッグ

ネットワーク機能

Java RMI の使用

RMI の作用

内部、リソース、パフォーマンス

その他

オブジェクト直列化

  1. ObjectOutputStream に書き込むために、クラスが Serializable を実装しなければならないのはなぜですか。
  2. JDK v1.1 システムクラスのうち、直列化可能になるものはどれですか。
  3. AWT コンポーネントを直列化復元できません。どうすればよいでしょうか。
  4. オブジェクト直列化では暗号化はサポートされますか。
  5. オブジェクトの直列化クラスはストリーム指向です。オブジェクトをランダムアクセスファイルに書き込むにはどうすればよいでしょうか。
  6. ローカルオブジェクトを直列化して Java RMI 呼び出しにパラメータとして渡すと、そのローカルオブジェクトのメソッドのバイトコードも渡されるのですか。リモート仮想マシン (VM) アプリケーションがオブジェクトハンドルを保持したままだと、オブジェクトの一貫性はどうなるのでしょうか。
  7. ファイルを間に介さないで ObjectOutputStream から ObjectInputStream を作成するには、どうすればよいでしょうか。
  8. オブジェクトを作成してから writeObject メソッドを使ってネット上を送信し、readObject メソッドを使って受信します。次に、オブジェクトのフィールドの値を変更してから同様に送信すると、readObject メソッドから返されるオブジェクトは最初のものと同じで、フィールドの新しい値が反映されていないようです。これは正しい動作なのですか。
  9. スレッドオブジェクトの直列化をサポートする予定はありますか。
  10. diff(serial(x),serial(y)) の計算はできますか。
  11. 自分の zip および unzip メソッドを使って、直列化表現のオブジェクトを圧縮できますか。
  12. isempty(zip(serial(x))) などの圧縮したバージョンのオブジェクトに対して、メソッドを実行することはできますか。
  13. フォントや画像のオブジェクトを直列化して別の VM で再構築しようとすると、アプリケーションが落ちます。なぜですか。
  14. オブジェクトツリーを直列化するにはどうすればよいでしょうか。
  15. クラス A が Serializable を実装せず、そのサブクラス B が Serializable を実装している場合、クラス B を直列化したとき、クラス A のフィールドは直列化されますか。

Java RMI

A.1 Naming.lookup を呼び出すと、予期しないホスト名やポート番号に対する例外が発行されます。なぜですか。

例外トレース中に示されるホスト名とポート番号は、ルックアップサーバーが待機している応答先のアドレスを表します。Java Remote Method Invocation (Java RMI) サーバーは理論的には任意のホストに配置できますが、通常はレジストリを実行しているホストの、別のポートを使います。

サーバーのホスト名や IP アドレスが誤っている (またはクライアントが解釈できないホスト名をサーバーが持っている) 場合でも、サーバーはその誤ったホスト名を使ってすべてのオブジェクトをエクスポートします。ただし、それらのオブジェクトを受け取ろうとするたびに例外が発生します。

レジストリの位置を示すために Naming.lookup で指定したホスト名は、サーバーへのリモート参照にすでに組み込まれているホスト名には効果がありません。

通常、不可解なホスト名はサーバーの修飾されていないホスト名、つまりクライアントのネームサービスに知らされていない非公開な名前です。Windows プラットフォームの場合、その名前はサーバーの「ネットワーク」->>識別情報>->「マシン名」で設定されています。

対策としては、サーバーの起動時にシステムプロパティー java.rmi.server.hostname を設定します。このプロパティーの値は、外部からアクセス可能なサーバーのホスト名 (または IP アドレス) でなければならず、これを Naming.lookup のホスト部分に指定したときの動作は成功します。

詳細については、コールバック完全指定のドメイン名に関する質問を参照してください。

A.2 クライアントの CLASSPATH_Stub ファイルをインストールする必要がありますか。ダウンロードできるのではないかと思いますが。

そのリモートオブジェクトをエクスポートしているサーバーが、整列化されたスタブインスタンスに、スタブクラスのロード元を示す java.rmi.server.codebase プロパティーを注釈として付けている場合は、スタブクラスをダウンロードできます。リモートオブジェクトをエクスポートするサーバー上の java.rmi.server.codebase プロパティーを設定する必要があります。リモートクライアントがこのプロパティーを設定できるようになると、指定されたコードベースだけからリモートオブジェクトを入手するように制限されます。すべてのクライアント VM が、オブジェクトの場所を示すコードベースを指定しているわけではありません。

リモートオブジェクトが Java RMI によって整列化されるとき (リモート呼び出しの引数として、または戻り値として)、スタブクラスのコードベースが Java RMI によって取得され、直列化されたスタブの注釈付けに使用されます。スタブが整列化解除されるときは、CLASSPATH またはアプレットコードベースなど、オブジェクトを受け取るためのコンテキストクラスローダーにすでにそのクラスが存在しないかぎりRMIClassLoader を使ってスタブのクラスファイルをロードするためにコードベースが使用されます。

_Stub クラスが RMIClassLoader によってロードされた場合は、Java RMI はすでに、注釈付けに使用するのはどのコードベースかを知っています。_Stub クラスが CLASSPATH からロードされた場合は明確なコードベースは存在しないので、Java RMI は java.rmi.server.codebase システムプロパティーを調べてコードベースを検索します。このシステムプロパティーが設定されていない場合は、スタブは null コードベースで整列化されます。つまり、クライアントがその CLASSPATH 内に _Stub クラスファイルに一致するコピーを持っていないかぎり、このコードベースは使用できません。

コードベースプロパティーを指定することは忘れがちです。この間違いを検出する方法の 1 つに、rmiregistry を個別に起動し、アプリケーションクラスにアクセスしないという方法があります。こうすると、コードベースが省略された場合は、必ず Naming.rebind が失敗します。

java.rmi.server.codebase プロパティーについての詳細は、チュートリアルの「Java RMI の使用による動的なコードのダウンロード (java.rmi.server.codebase プロパティーを使用)」を参照してください。

A.3 Java RMI では HTTP サーバーを使う必要がありますか。

いいえ。java.rmi.server.codebase プロパティーを、file または ftp など、任意の有効な URL プロトコルを使うように設定できます。HTTP サーバーを使っても、クラスファイルの自動ダウンロードメカニズムが提供されるだけです。

A.4 ClassNotFoundException が返されるのはなぜですか。

リモートオブジェクトをエクスポートしている VM 上で、java.rmi.server.codebase プロパティーが設定されていない (または正しく設定されていない) 可能性があります。チュートリアルの「Java RMI の使用による動的なコードのダウンロード (java.rmi.server.codebase プロパティーを使用)」を参照してください。

A.5 アプリケーションがカスタムソケットファクトリを使用するときに Java RMI 実装が多数のソケットを作成するのはなぜですか。また、同じリモートオブジェクトを参照する (カスタムソケットファクトリを使う) スタブが等しくないのはなぜですか。また、カスタムサーバーソケットファクトリを使用するときに Java RMI 実装がサーバー側のポートを再利用しないのはなぜですか。

Java RMI 実装は、リモート呼び出しで使用可能なオープンソケットを再利用しようとします。カスタムソケットファクトリを使うスタブ上のリモートメソッドが呼び出されると、そのソケットが同等のソケットファクトリによって作成されたものであるかぎり、Java RMI 実装は、開いた接続があればそれを再利用します。クライアントソケットファクトリはクライアントに対して直列化されるので、1 つのクライアントが同じ論理ソケットファクトリの別個のコピーを複数保有することがあります。Java RMI 実装がカスタムソケットファクトリによって作成されたソケットを再利用することを保証するためには、カスタムクライアントソケットファクトリクラスが hashCode メソッドおよび equals メソッドを適切に実装する必要があります。クライアントソケットファクトリがこれらのメソッドを正しく実装しない場合は、同じリモートオブジェクトを参照する (クライアントソケットファクトリを使う) スタブが等しくないという結果になります。

Java RMI 実装は、サーバー側のポートも再利用しようとします。ただし、同等のソケットファクトリによって作成されたポートの既存のサーバーソケットがある場合にかぎります。サーバーソケットファクトリクラスも hashCode メソッドおよび equals メソッドを実装する必要があります。

ソケットファクトリにインスタンス状態がない場合は、hashCode メソッドと equals メソッドを次のように実装します。

    public int hashCode() { return 57; }
    public boolean equals(Object o) { return this.getClass() == o.getClass() }

B.1 Java RMI に組み込みのデバッグメカニズムはありますか。

Java RMI にはデバッグ用に、簡単な呼び出しログ機能があります。しかし、現時点ではフル装備のインタラクティブなリモートデバッガについては計画していません。

B.2 Windows 95 でのデバッグに苦労しています。何か良い案はありますか。

javaw コマンドは、stdoutstderr に出力を行うので、デバッグ目的では java コマンドを別のウィンドウで実行してエラーの出力を表示できるようにする方が良いでしょう。これを行うには、次のようにコマンドを実行します。
        start java EchoImpl

開発時には、javaw コマンドの使用はお勧めしません。サーバーの活動を観察するには、-Djava.rmi.server.logCalls=true を指定してサーバーを起動します。

B.3 プログラムの実行中に java.lang.ClassMismatchError が返されます。なぜでしょうか。

プログラムの実行中に Java RMI が使用しているクラスを 1 つ以上修正したものと考えられます。すべての Java RMI アプリケーション (java.rmi.registry.RegistryImpl を含む) を再起動してみてください。これで問題がなくなります。

B.4 リモートオブジェクトの配列を送信すると、ArrayStoreException が発行されます。どうなっているのですか。

Java RMI はリモートオブジェクトをスタブに置き換えるので、配列はインタフェースと同じ型でなければなりません。コードは次のようになります。
   FooRemote[] f = new FooRemote[10];
   for (int i = 0; i < f.length; i++) {
      f[i] = new FooRemoteImpl();
   }

このようにすれば、Java RMI が配列の各セルにスタブを入れても、リモート呼び出しでの例外は発生しません。

B.5 同期する複数のローカルオブジェクトがあります。これらをリモートにすると、アプリケーションがハングアップします。何が問題なのですか。

これは、分散型デッドロックです。ローカル VM の場合、VM は呼び出し元のオブジェクト A がロックを所有していることを通知して、A へのコールバックの続行を許可します。分散環境では、このような決定を行うことはできないのでデッドロックが起こります。

分散オブジェクトはローカルオブジェクトとは動作が異なります。ロックと障害を処理することなく、ローカルの実装を再利用するだけでは、予測不可能な結果が生じる可能性があります。

B.6 リモートオブジェクトをレジストリに登録しようとすると、スタブクラスに ClassNotFoundException が発行されます。どうなっているのですか。

オブジェクトをバインドするためにレジストリへの呼び出しを行うと、レジストリは実際にはそのリモートオブジェクトのスタブへの参照をバインドします。スタブオブジェクトのインスタンスを生成するには、レジストリ VM からそのクラス定義がロード可能である必要があります。直列化形式のスタブをリモートメソッド呼び出しでレジストリに送信する VM (この場合はサーバー VM) は、そのクラスをどこからダウンロードできるかをスタブの注釈として付けます。スタブに適切な注釈が付けられていない場合、Java RMI はスタブのインスタンス生成を試みる際に、ClassNotFoundException をスローします。

クラスに適切な注釈を付けるために、サーバーは java.rmi.server.codebase プロパティーの値をスタブクラスの位置に設定する必要があります。Java 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 だけです。この方法により、アプリケーション配備はよりシンプルになり、また稼働中の分散型システムへの新バージョンのスタブ導入が可能になります。

Java RMI でのコードの動的なダウンロードについては、「Java RMI の使用による動的なコードのダウンロード (java.rmi.server.codebase プロパティーを使用)」を参照してください。

B.7 サーバーが落ちました。サーバーの動作のトレースをとることはできますか。

サーバーの活動のトレースをとるには、次のようにしてサーバーを起動します。
    java -Djava.rmi.server.logCalls=true YourServerImpl
ここで YourServerImpl はサーバー名です。サーバーがハングしたら、Solaris™ オペレーティングシステム (Solaris OS) では ctrl-\、Windows プラットフォームでは ctrl-break キーを押すことでモニターダンプとスレッドダンプが得られます。

B.8 Java RMI アプリケーションの実装およびデバッグに使用可能なシステムプロパティーのリストはどこにありますか。

java.rmi.で始まるプロパティーは、公開された仕様の一部であり、Java RMI 仕様にドキュメント化されています。

sun.rmi.で始まるプロパティーは、Sun Microsystems が提供する Java SE Development Kit (JDK) の特定のバージョンでのみサポートされています。これらのsun.rmi.*プロパティーは、実行時のデバッグおよびチューニングに便利ですが、public API の一部とは見なされていないこと、および将来の実装でその使用法が変更される (または完全に削除される) 可能性があることに注意してください。

C.1 Java RMI クライアントは、どのようにしてリモート Java RMI サーバーにコンタクトするのですか。

下に説明のある、Java RMI クライアントがリモート Java RMI サーバーにコンタクトする手段を図示する。

Java RMI クライアントがリモート Java RMI サーバーにコンタクトするためには、クライアントは最初にサーバーへの参照を取得する必要があります。クライアントが最初にリモートサーバーへの参照を取得するもっとも一般的なメカニズムは、Naming.lookup メソッド呼び出しによる方法です。リモート参照はほかの方法でも取得することができます。たとえば、すべてのリモートメソッド呼び出しではリモート参照が返されます。Naming.lookup メソッドは、よく知られたスタブを使って rmiregistry へのリモートメソッド呼び出しを行い、rmiregistry は lookup メソッドが要求したオブジェクトへのリモート参照を返します。

すべてのリモート参照にはサーバーのホスト名とポート番号が含まれ、クライアントはそれを使って特定のリモートオブジェクトのサーバーである VM の位置を知ることができます。リモート参照の取得後に、Java RMI クライアントはその参照から得られるホスト名とポート番号を使ってリモートサーバーへのソケット接続を開くことができます。

Java RMI では、クライアントサーバーという用語は、同一のプログラムを指す場合があることに留意してください。Java RMI サーバーとして機能する Java プログラムには、エクスポートされたリモートオブジェクトが含まれます。Java RMI クライアントは、別の仮想マシン内のリモートオブジェクト上の 1 つまたは複数のメソッドを呼び出すプログラムです。1 つの VM が両方の機能を実行する場合、この VM は RMI クライアントと Java RMI サーバーの両方となります。

C.2 リモートメソッドまたはコールバックルーチンが、ネストされた java.net.UnknownHostException を発行して失敗します。なぜですか。

多くの JDK のバージョン (v1.1 を除くすべてのバージョンの JDK と最新リリース) では、Java RMI はデフォルトで解釈不可能なサーバーホスト名 (非修飾名、Windows インターネットネームサービス (WINS) 名、修飾されていない DHCP 名など) を使用します。Java RMI クライアントが、解釈不可能なサーバーホスト名を含む参照を使ってリモートメソッドを呼び出すと、クライアントは UnknownHostException を発行します。

実際に機能するリモート参照を作成するには、Java RMI サーバーはすべての Java RMI クライアントが解釈可能な完全指定のホスト名または IP アドレスを提供できなければなりません (完全指定ホスト名の例: foo.bar.com). ある Java RMI プログラムがリモートコールバックを行う場合、このプログラムは Java RMI オブジェクトとして働きます。そのため、Java RMI クライアントに渡すリモート参照中のサーバーホスト名として使用する、解釈可能なホスト名を決定できなければなりません。リモートオブジェクトとして機能するアプレットを呼び出す VM は、アプレットが有効なサーバーホスト名を提供できなかった場合には UnknownHostException を発行することがあります。

Java RMI アプリケーションが UnknownHostException を発行した場合は、スタックトレースの結果を調べて、クライアントがリモートサーバーへのコンタクトに使用しているホスト名が正しいか、完全指定されているかどうかを確認してください。必要な場合、サーバーの java.rmi.server.hostname プロパティーにサーバーマシンの正しい IP アドレスまたはホスト名を設定すると、Java RMI はこのプロパティーの値を使用してサーバーへのリモート参照を作成します。

C.3 サーバーで完全指定ドメイン名または IP アドレスを使っていますが、それでも UnknownHostException が発行されます。なぜですか。

ネットワークのネームサービスの構成によっては、ある Java RMI ホストで認識できる完全指定ホスト名が別の Java RMI ホストから解釈できないことがあります。次に、この状況が生じる例をいくつか示します。

C.4 最新リリースの JDK を使っています。ホストには複数の IP アドレスがあります。Java RMI がサーバーホスト名に間違った IP アドレスを選択するのですが、どうしたらよいでしょうか。

java.rmi.server.hostname プロパティーを、Java RMI サーバーマシンの正しい IP アドレスに設定します。このプロパティーを設定して、サーバーがネームサービスから取得した完全指定のホスト名を使用するように指定することもできます。
    java.rmi.server.useLocalHostname=true

C.5 JDK の各バージョンで、Java RMI はどのようにしてサーバーホスト名を取得するのですか。

JDK の各バージョンで Java RMI がサーバーホスト名を取得するために使用する手段は、それぞれ次のとおりです。

JDK v1.1

Java RMI は java.net.InetAddress.getLocalHost() に依存して完全指定ドメイン名を返していました。InetAddress オブジェクトは、コードの static ブロックでローカルホスト名を初期化して、ローカル IP アドレスを逆ルックアップしてローカルホスト名を取得しました。しかし、ネットワークに接続されていないマシンでは、InetAddress が探しているホスト名が見つからないために、この動作によりハングアップが起こりました。

JDK v1.1.1-1.1.6

スタンドアロンシステムでの JDK v1.1 の問題に対処するため、JDK v1.1.1 では InetAddress を修正して、ネームサービスに問い合わせをしないネイティブのシステムコールから返される「修飾されていない可能性がある」ホスト名だけを取得するようにしました。Java RMI はこの変更を補うように修正されませんでした。その理由は、ユーザーが java.rmi.server.hostname プロパティーを設定することにより、InetAddress から提供される正しくないホスト名を無視することができるからです。Java RMI はネームサービスへの問い合わせをせず、デフォルトで非修飾ホスト名を使用することができます。

そのあとのバージョン

JDK v1.1.1 での InetAddress の機能上の変更により生じた多くの問題点を補うため、最近の JDK のバージョンには、次の動作が組み込まれています。

Java RMI はあるリモートオブジェクトのサーバーのマシンを識別するために、IP アドレスまたは完全指定のドメイン名を使います。サーバーのホスト名は、次の動作によって取得した値に初期化されます。

  1. デフォルトでは、Java RMI はリモート参照のサーバー名としてサーバーホストの IP アドレスを使用します。
  2. java.rmi.server.hostname プロパティーが設定されている場合は、Java RMI はそのプロパティーの値をサーバーのホスト名として使用し、ほかの方法で完全指定のドメイン名を探すことはありません。このプロパティーは、Java RMI サーバー名を探すほかのすべての方法に優先します。
  3. java.rmi.server.useLocalHostname プロパティーが true (デフォルトでは、このプロパティーの値は false) に設定されている場合、Java RMI は次の手順で Java RMI サーバーのホスト名を取得します。
    1. InetAddress.getLocalHost().getHostName() メソッドから返された値に「.」文字が含まれている場合は、Java RMI はこの値をサーバーの完全指定ドメイン名と見なし、サーバーホスト名として使用します。
    2. それ以外の場合は、Java RMI はスレッドを生成してローカルのネームサービスに Java RMI サーバーの完全指定ドメイン名を問い合わせます。ネームサービスから戻るのに時間がかかりすぎたり、ネームサービスからの戻り値に「.」が含まれない場合は、Java RMI は InetAddress.getLocalHost().getHostAddress() により取得したサーバーの IP アドレスを使用します。
    Java RMI が完全指定ドメイン名を検索するデフォルト時間 (10 秒 = 10000 ミリ秒) を、次のプロパティーを設定することにより無視できます。
    sun.rmi.transport.tcp.localHostnameTimeOut=timeOutMillis
    ここで timeOutMillis には Java RMI の待機時間をミリ秒単位で指定します。たとえば、
                java -Dsun.rmi.transport.tcp.localHostnameTimeOut=2000 MyServerApp
            
    
起動可能なリモートオブジェクトを使用する場合は、Java RMI サーバーの java.rmi.server.useLocalHostname プロパティーを true に設定することを推奨します。一般的に、ホスト名は IP アドレスよりも永続性があります。起動可能なリモートオブジェクトは、一時リモートオブジェクトよりも長く存在する傾向にあります (たとえば、リブート後も存在する)。Java RMI クライアントは、明示的な IP アドレスを使うよりも修飾されたホスト名を使う方が、長期にわたってリモートオブジェクトの場所を特定しやすくなります。

C.6 Windows プラットフォームで Naming.bindNaming.lookup に非常に時間がかかるのはなぜですか。

ホストのネットワーク設定が正しくないことが考えられます。Java RMI では Java API のネットワーククラスを使います。特に、java.net.InetAddress を使って TCP/IP ホスト名を検索します (InetAddress クラスは、セキュリティー上の理由から、ホストからアドレスへのマッピングとアドレスからホスト名へのマッピングの両方を行う)。Windows プラットフォームでは、ルックアップ機能はネイティブのソケットライブラリによって行われるので、遅れは Java RMI ではなくライブラリの中で起こります。ホストが DNS を使用するよう設定されていて、DNS サーバーが通信に関係するホストについての情報を持たず、DNS ルックアップタイムアウトが起こっている場合は問題です。関連するすべてのホストのホスト名またはアドレスをローカルファイル \winnt\system32\drivers\etc\hosts または \windows\hosts に書き込んでください。ホストファイルの書式は一般的に、次のようなものです。
    IPAddress     Machine Name
たとえば:
    192.0.2.61   homer
この処置により、最初のルックアップに要する時間を大幅に短縮できます。

C.7 ネットワークに接続されていないスタンドアロンの Windows 95 マシンでは、どのようにして Java RMI を使うのでしょうか。

ネットワークに接続されていない Windows 95 マシンで Java RMI を動作させるには、TCP/IP を構成する必要があります。1 つの方法は、使用していない COM ポートを専用の PPP または SLIP 接続として構成することです。次に、DHCP を無効にしてから手動で IP アドレス (192.168.1.1) を構成します。すると、これを DOS シェルから検出して ping を実行することができます (ping mymachine)。これで、マシン上で Java RMI を使用できます。

C.8 レジストリを実行しようとすると「java.net.SocketException: Address already in use」という例外が発行されるのですが、なぜですか。

この例外は、RegistryImpl が使用するポート (デフォルトは 1099) がすでに使用されていることを意味します。マシン上に実行中の別のレジストリがあると思われるので、それを停止させてください。

C.9 ファイアウォール経由で Java RMI 呼び出しを実行するにはどうすればよいでしょうか。

その方法は、ファイアウォールの外側への呼び出し内側への呼び出しのどちらを実行するかによって異なります。

C.10 ローカルのファイアウォールの内側から外側へ Java RMI 呼び出しを行うには、どうすればよいでしょうか。

主な方法は 3 つあります。HTTP トンネリング、SOCKS、およびダウンロードされたソケットファクトリです。

HTTP トンネリング

これはよく使われる方法で、セットアップがほとんど必要ないので人気があり、プロキシを通じて HTTP を扱えるファイアウォール環境では非常にうまく機能します。ただし、通常の外向きの TCP 接続はできません。

Java RMI が目的のサーバーへの通常の (SOCKS) 接続の作成に失敗し、HTTP プロキシサーバーが構成されていると通知した場合は、そのプロキシサーバーを通じて Java RMI 要求を 1 つずつトンネリングしようとします。

HTTP トンネリングには 2 つの形式があり、順番に試みられます。1 つ目の形式は、http-to-port で、2 つ目は http-to-cgi です。

http-to-port トンネリングでは、Java RMI は目的のサーバーの正確なホスト名とポート番号を指す http: URL への HTTP POST 要求を試みます。HTTP 要求には 1 つの Java RMI 要求が含まれます。HTTP プロキシはこの URL を受け付けると、待機中の Java RMI サーバーにこの POST 要求を転送します。Java RMI サーバーは要求を認識してラップを解除します。呼び出しの結果は HTTP 応答にラップされ、同じプロキシ経由で返送されます。

HTTP プロキシは、異常なポート番号に対する要求を拒否することがよくあります。この場合、Java RMI は http-to-cgi トンネリングを実行しようとします。Java RMI 要求は前回同様 HTTP POST 要求内にカプセル化されますが、要求 URL の形式は http://hostname:80/cgi-bin/java-rmi.cgi?port=n (hostnamen は目的のサーバーのホスト名とポート番号) になります。サーバーホストのポート 80 で待機している HTTP サーバーが必要で、これは java-rmi.cgi スクリプト (JDK で提供) を実行します。次に、このスクリプトはポート n で待機中の Java RMI サーバーに要求を転送します。Java RMI は HTTP トンネリングされた要求を、HTTP サーバーや CGI スクリプトなどの外部からの助けなしにラップ解除できます。そのため、クライアントの HTTP プロキシがサーバーのポートに直接接続できる場合は、java-rmi.cgi スクリプトはまったく必要ありません。

HTTP トンネリングの使用をトリガーするためには、標準システムプロパティー http.proxyHost をローカル HTTP プロキシのホスト名に設定する必要があります。報告によると、Navigator の一部のバージョンではこのプロパティーが設定されません。

HTTP トンネリングの最大の短所は、内向きの呼び出しや多重接続を許可しない点です。第 2 の短所は、http-to-cgi 方式ではサーバー側に深刻なセキュリティーホールができてしまうという点です。その理由は、この方式では、修正しないかぎり、どんなポートへの内向きの要求もすべてリダイレクトされてしまうからです。

SOCKS

JDK のソケットのデフォルト実装では、SOCKS サーバーが利用可能で構成済みの場合には、それを使用します。システムプロパティー socksProxyHost は、SOCKS サーバーのホスト名に設定されている必要があります。SOCKS サーバーのポート番号が 1080 でない場合は、socksProxyPort プロパティーで設定しなければなりません。

この方法が、もっとも一般的に使用できる方法と考えられます。現在のところ、ServerSockets は SOCKS を使用しないので、内向きの呼び出しには別のメカニズムを使う必要があります。

ダウンロードされたソケットファクトリ

これは JDK の新機能で、この機能を使用すると、クライアントが使用するソケットファクトリをサーバーが指定することができます。クライアント側では、J2SE v1.2 以降が稼働している必要があります。詳細は、「Java RMI によるカスタムソケットファクトリの使用」チュートリアルを参照してください。

この方法の短所は、ファイアウォールの通過を Java RMI サーバー側から提供されたコードを使って行わなければならない点です。Java RMI サーバー側では正しい通過方法がわかっているとはかぎらず、またファイアウォールを通過するための十分な特権を自動的に保持するとはかぎりません。

C.11 ローカルのファイアウォールの外側から内側へ Java RMI 呼び出しを行うには、どうすればよいでしょうか。

主な方法は 3 つあります。既知のポート、トランスポートレベルブリッジ、およびアプリケーションレベルプロキシです。

既知のポート

エクスポートされるオブジェクトがすべて、既知のホストの既知のポートでエクスポートされる場合には、そのホストとポートをファイアウォールで明示的に許可することができます。通常、Java RMI はポート 0 (「任意のポート」のコード) を要求します。JDK では、exportObject メソッドには正確なポート番号を指定するための特別な引数があります。JDK v1.1 では、サーバーは RMISocketFactory をサブクラス化して、createServerSocket(0) への要求を横取りし、特定のポート番号に結び付ける要求と置き換えます。

この方法の短所は、ローカルファイアウォールの責任者であるネットワーク管理者の助けが必要なことです。エクスポートされたオブジェクトが別の場所で実行される (コードがそのサイトにダウンロードされたため) 場合、ローカルファイアウォールを管理するネットワーク管理者には、実行しようとしているユーザーがだれかわかりません。

トランスポートレベルブリッジ

トランスポートレベルブリッジは、1 つの TCP 接続からバイトデータを読み込んで別の TCP 接続に書き込む (およびその逆) プログラムで、バイトデータの内容については関知しません。

ここでの考え方は、「ファイアウォールの外側からそのオブジェクトのリモートメソッドを呼び出そうとする者はだれでも、代わりに (おそらく別のマシンの) 別のポートにコンタクトする」という方法でオブジェクトをエクスポートするということです。この別のポートでは、実際のサーバーへの二次的な接続を作成して両方向にバイトデータを送り出すプログラムが稼働しています。

この方法で難しいのは、ブリッジに接続することをクライアントに納得させることです。ダウンロード可能なソケットファクトリ (JDK, v1.2 以降) を使うと効率的にこれを行うことができます。ソケットファクトリを使わない場合は、java.rmi.server.hostname プロパティーを設定することにより、ブリッジホストに名前を付けてポート番号を同じにできます。

アプリケーションレベルプロキシ

この手法はかなり手間がかかりますが、とても確実な成果を得ることができます。プロキシプログラムは、ファイアウォールホスト上 (外部からも内部からもアクセス可能なホスト) で稼働します。内部サーバーが、外部から利用可能なエクスポートオブジェクトを作成しようとするときは、プロキシサーバーにコンタクトしてリモート参照を渡します。プロキシサーバーは、元のオブジェクトと同じリモートインタフェースを実装したプロキシオブジェクト (プロキシサーバーに属する新しいリモートオブジェクト) を作成します。プロキシサーバーは、新しいプロキシオブジェクトへのリモート参照を内部サーバーに返します。内部サーバーは、その参照を何らかの方法で外部に伝えます。

外部からプロキシに呼び出しがあると、プロキシはただちにその呼び出しを内部サーバー上の元のオブジェクトに転送します。プロキシの使い方は外部から見えますが、通信時に元の参照またはプロキシ参照のどちらを渡すかを決定する内部サーバーからは見えません。

言うまでもなく、これには大量の設定とローカルネットワーク管理者間の協力が必要です。

C.12 2 つのファイアウォールを越えて Java RMI 操作をするにはどうすればよいでしょうか。

まず、クライアント側のファイアウォールからどのような協力が得られるかが問題です。

最悪の場合は、クライアント側のファイアウォールが直接の TCP 接続を一切許可せず、そのファイアウォール内のクライアントが「Web サーフィン」するための HTTP プロキシサーバーだけを持っているケースです。この場合、サーバーホストは、HTTP 要求に埋め込まれた Java RMI 要求を含むポート 80 での接続を受け取ります。HTTP サーバーで java-rmi.cgi プログラムを使用するか、Java RMI サーバーをポート 80 で直接実行できます。どちらの方法でも、サーバーはクライアントがエクスポートしたコールバックオブジェクトを使用することはできません

それより良いケースに、クライアントがサーバーへの直接接続を作成できるがサーバーからの接続は受け取れないという場合があります。この場合も、コールバックオブジェクトは普通には使用できません。

クライアントファイアウォールの管理者から協力を得られない場合、もっとも保守的な方法は、次のとおりです。

C.13 JDK ディストリビューションに付属している java-rmi.cgi スクリプトをサーブレットを使って置換することは可能ですか。

サーブレットを使って java-rmi.cgi スクリプトを実装する方法を示したを参照してください。この例では、サーブレット VM でリモートオブジェクトを実行する方法についても説明しています。

注:HTTP を介してリモートメソッド呼び出しのトンネリングを行うときの java-rmi.cgi の動作については、Java RMI 内の HTTP トンネリングに関する FAQ を参照してください。

D.1 リモート VM に障害が発生したときに、自動的にすぐに通知を受け取る方法はありますか。

現時点では、ありません。

D.2 仮想マシン内から、リモートマシン上に新しい仮想マシンを生成できますか。

JDK にはオブジェクトの起動機能が含まれており、その使用法を説明するチュートリアルがあります。

D.3 すべてのクライアントの接続が切れたときにリモートオブジェクトに通知することはできますか。

はい。リモートオブジェクトは java.rmi.server.Unreferenced インタフェースを (その他の必要なインタフェースに加えて) 実装する必要があります。Java RMI は、すべての接続が切り離されたときに unreferenced メソッドを呼び出して通知することができます。unreferenced メソッドの実装により、リモートオブジェクトが通知を受け取る方法が決められます。ただし、レジストリに参照がある場合は、Unreferenced.unreferenced メソッドは呼び出されません。

D.4 すべてのクライアントの接続が切れたときにサーバープログラムが終了しません。なぜでしょうか。

Java RMI では、サーバー VM は、次の場合は終了することになっています。 しかし、あるリモートオブジェクトへのローカル参照またはリモート参照が存在しないというだけで、そのオブジェクトが適時ガベージコレクションされるわけではありません。この場合は、メモリー割り当てを満たすためにそのリモートオブジェクトのメモリーがコレクションされます。メモリーコレクションが行われないと、メモリー割り当ては (OutOfMemoryError を発行して) 失敗します。

Java API ではコレクションの時期を指定しませんが、JDK v1.1 実装でリモートオブジェクトのコレクションが無期限に遅れるように見えるのには特別な理由があります。Java RMI ランタイムは内部的に、エクスポートされたリモートオブジェクトへの「弱参照」をテーブルに保持しています (オブジェクトへのローカルとリモートの参照を追跡するため)。JDK v1.1 で利用可能な弱参照のメカニズムでは、VM は攻撃的でないキャッシングコレクションポリシー (ブラウザに最適) を使います。そのため、「弱く参照されている」オブジェクトは、ローカル GC が次のメモリー割り当てで、そのメモリーが本当に必要だと判断するまではコレクションされません。これはアイドル状態のサーバーには決して起こりません。しかし、メモリーが必要な場合には参照されていないサーバーオブジェクトがコレクションされます。

Java SE プラットフォームには Java RMI が使用する新しいインフラストラクチャーが含まれ、この問題が発生する状況を大幅に減らすことができます。

D.5 分散型ガベージコレクタは、接続の切れたクライアントをどのようにして検出するのですか。クライアントを適切に終了するために System.exit を使うのは賢明でしょうか。

クライアント VM の Java RMI ランタイムが、あるリモートオブジェクトがローカルで参照されなくなったことを検出すると、比較的早くサーバーに非同期で通知し、サーバーがそのオブジェクトの参照されたセットを通知に従って更新できるようにします。分散ガベージコレクタは、クライアントが保持している各リモートオブジェクト参照に関連付けられているリースを使用し、クライアントがリモートオブジェクトへの参照を保持している間はそのオブジェクトのリースを更新します。リースの更新メカニズムの目的は、サーバーがクライアントの異常終了を検出することです。これにより、クライアントが実行を停止する前に適切な「参照なし」のメッセージを送ることができなくなったことによって、サーバーが永久にリモートオブジェクトを保持することを防ぎます。System.exit() が呼び出されると RMI ランタイムはサーバーに適切な「参照なし」のメッセージを送ることができないので、これを呼び出したクライアントは異常終了と見なされます。終了の前にクライアントで System.runFinalizersOnExit を実行するだけでは不十分です。その理由は、ファイナライザでは必要な処理がすべて行われるわけではなく、「参照なし」のメッセージがサーバーに送られないからです。(runFinalizersOnExit の使用は一般的に推奨できず、デッドロックを起こしやすい)。

System.exit() を使ってクライアント VM を終了する必要がある場合は、VM 内に保持されているリモート参照がより早く確実に消去されるように、アクセス可能なリモート参照がすでに存在しないようにする必要があります。そのためには、ローカル参照をすべて明示的に null にして、実行中のスレッドからアクセスできないようにします。完全なガベージコレクションを実行し、ファイナライザを実行してから終了するという方法もあります。

    System.gc();
    System.runFinalization();

D.6 クライアントがクラッシュしたことをサーバーはどのように知るのですか。

クライアントのリース期限が切れるまで待つ場合は、Java RMI 実装によって unreferenced() メソッドが呼び出されます (レジストリはすべてのバインディングへの参照を保持しているため、レジストリもまた、この目的ではクライアントとなる)。

クライアントがリモート参照を保持している場合は、その参照のリースも保持しており、それを更新する (サーバーにコンタクトして dirty() 呼び出しを行うことにより) 必要があります。エクスポートされたオブジェクトに対する最後のリースが期限切れになるか閉じられるかすると、そのオブジェクトは参照されていないと見なされ、java.rmi.Unreferenced を実装している場合は unreferenced() メソッドが呼び出されます。

複数のクライアントが同一のリモートオブジェクトへの参照を持っている場合は、そのオブジェクトに対するすべてのクライアントのリースが期限切れにならないかぎり、unreferenced() メソッドは呼び出されません。したがって、この手法を使って個々のクライアントを追跡する場合は、個々のクライアントが Unreferenced オブジェクトに対する参照を保持している必要があります。

D.7 リモートオブジェクトの使用をやめてから unreferenced() メソッドが呼び出されるまで 10 分かかります。この時間を短縮する方法を教えてください。

リースの終了期限はサーバーによって指定されます。システムプロパティー java.rmi.dgc.leaseValue を使ってミリ単位で指定されます。この時間を短くするには (30 秒など)、次のようにしてサーバーを起動します。
    java -Djava.rmi.dgc.leaseValue=30000 ServerMain

デフォルト値は 600000 ミリ秒 (10 分) です。

クライアントは期限の半分を過ぎると各自のリースを更新します。リース期間が短すぎると、クライアントは無駄なリース更新のためにネットワーク帯域幅を浪費することになります。リース期間が極端に短い場合にはクライアントのリース更新が間に合わなくなり、結果としてエクスポートされたオブジェクトが削除されることもあります。

Java RMI の将来のリリースでは、リースの更新が失敗するとリモート参照が無効になります (参照の整合性を維持するため)。失効したリモートオブジェクトへの参照の使用をあてにすべきではありません。

クライアントマシンがクラッシュした場合は、単にタイムアウトを待つだけでよいのです。接続が切れたときにクライアントがまだ何らかの制御を維持している場合は、クライアントはすばやく DGC clean 呼び出しを行い、Unreferenced をタイムリーに使用できます。この処理をうまく進めるには、クライアントがリモートオブジェクトに対して保持している可能性のある参照をすべて null にしてから、System.gc() を呼び出します。(v1.1.x では、ファイナライザを同期して実行させてから、もう一度 GC を実行することが必要)。

D.8 クライアントがクラッシュしたときに、すぐに通知を受け取れないのはなぜですか。

サーバーには、ネットワークの遅延とクラッシュしたホストを区別する手段がないからです。

クラッシュしたクライアントがあとで再起動してサーバーにコンタクトすれば、その時にサーバーはクライアントがそれまでの間にクラッシュしたかどうかを推察することができます。クライアントとサーバーが対話している間、両者の間で TCP 接続がずっと開いているなら、あとでその接続への書き込み (1 時間ごとの TCP 維持パケットが有効な場合は、それを含む) が失敗したときに、サーバーはクライアントが再起動したことを検出できます。しかし、そのような永続的な接続はスケーラビリティーを損なうほかにあまり役に立たないので、Java RMI は永続的な接続を必要としないように設計されています。

ネットワークピアがいつクラッシュしたり利用できなくなったかを簡単に判断することは、まったく不可能なので、ピアが応答しなくなったときのアプリケーションの動作を決めておく必要があります。

このタスクに使う主な手段はタイムアウトとリセットです。タイムアウトが起きた場合、ピアが通信不可能になったと判断してもかまいません。しかし、そのピアがこちらへ通信しようとするのをやめるように、タイムアウトしたことがピアにわかる必要があります。リースのメカニズムは、これを半自動的に行うためのものです。

リセットとは、ピアのために保持されている状態を一掃することです。たとえば、クライアントはサーバーに最初に登録したときにリセットを行なって、サーバーがそのクライアントのためにそれまで保持していた状態を破棄するようにします (クライアントがそれ以前の「死んだ」セッションの記憶を持たずに再起動したと考えて)。

多くの場合、目的は、サーバーでクライアントの明確なリストを持ち、誤りや失敗なしにそのリストを最新の状態に維持することです。ネットワークシステムでは故障や遅延はいつでも起こる可能性があるので、リストにはある程度の誤りがあることを予期する必要があります。リースなどのメカニズムを使ってタイムアウトを強制すれば、リソースの漏れの問題は解決します。失効したデータの問題がもっと深刻で、正常な動作を妨害するような場合があります。問題を取り除かなければ悪影響がある場合には、明示的に取り除く必要があります。

たとえば、ユーザーが編集するため、あるビジネスオブジェクトがロックされているときにセッションが異常終了した場合には、何らかの方法でロックを解除する必要があります。この場合、ロックにはタイムアウトが必要かもしれません。しかし、すぐに同一ユーザーがログインし、そのユーザーはタイムアウトまで待つ必要はないと思っているなら、新しいセッションではロックを引き継ぐか、ユーザーがロックを保持していないと断定する (サーバーが安全にロックを解除できるようにする) 必要があります。

D.9 DOS のバッチファイルで rmic コマンドを実行させるにはどうすればよいでしょうか。

DOS のバッチファイルで、制御がバッチファイルに戻るようにするためには、実行可能コマンドの前に call コマンドを挿入する必要があります。たとえば、
    call rmic ClientHandler
    call rmic Server
    call rmic ServerHandler
    call rmic Client

D.10 リモートオブジェクトの実装で、リモートメソッドの呼び出し元のホスト名を知るにはどうすればよいでしょうか。

java.rmi.server.RemoteServer.getClientHost メソッドが、現在のスレッド上の現在の呼び出し元のクライアントホスト名を返します。

D.11 Java RMI では (CORBA のように) OUT、INOUT パラメータを取り扱えますか。

Java RMI では、OUT および INOUT パラメータを、その他のコア Java プログラミング言語と同じくサポートしません。リモート呼び出しはすべてリモートオブジェクトのメソッドです。ローカルオブジェクトはコピーによって、リモートオブジェクトはスタブへの参照によって渡されます。詳細については、Java RMI 仕様の「リモートメソッド呼び出しでのパラメータ引き渡し」を参照してください。

D.12 通常、Java プログラミング言語では、インタフェースのインスタンスを、生成元のクラスのインスタンスにキャストして、その結果を使用することができます。Java RMI では、なぜこれができないのですか。

Java RMI では、クライアントは元のオブジェクトのスタブだけにアクセスします。スタブはリモートインタフェースとそのリモートメソッドだけを実装します。またスタブなので、元の実装クラスへキャストバックすることはできません。

そのため、リモートオブジェクト参照をサーバーからクライアントへ渡してから、サーバーに送り返し、元の実装クラスにキャストバックすることはできません。ただし、サーバー上のリモートオブジェクト参照を使ってそのオブジェクトへのリモート呼び出しを行うことはできます。

もう一度実装クラスを見つける必要がある場合は、リモート参照を実装クラスにマッピングするテーブルを保持する必要があります。

E.1 必要な JDK または Java SE のバージョンをブラウザがサポートしていない場合はどうすればよいでしょうか。

必要な JDK または Java SE のバージョンをサポートしないブラウザでは、Java Plug-in を使用してみてください。

E.2 Java RMI にリモートの Observer および Observable オブジェクトを実装できますか。

java.util.Observablejava.util.Observer を新しいインタフェースで「ラップ」することができます (それぞれを RemoteObservableRemoteObserver と呼ぶ)。これらの新しいインタフェースで、各メソッドが java.rmi.RemoteException をスローするようにします。次に、リモートオブジェクトでこれらのインタフェースを実装します。

リモートでないラップされたオブジェクトは java.rmi.server.UnicastRemoteObject を継承していないので、そのオブジェクトを UnicastRemoteObjectexportObject メソッドを使って明示的にエクスポートする必要があります。ただし、これを行うと、equalshashCodetoString の各メソッドの java.rmi.server.RemoteObject 実装を失います。

F.1 クライアントとサーバーの間に「ライブ」接続ができるのはどの時点ですか。また、接続の管理はどのように行われるのですか。

クライアントがルックアップ操作を行うときは、指定されたホストの rmiregistry への接続が作成されます。一般に、リモート呼び出しのための新しい接続は、作成される場合と作成されない場合があります。接続は、将来の利用に備えて、Java RMI トランスポートによってキャッシュされます。そのため、あるリモート呼び出しの正しい呼び出し先への接続が空いているときは、その接続が使用されます。接続は Java RMI トランスポートのレベルで管理されているので、クライアントがサーバーへの接続を明示的に閉じることはできません。接続は、一定期間使われないとタイムアウトになります。

F.2 Java プラットフォームはリモートメソッドの呼び出し中、すべてのリモートオブジェクトをスタブに置き換えるのですか。

JRMP および Java RMI-IIOP の実装は、直列化されたオブジェクトのグラフの奥深くにあるものも含めて、各リモートオブジェクトを、同じプロトコルの対応するスタブで置き換えます。

F.3 ソケットを使用しない、Java RMI 用の新しいトランスポート層を記述することはできますか。また、TCP ベースでないソケットを使うトランスポート層はどうでしょうか。

トランスポートインタフェースのさまざまな実装を Java RMI で使用できるように、トランスポートインタフェースを設計しました。初期のリリースでは、この abstract クラスは当社で内部的な目的に使用し、一般には公開していませんでした。現在の JDK では、Java RMI でクライアントとサーバーのソケットファクトリがサポートされ、TCP ベース以外のソケットを介した Java RMI 呼び出しを作成できます。

F.4 レジストリが CPU リソースを使い続けているのに気付きました。あたかも、select() 呼び出しでブロックしているのではなくポーリングしているようです。レジストリはポーリングで実装されているのですか。

Java RMI は select の呼び出しでポーリングしません。あるスレッドが頻繁に呼び起こされ、エクスポートされたリモートオブジェクトのテーブルをポーリングします。この「刈り取り」スレッドは分散型ガベージコレクタ用に使用されます。

F.5 クライアントプロセスにスタブがいくつあっても、クライアントプロセスとサーバーの間にはソケット接続は 1 つしか存在しないのでしょうか。

Java RMI は、クライアントとサーバー間のソケット接続をできるかぎり再利用します。現在の実装では、ソケットの必要時に要求に応じて追加のソケットが作成されます。たとえば、ある呼び出しが既存のソケットを使用中に別の呼び出しがあると、新しい呼び出し用の新しいソケットが作成されます。通常、リモートオブジェクトがサーバーから返されたときに分散型ガベージコレクタがリモート呼び出しを行うため、最低 2 つのソケットが開いている必要があります。キャッシュされた接続が一定期間未使用のままになると、その接続は閉じられます。

G.1 Java RMI の使用に関するライセンスの問題はどうなっていますか。

Java RMI は Java SE プラットフォームの一部であるため、J2SE のライセンス条項の対象になります。

G.2 Java RMI 呼び出しを開始するユーザーコマンドを標準入力で受け付ける、シングルスレッドプログラムを使っています。しかし、このプログラムが標準入力をブロックしているらしく、リモートオブジェクトがこのリモート呼び出しに対応できません。何が問題なのですか。

これは Java RMI の問題ではなく、標準入力を読み込むスレッドに関するよく知られた問題です。このスレッドはブロックリードの際にほかに譲らずに実行し続けるので、リスナーはほとんどサイクルを得ることができません。次の 2 つの方法がうまくいくようです。メインスレッド (標準入力を読み込むスレッド) の優先順位を下げます。または、ストリーム内にバイトがないときはほかに実行を譲り、そのあとで実際に読み込むようにします。

G.3 リモートサーバーに配列要素をコピーしてその値を変更しても、インクリメント後の値がクライアントにコピーし直されません。なぜですか。

リモートでないオブジェクトはコピーで渡されるので、クライアントに配列の新しい値を反映させるには、戻り引数として送り返す必要があります。

G.4 リモートインタフェースに static のフィールドを持つことはできるでしょうか。

はい。各 VM でイニシャライザを実行してからリモートインタフェースをロードし、指定された値で static 変数を新規作成します。したがって、リモートインタフェースをロードする各 VM で、この static 値の別々のコピーを持つことができます。

G.5 レジストリの位置がわかったのですが、そこにはレジストリがないようです。どうなっているのですか。

LocateRegistry.getRegistry(String host) メソッドは、ホスト上のレジストリに直接アクセスするのではなく、レジストリが存在するかどうかを確認するためにホストをルックアップするだけです。したがって、このメソッドが正常に終了しても、必ずしも指定されたホストでレジストリが実行中であるとはかぎりません。その時点でレジストリにアクセス可能なスタブが返されるだけです。

G.6 ここには、私の質問に対する回答がありません。どこを調べればよいでしょうか。

River プロジェクトの外部にあるメーリングリストおよびドキュメントの保存に多くの情報があります。


オブジェクト直列化

1. ObjectOutputStream に書き込むために、クラスが Serializable を実装しなければならないのはなぜですか。

「クラスが java.io.Serializable インタフェースを実装すること」という条件は安易に決定されたわけではありません。予測可能で安全なメカニズムを提供するため、設計には開発者からの要求とシステムの要求のバランスをとることが求められました。設計上のもっとも困難な制約は、Java プログラミング言語のクラスの安全性とセキュリティーでした。

「クラスが直列化可能と明示されなければならない」という設計にすると、開発者が (その条件を忘れたり、無視するなどの理由で) クラスを Serializable と宣言しなかった場合には、そのクラスが RMI で使用できなくなったり持続性を持たせるという目的に使用できなくなる恐れがあります。この条件によって、開発者に「あるクラスが将来、他人によってどのように使われるか」という本質的に予知不可能なことを考えなければならないという負担をかける恐れもあります。実際、予備設計ではアルファ API に反映されているように、「デフォルトでは、クラス内のオブジェクトは直列化可能とする」と結論に達しました。その設計を変更したのは、セキュリティーと正確さを考慮した結果、デフォルトではオブジェクトは直列化可能であってはならないと確信したからです。

セキュリティーによる制限

オブジェクトのデフォルト動作を変更する理由になった最初の考慮点は、セキュリティー、特に private、package protected、protected のいずれかに宣言されているフィールドの機密性に関する問題でした。Java プラットフォームは、Runtime 内のオブジェクトのサブセットの読み出しまたは書き込みの目的での、このようなフィールドに対するアクセスを制限します。

オブジェクトを直列化可能にすると、このような制限を与えることはできなくなります。オブジェクトの直列化の結果であるバイトストリームは、そのストリームへアクセスできるどんなオブジェクトによっても読み出しと修正が可能です。これにより、直列化されたオブジェクトの状態にはどんなオブジェクトからもアクセスすることができるので、ユーザーが期待する機密性の保証が破られます。さらに、ストリーム内のバイトを任意に変更できるので、Java プラットフォームによる保護下では不可能だったオブジェクトの再構築が可能になります。このようなオブジェクト再構成により、Java プラットフォームのユーザーが期待する機密性保証だけでなく、プラットフォームそのものの整合性が脅かされる場合があります。

このような破壊行為を防ぐことはできません。その理由は、直列化の概念そのものが、オブジェクトを Java プラットフォームの外部 (つまり、Java 環境の機密性と安全性の保証の圏外) に持ち出すことのできる形に変換し、そのあとで Java プラットフォームに戻すことを可能にするためのものだからです。オブジェクトを直列化可能に宣言するという条件は、「クラスの設計者は、機密面や安全面でのそのような侵害の可能性を許容するという積極的な決断を行う必要がある」ことを意味します。直列化に関する知識のない開発者が、知識の欠如のせいで無防備になってはなりません。また、クラスを Serializable と宣言する場合は、その宣言の結果をよく考慮することが望ましいのです。

この種のセキュリティー問題は、セキュリティーマネージャーのメカニズムによって解決できる問題ではありません。直列化の目的は、仮想マシン間のオブジェクトの移送 (RMI の場合のような空間上の移送、または、ファイルにストリームを保存する場合のような時間上の移送) を可能にすることです。したがって、セキュリティーに使用されるメカニズムは特定の仮想マシンの実行時環境とは無関係であることが必要です。私たちは、可能なかぎり「ある仮想マシンでオブジェクトを直列化でき、別の仮想マシンではそのオブジェクトを直列化解除できない」という問題を避けようとしました。セキュリティーマネージャーは実行時環境の一部なので、もし直列化にセキュリティーマネージャーを使用すれば、この要求を達成できなかったでしょう。

意識的な決定の強制

設計変更の第一の理由はセキュリティーの考慮ですが、「直列化はある程度の設計上の考慮のあとにクラスに追加するべきだ」ということも理由として説得力を持つと考えます。直列化と直列化復元により崩壊してしまうクラスを設計するのはいとも簡単です。クラスの設計者に対して直列化インタフェースのサポートの宣言を要求することにより、そのクラスの直列化のプロセスについての考慮を促したいと考えました。

実例は数多くあります。多くのクラスは、特定のオブジェクトが存在する実行時環境でだけ意味を持つ情報 (ファイルハンドル、オープンソケット接続、セキュリティー情報など) を処理します。このようなデータは、フィールドを transient と宣言するだけで簡単に取り扱うことができますが、このような宣言が必要なのはオブジェクトが直列化される予定のときだけです。不慣れなプログラマの場合は、クラスが Serializable インタフェースを実装していることを宣言するのを怠る場合と同様に、フィールドを transient と宣言するのを怠るかも知れません。こうしたケースが不正な動作につながらないようにする必要があります。そのための方法が、Serializable インタフェースの実装が宣言されていないオブジェクトは、直列化しないことなのです。

別の例として、多数のオブジェクトにまたがるグラフのルートとなっている「単純な」オブジェクトのことを考えてみましょう。直列化はグラフ全体に機能するので、このオブジェクトを直列化すると、ほかの多くのオブジェクトを直列化することになります。このような直列化は意識的に決定すべきことであり、デフォルトの動作で引き起こされるべきことではありません。

基本 Java API クラスライブラリの検討作業中に、私たちはこの問題を考慮する必要性を痛感しました。当初はライブラリのシステムクラスが適宜、直列化可能と宣言されているという設計でした。私たちは当初、この設計を実現するのは簡単だと考えました。「ほとんどのシステムクラスに Serializable インタフェースの実装を宣言できるため、これをデフォルトの実装にすればほかに何も変更しなくてもそのまま使用できるだろう」と考えたのです。しかし、ほとんどの場合、予想どおりにはなりませんでした。非常に多くのクラスで、フィールドを transient と宣言すべきかどうか、またそもそもクラスを直列化可能にすることに意味があるのかどうかについて慎重に考える必要がありました。

もちろん、プログラマやクラスの設計者がクラスを直列化可能と宣言するときに、実際にこの問題について考えるという保証はありません。しかし、クラスに Serializable インタフェースの実装を宣言することを要求することにより、プログラマには何らかの考慮を払うことが求められます。直列化をオブジェクトのデフォルト状態とすると、考慮の不足によって、プログラムに何らかの悪影響 (Java プラットフォーム全体の設計で防ごうとしたもの) を及ぼす可能性があります。

2. JDK v1.1 システムクラスのうち、直列化可能になるものはどれですか。

削除されました。この情報は、javadoc ツールによって生成される API ドキュメントから入手できます。

3. JDK v1.0.2 で AWT コンポーネントを直列化復元できません。どうすればよいでしょうか。

AWT ウィジェットを直列化すると、その AWT 機能をローカルウィンドウシステムにマップするピアオブジェクトも直列化されます。AWT ウィジェットを直列化解除 (再構築) すると、古いピアが再生されますが、その時点ではもう使えません。ピアはローカルウィンドウシステムに対してネイティブであり、ローカルアドレス空間内のデータ構造へのポインタを持っているので、別の場所に移動することはできません。

対処法として、まずトップレベルのウィジェットをコンテナから削除してください (これで、ウィジェットは「アクティブ」ではなくなる)。この時点でピアは破棄され、AWT ウィジェットの状態だけを保存できます。あとでウィジェットを直列化解除して読み出すとき、フレームにトップレベルのウィジェットを追加して、AWT ウィジェットを表示させます。show 呼び出しを追加することが必要な場合があります。

JDK v1.1 以降の AWT ウィジェットは直列化可能です。java.awt.Component クラスは Serializable インタフェースを実装します。

4. オブジェクト直列化では暗号化はサポートされますか。

オブジェクトの直列化には、暗号化や暗号解読は含まれていません。直列化は Java API の標準ストリームへの書き込みと、Java API の標準ストリームからの読み出しを行うので、利用可能な任意の暗号化技術と組み合わせて使用することができます。オブジェクトの直列化は、さまざまな場面で使用できます。単にファイルを読み書きするだけでなく、Java RMI によるホスト間通信にも使用できます。

RMI で直列化を使用する場合は、暗号化と暗号解読を下位のネットワークトランスポートに委ねます。安全なチャネルが必要な場合は、ネットワーク接続には SSL のようなものを使用することをお勧めします (「Java RMI での SSL の使用」を参照)。

5. オブジェクトの直列化クラスはストリーム指向です。オブジェクトをランダムアクセスファイルに書き込むにはどうすればよいでしょうか。

現在は、ランダムアクセスファイルにオブジェクトを直接書き込む方法はありません。

ランダムアクセスファイルに対してバイトの読み書きを行う中間の場所として ByteArrayInputStream および ByteArrayOutputStream オブジェクトを使用し、バイトストリームから ObjectInputStream および ObjectOutputStream を作成してオブジェクトの転送を行うことができます。バイトストリームにオブジェクト全体が入るように注意してください。そうしないと、オブジェクトへの読み書きに失敗します。

たとえば、java.io.ByteArrayOutputStream を使って ObjectOutputStream のバイトを受け取ります。バイト配列の形式で結果を 1 つ受け取ります。次に、このバイト配列を ByteArrayInputStreamObjectInput ストリームへの入力として使用します。

6. ローカルオブジェクトを直列化して Java RMI 呼び出しにパラメータとして渡すと、そのローカルオブジェクトのメソッドのバイトコードも渡されるのですか。リモートの VM アプリケーションがオブジェクトハンドルを保持したままだと、オブジェクトの一貫性はどうなるのでしょうか。

ローカルオブジェクトのメソッドのバイトコードは直接 ObjectOutputStream には渡されませんが、そのオブジェクトのクラスがまだローカルで利用可能になっていない場合は、そのクラスを受信側でロードする必要があります。クラスファイルそのものではなく、クラス名だけが直列化されます。直列化を解除するときには、すべてのクラスは通常のクラスロードメカニズムを使ってロード可能でなければなりません。アプレットの場合は AppletClassLoader でロードされます。

ローカルオブジェクトがリモート VM に渡されるときは、オブジェクトの内容がコピーされて渡される (真の値渡し) ので、一貫性は保証されません。

7. ファイルを間に介さないで ObjectOutputStream から ObjectInputStream を作成するには、どうすればよいでしょうか。

ObjectOutputStreamObjectInputStream は、どんなストリームオブジェクトに対しても機能します。ByteArrayOutputStream を使用し、配列を取得して ByteArrayInputStream に挿入することもできます。同様に piped stream クラスも使用できます。OutputStream クラスと InputStream クラスを拡張するすべての java.io クラスを使用できます。

8. オブジェクトを作成してから writeObject メソッドを使ってネット上を送信し、readObject メソッドを使って受信します。次に、オブジェクトのフィールドの値を変更してから同様に送信すると、readObject メソッドから返されるオブジェクトは最初のものと同じで、フィールドの新しい値が反映されていないようです。これは正しい動作なのですか。

ObjectOutputStream クラスは直列化した各オブジェクトを追跡し、それ以降そのオブジェクトがストリームに書き込まれるときは、ハンドルだけを送ります。これは、このクラスがオブジェクトのグラフを扱う方法です。対応する ObjectInputStream は、生成したすべてのオブジェクトとハンドルを追跡し、もう一度ハンドルが参照されたときに、同じオブジェクトを返せるようにします。出力ストリームと入力ストリームは、どちらも解放されるまでこの状態を維持します。

また、ObjectOutputStream クラスは reset メソッドを実装しています。このメソッドを使うとオブジェクトを送信したという記録が破棄されるので、オブジェクトをもう一度送るとコピーが作成されます。

9. スレッドオブジェクトの直列化をサポートする予定はありますか。

スレッドは直列化可能になりません。現在の実装で、スレッドを直列化してから直列化解除しようとすると、新しいネイティブスレッドやスタックは明示的に割り当てられません。ネイティブ実装なしでオブジェクトが割り当てられたシステムリソースになるだけです。このオブジェクトは機能せず、予測不可能な動作を起こします。

スレッドの直列化が難しいのは、スレッドは仮想マシンに複雑に結び付いた多くの状態を持っているために、別の場所にコンテキストを再確立することが困難または不可能だからです。たとえば、VM 呼び出しスタックを保存するだけでは不十分です。その理由は、多くのネイティブメソッドが C のプロシージャーを呼び出し、そのプロシージャーが Java プラットフォームのコードを呼び出している場合、Java プログラミング言語の構造と C のポインタが複雑に混合したものに対処する必要があるからです。また、スタックを直列化することは、任意のスタック変数からアクセス可能なすべてのオブジェクトを直列化することを意味します。

同じ VM 内でスレッドが再開されると、そのスレッドは多くの状態を元のスレッドと共有します。両方のスレッドが同時に実行されると、ちょうど 2 つの C のスレッドがスタックを共有しようとした場合のように、予測不可能な動作を起こします。また、別の VM で直列化解除した場合の結果は予想できません。

10. diff(serial(x),serial(y)) の計算はできますか。

diff は、同じオブジェクトが直列化されるたびに同じストリームを生成します。それぞれのオブジェクトを直列化するには、新しい ObjectOutputStream を作成する必要があります。

11. 自分の zip および unzip メソッドを使って、直列化表現のオブジェクトを圧縮できますか。

ObjectOutputStreamOutputStream を生成します。zip オブジェクトが OutputStream クラスを継承していれば、圧縮しても問題ありません。

12. isempty(zip(serial(x))) などの圧縮したバージョンのオブジェクトに対して、メソッドを実行することはできますか。

オブジェクトのエンコードの問題により、任意のオブジェクトに対しては実行できません。特定のオブジェクト (String など) では、結果のビットストリームを比較できます。この場合はエンコード方法は安定しており、同じオブジェクトは毎回同じビット構成にエンコードされます。

13. フォントや画像のオブジェクトを直列化して別の VM で再構築しようとすると、アプリケーションが落ちます。なぜですか。

削除されました。現在フォントは直列化可能ですが、イメージは直列化できません。

14. オブジェクトツリーを直列化するにはどうすればよいでしょうか。

オブジェクトのツリーの直列化の例を示します。

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();
        }
    }
}

15. クラス A が Serializable を実装せず、そのサブクラス B が Serializable を実装している場合、クラス B を直列化したとき、クラス A のフィールドは直列化されますか。

Serializable オブジェクトのフィールドだけが書き出されて復元されます。オブジェクトは、クラス A が、直列化が不可能なスーパータイプのフィールドを初期化する引数のないコンストラクタを持っている場合にだけ復元されます。サブクラスがスーパークラスの状態にアクセスする場合は、その状態の保存と復元用に writeObjectreadObject を実装できます。

Copyright © 1993, 2013, Oracle and/or its affiliates. All rights reserved.