[技術講座]
第一回目では、CORBA の仕組みと簡単な CORBA アプリケーションの作成方法について取り上げました。銀行口座の CORBA オブジェクトを IDL で定義し、その CORBA オブジェクトの IOR をファイル経由で CORBA サーバから CORBA クライアントに渡しました。IOR は、分散システムの中に存在する CORBA オブジェクトの位置を特定するための識別子のようなものであり、CORBA クライアントが何らかの方法で取得できれば良いものです。今回は、その IOR をファイル経由ではなく、ネーミングサービスを利用して入手する方法を解説します。ネーミングサービスは、CORBA サービスの一つであり、CORBA オブジェクトの位置情報を名前で検索、登録するための機能をアプリケーション開発者に提供します。
ちなみに、CORBA サービスはアプリケーションを開発する上で便利な共通のサービスを定義したものであり、他にも トレーダサービスやトランザクションサービスなどの 10 種類以上のサービスが仕様で定義されております。次回以降、出来るだけ多くの CORBA サービスを取り上げていきたいと思います。
さて、第二回目では、 CORBA 準拠の ORB 製品として VisBroker ( BES VisiBroker Edition )、JavaIDL ( J2SE 1.4.1 )、そして、新しく JacORB を利用します。JacORB は、 CORBA 2.3 準拠のフリーの ORB 製品です。現在の最新バージョンである JacORB 1.4.1 を利用します。
まず、最初に今回作成するシステムの構成についてみておきましょう ( 図 1 )。
![]() |
図 1: システム構成
|
サーバ側に VisiBroker と JacORB を利用し、クライアント側に JavaIDL を利用します。そして、ネーミングサービスには、VisiBroker のネーミングサービスを利用します。また、Java と C++ の実装の違いを比較するために、JacORB では Java を、VisiBroker では C++ を使用することにします。
次に、準備する環境について見ておきましょう。3 つの CORBA 製品を利用しますが、1 台の PC 上で動作させます。Windows 2000 マシンを用意して以下のようなディレクトリ構成を作成してください。 bat ファイルでそれぞれの CORBA 製品の環境を切り替え、対応するディレクトリの中で CORBA サーバまたは CORBA クライアントを実装していきます。
CORBA
VisiBroker
BES VisiBroker Edtion 5.1 用のディレクトリ JavaIDL
Java IDL (J2SE 1.4) 用のディレクトリ JacORB
JacORB 1.4.1 用のディレクトリ
VisiBroker では、C++ を使用するので、Windows 2000 環境では Microsoft の Visual C++ 6.0 のコンパイラが必要になります。VisiBroker、JavaIDL、JacORB の環境設定に関しては、補足を参照してください。
今回も引き続き銀行口座オブジェクトを利用します ( 図 2 )。銀行口座オブジェクトの変更点はありません。今回も UML で表現するクラス図には UML
Profile for CORBA [2] で定められているステレオタイプを利用します。CORBAInterface
は CORBA オブジェクトを表すステレオタイプです。
![]() |
図 2: 銀行口座 CORBA オブジェクトのクラス図
|
Account CORBA オブジェクトの IDL を以下に示します。IDL の内容については、第一回目の「 4. IDL で CORBA オブジェクトを定義する 」を見てください。
// Bank.idl module Bank { interface Account { long getBalance(); string getName(); }; };
ネーミングサービスは、CORBA オブジェクトのオブジェクトリファレンスに名前を付けて登録し、逆に名前からオブジェクトリファレンスを検索する機能をアプリケーション開発者に提供します。また、関連付ける名前は、ファイルシステムのようにディレクトリ構造を作成することによって階層的に管理することができます。ネーミングサービスでは、ディレクトリに相当するオブジェクトをネーミングコンテキストと呼びます。
![]() |
図 3: ネーミングサービスの利用イメージ
|
図 3 はネーミングサービスを利用したイメージ図です。最上位にあるルートコンテキストの直下にある Bank ネーミングコンテキストに CORBA オブジェクトのリファレンスを Nagata という名前で登録しています。また、Bank ネーミングコンテキストに別の CORBA オブジェクトのリファレンスを OGIS-RI という名前で登録しています。そして、クライアントは、ネーミングサービスに対して名前で検索することによって Nagata に関連付けられた CORBA オブジェクトのリファレンスを取得しています。
では、ネーミングサービスが提供するサービスについて見ていくことにしましょう。まず、全体像を知ってもらうためにネーミングサービスが提供する CosNaming
モジュールを
図 4 に示します。UML Profile for CORBA [2] を使用して表現しているため、IDL
で定義している型がステレオタイプとして表記されています。IDL で定義している型との対応関係を知りたい方は、CosNaming.idl
ファイルと図 4 を見比べてください。図 4 をクリックすると大きな画像が別ウィンドウとして表示されます。
図 4: ネーミングサービスのクラス図 (CosNaming.idl)
|
最も使用される頻度が高いサービスを赤色の背景で示しています。ここでは、赤色の背景のサービスだけを説明することにします。
まず、ネーミングサービスで利用される名前ですが、これは以下のような id
と kind
の 2 つの string
型の属性からなる構造体 (NameComponent
) で表現されます。これはネームコンポーネントと呼ばれます。そして、このネームコンポーネントの
id
と kind
の属性によって、ネーミングコンテキストの中で一意なオブジェクトが識別されます。
module CosNaming { typedef string Istring; struct NameComponent { Istring id; Istring kind; }; typedef sequence<NameComponent> Name; };
ネームコンポーネントの id
と kind
の属性は、null
文字と印刷不可能な文字を除く ISO 8859-1 (Latin-1) 文字セットの文字である必要があります。従って、日本語等のマルチバイト文字列は使用することはできません。どちらの文字列も、最大文字数は
255 文字までです。ネームコンポーネントの id
属性は空の文字列にすることはできませんが、kind
属性は可能です。 kind
属性は、名前に対する追加の特性を記述します。また、ネームコンポーネントのシーケンスは、コンパウドネームと呼ばれます。これは、IDL
の typedef
を利用して NameComponent
のシーケンス( Name
型 ) として定義されています。このコンパウドネームを利用すると、ネーミングコンテキストからみた相対名を指定して、CORBA オブジェクト、または、ネーミングコンテキストを取得することが出来ます。
次は、ネーミングコンテキストが提供するサービスについて見てみましょう。
module CosNaming { interface NamingContext { void bind(in Name n, in Object obj) raises(NotFound, CannotProceed, InvalidName, AlreadyBound); void rebind(in Name n, in Object obj) raises(NotFound, CannotProceed, InvalidName); void bind_context(in Name n, in NamingContext nc) raises(NotFound, CannotProceed, InvalidName, AlreadyBound); void rebind_context(in Name n, in NamingContext nc) raises(NotFound, CannotProceed, InvalidName); Object resolve (in Name n) raises(NotFound, CannotProceed, InvalidName); void unbind(in Name n) raises(NotFound, CannotProceed, InvalidName); NamingContext new_context(); NamingContext bind_new_context(in Name n) raises(NotFound, AlreadyBound, CannotProceed, InvalidName); void destroy() raises(NotEmpty); void list(in unsigned long how_many, out BindingList bl, out BindingIterator bi); }; };
IDL のさまざまなキーワードが出てきましたので併せて説明しておきます。まず、オペレーションの引数ですが、よくみると型名の前に in
や out
などのキーワードが付いています。これは、オペレーションの引数として渡すデータのモードを表すキーワードであり、以下の 3 つの種類が存在します。
in |
入力引数を表す(クライアントからサーバに対してデータが渡される) |
out |
出力引数を表す(サーバからクライアントに対してデータが渡される) |
inout |
入出力引数を表す( in と out を併せたモード ) |
また、オペレーションの後に raises
というキーワードがあります。これは、そのオペレーションが発生させる(可能性のある)例外を定義したものです。CORBA
の IDL で定義した例外を raises
キーワードの ( ) の中に、「,
」区切りで例外名を並べて定義します。これらの例外名も
CosNaming
モジュールの中で exception
キーワードを使って定義されています。
IDL について理解できたところで、ネーミングコンテキストが提供するオペレーションについて見てみましょう。それぞれのオペレーションは以下の機能を提供します。
bind |
指定された Object を指定された Name で登録します。 |
rebind |
指定された Name で既に別のオブジェクトが登録されている場合、その登録されているオブジェクトを指定された
Object に置き換えます。 |
bind_context |
指定された NamingContext を指定された Name で登録します。 |
rebind_context |
指定された Name で既に別のネーミングコンテキストが登録されている場合、そのネーミングコンテキストを指定された
NamingContext に置き換えます。 |
resolve |
指定された Name を解決し、オブジェクトリファレンスを返します。 |
unbind |
指定された Name に関連付けられている登録を削除します。 |
new_context |
新しいネーミングコンテキストを作成します。 |
bind_new_context |
新しいネーミングコンテキストを作成し、指定された Name にそのコンテキストを登録します。 |
destroy |
ネーミングコンテキストを非活性化します。このネーミングコンテキストに登録されているオブジェクトをすべて
unbind オペレーションによって登録を削除しておく必要があります。 |
list |
ネーミングコンテキストに保持されているすべての登録情報を返します。how_many 個までの登録情報が BindList に返されます。残りの登録情報は、BindingIterator を介して返されます。 |
例えば、bind
オペレーションは、ネーミングコンテキスト内に CORBA オブジェクトを登録します。また、bind_context
オペレーションでは、ネーミングコンテキスト内にネーミングコンテキストを登録することができます。ネーミングサービスから
CORBA オブジェクトを検索するときは、resolve
オペレーションを使用します。resolve
オペレーションの引数に名前を指定することによって、ネーミングコンテキストに登録されている
CORBA オブジェクト、または、ネーミングコンテキストのリファレンスを取得することができます。
さて、
ネーミングサービスでは、もう一つよく使うインタフェースとして NamingContextExt
インタフェースがあります。NamingContextExt
インタフェースは NamingContext
インタフェースを拡張したインタフェースであり、名前の指定方法が NamingContext
インタフェースよりも簡単になっています。NamingContext
インタフェースでは、Name
型を作成してオペレーションの引数に渡す必要がありましたが、NamingContextExt
では、Name
型を作成してなくても、文字列形式で名前を指定することが可能となっています。こちらの利用方法は 「 6.
クライアントの実装:名前の検索 ( JavaIDL ) 」 で取り上げることにします。
では、ネーミングサービスに登録するサーバの処理について見ていきましょう。
サーバでは、CORBA クライアントのリクエストを受け付ける CORBA サーバと CORBA オブジェクトの実装であるサーバントを実装します。
まず、サーバントについて見ていきましょう。
先に、JacORB を使用して Java の Account サーバントを実装します。Account のスケルトンを作成するために Bank.idl
ファイルを JacORB の IDL コンパイラである idl
コマンドを使ってコンパイルしてください。idl
コマンドは補足で作成する
bat ファイルです。実際は、java
コマンドで org.jacorb.idl.parser
クラスを起動していることに注意してください。
JacORB> idl Bank.idl
idl
コマンドを実行すると、実行したディレクトリに Bank というディレクトリが作成され、スタブソース、スケルトンソース、各種ユーティリティソースが生成されます。サーバントの実装は第一回目と同一です。実装の詳しい説明については、第一回目の「
7. CORBA オブジェクトの実装:サーバント 」を見てください。
// AccountImpl.java
public class AccountImpl extends Bank.AccountPOA {
private String _name;
private int _balance;
public AccountImpl(String name, int balance) {
_name = name;
_balance = balance;
}
public int getBalance() {
return _balance;
}
public String getName() {
return _name;
}
}
次は、VisiBroker を使用して C++ の Account サーバントを実装します。C++ でも、Account のスケルトンを作成して実装します。Bank.idl
ファイルを C++ 用 IDL コンパイラである idl2cpp
コマンドを使用してコンパイルします。-src_suffix
オプションは生成される
C++ のソースファイルの拡張子を .cpp
にするためのオプションです。
VisiBroker> idl2cpp -src_suffix cpp Bank.idl
以下のスタブソース、スケルトンソースが自動生成されます。
実装するべきオペレーションは、Bank_s.hh ヘッダファイルの以下の 2 つのオペレーションになります。
class POA_Bank { public: class Account : public Bank::Account_ops ,public virtual PortableServer_ServantBase { ・ ・ ・ // The following operations need to be implemented virtual ::CORBA::Long getBalance() = 0; virtual char* getName() = 0; ・ ・ ・ }; };
では、サーバントを実装します。今回は、サーバントの実装を BankImpl.h というヘッダファイルの中に実装することにします。
#include "Bank_s.hh" // USE_STD_NS is a define setup by VisiBroker to use the std namespace USE_STD_NS class AccountImpl : public virtual POA_Bank::Account, public virtual PortableServer::RefCountServantBase { public: AccountImpl(CORBA::Long balance, char *name){ _balance = balance; _name = name; }; CORBA::Long getBalance() { return _balance; } char * getName() { return _name; } private: CORBA::Long _balance; CORBA::String_var _name; };
Java と C++ のサーバントの実装の違いについて比較してみてください。Java も C++ もスケルトン(Java の場合:Bank.AccountPOA
、C++
の場合:POA_Bank::Account
)を継承している部分においては共通です。しかし、C++ では、PortableServer::RefCountServantBase
クラスも継承しています。
PortableServer::RefCountServantBase
クラスを継承するのは、C++ ではメモリ管理を意識する必要があるためです。
new
によって確保したサーバントのためのメモリ領域は、不要になった後、delete
によって開放すれば良いのですが、サーバントのメモリ領域は ORB
からも参照されている可能性があるため不用意に開放することはできません。プログラマ側で不用意にサーバントのメモリ領域を開放してしまうと、メモリアクセスエラー等を発生させる可能性があります。そこで、CORBA
ではサーバントのメモリ領域を自動的に管理するためのリファレンスカウンタが導入されております。
リファレンスカウンタとは、そのサーバントを指しているリファレンスの個数を記録するカウンタです。サーバントに対するリファレンスが 1 個増えるごとに、リファレンスカウンタに 1 を加えます。サーバントを指していたリファレンスが 1 つ減るごとに、リファレンスカウンタから 1 を引きます。そして、リファレンスカウンタの値が 0 になったら、そのサーバントのメモリ領域を解放します。このリファレンスカウンタの働きによって、不要になったサーバントのメモリ領域が確実に解放されることになります。
PortableServer::RefCountServantBase
クラスには、リファレンスカウンタに関する操作の実装があります。従って、PortableServer::RefCountServantBase
クラスを継承することによって、サーバントに対してリファレンスカウンタの機能が追加されます。
では、クライアントからのリクエストを受け付ける CORBA サーバを実装します。 CORBA サーバの処理は基本的に第一回目と同一です。5 と 9 の間でネーミングサービスに対して CORBA オブジェクトのリファレンスを登録している部分が異なります。
では、JacORB を使用する Java の CORBA サーバから見ていきましょう。
1:// Server.java 2: 3:import org.omg.PortableServer.*; 4:import org.omg.CosNaming.*; 5:import org.omg.CosNaming.NamingContextPackage.*; 6:import java.io.*; 7: 8:public class Server { 9: 10: public static void main(String[] args) { 11: try { 12: // ORB の初期化 13: org.omg.CORBA.ORB orb = org.omg.CORBA.ORB.init(args,null); 14: 15: // RootPOA のオブジェクトリファレンスを取得 16: org.omg.CORBA.Object obj = orb.resolve_initial_references("RootPOA"); 17: POA rootPOA = POAHelper.narrow(obj); 18: 19: // サーバントの生成 20: AccountImpl accountServant = new AccountImpl("OGIS-RI",5000); 21: 22: // RootPOA 上でサーバントを活性化 23: rootPOA.activate_object(accountServant); 24: 25: // POA マネージャの活性化 26: rootPOA.the_POAManager().activate(); 27: 28: // サーバントからオブジェクトリファレンスを取得 29: org.omg.CORBA.Object reference = rootPOA.servant_to_reference(accountServant); 30: 31: // ネーミングサービスのルートコンテキストのリファレンスを取得 32: obj = orb.resolve_initial_references("NameService"); 33: 34: // ルートコンテキストのリファレンスを NamingContext 型に変換 35: NamingContext rootCtx = NamingContextHelper.narrow(obj); 36: 37: // 名前を作成する 38: NameComponent[] nameCtx = { new NameComponent("Bank", "") }; 39: 40: NamingContext bankCtx; 41: try{ 42: // ネーミングコンテキストを取得する 43: obj = rootCtx.resolve(nameCtx); 44: bankCtx = NamingContextHelper.narrow(obj); 45: }catch(NotFound e){ 46: // ネーミングコンテキストが存在しない場合 47: // 作成した名前でネーミングコンテキストを登録 48: System.out.println("Exception: " + e); 49: bankCtx = rootCtx.bind_new_context(nameCtx); 50: } 51: 52: // 登録する名前を作成する 53: NameComponent[] nameObj = { new NameComponent("OGIS-RI", "Account") }; 54: try{ 55: // 作成した名前で Account のオブジェクトリファレンスを登録 56: bankCtx.bind(nameObj,reference); 57: }catch(AlreadyBound e){ 58: // 既にオブジェクトリファレンスが登録されている場合 59: // 再度、作成した名前でオブジェクトリファレンスを登録 60: System.out.println("Exception: " + e); 61: bankCtx.rebind(nameObj, reference); 62: } 63: 64: // クライアントからのリクエストに待機 65: System.out.println("Server is ready."); 66: orb.run(); 67: } 68: catch (Exception e) { 69: e.printStackTrace(); 70: } 71: } 72:}
31 行目から 72 行目までがネーミングサービスに対して CORBA オブジェクトを登録するために追加されたコードです。それ以外のコードについては、第一回目の 「 9. CORBA サーバの実装 ( VisiBroker ) 」 を見てください。
ネーミングサービスを利用するには、まず、ネーミングサービスのルートコンテキストのリファレンスを取得する必要があります。これは、32
行目にあるように、resolve_initial_references
メソッドの引数に対して NameService
の識別名
(文字列) を指定することによって取得します。resolve_initial_references
メソッドは、ネーミングサービスに限らず、さまざまなサービスのリファレンスを取得するときに利用するメソッドです。RootPOA
のリファレンスを取得するときにも利用されています。
次に、ネーミングサービスに対して登録する名前を作成します。登録する名前は、ネームコンポーネントのシーケンスである Name
型を利用する必要があります。Java
では、NameComponent[]
として表現します。38 行目で登録する Bank という名前を作成しています。43
行目では、その Bank という名前で検索を行なっています。既に Bank という名前が登録されていれば、登録された名前から取得したリファレンスをネーミングコンテキストに変換します。もし、Bank
という名前が存在しなければ、resolve
メソッドは、NotFound
例外をスローします。NotFound
例外がスローされると、新しい Bank
という名前のネーミングコンテキストを作成します ( 49 行目)。
何からの方法で Bank ネーミングコンテキストのリファレンスを取得した後、Account の CORBA オブジェクトを登録するための名前を作成しています
( 53 行目 )。ここでは、ネームコンポーネントの id
と kind
に対して、それぞれ "OGIS-RI"
と "Account"
を設定しています。そして、その名前で
Bank ネーミングコンテキストに対して Account CORBA オブジェクトのリファレンスを登録しています。もし、その名前が既に使用されていると
bind
メソッドは、AlreadyBound
例外をスローします。AlreadyBound
例外がスローされると、rebind
メソッドを利用して、その名前で登録されている CORBA オブジェクトを、強制的に登録する Account の CORBA オブジェクトのリファレンスに置き換えています。
以上が、ネーミングサービスに対して CORBA オブジェクトのリファレンスを登録する流れです。
では、VisiBroker の C++ のサーバを見てみましょう。
1:#include "BankImpl.h" 2:#include "CosNaming_c.hh" 3:#include "CosNamingExt_c.hh" 4: 5:// USE_STD_NS is a define setup by VisiBroker to use the std namespace 6:USE_STD_NS 7: 8:int main(int argc, char* const* argv) 9:{ 10: try { 11: // ORB の初期化 12: CORBA::ORB_var orb = CORBA::ORB_init(argc, argv); 13: 14: // RootPOA のオブジェクトリファレンスを取得 15: CORBA::Object_var obj = orb->resolve_initial_references("RootPOA"); 16: PortableServer::POA_var rootPOA = PortableServer::POA::_narrow(obj); 17: 18: // サーバントの生成 19: AccountImpl accountServant(10000.0,"NAGATA"); 20: 21: // RootPOA 上でサーバントの活性化 22: rootPOA->activate_object(&accountServant); 23: 24: // POA マネージャの取得 25: PortableServer::POAManager_var poa_manager = rootPOA->the_POAManager(); 26: 27: // POA マネージャの活性化 28: poa_manager->activate(); 29: 30: // サーバントからオブジェクトリファレンスを取得 31: CORBA::Object_var reference = rootPOA->servant_to_reference(&accountServant); 32: 33: // ネーミングサービスのルートコンテキストのリファレンスを取得 34: obj = orb->resolve_initial_references("NameService"); 35: 36: // ルートコンテキストのリファレンスを NamingContext 型に変換 37: CosNaming::NamingContext_var rootCtx = CosNaming::NamingContext::_narrow(obj); 38: 39: // 名前を作成する 40: CosNaming::Name nameCtx; 41: nameCtx.length(1); 42: nameCtx[0].id = CORBA::string_dup("Bank"); 43: nameCtx[0].kind = CORBA::string_dup(""); 44: 45: CosNaming::NamingContext_var bankCtx; 46: try{ 47: // ネーミングコンテキストを取得する 48: obj = rootCtx->resolve(nameCtx); 49: bankCtx = CosNaming::NamingContext::_narrow(obj); 50: }catch(const CosNaming::NamingContext::NotFound& e){ 51: cout << "Exception: " << e << endl; 52: // ネーミングコンテキストが存在しない場合 53: // 作成した名前でネーミングコンテキストを登録 54: bankCtx = rootCtx->bind_new_context(nameCtx); 55: } 56: 57: // 登録する名前を作成する 58: CosNaming::Name nameObj; 59: nameObj.length(1); 60: nameObj[0].id = CORBA::string_dup("Nagata"); 61: nameObj[0].kind = CORBA::string_dup("Account"); 62: 63: try{ 64: // 作成した名前で Account のオブジェクトリファレンスを登録 65: bankCtx->bind(nameObj,reference); 66: }catch(const CosNaming::NamingContext::AlreadyBound& e){ 67: // 既にオブジェクトリファレンスが登録されている場合 68: // 再度、作成した名前でオブジェクトリファレンスを登録 69: cout << "Exception: " << e << endl; 70: bankCtx->rebind(nameObj, reference); 71: } 72: 73: // クライアントからのリクエストに待機 74: cout << "Server is ready" << endl; 75: orb->run(); 76: } 77: catch(const CORBA::Exception& e) { 78: cerr << e << endl; 79: return 1; 80: } 81: return 0; 82:}
Java のコードと比較するとわかると思いますが、基本的には同じです。注意点としては、変数の型として <名前>_var
クラスを利用しているところです。C++
には、CORBA の変数の型として <名前>_ptr
型と <名前>_var
型が存在します。<名前>_var
型を利用することによって、メモリ管理を意識せずに実装ができるので、
C++ では <名前>_var
型を利用することが推奨されています。
<名前>_ptr 型 |
<名前> クラスに対するポインタを typedef で宣言した型。従って、この型を使用する場合は、確保したメモリ領域を明示的に開放しなければなりません。 |
<名前>_var 型 |
メモリ管理を簡単するために用意されたクラス。このクラスを使用した場合は、オブジェクトのインスタンスが破棄されたり、新しい値がオブジェクトに割り当てられたときに自動的にメモリ領域が解放されます。また、スコープから抜ける場合にも自動的にメモリ領域が開放されます。 |
では、Java と C++ の両サーバの実装が完成したのでそれぞれをコンパイルしましょう。JacORB では、javac
コマンドを利用します。
JacORB> javac -classpath %CLASSPATH%;. *.java
VisiBroker では、nmake
コマンドを使ってコンパイルします。
VisiBroker> nmake -f Makefile.cpp
コンパイルには、Makefile.cpp ファイルと stdmk_nt ファイルが必要になります。これは、VisiBroker の環境を設定するための make ファイルです。
CORBA クライアントは、JavaIDL の ORB を利用します。まず、CORBA クライアントを実装するためのスタブを IDL ファイルから生成します。
JavaIDL> idlj -fclient Bank.idl
CORBA クライアントでは、基本的に次のような流れで処理を実装します。
では、CORBA クライアントのコードを見ながら、どのようにネーミングサービスに対して名前を検索するのか見ていきましょう。
1:// Client.java 2: 3:import org.omg.CosNaming.*; 4:import java.io.*; 5: 6:public class Client { 7: 8: public static void main(String[] args) { 9: try { 10: // ORB の初期化 11: org.omg.CORBA.ORB orb = org.omg.CORBA.ORB.init(args,null); 12: 13: // ネーミングサービスのルートコンテキストのリファレンスを取得 14: org.omg.CORBA.Object obj = orb.resolve_initial_references("NameService"); 15: 16: // ルートコンテキストのリファレンスを NamingContextExt 型に変換 17: NamingContextExt rootCtx = NamingContextExtHelper.narrow(obj); 18: 19: Bank.Account account; 20: 21: System.out.println(""); 22: System.out.println("------------------- Case 1 -------------------"); 23: // Bank のネーミングコンテキストを取得してから Account オブジェクトの 24: // リファレンスを取得する方法 25: 26: // Bank のネーミングコンテキストを取得する 27: obj = rootCtx.resolve(rootCtx.to_name("Bank")); 28: NamingContextExt bankCtx = NamingContextExtHelper.narrow(obj); 29: 30: // JacORB で登録した Account オブジェクトを取得する 31: // NamingContextExt の to_nmae() メソッドを利用して文字列から名前を作成している 32: obj = bankCtx.resolve(bankCtx.to_name("OGIS-RI.Account")); 33: account = Bank.AccountHelper.narrow(obj); 34: 35: // Account オブジェクトの情報を表示する 36: infoAccount(account); 37: 38: // VisiBrokerORB で登録した Account オブジェクトを取得する 39: // NamingContextExt の resolve_str() メソッドを利用して 40: // 文字列から直接リファレンスを取得している 41: obj = bankCtx.resolve_str("Nagata.Account"); 42: account = Bank.AccountHelper.narrow(obj); 43: 44: // Account オブジェクトの情報を表示する 45: infoAccount(account); 46: 47: System.out.println("------------------- Case 2 -------------------"); 48: // Account オブジェクトの名前階層を直接指定して取得する方法 49: 50: // 取得したいオブジェクトの名前を作成する 51: NameComponent[] name = { new NameComponent("Bank", "") 52: ,new NameComponent("OGIS-RI", "Account") }; 53: 54: // JacORB で登録した Account オブジェクトを取得する 55: obj = rootCtx.resolve(name); 56: account = Bank.AccountHelper.narrow(obj); 57: 58: // Account オブジェクトの情報を表示する 59: infoAccount(account); 60: 61: // VisiBrokerORB で登録した Account オブジェクトを取得する 62: // NamingContextExt の to_name メソッドを利用して文字列から名前を作成している 63: obj = rootCtx.resolve(rootCtx.to_name("Bank/Nagata.Account")); 64: account = Bank.AccountHelper.narrow(obj); 65: 66: // Account オブジェクトの情報を表示する 67: infoAccount(account); 68: 69: System.out.println(""); 70: System.out.println("------------------- Case 3 -------------------"); 71: // 名前を指定せずに名前階層を検索して Account オブジェクトを取得する方法 72: listAccount(rootCtx); 73: } 74: catch(Exception e) { 75: e.printStackTrace(); 76: } 77: } 78: 79: // 指定された Account オブジェクトの情報を表示するメソッド 80: private static void infoAccount(Bank.Account account){ 81: System.out.println 82: ("The balance in " + account.getName() + "'s account is " 83: + account.getBalance() + " yen."); 84: } 85: 86: // 指定されたネーミングコンテキスト以下の名前階層を検索し、 87: // Account オブジェクトの情報を表示するメソッド 88: private static void listAccount(NamingContext ctx){ 89: try{ 90: int how_many = 0; 91: 92: // list メソッドに渡す out モードの Holder クラスを作成 93: BindingListHolder listHdr = new BindingListHolder(); 94: BindingIteratorHolder iteratorHdr = new BindingIteratorHolder(); 95: 96: // ctx ネーミングコンテキストに登録されているオブジェクトを取得する 97: ctx.list(how_many, listHdr, iteratorHdr); 98: 99: // list メソッドの結果を Holder クラスから取り出す 100: BindingIterator iterator = iteratorHdr.value; 101: 102: // iterator メソッドに渡す out モードの Holder クラスを作成 103: BindingHolder nodeHdr = new BindingHolder(); 104: 105: // ctx ネーミングコンテクスト直下に登録されている 106: // すべてのオブジェクトに対して以下の処理を繰り返す 107: while( iterator.next_one(nodeHdr) == true ){ 108: // next_one メソッドの結果を Holder クラスから取り出す 109: Binding node = nodeHdr.value; 110: 111: // 登録タイプがネーミングコンテキストの場合 112: if( node.binding_type == BindingType.ncontext ){ 113: try{ 114: // 登録されているネーミングコンテキストを取得し、 115: // そのネーミングコンテキスト以下に Account オブジェクトが 116: // 存在するかどうか検索する 117: org.omg.CORBA.Object obj = ctx.resolve(node.binding_name); 118: NamingContext subCtx = NamingContextHelper.narrow(obj); 119: listAccount(subCtx); 120: }catch(Exception e){ 121: e.printStackTrace(); 122: } 123: } 124: 125: // 登録タイプが CORBA オブジェクトの場合 126: if( node.binding_type == BindingType.nobject ){ 127: try{ 128: // 登録されている CORBA オブジェクトのリファレンスを取得する 129: // 登録されている CORBA オブジェクトが Account オブジェクトで 130: // ない場合は、CORBA.BAD_PARAM 例外が発生する 131: org.omg.CORBA.Object obj = ctx.resolve(node.binding_name); 132: Bank.Account account = Bank.AccountHelper.narrow(obj); 133: 134: // Account オブジェクトの情報を表示する 135: infoAccount(account); 136: }catch(org.omg.CORBA.BAD_PARAM exp){ 137: // Account オブジェクトでない場合は何もしない 138: }catch(Exception e){ 139: e.printStackTrace(); 140: } 141: } 142: } 143: }catch(Exception e){ 144: e.printStackTrace(); 145: } 146: } 147:}
クライアントでは Account オブジェクトを取得するための検索例を 3 つ紹介しています。それぞれの例について説明します。
ルートコンテキストから一旦 Bank ネーミングコンテキストを取得し、その Bank ネーミングコンテキストから 2 つの Account CORBA
オブジェクトのリファレンスを取得している例です。27 行目や 32行目では NamingContextExt
インタフェースの to_name
メソッドを利用して、名前の文字列形式から
CORBA オブジェクトのリファレンスを取得しています。名前の文字列形式では、ネームコンポーネントの id
と kind
を
"<id
名>.<kind
名>"
という文字列で表現します。kind
が空文字の場合は、「.
」も省略することができます。
22: System.out.println("------------------- Case 1 -------------------"); 23: // Bank のネーミングコンテキストを取得してから Account オブジェクトの 24: // リファレンスを取得する方法 25: 26: // Bank のネーミングコンテキストを取得する 27: obj = rootCtx.resolve(rootCtx.to_name("Bank")); 28: NamingContextExt bankCtx = NamingContextExtHelper.narrow(obj); 29: 30: // JacORB で登録した Account オブジェクトを取得する 31: // NamingContextExt の to_nmae() メソッドを利用して文字列から名前を作成している 32: obj = bankCtx.resolve(bankCtx.to_name("OGIS-RI.Account")); 33: account = Bank.AccountHelper.narrow(obj); 34: 35: // Account オブジェクトの情報を表示する 36: infoAccount(account); 37: 38: // VisiBrokerORB で登録した Account オブジェクトを取得する 39: // NamingContextExt の resolve_str() メソッドを利用して 40: // 文字列から直接リファレンスを取得している 41: obj = bankCtx.resolve_str("Nagata.Account"); 42: account = Bank.AccountHelper.narrow(obj); 43: 44: // Account オブジェクトの情報を表示する 45: infoAccount(account);
ルートコンテキストから一気に 2つ の Account
CORBA オブジェクトのリファレンスを取得している例です。55 行目では、51 行目で作成したコンパウドネームを使用して名前解決を行なっています。63
行目では、NamingContextExt
インタフェースの to_name
メソッドを利用して、名前の文字列形式から
CORBA オブジェクトのリファレンスを取得しています。名前の文字列形式では、名前階層の区切りを
"/"
で表現します。
47: System.out.println("------------------- Case 2 -------------------"); 48: // Account オブジェクトの名前階層を直接指定して取得する方法 49: 50: // 取得したいオブジェクトの名前を作成する 51: NameComponent[] name = { new NameComponent("Bank", "") 52: ,new NameComponent("OGIS-RI", "Account") }; 53: 54: // JacORB で登録した Account オブジェクトを取得する 55: obj = rootCtx.resolve(name); 56: account = Bank.AccountHelper.narrow(obj); 57: 58: // Account オブジェクトの情報を表示する 59: infoAccount(account); 60: 61: // VisiBrokerORB で登録した Account オブジェクトを取得する 62: // NamingContextExt の to_name メソッドを利用して文字列から名前を作成している 63: obj = rootCtx.resolve(rootCtx.to_name("Bank/Nagata.Account")); 64: account = Bank.AccountHelper.narrow(obj); 65: 66: // Account オブジェクトの情報を表示する 67: infoAccount(account);
Account CORBA オブジェクトの登録名がわからない場合の検索例です。BindingList
と BindIterator
を引数にとる
list
メソッドを利用しています。
list
メソッドは、引数として out
モードの BindingList
と BindIterator
を受け取ります。Java
では、out
モードの引数に対しては、Holder
クラスを利用します。93
行目から 100 行目までを見てください。それぞれの Holder
クラスのオブジェクトを生成し、それらの
Holder
オブジェクトを out
モードの引数に渡しています。out
モードの引数に Holder
オブジェクトを渡すと、そのオペレーションの実行結果が、Holder
オブジェクトの value
属性に格納されます。100
行目では、list
メソッドの結果を
Holder
オブジェクトから取り出しています。
さて、プログラムの中身についてですが、少々複雑です。list
メソッドを使用して、ネーミングコンテキスト内に登録されているオブジェクトを
BindIterator
経由で取得しています。BindIterator
によって参照できる Binding
オブジェクトが CORBA
オブジェクトのタイプであれば、Account CORBA オブジェクトかどうかを確認して内容を表示しています。一方、Binding
オブジェクトがネーミングコンテストのタイプであれば、そのネーミングコンテキスト内に
Account オブジェクトが存在するか検索を行います。これは、listAccount
メソッドを再帰的に呼び出すことで実現しています。これによって、listAccount
メソッドに渡された最初のネーミングコンテキスト以下のすべての Account オブジェクトが検索されます。
70: System.out.println("------------------- Case 3 -------------------"); 71: // 名前を指定せずに名前階層を検索して Account オブジェクトを取得する方法 72: listAccount(rootCtx); 86: // 指定されたネーミングコンテキスト以下の名前階層を検索し、 87: // Account オブジェクトの情報を表示するメソッド 88: private static void listAccount(NamingContext ctx){ 89: try{ 90: int how_many = 0; 91: 92: // list メソッドに渡す out モードの Holder クラスを作成 93: BindingListHolder listHdr = new BindingListHolder(); 94: BindingIteratorHolder iteratorHdr = new BindingIteratorHolder(); 95: 96: // ctx ネーミングコンテキストに登録されているオブジェクトを取得する 97: ctx.list(how_many, listHdr, iteratorHdr); 98: 99: // list メソッドの結果を Holder クラスから取り出す 100: BindingIterator iterator = iteratorHdr.value; 101: 102: // iterator メソッドに渡す out モードの Holder クラスを作成 103: BindingHolder nodeHdr = new BindingHolder(); 104: 105: // ctx ネーミングコンテクスト直下に登録されている 106: // すべてのオブジェクトに対して以下の処理を繰り返す 107: while( iterator.next_one(nodeHdr) == true ){ 108: // next_one メソッドの結果を Holder クラスから取り出す 109: Binding node = nodeHdr.value; 110: 111: // 登録タイプがネーミングコンテキストの場合 112: if( node.binding_type == BindingType.ncontext ){ 113: try{ 114: // 登録されているネーミングコンテキストを取得し、 115: // そのネーミングコンテキスト以下に Account オブジェクトが 116: // 存在するかどうか検索する 117: org.omg.CORBA.Object obj = ctx.resolve(node.binding_name); 118: NamingContext subCtx = NamingContextHelper.narrow(obj); 119: listAccount(subCtx); 120: }catch(Exception e){ 121: e.printStackTrace(); 122: } 123: } 124: 125: // 登録タイプが CORBA オブジェクトの場合 126: if( node.binding_type == BindingType.nobject ){ 127: try{ 128: // 登録されている CORBA オブジェクトのリファレンスを取得する 129: // 登録されている CORBA オブジェクトが Account オブジェクトで 130: // ない場合は、CORBA.BAD_PARAM 例外が発生する 131: org.omg.CORBA.Object obj = ctx.resolve(node.binding_name); 132: Bank.Account account = Bank.AccountHelper.narrow(obj); 133: 134: // Account オブジェクトの情報を表示する 135: infoAccount(account); 136: }catch(org.omg.CORBA.BAD_PARAM exp){ 137: // Account オブジェクトでない場合は何もしない 138: }catch(Exception e){ 139: e.printStackTrace(); 140: } 141: } 142: } 143: }catch(Exception e){ 144: e.printStackTrace(); 145: } 146: } 147:}
では、Client.java ファイルをコンパイルしてみましょう。JavaIDL を利用するので、次のように java
コマンドを使用してコンパイルしてください。
JavaIDL> javac Client.java
これで、サーバ、クライアントが完成しましたが、サーバ、クライアントはどのようにしてネーミングサービスのリファレンスを取得しているのでしょうか。resolve_initial_references("NameService")
メソッド
で返されるリファレンスはどのリファレンスになるのでしょうか。
初期のネーミングサービスの仕様では、ネーミングサービスのリファレンスの取得方法はベンダ依存となっていました。つまり、resolve_initial_references
("NameService")
メソッドで返されるリファレンスは ORB の実装に依存していました。従って、ネーミングサービスを利用したシステムを構築する場合には、同一の
CORBA 製品を利用する必要がありました。しかし、1999 年にインタオペラブルネーミングサービスが仕様化されることによって、ネーミングサービスのリファレンス(初期ネーミングコンテキスト)の与え方が規定されました。つまり、このインタオペラブルネーミングサービスが仕様化されたことによって、他のベンダのネーミングサービスとの相互運用が可能となったのです。
インタオペラブルネーミングサービスでは、従来のネーミングサービスに対して以下の機能が追加されています [5]。
「名前の文字列形式」と「名前の文字列形式とオブジェクト URL を使用したオペレーション」については、クライアントの実装で取り上げました。NamingContextExt
インタフェースの to_name
オペレーション等がそれに該当します。
ここでは、初期ネーミングコンテキストの与え方とオブジェクト URL について説明します。
初期ネーミングコンテキストは、クライアント、サーバの起動時に -ORBInitRef
を利用することによって与えます。例えば、以下のようにコマンドライン引数として
-ORBInitRef
を指定したとします。ここで、IOR:00000000000.......
は文字列形式の
IOR を表しています。
prompt> java Server -ORBInitRef NameService=IOR:00000000000.......
この場合、-ORBInitRef
で渡されたコマンドライン引数は、ORB::ORB_init
オペレーションに渡されます。そして、ORB::resolve_initial_references
オペレーションの引数として NameService
という識別名が指定された場合、-ORBInitRef
で指定されたオブジェクトリファレンスが返されることになります。これによって、ORB::resolve_initial_references
オペレーションに与える任意の識別名の初期リファレンスを与えることができます。
さて、-ORBinitRef
を使用して、ネーミングサービスの初期リファレンスを与える方法はわかりましたが、文字列化された
IOR を渡すのは非常に面倒です。これを回避するための方法として corbaloc URL または corbaname URL によるオブジェクト URL
の指定方法があります。ちなみに、文字列化された IOR を渡す方法もオブジェクト
URL に含まれます。
IOR 形式のオブジェクト URL は、GIOP のエンコーディング規則である CDR でエンコードされた IOR を 16 進表示したものです。従って、人が読んですぐに理解できる形式ではありません。これに比べて、corbaloc URL と corbaname URL は非常にわかりやすい形式を提供してくれます。
corbaloc URL 形式では、例えば、ローカルホスト上で起動されているポート番号 10000 のオブジェクトキー NameService
を持ったオブジェクトに対するリファレンスは以下のように表されます。
corbaloc:iiop:1.2@localhost:10000/NameService
この corbaloc URL 形式を利用して -ORBInitRef
に以下のように指定すると、ORB
は内部的にローカルホストのポート番号 10000 に IIOP のバージョン 1.2 を使って、オブジェクトキー NameService
に対するオブジェクトリファレンスを要求します。そして、ローカルホストのポート番号
10000 で起動されているネーミングサービスからそのリファレンスが返されます。
prompt> java Server -ORBInitRef NameService=corbaloc:iiop:1.2@localhost:10000/NameService
IIOP のプロトコル名とバージョン番号は省略することができます。その場合は、以下のような形式になります。なお、バージョンが省略された場合は、バージョン 1.0 が利用されます。
corbaloc::localhost:10000/NameService
corbaname URL は、URL でネーミングサービス内のエントリまで指定できるように corbaloc URL を拡張したものです。例えば、corbaname
URL を使って "OGIS-RI.Account"
の名前で登録された CORBA オブジェクトのリファレンスを取得する場合には、以下の形式で表現されます。
corbaname::localhost:10000/NameService#Bank/OGIS-RI.Account
この corbaname URL を使うと、例えば、ORB::string_to_object
オペレーションを使用して、指定した
CORBA オブジェクトのリファレンスが取得できます。
org.omg.CORBA.Object obj = orb.string_to_object("corbaname::localhost:10000/NameService#Bank/OGIS-RI.Account");
では、実際に動かしてみましょう。まず、VisiBroker のネーミングサービスを起動します。
VisiBroker> nameserv -J-Dvbroker.se.iiop_tp.scm.iiop_tp.listener.port=10000 \ -J-Dvbroker.agent.enableLocator=false
正常に起動すると、ネーミングサービスの文字列化された IOR がコンソール上に出力されます。また、その IOR も ns.ior というファイル名に出力されます。IOR
の中身について知りたい方は、VisiBroker の printIOR
コマンドを使用して ns.ior ファイルの中身を見てください。
-Dvbroker.se.iiop_tp.scm.iiop_tp.listener.port
プロパティは、ネーミングサービスが起動するポート番号を設定するプロパティです。今回は、ポート番号を
10000 に設定しています。また、-Dvbroker.agent.enableLocator プロパティ
は、VisiBroker
が提供する
Smart Agent の機能を設定するためのプロパティです。false
に設定することで無効にしています。このプロパティは第一回目の 「 11.アプリケーションの実行 」でも出てきました。
次は、CORBA サーバを起動します。JacORB または VisiBroker のどちらの CORBA サーバを先に起動しても問題ありません。ここでは、VisiBroker の CORBA サーバを先に起動することにしましょう。
VisiBroker> Server -ORBInitRef NameService=corbaloc::localhost:10000/NameService \ -Dvbroker.agent.enableLocator=false Exception: Exception: ::CosNaming::NamingContext::NotFound Server is ready
-ORBInitRef
を指定して、resolve_initial_references
メソッドで取得する初期ネーミングコンテキストをネーミングサービスのルートコンテキストに指定しています。また、Bank
ネーミングコンテキストの名前が解決できなかったため、NotFound
例外をキャッチしています。「 5-2.
CORBA サーバの実装 」 のプログラムを見てもらえればわかりますが、NotFound
例外をキャッチすると、Bank
ネーミングコンテキストを新たに作成します。「Server
is ready
」というメッセージがコンソール上に表示されたら O.K. です。
続いて、JacORB の CORBA サーバを起動しましょう。JacORB をネーミングサービスを利用して動かすには、jacorb.properties というプロパティファイルが必要になります。JacORB のインストールディレクトリに jacorb_properties.template というファイルが存在するので、このファイル名を jacorb.properties に変更して、CORBA サーバを実行するディレクトリに置いてください。 また、jacorb_properties.template ファイル中に存在する以下の行をコメントアウトしてください。以上で、起動のための設定は終了です。
ORBInitRef.NameService=https://www.x.y.z/~user/NS_Ref
JacORB のCORBA サーバは以下のコマンドで起動します。こちらも -ORBInitRef
を使用して初期ネーミングコンテキストを指定しています。
JacORB> jaco Server -ORBInitRef NameService=corbaloc::localhost:10000/NameService JacORB V 1.4.1, www.jacorb.org (C) Gerald Brose, FU Berlin/XTRADYNE Technologies, July 2002 [ POA RootPOA - ready ] [ Connected to 127.0.0.1:10000 ] [ Connected to 158.201.101.62:10000 ] Server is ready.
今度は、NotFound
例外はキャッチされませんでした。これは、VisiBroker の CORBA サーバが既に Bank ネーミングコンテキストを作成しているためです。こちらも最後に
「Server
is ready
」というメッセージがコンソール上に表示されたら O.K. です。
ちなみに、jaco
コマンドは bat ファイルです。bat ファイルの内容は補足を参照してください。 java
コマンドに対して JacORB の ORB を利用するようにプロパティで設定しています。
では、最後に JavaIDL のCORBA クライアントを起動しましょう。当然、クライアントからも -ORBInitRef
で初期ネーミングコンテキストを指定する必要があります。
JavaIDL> java Client -ORBInitRef NameService=corbaloc::localhost:10000/NameService ------------------- Case 1 ------------------- The balance in OGIS-RI's account is 5000 yen. The balance in NAGATA's account is 10000 yen. ------------------- Case 2 ------------------- The balance in OGIS-RI's account is 5000 yen. The balance in NAGATA's account is 10000 yen. ------------------- Case 3 ------------------- The balance in OGIS-RI's account is 5000 yen. The balance in NAGATA's account is 10000 yen.
上記メッセージがコンソール上に表示されましたか。
さて、今回は、ネーミングサービスについて取り上げ、ネーミングサービスに対する CORBA オブジェクトの登録方法と検索方法について見ていきました。また、CORBA クライアントからの様々な検索方法も紹介しました。 ネーミングサービスの機能についてすべてを紹介したわけではありませんが、この記事をきっかけとしてネーミングサービスに対する理解を深めていただけると嬉しいです。
本記事は、実際に動かすのに必要な情報をすべて掲載しています。もし、本記事を参考に動かして頂いた方がおられましたら、ご意見、ご感想を頂けると大変嬉しいです。また、取り上げて欲しい CORBA の技術、または、CORBA サービス等がありましたら、アンケートのコメント欄にでもご記入お願いします。出来る限り取り上げたいと思います。
今回使用する ORB 製品の環境の設定方法について説明します。本記事では、複数の ORB 製品を利用することになるので、基本的にシステム環境変数には、ソフトウェアに関する環境変数を設定しません。替わりにバッチファイルを利用して、利用する ORB 製品を切り替えます。
BES VisiBroker Edition 5.1、J2SE 1.4.1、JacORB 1.4.1 を以下のサイトよりダウンロードして下さい。
ダウンロードが完了したらインストールを行ってください。 インストールディレクトリ以外はデフォルトのままでインストールしてかまいません。以下のディレクトリにインストールすると仮定します。
C:\bes51
C:\jdk141
C:\JacORB141
JacORB 1.4.1 は、zip ファイルを展開するだけでインストール完了です。展開したファイルを C:\JacORB141
ディレクトリに移動してください。そして、bin
ディレクトリ以下に存在する
idltemplate.bat と jacotemplate.bat をそれぞれ idl.bat と jaco.bat にファイル名を変更し、ファイルの内容を以下のように修正してください。
@echo off rem call java interpreter java -Dorg.omg.CORBA.ORBClass=org.jacorb.orb.ORB -Dorg.omg.CORBA.ORBSingletonClass=org.jacorb.orb.ORBSingleton %*
また、各 ORB 製品の環境を切り替えるための次のようなバッチファイルを作成してください。
@echo off set VBROKERDIR=C:\bes51 set VBROKER_ADM=%VBROKERDIR%\var\servers\%COMPUTERNAME%\adm set JAVA_HOME=%VBROKERDIR%\jdk set include=%include%;%VBROKERDIR%\include; set lib=%lib%;%VBROKERDIR%\lib set PATH=%VBROKERDIR%\bin;%JAVA_HOME%\bin;%path% echo Set Environment for Borland Enterprise Server 5.1
@echo off set JAVA_HOME=C:\jdk141 set PATH=%JAVA_HOME%\bin;%PATH% echo Set Environment for JDK 1.4.1
@echo off REM JacORB 1.4.1 Enviroment set JACORB_HOME=C:\JacORB141 set CLASSPATH=%JACORB_HOME%\lib\jacorb.jar;%CLASSPATH% set CLASSPATH=%JACORB_HOME%\lib\idl.jar;%CLASSPATH% set PATH=%JACORB_HOME%\bin;%PATH% echo Set Environment for JacORB 1.4.1
「2. システム構成 」 で示した各ディレクトリで対応する製品の bat ファイルを実行してください。これによって今回使用する ORB 製品の環境が構築できます。なお、JacORB 1.4.1 は、別途 JDK 1.2 以上の JDK が必要になります。
© 2003 OGIS-RI Co., Ltd. |
|