モジュール化は、大きなJavaシステムにおいて重要な側面である。ビルドを改善するために、ビルドスクリプトやプロジェクトをしばしばモジュールに分割するが、このことが実行時に考慮されることは、まれである。
Modular Javaシリーズの第2弾として、静的なモジュール化を扱う。バンドルの作り方、OSGiのエンジンにそれらをインストールする方法、バンドル間の(バージョン付きの)依存性の設定のしかたなどについて書く。次回は、動的なモジュール化を考察し、バンドルが動いている他のバンドルにどう反応するかを示す予定である。
前回の記事、 Modular Java: What Is It?に書かれたように、Javaは、いつも開発時にモジュールの単位としてpackage(パッケージ)を持ち、デプロイ時にモジュールの単位としてJARを持っている。しかし、Mavenのようなビルドツールは、コンパイル時に、packageとJARのある組み合わせについては、保証しているが、これらの依存性が、実行時のクラスパスとは、矛盾しているかもしれない。この問題の解決として、モジュールは、実行前にチェックされるように、実行時に、依存性の要求を宣言できる。
OSGiは、Javaのための実行時の動的モジュールシステムである。 スペック は、OSGi ランタイムがどのように動くのかを記述している。現在のリリースは、 OSGi R4.2 (以前、 InfoQで扱われた )。
OSGiのモジュール(bundle(バンドル)としても知られている)は、単純なJARファイルで、アーカイブのMANIFEST.MF
に追加情報が記述されている。最低限、bundleのマニフェストが含まなければならないのは:
- Bundle-ManifestVersion: OSGi R4 bundleに対しては、2でなければならない(OSGi R3 bundleに対しては1である)
- Bundle-SymbolicName: bundleのテキスト形式の識別子、しばしば
com.infoq
のようにドメイン名の形式を逆順にし、そこに含まれるpackageに対応する - Bundle-Version: メジャー.マイナー.マイクロ.限定子の形のバージョンで、最初の3要素は、数値(既定値は0)で、限定子は、テキスト(既定値は、空の文字列)
バンドルの作成
最も単純なバンドルは、純粋に以下のようなマニフェストファイルを含む:
Bundle-ManifestVersion: 2 Bundle-SymbolicName: com.infoq.minimal Bundle-Version: 1.0.0
作るのがすごくわくわくするものではないが、activatorのあるものを1つ作ってみよう。これは、OSGi特有のコード片で、バンドル毎のmain メソッドのように、バンドルが開始されるときに、呼ばれるものだ。
package com.infoq; import org.osgi.framework.*; public class ExampleActivator implements BundleActivator { public void start(BundleContext context) { System.out.println("Started"); } public void stop(BundleContext context) { System.out.println("Stopped"); } }
OSGiがどのクラスがactivatorであるかを知るために、マニフェストにもう2つの項目を加える必要がある:
Bundle-Activator: com.infoq.ExampleActivator Import-Package: org.osgi.framework
Bundle-Activator
は、インスタンス化するクラスを宣言し、バンドルが開始される時にstart()
メソッドを呼ぶ;同様に、バンドルが停止するときに、stop()
メソッドが呼ばれる。
Import-Package
とは何か?各バンドルは、実行時に必要なすべてのコードがあるかを判断するために、マニフェストに自分が依存するものを明記する必要がある。この例では、ExampleActivator
は、org.osgi.framework
packageからのBundleContext
に依存する。もしマニフェストにその依存性が宣言されていないと、実行時にNoClassDefFoundError
になる。
OSGiエンジンのダウンロード
バンドルをコンパイルしてテストするには、OSGiエンジンが必要である。OSGi R4.2として、以下にあげた、いくつかのオープンソース版が入手できる。Reference API をダウンロードすることもできるので、コンパイルにも役立つ(プラットフォーム特有のフィーチャを使う時に、問題を起こさないように) :
Equinox | |
---|---|
ライセンス | Eclipse Public License |
ドキュメント | http://www.eclipse.org/equinox/ |
ダウンロード | org.eclipse.osgi_3.5.0.v20090520.jar |
注記 |
|
フレームワーク | org.eclipse.osgi_3.5.0.v20090520.jar |
Felix | |
ライセンス | Apache License |
ドキュメント | http://felix.apache.org/ |
ダウンロード | Felix Framework Distribution 2.0.0 |
注記 | OSGiエンジンの仕様に最も厳密に従っており、GlassFishや他の多くのオープンソース製品に使われている。 java -jar felix.jar ではなく、java -jar bin/felix.jar で走らせる必要がある。カレント・ディレクトリから自動的に開始するには、パスによりバンドルを探すからである。 |
フレームワーク | bin/felix.jar |
Knopflerfish | |
ライセンス | Knopflerfish License (BSD-esque) |
ドキュメント | http://www.knopflerfish.org/ |
ダウンロード | knopflerfish_fullbin_osgi_2.3.3.jar |
注記 | このJARは、自己解凍のzipファイルで、最初、 java -jar で解凍する。代替である'bin_osgi ' をダウンロードしないように、これでは、スタートに失敗するので。 |
フレームワーク | knopflerfish.org/osgi/framework.jar |
組み込みデバイスをターゲットにした( Conciergeのような)より小さなOSGi R3ランタイムもあるが、このシリーズでは、OSGi R4に焦点をあてる。
バンドルをコンパイルし、走らせる
framework.jar
を入手して上記の例をコンパイルしながら、クラスパスにOSGiフレームワークを加える、それからJARにそれをパッケージする:
javac -cp framework.jar com/infoq/*/*.java
jar cfm example.jar MANIFEST.MF com
各エンジンは、シェルのようなものを持っていて似たような(同じでないが)コマンドがある。この演習の目的は、どのようにエンジンを起動し、走らせ、バンドルをインストールして、開始/停止するかを見るだけである。
一旦エンジンが起動し、走りだすと( file://
URLを指定して)バンドルをインストールし、戻された数値のbundle IDを使ってスタートし、停止する。
Equinox | Felix | Knopflerfish | |
---|---|---|---|
起動 | java -jar org.eclipse.osgi_*.jar -console |
java -jar bin/felix.jar |
java -jar framework.jar -xargs minimal.xargs |
ヘルプ | help |
||
リスト | ss |
ps |
bundles |
インストール | install file:///path/to/example.jar |
||
スタート | start id |
||
アップデート | update id |
||
停止 | stop id |
||
アンインストール | uninstall id |
||
シャットダウン | exit |
shutdown |
同じように動くが、コマンド間の微妙な差が少々紛らわしい。いくつかの調和のとれたコンソールプロジェクト(Pax Shell, Posh )とランチャー(Pax Runner) が入手できる; OSGi RFC 132 は、コマンドシェルを試しそして標準化するために、提案中のものである。 Apache Karafは、Equinox や Felixの上で走ることを目指しているディストリビューションで、他のフィーチャとともに調和のとれたシェルを提供している。本当のデプロイには、これらを使うことを勧めるが、このシリーズでは、素のOSGiフレームワークの実装に焦点を当てる。
OSGiフレームワークを起動したら、上記から(リンクのアドレスと install
コマンドを使ってサイトから直にインストールすることもできる) com.infoq.minimal-1.0.0.jar
をインストールできるべきである。バンドルを install
する毎に、そのバンドルの数値IDを表示する。
システムの他のどのバンドルに依存しているかにより、インストール時にどのバンドル識別子で終わるかを知るのは、不可能である。しかし見つけるのに適当なコマンドを使って、インストールされたバンドルをリストできるようにすべきである。
依存性
これまで、1つのバンドルしか扱わなかった。モジュール化の利点の1つは、システムをいくつものより小さなモジュールに分解できることである。そうすることにより、アプリケーションの複雑さを減らすことができる。ある程度、これは、Javaのパッケージですでに済んでいる、client
と server
packageといっしょにcommon
packageが、しばしばネットワーク・アプリケーションに使われている。言外の意味は、クライアントとサーバのパッケージは、独立しており、両方ともcommon
パッケージに依存している、ということである; Jettyの最近の例 (クライアントが偶然にサーバに依存してしまった)のように、このことは、実現するのは、いつも易しいわけではない。実際、OSGiがプロジェクトにもたらした成功のいくつかは、純粋にモジュール間における強制的なモジュール化の制約である。
モジュール化の別の利点は、公開のパッケージを非公開のパッケージから分離することである。Javaのコンパイル時のシステムは、隠れた非公開クラス(特定のパッケージ内では、それらは見える)を許しているが、それ以上の柔軟性は提供してはいない。しかし、OSGiのモジュールでは、どのパッケージをexport(エクスポート)するかを選べる。この場合、非exportのパッケージは、他のモジュールからは、見えない。
URI Templates( Restletに使われているような)をインスタンス化する関数を開発しているとしよう。これは、おそらく再利用できるので、これをそれ自身のモジュールに入れたいと思う、そしてそれを使う必要のあるクライアントを我々に依存させたいと思う(通常、バンドルは、このような細かな粒度のものではないが、原理を示すためである)。関数は、 http://www.amazon.{tld}/dp/{isbn}/
のようなテンプレートをとり、 Map
が tld=com,isbn=1411609255
持っていると、 URL http://www.amazon.com/dp/1411609255/
を生成することができる (こうする理由の1つは、もしAmazonの URLのスキーマが変わったらテンプレートを変更できるようにするためである。 クールなURIは変わらないけれど)
異なった実装間で簡単に切り替えられるように、インターフェースとファクトリを提供する。こうすると、クライアントに機能は提供するが、実装を隠すことができる。(いくつかのソースファイルを跨ぐ)コードは、次のようであろう:
package com.infoq.templater.api; import java.util.*; public interface ITemplater { public String template(String uri, Map data); } // --- package com.infoq.templater.api; import com.infoq.templater.internal.*; public class TemplaterFactory { public static ITemplater getTemplater() { return new Templater(); } } // --- package com.infoq.templater.internal; import com.infoq.templater.api.*; import java.util.*; public class Templater implements ITemplater { public String template(String uri, Map data) { String[] elements = uri.split("\\{|\\}"); StringBuffer buf = new StringBuffer(); for(int i=0;i
実装は、完全にcom.infoq.templater.internal
packageに隠されている、一方で公開のAPIは、 com.infoq.templater.api
packageにある。こうすると、必要なら、後でもっと効率のいい方法に、実装を変更する柔軟性が得られる(internal
package の名前は、幾分お決まり的なものだが、好きな名前をつけて良い)。
他のバンドルから公開のAPIにアクセスできるようにするために、バンドルからそれを export する必要がある。マニフェストは次のようになる:
Bundle-ManifestVersion: 2 Bundle-SymbolicName: com.infoq.templater Bundle-Version: 1.0.0 Export-Package: com.infoq.templater.api
クライアントのバンドルを作成する
さて、templaterを使うクライアントを作ることができる。上記の例を使って、activatorを作ると、その start()
メソッドは、このようになる:
package com.infoq.amazon; import org.osgi.framework.*; import com.infoq.templater.api.*; import java.util.*; public class Client implements BundleActivator { public void start(BundleContext context) { Map data = new HashMap(); data.put("tld", "co.uk"); // or "com" or "de" or ... data.put("isbn", "1411609255"); // or "1586033115" or ... System.out.println( "Starting\n" + TemplaterFactory.getTemplater().template( "http://www.amazon.{tld}/dp/{isbn}/", data)); } public void stop(BundleContext context) { } }
明示的にtemplater APIをimportするようにマニフェストを定義する必要がある、さもないとバンドルは、コンパイルされない。 Import-Package
か Require-Bundle
を使って依存性を特定できる。前者を使うと、個別にパッケージをインポートできる;後者だと、バンドルからエクスポートされたパッケージをすべて暗黙にインポートする(複数のパッケージやバンドルは、カンマで区切る)。
Bundle-ManifestVersion: 2 Bundle-SymbolicName: com.infoq.amazon Bundle-Version: 1.0.0 Bundle-Activator: com.infoq.amazon.Client Import-Package: org.osgi.framework Require-Bundle: com.infoq.templater
注意して欲しいのは、もっと前の例で、既にorg.osgi.framework
をインポートするために Import-Package
を使っていることである。 この場合、 Bundle-SymbolicName
を使ったRequire-Bundle
の使い方を示している。Import-Package: org.osgi.framework, com.infoq.templater.api
を使って、全く同じようにそれを加えることもできたが。
templater
バンドルへの依存性をどう宣言するかにかかわらず、唯一のエクポートされたパッケージ com.infoq.templater
へのアクセスを得ただけである。クライアントは、TemplaterFactory.getTemplater()
を介してtemplaterにアクセスできるが、internal
パッケージのクラスに直接アクセスは、できない。このお陰で、クライアントコードを変更せずに、実装を変更できるという柔軟性が得られる。
システムをテストする
あらゆるOSGiアプリケーションは、本当に、単なるバンドルのセットである。この場合、必要なのは、(以前のように)バンドルをコンパイルし、JARにして、OSGiエンジンを起動して、2つのバンドルをインストールすることである。実際、Equinoxを使って、どのようになるかといえば:
java -jar org.eclipse.osgi_* -console osgi> install file:///tmp/com.infoq.templater-1.0.0.jar Bundle id is 1 osgi> install file:///tmp/com.infoq.amazon-1.0.0.jar Bundle id is 2 osgi> start 2 Starting http://www.amazon.co.uk/dp/1411609255
Amazonのクライアント・バンドルがスタートする;そして起動すると、(明らかにハードコードしてしまっている)以前の値を使って、URI templateをインスタンス化する。それから、バンドル自身の起動中に、動いていることを確認するために、それを表示する。明らかに、実際のシステムは、こんな柔軟性のないものでは、ないだろう;しかしTemplaterのサービスは、どのアプリケーションでも使える(例えば、webベースのアプリケーションでリンクを生成するなど)。将来、OSGiのコンテキストで、webアプリケーションを見ることにする。
バージョン付き依存性
今回の最後のポイントは、ここまでの依存性には、バージョンがついていないことである。なので、どのバージョンでも使えてしまう。全体としてバンドルも個々のパッケージもバージョンをつけることができる。マイナー番号が増えることは、新しいフィーチャを意味する(しかし後方互換性を保っている)。例えば、org.osgi.framework
は、OSGi R4.1では1.4.0、OSGi R4.2では、1.5.0である(ついでながら、このことは、バンドルのバージョンをマーケティングのバージョンと別の概念とする、よい理由でもある、Scala言語は、まだ学ぶ必要があるものである)。
特定のバージョンに依存することを宣言するには Import-Package
か Require-Bundle
でそれを表明しなければならない。例えば、Require-Bundle: com.infoq.templater;bundle-version="1.0.0"
と明記すれば、動くために最小バージョンとして1.0.0が必要なことを意味している。同様に、同じことをImport-Package: com.infoq.templater.api;version="1.0.0"
と書ける。しかし覚えておく必要があるのは、packageのバージョンは、bundleのバージョンとは別物である、ということである。バージョンを明記しなければ、デフォルトの0.0.0となる。なので対応する Export-Package: com.infoq.templater.api;version="1.0.0"
がないと、このインポートは、解決されない。
バージョン範囲を明記することもできる。例えば、OSGiのバージョン番号における通常の意味では、メジャー番号の増加は、後方互換性における変更を示す。バージョンが1.xの範囲に制限したいとする。そうするには、(bundle-)version="[1.0,2.0)"
という風に依存性制限を明記できる。この場合、[ の意味は、 '含む'で、) は、'含まない'である、言い換えると、'1.0から2.0未満'である。実際、依存性制限を"1.0"とすると、 "[1.0,∞)" –と同じである。言い換えると1.0以上の如何なる数、ということである。
この記事のスコープ外であるが、OSGiのシステムでは、一度に複数バージョンのバンドルを持つことができる。もしバージョン2.0のAPIに依存している新しいクライアントと同時に、バージョン1.0のAPIに依存している古いクライアントもある場合に、これは役立つ。各バンドルの依存性が一貫している限り(すなわち、ひとつのバンドルが1.0と2.0を同時にインポートしようとしなければ)、アプリケーションはちゃんと動作する。読者への演習として、ジェネリックスを使って、Templater APIのバージョン2を作り、そしてバージョン1.xにのみに依存するクライアントとバージョン2.xのみを使うクライアントも作りなさい。
まとめ
この記事では、オープンソースのOSGiエンジンであるEquinox, Felix そして Knopflerfishを見てきた、依存しあうバンドルの1組を作った。バージョン付きの依存性についても触れた。ここまでは、このモジュール化は、静的なものである。OSGiの動的な性質については、触れなかった。次の回では、それを見ることにする。
インストール可能なバンドル(ソースも含んでいる):