シリーズの第3弾は、動的なモジュール化について述べる。どのようにバンドルのクラスが解決され、どのように生成され、消えるのか、どのようにお互いに通信するのかについて、書く。
前回の記事、 Modular Java: Static Modularityでは、どのようにJavaのモジュールがビルドされ、別々のJARとしてデプロイされるかを書いた。クライアントとサーバのバンドル(両方共同じVMにある)の例があり、クライアントは、ファクトリメソッドを介して、サーバを見つけた。その例では、ファクトリは、既知のクラスをインスタンス化したが、全く同じように、サービスを実装するのに、リフレクションを使うこともできた。Springは、Springオブジェクト同士をバインドするのにこの技法を多用している。
動的なサービスの話の前に、クラスパスについて振り返るのは、いいことである。というのは、標準のJavaとモジュール(化された)Javaとの違いのひとつは、実行時における依存性の結合方法にある。そのことを扱ったら、クラスのガーベッジコレクションについて、簡単に触れる。もし、そのことを良く知っているなら、そこを 飛ばして進んでよい。
バンドルのクラスパス
構造化されていないJavaプログラムでは、クラスパスは、ひとつしかない-アプリケーションがスタートしたところである。普通これは、コマンドラインで -classpath
を使って指定するか、CLASSPATH
環境変数を介して指定する。それからJavaのクラスローダは、実行時に、静的に(コンパイル時に、コードに埋め込んで)あるいは、動的に(リフレクションとclass.forName()を使って)クラスを解決しようとする時に、このパスをスキャンする。しかし実行時に、複数のクラスローダを使うことができる Jetty や Tomcat のようなwebアプリケーションエンジンは、アプリケーションの最新の(再)デプロイをサポートするのに、これを頻繁に使っている。
OGSiでは、各バンドルが、自身のクラスローダを持っている。他のバンドルからアクセスされるクラスは、そのアクセスしてきたバンドルのクラスローダに委譲される(delegated)。これまでのアプリケーションでは、ロギングライブラリ、クライアントのJARそしてサーバのJARからのクラスは、同じクラスローダによってロードされるかもしれないが、OSGiモジュールシステムでは、それぞれが自身のローダによってロードされる。
これからの当然の結果として、同じ名前の異なった Class
オブジェクトを持つVM中に、複数のクラスローダを持つことができる、ということである。com.infoq.example.App
という名前のクラスを、同じVM上のバンドルcom.infoq.example
のバージョン1とバージョン2の両方によってエクスポートできる。バージョン1にバインドされたクライアントバンドルは、バージョン1のクラスを得る。バージョン2にバインドされたクライアントバンドルは、バージョン2のクラスを得る。このことは、モジュールシステムにかなり普通に起きることである。同じVM上で、あるコードは、ライブラリの古いバージョンをロードする必要があり、一方(他のバンドルにある)新しいコードは、ライブラリの新しいバージョンが必要な場合である。幸いにも、OSGiは、そのような推移的な依存性を管理し、非互換なクラスに起因する問題がないことを保証してくれる。
クラスのガーベッジコレクション
各クラスは、定義したクラスローダへのリファレンスを持っている。違うバンドルからクラスをアクセスする場合、クラス(のインスタンス)への明示的なリファレンスを持つだけでなく、クラスへのリファレンスも持つことになる。1つのバンドルが他のバンドルのクラスを放さない間は、そのバンドルをメモリ上にピン留めする。前の例では、クライアントは、サーバにピン留めされる。
静的な世界では、自分を他のクラス(あるいはライブラリ)にピン留めしようが、しまいがどうでもよい。何も生まれたり、消滅したりしない。しかし動的な世界では、ライブラリあるいはユーティリティが実行時に、新しいバージョンのものと置き換わることができる。これは、複雑に聞こえるかもしれないが、実際に、webアプリケーションを無停止でデプロイする(最初のリリースが1999年の)Tomcatのようなwebアプリケーションエンジンの初期から、やられてきたことである。各webアプリケーションは、Servlet APIのバージョンと結びついていて、それが止まったときに、webアプリケーションをロードしたクラスローダが、落とされる。webアプリケーションが、再デプロイされた時に、新しいクラスローダが作成され、新しいバージョンのクラスがロードされる。サーブレットエンジンが、古いアプリケーションのリファレンスを握りっぱなしにしない限り、その時は、他のJavaのオブジェクトと同様に、クラスは、ガーベッジコレクトされる。
すべてのライブラリが、クラスリークの可能性を知っているわけではない、これは、メモリリークのようなもので、Javaのコードでは、可能性がある。明らかな例は、Log4Jの addAppender()
コールで、一旦実行されると、あなたのクラスは、Log4Jのバンドルと生涯離れられなくなる。あなたのバンドルが停止したとしても、Log4Jは、appenderへのリファレンスを保持しており、ログイベントを送り続ける(バンドルが停止する時に、適切な removeAppender()
を呼ばないと、このようなことが起きる)。
動的であるためには、サービスを探し、しかし(バンドルが消えたときには)永久にそれを握りぱなしにしない、仕組みが必要である。これを実現するために、単純なJavaのインターフェースと servicesとして知られているPOJOを使うのである。(POJOは、WS-DeathStarや他のXMLの重いインフラとは、関係ない。単にPlain Old Java Objects(単純な以前からあるJavaオブジェクト)である。
プロパティファイルを介して得られた、ある形のクラス名とそれに続くClass.forName()
を使って、典型的なファクトリは、実装されるが、OSGiは、'service registry' – を維持する。本質的に、クラス名とサービスを持っているマップである。なので、JDBCドライバを取得するのに class.forName("com.example.JDBCDriver")
を使わないで、OSGiシステムでは、代わりに context.getService(getServiceReference("java.sql.Driver"))
を使うことができる。これにより、クライアントは、特定のクライアントの実装について知る必要がなくなる。その代わり、実行時に入手可能などのドライバともバインドできる。異なったデータベースサーバに移行する時に、1つのモジュールを止めて、新しいモジュールをスタートする、場合である。クライアントは、再スタートする必要さえなく、どの設定ファイルを変更する必要もない。
これがうまく行く理由は、クライアントは、要求しているサービスのAPIだけを知ればいいからである。OSGiの仕様では、このために、どのようなクラスも使われることを許しているが、ほとんどいつも、インターフェースである。上の場合では、インターフェース名は、java.sql.Driver
である;返されるインターフェースのインスタンスは、データベースの実装である(そのクラスについては、知らないか、実際、どこでコーディングされたのかは、わからない)。さらに、サービスが入手できない(データベースがない、あるいは、データベースが一時的に止まっている)場合には、このメソッドは、そのようなサービスが入手できないことを示すために、null
を返す。
完全に動的であるためには、返された結果をキャッシュすべきでない。すなわち、サービスが必要なたびに、getService
は、再呼び出しされる必要がある。フレームワークは、見えないところでキャッシュするので、これによるパフォーマンスの心配は、要らない。しかし重要なのは、これで、データベースサービスが、コードの変更なしで、新しいものとこっそりと、替えることができることである–次の呼び出し時に、クライアントは、新しいサービスと透過的にバインドする。
動作させる
これを実演するために、URLを短くするのに役立つ、OSGiサービスを作る。これらの(ほとんど代替できる)サービスの意図は、 http://www.infoq.com/articles/modular-java-what-is-itのような長いURLから、 http://tr.im/EyH1のような短いURLに変換することである。Twitterのような広範囲なサイトで使われているが、長いURLをポストイットに書き留められるようなものに、複写できるように、このサービスを使うことも可能である。New Scientist や Macworldのような雑誌でさえ、印刷されたメディアリンクとして、それらを使っている。
サービスを実装するために必要なのは:
- 「短縮」サービスへのインターフェース
- スタートの時に短縮の実装を登録するバンドル
- 実演用のクライアント
これらをすべて同じバンドルに入れることを阻むものは何もないが、別々のバンドルに入れることにする(たとえ全てが同じバンドルにあっても、あたかも違うバンドルにあるかのように、サービスを介してバンドルが通信できるようにするのが、いい方法である;そのほうが他のプロバイダと統合する場合に簡単になる)。
短縮サービスへのインターフェースを他の実装(あるいはクライアント)とは、別のバンドルにしておくことは、重要である。インターフェースは、クライアントとサーバ間の共有のコードを意味し、そのようなものとして、全てのバンドルによってロードされる。これにより、インターフェースを別々のバンドル(OSGiのVMの寿命の間、走っている)に入れることによって、各バンドルを(特定のバージョンの)インターフェースに、サービス全体の寿命の間、ピン留めされているので、クライアントが生成したり、消滅したりできる。もしインターフェースをサービス実装の1つとして、同じバンドルに入れたら、もしそのサービスが、生成され、消滅したら、クライアントと再接続できないだろう。
「短縮」インターフェースのマニフェストと実装は、面白いものではない:
Bundle-ManifestVersion: 2 Bundle-Name: Shorten Bundle-SymbolicName: com.infoq.shorten Bundle-Version: 1.0.0 Export-Package: com.infoq.shorten --- package com.infoq.shorten; public interface IShorten { public String shorten(String url) throws IOException; }
これがやっているのは、後で、クライアントにエクスポートされる、1つのインターフェース(com.infoq.shorten.IShorten
)を持ったバンドル (com.infoq.shorten
)を設定しているだけである。引数として、1つのURLをとり、その短縮化したバージョンを返す。
実装は、もうちょっと面白い。最近、もっと短い名前が出てきているが、それらすべての祖先は、 TinyURL.comである( http://tinyurl.com が実際、 http://ow.ly/AvnC)のようなさらに短いURLに簡略化されているのは、ちょっと皮肉である)。色々人気なのがある; ow.ly, bit.ly, tr.im など。このことが、これらの短いURLへの解りやすいガイド(あるいは、推奨)を意味しているわけではない。実装は、他のサービスにも、同じように適用できる。この記事では、TinyURL と Tr.imを使う、匿名のGETベースのサブミッションができ、実装が簡単なのが理由である。
両方のクライアントの実装は、実際非常に似ている;両方共URLをパラメータ(短縮されようとしているもの)としてとり、次に新しく短くなったテキストを返す:
package com.infoq.shorten.tinyurl; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.URL; import com.infoq.shorten.IShorten; public class TinyURL implements IShorten { private static final String lookup = "http://tinyurl.com/api-create.php?url="; public String shorten(String url) throws IOException { String line = new BufferedReader( new InputStreamReader( new URL(lookup + url).openStream())).readLine(); if(line == null) throw new IllegalArgumentException( "Could not shorten " + url); return line; } }
Tr.imの実装も同様である、lookupとして URL http://api.tr.im/v1/trim_simple?url=
を使うとこだけが違う。両方のソースは、 com.infoq.shorten.tinyurl と com.infoq.shorten.trim のバンドルにある。
さて、短縮するサービスの実装ができたので、他からそれにアクセスさせるのに、どのようにするのか?OSGiフレームワークにサービスとして、これを登録する必要がある。 BundleContext
クラスの registerService(class,instance,properties)
メソッドにより、後で使用するために、サービスを定義出来る、そして一般的に、このメソッドは、バンドルの start()
の呼び出し中に、呼ばれる。つまり BundleActivator
を定義しなければならない。 前回書いたように、実装を見つけ出すために、クラスの実装同様、 Bundle-Activator
を MANIFEST.MF
に入れることを忘れてはならない。以下のようなコードになる:
Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: TinyURL Bundle-SymbolicName: com.infoq.shorten.tinyurl Bundle-Version: 1.0.0 Import-Package: com.infoq.shorten,org.osgi.framework Bundle-Activator: com.infoq.shorten.tinyurl.Activator --- package com.infoq.shorten.tinyurl; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import com.infoq.shorten.IShorten; public class Activator implements BundleActivator { public void start(BundleContext context) { context.registerService(IShorten.class.getName(), new TinyURL(),null); } public void stop(BundleContext context) { } }
registerService()
メソッドは、第1引数として、文字列をとり、 "com.infoq.shorten.IShorten"
ように書くのは、妥当であるが、あたかもパッケージのリファクタリングをやっているか、クラス名を変更してるかのように、 class.class.getName()
を使って、コンパイラによって名前を掴ませるのがよいやり方である。もし文字列だけを使って、悪いリファクタリングをやると、実行時まで、問題に気がつかないことになる。
registerService()
の2番目の引数は、インスタンス自身である。これは、1番目の引数と全く違う理由は、同じサービスインスタンスに、複数のサービスインターフェースをエクスポートさせることができるからである (インターフェースを進化させることができるように、もしあなたの要件にバージョン付きのAPIがあるなら、これは、役に立つ)。更に、1つのバンドルが同じタイプの複数のサービスをエクスポートする可能性は、非常に高い)。
最後の引数は、 service properties(サービスプロパティ)である。このお陰で、特別なメタデータでサービスをアノテートできる。例えば、他のよりも、このサービスがどのくらい重要であるかの優先度を示したり、呼び出し元に興味のある付加情報(説明やベンダーなど)である。
このバンドルがスタートした途端、短縮サービスがクライアントに使用可能となる。バンドルが停止した時、フレームワークが自動的に、サービスの登録を削除する。もし望むなら(例えば、エラーコードに応じたり、ネットワークインターフェースが利用できないなどの場合に context.unregisterService()
を使って)もっと早く登録を削除できる。
サービスを使う
ひとたび、サービスが起動して、走ったら、それにアクセスするためにクライアントを使うことができる。Equinoxを使っているなら、 services
コマンドを使って、インストールされたサービスと誰によって登録されたかを、リストできる:
{com.infoq.shorten.IShorten}={service.id=27} 登録したバンドル: com.infoq.shorten.trim-1.0.0 [1] サービスを使用中のバンドル無し。 {com.infoq.shorten.IShorten}={service.id=28} 登録したバンドル: com.infoq.shorten.tinyurl-1.0.0 [2] サービスを使用中のバンドル無し。
クライアントは、サービスをURLで呼び出す前に、それを解決する必要がある。 service reference(サービス・リファレンス)を得る必要がある。これにより、サービス自身のプロパティを内観(introspect)でき、次にそれを使って、実際に興味がある サービス を得ることができる。しかし、このことは、繰り返しできる必要がある(違うURLで)、そうできるようにEquinox か Felixのシェルにそれを組み込むことができる。実装がどうなるかを示す:
package com.infoq.shorten.command; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceReference; import com.infoq.shorten.IShorten; public class ShortenCommand { protected BundleContext context; public ShortenCommand(BundleContext context) { this.context = context; } protected String shorten(String url) throws IllegalArgumentException, IOException { ServiceReference ref = context.getServiceReference(IShorten.class.getName()); if(ref == null) return null; IShorten shorten = (IShorten) context.getService(ref); if(shorten == null) return null; return shorten.shorten(url); } }
shorten
メソッドが呼ばれた時、このコード片は、service referenceを調べ、それからサービスオブジェクトを得る。それからそれを IShorten
オブジェクトにキャストでき、次にそれを使って、以前に登録したサービスと交信する。注意して欲しいのは、これらすべては、同じVM上にあることである。リモート呼び出しや必須な例外もない。引数も、シリアライズされていない。単にPOJOが別のPOJOに話しているだけである。実際、これと始めの class.forName()
の例との違いは、どうやってshorten
POJOを得ているか、と言うことである。
Equinox と Felix内で、これを使うためには、ちょっと決まりきったコードを書く必要がある。すなわち、マニフェストを定義するときに、Felix と Equinoxのコマンドライン・インターフェースに選択的な依存性を書けるので、どちらにでもインストールすれば、走らせることができる。(もっといいやり方は、これらを別のバンドルにデプロイして、選択性をなくすことができる;しかし、もしバンドルが存在しないとactivatorは、失敗し、スタートできない。)興味があるなら、Equinox と Felixに特有なコマンドの接続部分のソースコードは、 com.infoq.shorten.command バンドルにある。
結果は、もしコマンド用クライアントのバンドルをインストールすれば、新しいコマンド、 shorten
が手に入り、OSGiシェルから呼び出すことができる。起動するには、 java -jar equinox.jar -console -noExit
あるいは java -jar bin/felix.jar
とする。動かすために、バンドルをインストールする必要がある;それからコマンドを使うことができる:
java -jar org.eclipse.osgi_* -console -noExit osgi> install file:///tmp/com.infoq.shorten-1.0.0.jar Bundle id is 1 osgi> install file:///tmp/com.infoq.shorten.command-1.0.0.jar Bundle id is 2 osgi> install file:///tmp/com.infoq.shorten.tinyurl-1.0.0.jar Bundle id is 3 osgi> install file:///tmp/com.infoq.shorten.trim-1.0.0.jar Bundle id is 4 osgi> start 1 2 3 4 osgi> shorten http://www.infoq.com http://tinyurl.com/yr2jrn osgi> stop 3 osgi> shorten http://www.infoq.com http://tr.im/Eza8
ここで留意すべきなのは、TinyURL とTr.imの両方のサービスが、実行時に入手可能で、しかし、明らかに、1つのサービスだけが、一度に使えることである。 Integer.MIN_VALUE
と Integer.MAX_VALUE
の間の整数である サービス順位(service ranking)を設定することができる。サービスを最初に登録するときに、 Constants.SERVICE_RANKING
キーに対応する値を入れればよい。より大きな値は、より高い順位を意味し、サービスを要求されたときに、最も高い順位のものが、返される。サービス順位が設定されていなかった(デフォルト値は、0)、あるいは、複数のサービスが、同じ順位だったりした場合、自動的に割り当てられた Constants.SERVICE_PID
が任意にサービス順を決めるために、使われる。
他に留意すべきなのは、他のサービスを止めた時に、クライアントは、自動的にリスト中の次のサービスにフェイルオーバーすることである。コマンドが走る度に、短縮の要求に使用する(カレントの)サービスを取得する。もし実行中に、サービスプロバイダが変わっても、コマンドは心配要らない、必要なのは、1つだけである(もし全プロバイダを止めたら、サービス検索は、null
を返すので、エラーが出力されることになる-良いコードは、サービスリファレンスに null
が返される可能性に対して、防御的なプログラムを保証すべきである)。
Service Tracker(サービス トラッカー)
毎回、サービスを検索する代わりに、 ServiceTracker
を使って同じことができる。こうすると、 ServiceReference
を取得するのに、中間のステップを飛ばせる。しかし、トラックサービスを始めるのに、(newによる)コンストラクションの後に、 open
を呼ぶ必要がある。
ServiceReference
で getService()
を呼べば、サービスインスタンスを取得できる。 waitForService()
で、サービスが入手できない時に、最長でtimeoutに指定した時間、(もしtimeoutが0だと永久に)待ち続ける:
package com.infoq.shorten.command; import java.io.IOException; import org.osgi.framework.BundleContext; import org.osgi.util.tracker.ServiceTracker; import com.infoq.shorten.IShorten; public class ShortenCommand { protected ServiceTracker tracker; public ShortenCommand(BundleContext context) { this.tracker = new ServiceTracker(context, IShorten.class.getName(),null); this.tracker.open(); } protected String shorten(String url) throws IllegalArgumentException, IOException { try { IShorten shorten = (IShorten) tracker.waitForService(1000); if (shorten == null) return null; return shorten.shorten(url); } catch (InterruptedException e) { return null; } } }
Service Trackerでの共通の問題は、コンストラクトされた後に、 open()
を呼び忘れることである。走らせるのに、MANIFEST.MF内で、パッケージとして org.osgi.util.tracker
をインポートすることも必要である。
サービスへの依存性を管理するために、 ServiceTracker
を使うのは、一般的に、関係を管理するよい方法と考えられる。サービスを使用していない時に、サービスを探すのは、ある微妙な複雑性に遭遇することになる。例えば、 ServiceReference
からサービスを生成しようとする時に、まだ ServiceReference
が入手できない時である。 ServiceReference
を持つ理論的な根拠は、同じインスタンスが、バンドル間で共有できること、そして、ある基準で、サービスを(マニュアルで)除くことに使うことができることである。しかし、入手できるサービスを制限するために、フィルタを使うこともできる。
サービスプロパティとフィルタ
サービスが登録されると、それでサービスプロパティを登録することができる。大抵の時は、これは、 null
でよい。しかし、URLについてOSGi特有な、そして一般的なプロパティの両方を与えることができる。例えば、好みでサービスの順位づけをしたいとしよう。初期登録の一部として、好みの数値で、 Constants.SERVICE_RANKING
を登録できる。例えば、サービスのホームページの場所やサイトの契約条件へのリンクなど、顧客が知りたがるメタデータも登録したいと思うだろう。こうするには、我々のactivatorを修正する必要がある:
public class Activator implements BundleActivator { public void start(BundleContext context) { Hashtable properties = new Hashtable(); properties.put(Constants.SERVICE_RANKING, 10); properties.put(Constants.SERVICE_VENDOR, "http://tr.im"); properties.put("home.page", "http://tr.im"); properties.put("FAQ", "http://tr.im/website/faqs"); context.registerService(IShorten.class.getName(), new Trim(), properties); } ... }
サービスの順位づけは、自動的に ServiceTracker
によって管理され、そして他のプロパティもそうであるが、それらをある特性によっても除外できる。 Filter
は、 プレフィックス表記法 を使って、複数のフィルタを実行できるLDAPスタイルのフィルタにより作れる。もっともよくあるのは、クラス (Constants.OBJECTCLASS
)の名前を与えることであるが、値についてテストすることもできる(そして連続した値の範囲を制限することさえできる)。Filterは、 BundleContext
を介して作られる。もしFAQを定義し、同時に IShorten
interface を実装するサービスをトラックしたいと思うなら、以下のようにすれば、できる:
... public class ShortenCommand public ShortenCommand(BundleContext context) { Filter filter = context.createFilter("(&" + "(objectClass=com.infoq.shorten.IShorten)" + "(FAQ=*))"); this.tracker = new ServiceTracker(context,filter,null); this.tracker.open(); } ... }
サービスを定義する時に、除外したり、設定したりする標準的なプロパティには、以下のものがある:
service.ranking
(Constants.SERVICE_RANKING) - 整数、サービスの優先順位づけに使うことができるservice.id
(Constants.SERVICE_ID) - 整数、サービスが登録される時にフレームワークにより、自動的に設定されるservice.vendor
(Constants.SERVICE_VENDOR) - 文字列、サービスの出所を示すために設定できるservice.pid
(Constants.SERVICE_PID) - 文字列 あるいは、文字列の配列で、サービスの永続的な識別子を表すservice.description
(Constants.SERVICE_DESCRIPTION) - サービスの説明objectClass
(Constants.OBJECTCLASS) - interfaceのリストで、このサービスは、その下に登録される
フィルタのシンタックスは、 OSGi コア仕様書のSection 3.2.7 "Filter syntax"に定義されている。本質的に、様々なオペレーションが使える、例えば、等号(=)、概算(~=)、以上、以下、それに部分文字列の比較。括弧でフィルタをまとめるが、and, or そして notのそれぞれモディファイア(変更子)として &, | そして ! が使え、これらと組み合わせることができる。アトリビュート名は、大文字/小文字で違いはないが、値としては、違う(~=で比較するのでなければ)。*はワイルドカードを意味し、com.infoq.*.*
のような部分文字列のマッチングのサポートに使える。
まとめ
この記事では、バンドル間で通信の方法として、直接クラスのリファレンス使わずに、どのようにサービスを使うことができるかをみてきた。サービスにより、モジュールシステムが動的になり、例えば、モジュールは、実行時に生成、消滅するサービスに反応する。サービスの優先順位付け、プロパティそしてフィルタについても触れた。生成、消滅するサービスにアクセスしたり、追跡したりするのを、簡単にする標準的なサービス・トラッカーも使った。
インストール可能なバンドル(ソースも含む):
- com.infoq.shorten-1.0.0.jar
- com.infoq.shorten.command-1.0.0.jar
- com.infoq.shorten.trim-1.0.0.jar
- com.infoq.shorten.tinyurl-1.0.0.jar