JSR 200: Network Transfer Format for Java Archives

Javaプログラムを公開する場合には、通常JAR(Java ARchive)を形式を利用するが、このJAR形式のファイルをネットワーク上で効率良く転送するための圧縮形式がある。それがJSR 200で定められている「Pack200」だ。Pack200はJARファイルのダウンロードサイズを減少させる目的で作られたファイル形式で、JAR形式の性質に特化して高い圧縮率を実現する。

Pack200形式はHTTP 1.1(RFC 2616)プロトコルによるHTTP圧縮でもサポートされており、クライアント側でJARファイルを要求するようなアプリケーションにおいて、Pack200を利用することによって転送量を最小限に抑えることができる。実際、J2SE 5.0以降のJava Web StartやJava Plug-inにおいてはRFC 2616準拠のHTTP圧縮によってPack200がサポートされている。

Pack200形式による圧縮と解凍

JSR 200の参照実装はJ2SE 5.0よりJava SEプラットフォームに統合されている。対象のクラス/インタフェースはjava.util.jar.Pack200およびその内部インタフェースであるjava.util.jar.Pack200.Packer、java.util.jar.Pack200.Unpackerの3つ。

Pack200.Packerはその名の通りPack200形式でファイルを圧縮するためのエンジンで、リスト1のように使用する。まずPack200クラスのnewPacker()メソッドでPack200.Packerインスタンスを取得し、pack()メソッドを用いて圧縮を実行する。pack()の第1引数には圧縮対象となるJarFileまたはJarInputStreamのインスタンスを、第2引数には圧縮結果を出力するOutputStreamのインスタンスを指定する。Pack200.Packerインタフェースに用意された各種定数を用いてプロパティを指定することにより、JSR 200仕様で定められているより高度な圧縮を行うこともできる。

リスト1 PackSample.java - Pack200形式でJARファイルを圧縮

package apisample;

import java.io.*;
import java.util.jar.Pack200;
import java.util.jar.Pack200.Packer;
import java.util.jar.JarFile;

