OSGiによるモジュールシステム

モジュールシステムに求められるモデル

ところで「Java/VM環境で完全なモジュールシステムが実現できる」とはどういう事でしょうか?Java/VM環境で構築されたシステムを言語構造の視点からレイヤに分割してみると、次のようになります。:

システム > アプリケーション > クラスやインタフェース > メソッド > 命令

Javaはオブジェクト指向な言語なので、クラスがあり、メソッドがあり、1行1行VMに対する命令を書く言語です。オブジェクト指向言語なので、カプセル化を駆使して実装を隠蔽し、再利用できる…、と思われて広く使われるようになりましたが、再利用ってうまくやれますか?例えばクラスの再利用を考えると、そのクラスに関連するクラスを一通り分離して、JARにするのではないでしょうか?:

システム > アプリケーション > JAR > クラスやインタフェース > メソッド > 命令
しかし、実はJARはコンポーネントとして扱うには不完全なのです。JARがコンポーネントとして不完全なのは、下記の2点の理由があるからです。
  • あるJARを利用しようとした時、そのJARの中のクラスを実行するときに必要としている他のJARに関する情報の記述がいまいちであること、つまり依存関係の記述がいまいち
  • JARをJava/VM環境にロードしたとき、気づかないうちに意図していないインスタンス同士が関連していること、つまりアクセス境界がいまいち

OSGiのチュートリアルを読むつもりだったのに、なんかまどろっこしいな…と思われた方。実はこの辺りがハマりどころであるため、大事なところなんです。それぞれ見てみましょう。

JARの依存関係

MavenやIvyは便利です。これらのツールでは、使いたいJARを宣言すれば、自動的に依存しているJARを取得できます。しかし、依存しているJARを取得するためには、どこかに依存情報がなければ取得のしようがありません。実はこれらのツールはJARの内部に依存関係を持たせていません。リポジトリや設定ファイルにXMLを用意し、そこに記述しているのです。

JAR自身の仕様に、依存関係の記述がないわけではありません。例えばMANIFEST.MFにClass-Pathを宣言することで、実行時に必要なJAR、すなわち依存するJARを記述できます。しかし残念な事にClass-Pathディレクティブはファイル名ベースなのです。例えば少しJARのバージョンが更新され、ファイル名が変更されればそれだけで利用できません。情報としてもろいのです。よって、依存関係の記述がいまいちなのです。

JARのアクセス境界

Webアプリケーションでもある程度の大きさになると、クラスパスに並ぶライブラリが20とか30とかになります。次第にEclipseのパッケージビューがライブラリだらけ、みたいになっている事はないですか?こういう状態の時に、あるライブラリのJARに脆弱性が発見されたので、実際に新しいJARに入れ替えたらシステムが起動しなくなった、と言う場面に出会ったことはないですか?そして調べてみると、そのJARを利用している別のJARも入れ替えないといけないらしい。そっちを入れ替えると他のJARも入れ替えないといけないみたいだ…。でもそれを入れ替えると、アプリケーションから利用していたメソッドがなくなっているみたいだ…。なんて事をたまに聞いたりしませんか?この事を JAR Hell と名付けられていたりします。

なぜJAR Hellが起きるかと言うと、Webアプリケーションはアプリケーション毎にクラスローダが用意されますが、同じクラスローダ上に展開されたクラスは、JARによる物理的な壁を超え、publicやprotectedなどの識別子、論理的な壁でアクセス境界が定められているためです。ライブラリとして実装しているJARの開発時に困るのが、JARの中のクラスしか参照させたくないクラスであっても、ライブラリ内の他のパッケージに公開したいのでpublicで宣言する必要が出てきた時。そういうとき、JavaDocでライブラリ内にのみ公開と明記しても…。実装者は不安ですよね。ライブラリを利用するユーザも不安です。つまり、現行の素のJava/VM環境ではアクセス境界自体がいまいちなのです。

ところでアクセス境界のいまいちさは、アプリケーション内においてインタフェースの宣言は不要にも結びついているように思えます。インタフェースを宣言し、実装クラスを切り離したところで、開発時には実装クラスを参照できてしまうからです。アクセス境界がはっきりしている世界ではインタフェースがより重要な役割を持つようになります。なぜならば、完全に実装クラスをモジュール外に非公開にすることが可能になるからです。アクセス境界がはっきりする事で、よりカプセル化が促進するのです。

なぜOSGiを用いるとモジュールシステムが構築できるのか?

OSGiによるモジュールシステムは、JARのみを使ったモジュールシステムが持つこれらの問題を解決する事ができます。それはなぜでしょうか?

