じゃぁ、1つで済ませましょ (4) [入れ子のJARの取り扱い: java.util.jar.JarInputStream]
2002-04-29
前回は、jarファイルの中身にプログラムからアクセスする方法について、ご紹介しました。自動的にクラスを探す範囲内にあるjarに格納されていれば、ClassLoaderクラスのgetResourceやgetResourceAsStreamメソッドだけで可能でした。一方で、範囲に含まれないjarファイルの中身であれば、JarFileからInputStreamを取得して、中身にあわせて読み出すことが必要になります。イメージなどであればバイト列として読み出すことになりますし、テキストなら文字列のストリームということになります。
- jarツール
- jarファイルとManifest、クラスパス
- プログラムからjarファイルを使う
- 入れ子のJARの取り扱い
第4回目の今回は、プログラムから扱う際に、jarが入れ子になっているような状態を考えることにします。ear(EJB JAR)やwar(Web
Application Archive)といった、Java2 Enterprise Editionで使われるフォーマットでは、JAR形式の入れ子が発生します。そのような状態になっているjarファイルをプログラムで処理する場合、一時的に展開するという手段もありますが、あえてそれをせずに別の方法を考えてみることにしましょう。
■Jarのストリーム
前回、jarファイルを扱うためのクラスを紹介しました。まず最初に今回は、jarファイルをストリームとして扱うために、新たに2つのクラスを紹介します。
| JarInputStream |
jarファイルをストリームから読み込む |
| JarOutputStream |
jarファイルをストリームに書き出す |
前回、jarファイルからデータを読み出すために、JarFileクラスのgetInputStreamメソッドを使用しました。このメソッドを使用すると、個々のエントリのストリームを取得することができました。JarInputStreamクラスは、個々のエントリではなく、jarファイル自体を入力のストリームとして扱います。出力のためのストリームも用意されており、それがJarOutputStreamになります。
JarFileクラスとJarInputStreamクラスは、用意されているメソッドにはあまり違いが無いように見えます。機能だけを見ると、同じjarファイルにアクセスするクラスが複数用意されているような印象をうけますが、これら2つが意味するものは全く違います。FileとFileInputStreamの関係になぞらえてJarFileとJarInputStreamを整理してみると、前者はファイルそのものの存在を抽象化している一方で、後者は統一的に中身を扱うための機能をひとまとまりにして提供している、と区別することができます。
また、以前にも触れましたが、jarファイルはzipファイルと同じ構造になっています。ストリームのクラスも、java.util.zipパッケージに含まれるzipのストリームを扱うクラスを継承しており、主な違いは以下のようになっています。
- JarEntryを扱うメソッドが追加されている
- Manifestを扱うメソッドが追加されている
- 内部のmeta-infフォルダのエントリに直接アクセスできなくなっている
■データを読み出す
JarInputStream使って、jarファイルからデータを読み出す処理の手順は、次のようになります。
- ファイルの中身にアクセスするFileInputStreamを作り、JarInputStreamのコンストラクタに引き渡す
- getNextJarEntryメソッドを繰り返し呼び出して、先頭から順にJarEntryを取得する
- getNextJarEntryメソッドは、ファイル中の読み出しの位置を、そのエントリの中身の先頭に移動する
- JarEntryのgetNameメソッドを使って、エントリの名前を調べ、
- 読み出しを行うエントリの場合、readメソッドで読み出しの処理を行う
- 読み出しを行うエントリではない場合、getNextJarEntryメソッドで次のエントリを取得する
JarInputStreamを使ったプログラムを作ってみました(ex1.java)。以下では、例外処理を省いています。中身を読み出す繰り返し処理では、ループの条件としてJarInputStreamのavailableメソッドとreadで読み出されたデータの長さを調べています。availableメソッドは、読み出し位置がそのエントリの最後を超えていないかどうかを調べるメソッドで、超えている場合は0を返し、それ以外は常に1を返します。
...
static String filename = "jis.jar";
static String readentry = "jarscape.java";
public static void main( String[]
args ) {
JarInputStream
jis = new
JarInputStream( new BufferedInputStream( new
FileInputStream( filename ) ) );
JarEntry
je;
while( (
je = jis.getNextJarEntry()
) != null ) {
System.out.println(
je.getName() );
if(
je.getName().equals(
readentry ) ) {
int
len;
int
bufsize = 1024;
byte[]
buf = new byte[bufsize];
ByteArrayOutputStream
baos = new ByteArrayOutputStream();
while(
jis.available()
> 0 && (
len = jis.read( buf, 0, bufsize ) ) != -1 ) {
baos.write(
buf, 0, len );
}
System.out.println(
baos.toString() );
}
}
...
|
| ex1.java |
JarInputStreamでファイルを読み出す |
実際に、jarファイルから中身を読み出す場合には、読み出したいエントリを知る必要があります。事前にJarFileのentriesメソッドなどを用いて、エントリの一覧を取得して、選択操作を行うことになるでしょう。読み出しのループ処理で繰り返しの条件に、availableメソッドとreadしたデータの長さの両方をチェックをしています。どちらか片方だけにして、特に問題が発生したことはないのですが、両方書いておいた方が安全でしょう。
■デコデコと飾り付け
ストリームのクラスでは、次々に別のストリームクラスのインスタンスを重ねて生成することで、機能の追加が行えるようになっています。前述の例では、FileInputStreamのインスタンスを引数に、BufferedInputStreamのインスタンスを生成し、それをさらに引数にしてJarInputStreamのインスタンスを生成しています。FileInputStreamをBufferedInputStreamでくるむことで、ファイルにアクセスする処理をバッファリングする機能を追加しています。
JarInputStream jis = new JarInputStream( new BufferedInputStream(
new FileInputStream( filename ) ) );
|
| |
インスタンスを重ねて生成することによる、ストリームの機能追加 |
JarInputStream自体も、アーカイブされた中身を圧縮状態から解凍し、フィルタリングして個別にアクセスする機能を追加している、と言い換えることができます。このような作りは、デザインパターンの1つであるDecoratorパターンに類するものです。覆うように、階層的に飾り付けをしていくと、機能が追加されていくわけです。
ストリームのクラスでは、通常、一度追加した機能をさらに追加してもあまり意味を持ちません。しかしながら、JarInputStreamのようなクラスについては、処理の意味上、二度、三度と追加することがあり得ます。JarEntryの対象となる中身がJarファイルである場合、すなわち、Jarが入れ子になっている場合がそれに当たります。
JarInputStreamをさらにJarInputStreamでデコレートするプログラムを作ってみました(ex2.java)。プログラムでは例外処理を記述していますが、以下では省いています。この例では、JarEntryを調べ、エントリの名前の拡張子が"jar"である場合、コンストラクタにJarInputStream(インスタンスの名前はjis)を指定して、さらなるJarInputStreamのインスタンス(名前はjis2)を作成しています。jisではあくまでも、そのエントリだけがjis2で処理される対象になりますから、getNextJarEntryで読み出し位置をエントリの先頭に移動させてから、jis2のインスタンスを生成する必要があります。jis2はjisの構造を気にすることなく、単なるストリームとしてその位置から中身を読み出します。
...
static String filename = "jis.jar";
static String readentryext = ".jar";
public static void main( String[]
args ) {
JarInputStream
jis = new JarInputStream( new BufferedInputStream( new FileInputStream(
filename ) ) );
JarEntry
je;
while( (
je = jis.getNextJarEntry() ) != null ) {
System.out.println(
je.getName() );
if(
je.getName().endsWith( readentryext ) ) {
JarInputStream
jis2 = new
JarInputStream( jis );
JarEntry
je2;
while(
jis.available()
> 0 && ( je2 = jis2.getNextJarEntry() ) != null )
{
System.out.println(
je.getName() + "# " + je2.getName() );
}
}
}
...
|
| ex2.java |
エントリがjarファイルの場合の中身の読み出し |
jis2のエントリを調べるループ処理で、繰り返しの条件にjisのavailableメソッドをチェックしています。jis2のストリームの読み出しで、エントリの終わりを超えて次のエントリに行ってしまうような動作をしたことはないのですが、念のために入れておきました。
また、内側のストリームを読み出す際に、読み出し位置が前後に移動するような場合、間にBufferedInputStreamを入れておくことが考えられます。これは例えば、一旦、全てのエントリをgetNextJarEntryで調べてから、再度、どれを展開するか指示する操作を行う、といった処理が考えられるためです。
このような作りによる動作で奇妙なのは、内側のストリームでは通常JarEntryが取得できないmeta-infフォルダの下のエントリも取得できてしまうことです。JarInputStreamのソースを一見したところでは、特に理由は見つかりませんでした。
■他はないの?
JarViewでも、このような作りを行い、入れ子のjarファイルを実際に展開することなく、中身が見られるように処理をしています。JarViewはjavaの標準のクラスしか使っていないため、zip形式しか展開できません。Windowsの世界で使うには、日本国内ではLHA形式も扱えないと不便です。gzip形式はzip形式同様、標準でもできますからUnixでは問題ないでしょうが、Macでの使用を考えると、sitやcptなどが扱えないと、これまた不便ということになります。
JarInputStreamは、ZipInputStreamを継承しているわけですが、ZipInputStreamも同じくjava.util.zipパッケージのInflaterInputStreamを継承して作られています。このクラスでは、伸張を行うための処理をInflaterクラスにカプセル化して実装しており、処理を委譲する形になっています。Inflaterがインタフェースでなくクラスなのが理解に苦しむところですが、LHAやsit、cptなどを扱えるように実装する際のモデルとなるのは確かです。それぞれ、どれも、ネイティブなライブラリは世の中にありますから、それらをうまく使えるように実装していければいいわけです。
これまで、4回にわたって、jarというキーワードを中心に話題を展開してきました。クラスのローディングパスや、ストリームなど、jarから話を広げて、そこら中に寄り道したので、結構、盛りだくさんになってしまいました。なかなか興味深いことが多く、javaのライブラリのスマートなところや首を傾げたくなるところなど、いろいろ見えてきたと思います。
他の人の作ったライブラリのエキスパートになっても、あまり意味はないかもしれませんが、膨大なソースの実例として考えると、何よりも多様なサンプルが目の前に積み上げられていると言えます。いろいろな角度から眺めていくのがいいのではないでしょうか。

|