よくある質問
Java RMIとオブジェクト直列化


Java RMI

全般

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

ネットワーク機能

Java RMIの使用

RMIの作用

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

その他

オブジェクト直列化

  1. ObjectOutputStreamに書き込むために、クラスがSerializableを実装しなければならないのはなぜですか。
  2. JDKシステム・クラスのうち、直列化可能になるものはどれですか。
  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.で始まるプロパティは、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を発行して失敗します。なぜですか。

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 Java RMIはどのようにしてサーバー・ホスト名を取得するのですか。

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を使用しないので、内向きの呼出しには別のメカニズムを使う必要があります。

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

これを使用すると、サーバーはクライアントが使用するソケット・ファクトリを指定できます。詳細は、「Java RMIによるカスタム・ソケット・ファクトリの使用」チュートリアルを参照してください。

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

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

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

既知のポート

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

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

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

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

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

この方法で難しいのは、ブリッジに接続することをクライアントに納得させることです。ダウンロード可能なソケット・ファクトリを使うと効率的にこれを行うことができます。それ以外の場合は、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ではコレクションの時期を指定しませんが、リモート・オブジェクトのコレクションが無期限に遅れるように見えるのには特別な理由があります。Java RMIランタイムは内部的に、エクスポートされたリモート・オブジェクトへの「弱参照」を表に保持しています(オブジェクトへのローカルとリモートの参照を追跡するため)。JDK 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()を呼び出します。

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プラットフォームの一部であるため、Java SEのライセンス条項の対象になります。

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

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

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

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

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

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

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

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

オブジェクト直列化

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システム・クラスのうち、直列化可能になるものはどれですか。

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

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

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

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

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

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

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

RMIで直列化を使用する場合は、暗号化と暗号解読を下位のネットワーク・トランスポートに委ねます。安全なチャネルが必要な場合は、ネットワーク接続にはSSLのようなものを使用することをお勧めします(「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, 2020, Oracle and/or its affiliates. All rights reserved.