そもそもOSGiはホームゲートウェイ向けに考えられた仕様です。ホームゲートウェイと言うと、インターネットと宅内ネットワークを接続する一種のルータの機能だけのように思えますが、要件はそれだけではありません。宅内ネットワークに接続されている機器同士のサービスの中継も考えられていました。機器の電源が入っていなかったり、そもそもネットワーク上に存在しない時など、ホームゲートウェイ上で連携できていたサービスを簡単に機能を切り離せなければなりません。これがホームゲートウェイとしての要件としてありました。OSGiはこの要件をモジュールシステムとして実現することで乗り越えた仕様でした。OSGiはJavaの仕様としてJCPに提案されましたが、最初の提案はJava ME向け、つまり組み込み向けだった事からもうなずけると思います。

OSGiをモジュールシステムに用いる

では、アプリケーションのモジュールシステムにOSGiの実装を使うと依存関係の定義やアクセス境界はどうなるのでしょうか?

OSGi仕様におけるモジュール

OSGi仕様におけるモジュールはBundleと呼ばれています。例えばEclipse自体もOSGiの実行エンジン上で動作します。 10分で作るOSGiアプリ でも記述しましたが、EclipseのPlug-inは全てBundleです。また、Bundle自体は実はJARを拡張したものです。そのため、 BundleはJARとして利用することも可能です。 BundleとJARで異なる点はBundleはJARに対してメタデータを付加する事でアクセス境界と依存関係を定義します。どちらもJARファイルのMANIFEST.MFに定義します。下記がBundleのMANIFEST.MFの例です。:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Example Bundle
Bundle-SymbolicName: org.foo.example
Bundle-Version: 1.0.0.qualifier
Bundle-RequiredExecutionEnvironment: JavaSE-1.6
Import-Package: org.osgi.framework
Export-Package: org.foo.example.service
Bundle-Activator: org.foo.example.Example
Bundle-ClassPath: .,lib/commons-io-1.4.jar

依存関係とアクセス境界は実は相補関係にあります。それぞれ見ていきましょう。

アクセス境界の定義方法

アクセス境界の定義は、Export-Packageにパッケージ名を記述します。Export-Packageに記述されたパッケージは他のBundleに公開されるパッケージです。この例では、「Export-Package: org.foo.example.service」と宣言していますが、org.foo.example.serviceというパッケージをBundle外に公開しています。逆にExport-Packageに宣言されていないパッケージは、他のBundleからアクセスができません。このようにパッケージによる宣言が、言い換えるとアクセス境界になっているのです。

では依存関係の定義はどうなっているのでしょうか?

依存関係の定義方法

依存関係の定義は、Import-Packageでパッケージ名を宣言しています。Import-Packageに記述されたパッケージは、このBundleの利用に必要なパッケージ、言い換えるとこのBundleが依存しているパッケージです。この例では「Import-Package: org.osgi.framework」と宣言していますが、org.osgi.frameworkというパッケージに依存しています。先ほどのExport-Packageで宣言されているパッケージとImport-Packageで宣言しているパッケージをOSGiの実行環境が結びつけるのです。

アクセス境界と依存関係の実装仕様

このようなアクセス境界や依存関係はどのように実装されているのでしょうか?

OSGiの仕様では、これらについてどのように実装すべきかを記述しています。それはBundleごとにクラスローダを生成し、Frameworkが依存関係とアクセス境界を元にBundleのクラスローダの結びつけと、境界を設定するというシンプルなアイディアです。依存関係ではバージョンを条件に加える事で、異なるバージョンのBundleを読み込む事を実現しています。(同一Bundleに複数のバージョンのクラスを読み込む事は、残念ながらクラスローダの仕様上できません。)

よって、Bundleが異なればクラスローダが異なるわけです。

ローカルなクラスパス

Bundleが異なる事でクラスローダが異なるため、Bundleの可搬性を高める意味でもローカルなクラスパスを構成できます。

まとめ

OSGiが様々なアプリケーションで利用が増えてきた最近では、JARとしてリリースされるライブラリの中にメタデータを含んでいるものも増えています。[#] そのため、実はただのJARと思っていたライブラリが、OSGiコンテナ上で利用できるBundleとして配布されているかもしれません。また、JVM上で動作するスクリプト言語の多くもOSGi Bundleとして利用できるようにする対応が始まっています。

[1]Maven2には依存関係を自動的にMANIFEST.MFに書き出してくれるプラグイン(maven-bundle-plugin)があるため、Mavenを利用しているプロジェクトでは比較的簡単に移行できます。
[2]クラスパスがBundleごとに定義できるので、既存のJARを展開しないでもメタデータを付加できます。先述したmaven-bundle-pluginは既存のJARをラップしたBundleの作成ができます。