いまさらJava RMI

Javaで単純なクライアント・サーバをプロトタイプしなければならなくなった.非同期の必要はないし,独自にプロトコルを切るのも面倒なので久しぶり(10年ぶり?)にいじってみたら,昔と違って随分扱いやすくなっていた.

スタブとスケルト

リモートオブジェクトの周りのプログラムの書き方はむかしと同じなんだけど,いつの間にかrmicでスタブとスケルトンを作る必要がなくなっていた.スタブは,クライアント側で稼働するダミークラス。スケルトンはサーバ側で稼働する実装オブジェクトのラッパ.いまだにrmicはパッケージに含まれているので,必要なのかと思っていたら,1.5から不要になっていたらしい.そもそもJDK1.6のパッケージに添付されているrmicを実行してもスタブはできるがスケルトンはできない.まずはスケルトンがなくなって,その後スタブも不要になったようだ.

考えてみると,スケルトンのほうはリフレクションを使えば簡単に代用できそう.スタブのほうは動的なクラス生成のようなテクニックを使う必要がありそう.そのうち実装を調べてみよう.

レジストリとサーバを同一VM

もう一つRMIで面倒だったのは,レジストリを別プロセスとして起動しておき,オブジェクトをホストするサーバが,オブジェクトをレジストリに登録する必要があること.概念的にはレジストリとリモートオブジェクトをホストするサーバは別物なので,この分離は正しいのだが,実際に使うときには,大体クライアントとサーバは1対1なので,ただただうざいだけだった.

このレジストリサーバを,リモートオブジェクトをホストするサーバと同じVM上で動かすことができる.おそらく以前からやればできたのだろうけど,これで大変に楽に使うことができるようになった.

Registry r = LocateRegistry.createRegistry(port);

セキュリティマネージャを無効化

RMIはクライアント,サーバともSecurityManagerを使用する必要がある.SecurityManagerにはレジストリに接続してそこからクラスをダウンロードする許可を設定しなければならないのだが,インハウスのプロトタイプ開発ではちょっと面倒.検索して引っかかるRMIのサンプルではたいてい何でも許してしまうような設定ファイルを使うようになっていたりする.

grant {
   permission java.security.AllPermission;
};

これをjava.policyというファイルに書いておいて,下記のように-Dでプロパティに指定して実行する.

java -Djava.security.policy=java.policy XXXX

しかし,どうせこうやってSecurityManagerを無効にしてしまうのであればもっと簡単な方法がある.SecurityManagerをカスタマイズしてすべてのチェックをバイパスすればいい.勿論この方法は*大変に*危険なので,お勧めはしない.

System.setSecurityManager(new SecurityManager(){
    public void checkPermission(Permission p) {}
});

ソースコード

エコーサーバのソースコード.Echo, EchoImplを内部クラスにしているが,それには特に意味はない.

import java.rmi.*; 
import java.rmi.registry.*;
import java.rmi.server.UnicastRemoteObject;
import java.security.Permission;

public class MinimalServer {
  public interface Echo extends Remote { 
    String echo(String str) throws RemoteException; 
  } 

  static class EchoImpl extends UnicastRemoteObject implements Echo{
    private static final long serialVersionUID = 1L;
    public EchoImpl() throws RemoteException {
      super();
    }
    public String echo(String str) throws RemoteException {
      return "Yahoo " + str;
    }
  }

  public static void main(String[] args) throws Exception{
    if (args.length < 1) {
      System.err.println("Usage: java Server PORT");
      System.exit(1);
    }
    // ***Caution! this is extremely dangerous!*** 
    System.setSecurityManager(new SecurityManager(){
      public void checkPermission(Permission p) {}
    });

    Registry r = LocateRegistry.createRegistry(Integer.parseInt(args[0]));
    r.bind("echo0", new EchoImpl()); 
  }
}

普通にコンパイルしてポートを指定して実行するだけ.

$ javac MinimalServer.java
$ java MinimalServer 1050

クライアントはこんな感じ.

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.security.Permission;

public class MinimalClient {
  public static void main(String args[]) throws Exception{
    if (args.length < 2) {
      System.err.println("Usage: java HOST PORT");
      System.exit(1);
    }
    // ***Caution! this is extremely dangerous!*** 
    System.setSecurityManager(new SecurityManager(){
      public void checkPermission(Permission p) {}
    });
    Registry r = LocateRegistry.getRegistry(args[0], Integer.parseInt(args[1]));
    MinimalServer.Echo echo = (MinimalServer.Echo)r.lookup("echo0");
    System.out.println(echo.echo("world"));
  }
}

コンパイル,実行は通常通り.当然だが.コンパイル時,実行時にMinimalServer$Echo.classがクラスパス内に必要.必要なものはそれだけ.

$ javac MinimalClient
$ java MinimalClient localhost 1050

所感

RMIは面倒くさいと思って食わず嫌いだったが,随分簡単に使えることが分かった.ちゃんと使うにはいろいろ考えなければならないことがあるのだろうけど,とりあえず当面はこれで十分使える.いや便利便利.

追記 (09/01/15)

その後調べていてDo I have to use RMI stubs for projects that use Java 5.0?という記事を見つけた.rmicは使わなくてもいいけど,使うとある種のバグがコンパイル時に検出できる.具体的には,Remoteオブジェクトに渡す引数クラスをSerializableでない,というバグ.なので,rmicは使ったほうがいいかもね,とのこと.なるほど.