レベル: 中級 馬場 剛, WebSphereテクニカル・セールス,
IBM
2004年 07月 06日 第1回「JNDIってなんだっけ?」
はじめに
みなさまこんにちは。
先日まで「使ってみたくなる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)するために利用されます。
ディレクトリー・サービス
ディレクトリー・サービスはネーミング・サービスの拡張版です。
ネーミング・サービスは名前から単にオブジェクト(への参照)を検索するのに対し、ディレクトリー・サービスはオブジェクトの属性も扱うことができます。
代表的な実装としてはDNS(Domain Name System)があります。
DNSはホスト名とIPアドレスの関連づけ(Aレコード)を保持しており、ホスト名からIPアドレスを解決することができます。
DNSはAレコードの他にも、SOAレコード(ドメインのゾーン情報)やNSレコード(ドメインのネーム・サーバー)、PTRレコード(逆引き情報)、MXレコード(ドメインのメール・サーバー)など、いくつかの属性を持っています。
これらの情報は、DNSサーバーの管理者が必要な設定を行うことでbindされます。
(*1) ちなみに、bindはUNIX系のDNSサーバーの名前にもなっていますね。
(*2) 住所も載っているので厳密にはディレクトリー・サービス(Telephone directoryというくらいですので)ですが、あまり気にしないで下さい。
|
JNDIのアーキテクチャーとサービス・プロバイダー
さて、上ではサービス・プロバイダーの例としてRMIレジストリーとDNSを挙げました (*1) 。
JNDI 1.2では他にも、ファイルシステム、NIS、LDAP、CosNamingサービス (*2) をサポートしています。WebSphere Application Server自身はCosNamingサービスを提供しています。
もちろん、WAS上のアプリケーションではCosNaming以外のサービスをJNDI経由で使用することも可能です。
JNDIのアーキテクチャーを図にすると、以下のようになります (*3) 。
図1:JNDIアーキテクチャー
ところで、DNSやRMIレジストリーをJNDI経由で利用できるというのはどういうことでしょう・・・?
そろそろ説明を読むのに飽きた方もいらっしゃると思います (*4) ので、サンプル・コードを書いて実際に動かしてみましょう。
(*1) 残念ながら、電話帳をJNDI経由で引くためのSPIは、今のところ製品化されていません。
(*2) CORBAで規定されるネーミング・サービスです。
(*3) SPI:Service Provider Interface
アプリケーション開発者が利用するのはAPI(Application Programming Interface)の方ですので、これを読んで下さっている方はあまり気にしなくて結構です。
(*4) 本当は、私自身が「動かさないと理解できない(信用できない)」クチだからです。世の中には資料を読んだだけで理解できる人も多くいるようですが、私にはとうてい無理です・・・。
|
やっぱり動かしてみるでしょ!
検証環境について
実際に動かしてみたい方は、Java 2 SDK 1.4以上の環境を用意して下のコードを打ち込んで(またはコピーして)コンパイル、実行してみて下さい。
私はWindows 2000上のWebSphere Studio Application Developer V5.1.2(トライアル版はこちらからから(US))で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."を受け取ることができるはずです。
図2:HelloWorld RMI C/Sシステムのアーキテクチャー
リモート・インターフェイス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 | スケルトン |
【リスト3:HelloWorldRMIObj.java】
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.
のように、"Hello World."が返ってくれば成功です。
「せっかくRMIなんだから、サーバーとクライアントを別のマシンで実行してみたい」とおっしゃる方は、クライアントとスタブ、リモート・インターフェイスのclassファイルを(Java 2 SDKのインストールされた)別のマシンの同じディレクトリーにコピーし、クライアントを実行してみて下さい。
|
(*) 今回ははやりのRMI-IIOPではなく、JRMP(Java Remote Method Protocol)ベースのRMIです。まずは基本から、です。IIOPと絡めた話は次回以降にどこかで書く・・・かもしれません。
|
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()の他にもいろいろなメソッドを持っています。詳しくはhttp://java.sun.com/(US)でJ2SE 1.4のJavadocを参照して下さい。
どうやら、JNDIを使っていろいろな操作を行うためには、InitialContext (*) を取得することが第一歩のようです。
では、InitialContextやそのコンストラクターに与えた(Propertiesに含まれる)INITIAL_CONTEXT_FACTORY、PROVIDER_URLとは何なのかを説明していきましょう。
|
(*) 今後、特に区別する必要のない場合は、InitialContextとInitialDirContextをまとめて、InitialContextまたはイニシャル(初期)コンテキストと呼ぶことがあります。InitialDirContextはextends InitialContextですので。
|
コンテキストとInitialContext
コンテキストとは・・・?
コンテキストとはバインディングの集合です。また、コンテキストにはサブコンテキストを含めることができます。
・・・と書いてしまうと何だか分かったような分からないような気がしてしまいますが、誤解を恐れずに書くと、ファイルシステムのディレクトリーに似ています。
JNDIネームスペースとファイルシステムを比較してみると、
| JNDIネームスペース | ファイルシステム |
|---|
| コンテキスト | ディレクトリー | | バインディング | ファイル | | サブコンテキスト | サブディレクトリー |
のような感じです。絵にしてみると・・・
図3: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の話もありましたね・・・。
なにをいつ書くかは本当に謎なのですが、きっと読んで損のない内容にしたいと思います。
では、次回もお付き合いのほど、よろしくお願いいたします。
参考文献
著者について  | |  | 馬場 剛, WebSphereテクニカル・セールス, IBM |
記事の評価
|