はじめに
みなさまこんにちは。
先日まで「使ってみたくなる EJB」を担当しておりました馬場でございます。ようやく 1年に渡る連載も最終回を迎え、「しばらく連載はないな・・・」と思っていたのですが、諸々の事情により再び連載を担当することになりました。
今回のネタは JNDI です。JNDI って、どちらかというと「地味な」技術なのですが、実は分散アプリケーションの稼働環境を支える「縁の下の力持ち」なのです。JNDI は EJB なんかよりもっともっと古くからありますし、もっともっと目立たない技術ですが、Java ベースの分散環境では必ずと言っていいほど用いられる技術です。設定やコーディングを間違ってしまうと「オブジェクトが見つからない~」と大騒ぎすることにもなってしまいます。
例によって、構成や掲載のタイミングは何も考えていませんが、「使ってみたくなる EJB」の時と同様に、
- 「使い方」だけでなく「基本」と「しくみ」を大切に、
- できるだけ、読者の方が実際に手を動かして理解できるような
記事を心掛けていきたいと思います。お付き合いのほど、よろしくお願いいたします。
JNDI、使ってますか?
「使ってますか?」と訊かれると、「ハイ、使ってます!」と元気に答えて下さる方、「???」な方といろいろいらっしゃると思います。ですが、これを読んで下さっている J2EE ベースのアプリケーション開発者のみなさまは、ほとんどの方が JNDI を使った経験をお持ちなのではないかと思います。
例えば・・・
InitialContext ictx = new InitialContext();
DataSource ds = (DataSource) ictx.lookup("java:comp/env/jdbc/SampleDS");
とか、
NantokaHome nh = (NantokaHome) ictx.lookup("java:comp/env/ejb/Nantoka");
のようなコードに見覚え (書き覚え) はありませんか?
実はこれらの操作は、JNDI ネームスペースに登録された、データソースや EJB ホームの検索を行っているのです。「『登録された』って、登録した覚えなんかないよ」ですって?WebSphere Application Server を普通に使っている限り、オブジェクトをネーミング・サービスへ登録する作業を意識して行うことはあまりないと思います。私自身も日ごろ何気なく使っている JNDI ですが、そういえば深く考えて使ったことって正直言ってなかったな・・・という気がしています。が、アプリケーション的にはそこらじゅうにあるコードですので、今さら人には訊けないな・・・というのが本当のところです。
というわけで、連載タイトルの「今さら人に訊けない」というのは、実は私のことです。
しくみを理解することはやっぱり大切だと思いますので、まずは「JNDI ってなんですか?」から始めたいと思います。WebSphere Application Server で JNDI がどう実装されていてどう使うのかについては、次回以降の話題にしたいと思います (*)。
(*) え?「まだ記事に書けるほどわかってないからだろ?」ですって?おっしゃる通りです・・・。
JNDI ってなんですか?
JNDI とは Java Naming and Directory Interface の頭文字を取ったもので、Java から
- ネーミング・サービス
- ディレクトリー・サービス
を扱うためのインターフェイスを規定した仕様です。現在の最新バージョンは JNDI 1.2 で、最新の WebSphere Application Server V5.1 で採用している J2SE 1.4 でもこのバージョンがサポートされています。
「インターフェイス」ですので、JNDI ではネーミング・サービスやディレクトリー・サービスそのものは提供しません。あくまでも、他の実装で提供されるサービスを Java から利用するためのしくみが JNDI です。
「じゃ、他の実装って?」ということが気になると思います。では、まずはネーミング・サービスとディレクトリー・サービスの説明をしましょう。
ネーミング・サービス
ネーミング・サービスとは、
- 名前をオブジェクトに関連づける(bind)(*1)
- 名前を基に、関連づけられたオブジェクトを検索する(lookup)
することのできるサービスを指します。
「何だか難しいな・・・」とお思いかもしれませんが、代表的な実装としては電話帳や RMI レジストリーがあります。電話帳は名前から電話番号を検索(lookup)することができます (*2)。名前と電話番号の関連づけは、通常は電話を設置する時に決まりますね。このような、名前とオブジェクトの関連付けを「バインディング」と呼びます。
RMI は Java でのリモート・メソッド呼び出し(Remote Method Invocation)を行うためのしくみです。RMI レジストリーはリモート・オブジェクトを登録(bind)し、登録されたオブジェクトを呼び出し側から検索(lookup)するために利用されます。
(*1) ちなみに、bind は UNIX 系の DNS サーバーの名前にもなっていますね。 (*2) 住所も載っているので厳密にはディレクトリー・サービス(Telephone directory というくらいですので)ですが、あまり気にしないで下さい。
ディレクトリー・サービス
ディレクトリー・サービスはネーミング・サービスの拡張版です。ネーミング・サービスは名前から単にオブジェクト(への参照)を検索するのに対し、ディレクトリー・サービスはオブジェクトの属性も扱うことができます。
代表的な実装としては DNS(Domain Name System)があります。DNS はホスト名と IP アドレスの関連づけ(A レコード)を保持しており、ホスト名から IP アドレスを解決することができます。
DNS は A レコードの他にも、SOA レコード(ドメインのゾーン情報)や NS レコード(ドメインのネーム・サーバー)、PTR レコード(逆引き情報)、MX レコード(ドメインのメール・サーバー)など、いくつかの属性を持っています。これらの情報は、DNS サーバーの管理者が必要な設定を行うことで bind されます。
JNDI のアーキテクチャーとサービス・プロバイダー
さて、上ではサービス・プロバイダーの例として RMI レジストリーと DNS を挙げました (*1) 。JNDI 1.2 では他にも、ファイルシステム、NIS、LDAP、CosNaming サービス (*2) をサポートしています。WebSphere Application Server 自身は CosNaming サービスを提供しています。
もちろん、WAS 上のアプリケーションでは CosNaming 以外のサービスを JNDI 経由で使用することも可能です。JNDI のアーキテクチャーを図にすると、以下のようになります (*3)。
(*1) 残念ながら、電話帳を JNDI 経由で引くための SPI は、今のところ製品化されていません。 (*2) CORBA で規定されるネーミング・サービスです。 (*3) SPI:Service Provider Interface アプリケーション開発者が利用するのは API(Application Programming Interface)の方ですので、これを読んで下さっている方はあまり気にしなくて結構です。 (*4) 本当は、私自身が「動かさないと理解できない(信用できない)」クチだからです。世の中には資料を読んだだけで理解できる人も多くいるようですが、私にはとうてい無理です・・・。
ところで、DNS や RMI レジストリーを JNDI 経由で利用できるというのはどういうことでしょう・・・?
そろそろ説明を読むのに飽きた方もいらっしゃると思います (*4) ので、サンプル・コードを書いて実際に動かしてみましょう。
やっぱり動かしてみるでしょ!
検証環境について
実際に動かしてみたい方は、Java 2 SDK 1.4 以上の環境を用意して下のコードを打ち込んで(またはコピーして)コンパイル、実行してみて下さい。
私は Windows 2000 上の WebSphere Studio Application Developer V5.1.2 で IBM 版 Java 2 SDK 1.4.1(build cn1411-20031011)を使用しています。みなさまは同様に Studio を使用するか Eclipse などでも構いませんし、Java 2 SDK だけを入手して適当なテキスト・エディターでソースを作成し、javac でコンパイルしても構いません。
DNS を JNDI から引いてみる
では手始めにお手軽そうな方から、DNS を JNDI 経由で引いてみましょう。コードは以下のようになります。
リスト 1. DNSSample.java
import java.util.*;
import javax.naming.*;
import javax.naming.directory.*;
public class DNSSample {
public static void main(String[] args) {
String name = args[0];
Properties props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.dns.DnsContextFactory");
props.put(Context.PROVIDER_URL, "dns://<DNS サーバの IP アドレス>");
try {
InitialDirContext idctx = new InitialDirContext(props);
Attributes attrs = idctx.getAttributes(name);
NamingEnumeration allAttr = attrs.getAll();
while (allAttr.hasMore()) {
Attribute attr = (Attribute) allAttr.next();
System.out.println("Attribute: " + attr.getID());
NamingEnumeration values = attr.getAll();
while (values.hasMore())
System.out.println("Value: " + values.next());
}
} catch (NamingException e) {
e.printStackTrace();
}
}
}
PROVIDER_URL の dns://の後には、みなさまのネットワーク環境の DNS サーバーの IP アドレスを記述して下さい。
コンパイルできたら、
java DNSSample <サーバー名>
のように実行できます。
実行結果は、
C:\WSDD_JNDI>java DNSSample www.ibm.com
Attribute: A
Value: 129.42.16.99
Value: 129.42.17.99
Value: 129.42.18.99
Value: 129.42.19.99
Value: 129.42.20.99
Value: 129.42.21.99
Attribute: SOA
Value: ns.webmaster.ibm.com. webmaste.us.ibm.com.
2004042101 10800 1800 1296000 3600
Attribute: NS
Value: ns.webmaster.ibm.com.
Value: ns.adtech.internet.ibm.com.
このようになります。
ネットワークにちょっと詳しい方なら、どこかで見たことのある表示ですね・・・。そうです、nslookup
で type=all
とした時の検索結果と同じですね。
RMI レジストリーを JNDI から引いてみる
今度はもうちょっと手の込んだものを試してみましょう。
冒頭で「JNDI は分散環境での縁の下の力持ち」といった話をしました。ということで、RMI (*) を用いて分散アプリケーションらしいものを動かしてみます。
String "Hello World."
を返す sayHelloWorld()
メソッドを持つリモート・オブジェクト(サーバーとして動作)HelloWorldRMIObj
を作成し、RMI レジストリーに bind します。このオブジェクトのリモート・インターフェイスを HelloWorldRMIClient
から JNDI を用いて RMI レジストリー上で lookup し、リモート・インターフェイスを通して HelloWorldRMIObj
の sayHelloWorld()
を呼び出します。うまく行けば、呼び出しの結果として "Hello World."
を受け取ることができるはずです。
リモート・インターフェイス HelloWorldRMI を作る
まずはリモート・インターフェイスを作成します。
リスト 2. HelloWorldRMI.java
import java.rmi.*;
public interface HelloWorldRMI extends Remote {
String sayHelloWorld() throws RemoteException;
}
リモート・オブジェクト HelloWorldRMIObj を作る
次に、リモート・オブジェクトを作成します。
リモート・オブジェクトのコンパイルには javac ではなく、RMI コンパイラの rmic(Java 2 SDK に添付)を使用します。使い方は javac と同様で OK です。コンパイルに成功すると、以下のクラス・ファイルが生成されます。
HelloWorldRMI.class
リモート・インターフェイスHelloWorldRMIObj.class
リモート・オブジェクトHelloWorldRMIObj_Stub.class
スタブHelloWorldRMIObj_Skel.class
スケルトン
import java.rmi.*;
import java.rmi.server.*;
import javax.rmi.*;
public class HelloWorldRMIObj extends UnicastRemoteObject implements HelloWorldRMI {
public HelloWorldRMIObj() throws RemoteException {
super();
}
public String sayHelloWorld() throws RemoteException {
return "Hello World.";
}
}
RMI レジストリーへのリモート・オブジェクト登録用クラスを作る
上で作成したリモート・オブジェクトを RMI レジストリーに bind するための HelloWorldRMIRegist を作成します。いよいよ JNDI を使用して、オブジェクトを RMI レジストリーに登録することになります。
main() メソッドの冒頭で、InitialContext(詳しくは後で触れます)取得のための準備をしています。また、try ブロックの中でリモート・オブジェクトをインスタンス化し、RMI レジストリーへ bind しています。Context.PROVIDER_URL には RMI レジストリーを起動(詳しくは後で説明します)するホストを示す URL を記述します。HelloWorldRMIRegist を実行するマシンと同じであれば、rmi://localhost のままで構いません。コンパイルは通常の javac で OK です。
リスト 4. HelloWorldRMIRegist.java
import java.rmi.*;
import java.util.*;
import javax.naming.*;
public class HelloWorldRMIRegist {
public static void main(String[] args) {
Properties props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.rmi.registry.RegistryContextFactory");
props.put(Context.PROVIDER_URL, "rmi://localhost");
try {
HelloWorldRMIObj hwobj = new HelloWorldRMIObj();
InitialContext ictx = new InitialContext(props);
ictx.rebind("HelloWorldRMI", hwobj);
} catch (RemoteException re) {
re.printStackTrace();
} catch (NamingException e) {
e.printStackTrace();
}
System.out.println("HelloWorldRMI server ready....");
}
}
クライアント HelloWorldRMIClient を作る
最後にクライアントを作成します。
クライアントでは RMI レジストリーに登録されたリモート・オブジェクト(のリモート・インターフェイス)を検索し、リモート・メソッド呼び出しを行います。
HelloWorldRMIRegist と同様、main() の冒頭で InitialContext のコンストラクターに与えるパラメータを設定しています。try ブロックではリモート・インターフェイスの検索とメソッド呼び出しを行っています。HelloWorldRMIRegist と同様に、PROVIDER_URL には RMI レジストリーを起動するホストを示す URL を記述します。これもコンパイルは javac で OK です。
リスト 5. HelloWorldRMIClient.java
import java.rmi.*;
import java.util.*;
import javax.naming.*;
public class HelloWorldRMIClient {
public static void main(String[] args) {
Properties props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
props.put(Context.PROVIDER_URL, "rmi://localhost");
try {
InitialContext ictx = new InitialContext(props);
HelloWorldRMI hw = (HelloWorldRMI) ictx.lookup("HelloWorldRMI");
System.out.println(hw.sayHelloWorld());
} catch (RemoteException e) {
e.printStackTrace();
} catch (NamingException e) {
e.printStackTrace();
}
}
}
HelloWorld RMI C/S システムを動かしてみる
この連載のテーマは JNDI であって RMI ではありませんので、JNDI を扱っている部分のコードを眺めていただいて、書き方のお作法を理解していただければ OK です。とは言え、せっかくここまで作ってしまいましたので、動かさないわけにはいきませんよね。
まずは RMI レジストリーの起動です。Java 2 SDK に添付の rmiregistry を起動します。<JAVA_HOME>\bin
下の rmiregistry を起動して下さい。何も返ってきませんが、これで正常起動です。
次に、別のコマンド・プロンプト(UNIX 系の方は端末エミュレーター)を起動して、そこからリモート・オブジェクトを RMI レジストリーに登録します。
C:\WSDD_JNDI>java HelloWorldRMIRegist
のようにして、HelloWorldRMIRegist を起動して下さい。
HelloWorldRMI server ready・・・.
と返ってくれば、登録(サーバーの起動)は成功です。
最後に、もう 1つ別のコマンド・プロンプトを起動し、そこでクライアント HelloWorldRMIClient を起動します。
C:\WSDD_JNDI>java HelloWorldRMIClient
Hello World.
(*) 今回ははやりの RMI-IIOP ではなく、JRMP(Java Remote Method Protocol)ベースの RMI です。まずは基本から、です。IIOP と絡めた話は次回以降にどこかで書く・・・かもしれません。
のように、"Hello World."
が返ってくれば成功です。「せっかく RMI なんだから、サーバーとクライアントを別のマシンで実行してみたい」とおっしゃる方は、クライアントとスタブ、リモート・インターフェイスの class ファイルを(Java 2 SDK のインストールされた)別のマシンの同じディレクトリーにコピーし、クライアントを実行してみて下さい。
JNDI によるオブジェクトの検索
何だかすっかり RMI の記事になってしまった気がしますが、気を取り直して JNDI の話に戻りましょう。
DNS と RMI、両方のサンプルのコードを眺めてみると、JNDI の取り扱いについてはほとんど同じ手順であることに気づきますね。
ここで、JNDI によるオブジェクト操作の手順をまとめてみると、
- Properties オブジェクトを用意する。
- Properties に INITIAL_CONTEXT_FACTORY を put する。
- 同様に PROVIDER_URL を put する。
- Properties を引数として InitialContext(または InitialDirContext)を new する。
- InitialContext や InitialDirContext を利用して、bind したり lookup したりする。
となります。
InitialContext や InitialDirContext は bind()
や lookup()
、getAttributes()
の他にもいろいろなメソッドを持っています。詳しくはhttps://www.oracle.com/jp/java/technologies/で J2SE 1.4 の Javadoc を参照して下さい。
どうやら、JNDI を使っていろいろな操作を行うためには、InitialContext (*) を取得することが第一歩のようです。
(*) 今後、特に区別する必要のない場合は、InitialContext と InitialDirContext をまとめて、InitialContext またはイニシャル(初期)コンテキストと呼ぶことがあります。InitialDirContext は extends InitialContext ですので。
では、InitialContext やそのコンストラクターに与えた(Properties に含まれる)INITIAL_CONTEXT_FACTORY、PROVIDER_URL とは何なのかを説明していきましょう。
コンテキストと InitialContext
コンテキストとは・・・?
コンテキストとはバインディングの集合です。また、コンテキストにはサブコンテキストを含めることができます・・・と書いてしまうと何だか分かったような分からないような気がしてしまいますが、誤解を恐れずに書くと、ファイルシステムのディレクトリーに似ています。
JNDI ネームスペースとファイルシステムを比較してみると、
JNDI ネームスペース | ファイルシステム |
---|---|
コンテキスト | ディレクトリー |
バインディング | ファイル |
サブコンテキスト | サブディレクトリー |
のような感じです。絵にしてみると・・・
となります。
上の 2つの図には微妙な違いがあるのですが、お気づきでしょうか・・・?
そうですね、JNDI ネームスペースの図では片矢印なのに、ファイルシステムの図では両矢印になっています。
ファイルシステムでは、(サブ)コンテキストやファイル(java.io.File
オブジェクト)は、親ディレクトリーの情報を持っています。コマンド・プロンプトで cd ..
とすると、カレント・ディレクトリーを親ディレクトリーに移すことができますね。
対して、コンテキスト(javax.naming.Context
オブジェクト)は親のコンテキストについての情報を持っていません。また、自分自身についての情報も持ちません (*)。コンテキストはあくまで、自分の子であるバインディングと(サブ)コンテキストの情報しか持っていません。
ここが、JNDI ネームスペースとファイルシステムの大きな違いです。
InitialContext とは・・・?
javax.naming.InitialContext
は、上の図で説明した「初期コンテキスト」を表す Java オブジェクトです。みなさまは先ほどのサンプルの中で既に目にしていますね。使い方についても簡単ですが先に触れました。
ここでは InitialContext の取得のしかた、メソッドの使い方についてもう少し詳しく見ていきます。
まず、取得のしかたは以下のようになります。サンプルにも何度か出てきていますが、おさらいしてみましょう。
Properties props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY, "<サービス・プ
ロバイダーのファクトリー・クラス名>");
props.put(Context.PROVIDER_URL, "<サービス・プロバイダーを示す URL>");
InitialContext ictx = new InitialContext(props);
ファクトリー・クラス
サービス・プロバイダーのファクトリー・クラス名は、サービス・プロバイダーごとに決まったものを使用します。よく知られているサービス・プロバイダーとそのファクトリー・クラス名を挙げておきます。
サービス・プロバイダー | ファクトリー・クラス |
---|---|
ファイルシステム | com.sun.jndi.fscontext.FSContextFactory , com.sun.jndi.fscontext.RefFSContextFactory |
LDAPv3 | com.sun.jndi.ldap.LdapCtxFactory |
DNS | com.sun.jndi.dns.DnsContextFactory |
NIS | com.sun.jndi.nis.NISCtxFactory |
RMI レジストリー | com.sun.jndi.rmi.registry.RegistryContextFactory |
WebSphere Application Server V5 CosNaming | com.ibm.websphere.naming.WsnInitialContextFactory |
おっ、WAS V5 の CosNaming を利用するためのファクトリー・クラス名が出てきました。次回以降はこのクラスを使って、WAS V5.1 で検証を行ってみることにしましょう。
プロバイダーURL
プロバイダーURL には、サービス・プロバイダーがサービスを JNDI 向けにネーミング・サービスを提供しているホストとポート(デフォルトでよければ省略可能)を指定します。
これも、サービス・プロバイダーごとに決まっています。同様に例を挙げておきます。
サービス・プロバイダー | プロバイダーURL の例 |
---|---|
ファイルシステム | file:/// |
LDAPv3 | ldap://<ホスト名> |
DNS | dns://<ホスト名> |
NIS | nis://<ホスト名>/<ドメイン名> , nis://<ドメイン名> |
RMI レジストリー | rmi://<ホスト名> |
WebSphere Application Server V5 CosNaming | corbaloc:iiop:<ホスト名> |
いずれも(ファイルシステムを除く)、デフォルトと異なるポートでサービスが提供されている場合は、URL の末尾に、
:<ポート番号>
を追加して、ポート番号を明示的に指定することができます。
InitialContext の使い方
さて、以上のようにして InitialContext を取得できたら、例えば次のようにして、目的のオブジェクトを bind することができます。
hwobj = new HelloWorldObj();
ictx.rebind("HelloWorld", hwobj);
bind には bind() と rebind() を使用することができます。違いは、既に同じ名前で bind されているオブジェクトがある場合、bind() では NamingException が throw されるのに対し、rebind() ではバインディングを上書きしてくれる点です。場合に応じて使い分けるのがいいでしょう。また、lookup は次のようになります。InitialContext の取得まではサーバー側と同じです。
あとは、
HelloWorldRMI hw = (HelloWorldRMI) ictx.lookup("HelloWorld");
のようにすれば OK です。lookup メソッドの引数は、目的のオブジェクトの JNDI 名です。
戻り値としてはさまざまな型のオブジェクトが考えられますので、(すべての Java クラスのスーパークラスである)Object 型で返されます。ですので、返されたオブジェクトはアプリケーション側で適切な型にキャストする必要があります。
以上の操作で、クライアント側では目的のオブジェクト(今回の例では HelloWorldRMIObj)がどのサーバーで実行されているかを知らなくても、JNDI 名さえ分かっていれば目的のオブジェクトを入手することが可能になります。
(*) つまり、親コンテキストへの参照や、自分自身の絶対パスを返すことができない、ということです。
これが分散アプリケーション、分散オブジェクト環境を利用することのメリットです。
さぁ~て、次回の「今さら人に訊けない JNDI」は~?
すみません、次回は何を書くか、まだ何も考えていません。「使ってみたくなる EJB」の時にも同じことを書きましたが、あの時は実は 2、3 回分の内容はなんとなくですが決まっていたのです。
今回は本当になにも決められていないのですが、WebSphere Application Server の JNDI サポートの範囲とネームスペースの論理構造(特に WAS ND で複数サーバー構成を取った場合)について、あとは実際に CosNaming を使ってなにかしなくちゃならないな、とは思っています。あ、RMI-IIOP の話もありましたね・・・。
なにをいつ書くかは本当に謎なのですが、きっと読んで損のない内容にしたいと思います。
では、次回もお付き合いのほど、よろしくお願いいたします。