public class PackSample {
    public static void main(String[] args) {
        // Packerを生成
        Packer packer = Pack200.newPacker();

        try {
            // ストリームを用意
            JarFile jarFile = new JarFile("sample.jar");
            FileOutputStream output = new FileOutputStream("sample.jar.pack");

            // 圧縮
            packer.pack(jarFile, output);
            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Pack200.UnpackerはPack200形式で圧縮されたファイルをJARファイルに変換するエンジンで、リスト2のように使用する。Packerの場合と同様に、まずPack200クラスのnewUnacker()メソッドでPack200.Unpackerインスタンスを取得し、unpack()メソッドを用いて変換する。第1引数にはPack200ファイルのFileインスタンスかまたはInputStreamインスタンスを、第2引数にはJARファイルを出力するJarOutputStreamのインスタンスを指定する。

リスト2 UnpackSample.java - Pack200ファイルからJARファイルへの展開

package apisample;

import java.io.*;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import java.util.jar.Pack200.Unpacker;

public class UnpackSample {
    public static void main(String[] args) {
        // Unpackerを生成
        Unpacker unpacker = Pack200.newUnpacker();

        try {
            // ストリームを用意
            File packedFile = new File("sample.jar.pack");
            JarOutputStream output = new JarOutputStream(
                    new FileOutputStream("sample.jar"));

            // 展開
            unpacker.unpack(packedFile, output);
            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Pack200を使ってJARファイルを転送する

Pack200はもともとJARファイルをネットワークで効率良く転送するために作られた圧縮形式である。pack()メソッドやunpack()メソッドは入力元および出力先としてストリームを指定できるようになっており、ネットワーク通信との相性がいいように設計されている。リスト1およびリスト3.2は、Pack200形式で圧縮したファイルをソケットを用いて送受信するプログラムの例である。pack()の出力側、unpack()の入力側に、それぞれソケットから取得したストリームを渡してあげるだけでよい (ただし、この例のようにリクエストを受けてから圧縮するやり方は実用的ではない)。

リスト3 PackSendSample.java - Pack200ファイルの送信

package apisample;

import java.net.*;
import java.io.*;
import java.util.jar.JarFile;
import java.util.jar.Pack200;
import java.util.jar.Pack200.Packer;

public class PackSendSample {
    public PackSendSample() {
        try {
            // 接続待ち
            ServerSocket serverSocket = new ServerSocket(8080);
            Socket socket = serverSocket.accept();

            // ファイルを圧縮して転送
            JarFile jarFile = new JarFile("sample.jar");
            OutputStream output = socket.getOutputStream();

            Packer packer = Pack200.newPacker();
            packer.pack(jarFile, output);

            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        PackSendSample sample = new PackSendSample();
    }
}

リスト4 PackReceiveSample.java - Pack200ファイルの受信

package apisample;

import java.net.Socket;
import java.io.*;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import java.util.jar.Pack200.Unpacker;

public class PackReceiveSample {
    public PackReceiveSample() {
        try {
            // 接続
            Socket socket = new Socket("localhost", 8080);

            // ファイルの受信と展開
            InputStream receiveStream = socket.getInputStream();
            JarOutputStream jarOut = new JarOutputStream(new FileOutputStream("sample.jar"));

            Unpacker unpacker = Pack200.newUnpacker();
            unpacker.unpack(receiveStream, jarOut);

            jarOut.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        PackReceiveSample sample = new PackReceiveSample();
    }
}

Pack200ファイルをWebサーバに配備する

続いて、WebアプリケーションにおいてPack200形式の圧縮ファイルを利用する例を紹介する。HTTP圧縮ではgzipおよびpack200-gzipがサポートされており、クライアントアプリケーションはHTTPリクエストのaccept-encodingフィールドにこれらの形式を指定することができる。サーバ・アプリケーション側では、リクエスト・ヘッダにこれらの値が指定されている場合にはクライアントがPack200やGZIP形式を処理できると判断し、content-encordingフィールドを設定して対応するファイルを送信すればよい。

この仕組みをサーブレットで実現する場合、手順は次のようになる。ただし、今回は簡略化のため (Pack200を使用しない) GZIP形式は省略している。

  1. クライアントがaccept-encodingにpack200-gzipを指定してsample.jarを要求する
  2. サーブレットはsample.jar.pack.gzを検索する
  3. sample.jar.pack.gzが存在する場合にはレスポンスヘッダのcontent-encordingフィールドにpack200-gzip、content-typeフィールドにapplication/java-archiveを指定して返す
  4. 存在しない場合にはcontent-encordingフィールドにnull、content-typeフィールドにapplication/java-archiveを指定して、sample.jarをそのまま返す

実際のサーブレットをリスト5に示す。

リスト5 Pack200Servlet.java - Pack200形式をサポートするサーブレットの例

package apisample;

import java.io.*;
import java.net.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class Pack200Servlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        // リクエストヘッダの取得
        String encoding = request.getHeader("accept-encoding");
        String contentType = request.getContentType();
        String pathInfo = request.getPathInfo();
        String pathTranslated = request.getPathTranslated();

        String outFile = pathInfo;
        String contentEncoding = null;

        // 圧縮ファイルの検索
        if (encoding != null && contentType != null &&
                contentType.compareTo("application/x-java-archive") == 0 &&
                encoding.toLowerCase().indexOf("pack200-gzip") > -1) {
            contentEncoding = "pack200-gzip";
            File file = new File(pathTranslated.concat(".pack.gz"));
            if (file.exists()) {
                outFile = pathInfo.concat(".pack.gz") ;
            }
        } 

        // レスポンスヘッダの設定
        response.setHeader("content-encoding", contentEncoding);
        response.setContentType(getServletContext().getMimeType(pathInfo));

        // ファイルの転送
        InputStream input = getServletContext().getResourceAsStream(outFile);
        OutputStream output = response.getOutputStream();
        if (input != null) {
            byte buf[] = new byte[1024];
            int readsize;
            while ((readsize = input.read(buf)) > 0) {
                output.write(buf,0,readsize);
            }
            output.flush();
            output.close();
            input.close();
        }
    } 
}

web.xmlには、JARファイルに対する要求をこのサーブレットで処理するように記述すればよい。クライアントプログラムではリスト6のように、受信したファイルをJAR形式に戻して利用することになる (content-encordingがnullだった場合の処理は省略)。

リスト6 ConnectionSample.java - Pack200形式をサポートするクライアントプログラムの例

package apisample;

import java.io.*;
import java.net.*;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import java.util.jar.Pack200.Unpacker;
import java.util.zip.GZIPInputStream;

public class ConnectionSample {
    public static void main(String[] args) {
        try {
            // HTTPによる接続
            URL url = new URL("http://localhost:8080/Pack200Servlet/sample.jar");
            HttpURLConnection conn = (HttpURLConnection)url.openConnection();
            conn.setRequestMethod("GET");
            conn.setRequestProperty("accept-encoding", "pack200-gzip");
            conn.setRequestProperty("content-type", "application/x-java-archive");
            conn.connect();

            // ファイルの取得と展開
            GZIPInputStream input = new GZIPInputStream(conn.getInputStream());
            JarOutputStream jarOut = new JarOutputStream(new FileOutputStream("sample.jar"));

            Unpacker unpacker = Pack200.newUnpacker();
            unpacker.unpack(input, jarOut);

            jarOut.close(); 
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

今回紹介したのは非常に単純な利用方法でしかないが、うまく活用すればネットワークの効率的な利用を実現することができるはず。すでにコアプラットフォームに統合されているので、必要に応じて利用を検討してみるといいだろう。