注: この Java 配備ガイドでは、Java SE 6 update 10 リリースより前にリリースされた機能について説明します。最新情報については、Java Rich Internet Application の開発および配備を参照してください。
この章では、次のトピックについて説明します。
サーバーやネットワークの利用度および帯域幅を向上させるために、Java アプリケーションやアプレットの配備の際に gzip と Pack200 という 2 つの圧縮形式を利用できるようになりました。
どちらかの技術を使用して圧縮した JAR ファイルをネットワーク上で伝送し、受信側のアプリケーションで圧縮解除して復元します。
HTTP 1.1 (RFC 2616) プロトコルでは、HTTP 圧縮を扱います。HTTP 圧縮により、アプリケーションでは JAR ファイルを圧縮された JAR ファイルのまま配備することができます。サポートされる圧縮技術は gzip、compress、deflate です。
SDK/JRE バージョン 5.0 では、HTTP 圧縮は RFC 2616 に準拠して Java Web Start と Java Plug-in に実装されています。サポートされる技術は gzip および pack200-gzip です。
要求側のアプリケーションは、HTTP 要求をサーバーに送信します。HTTP 要求にはフィールドが複数あります。Accept-Encoding (AE) フィールドは pack200-gzip または gzip に設定され、アプリケーションが pack200-gzip または gzip 形式を処理できることをサーバーに通知します。
サーバーの実装では、ファイル拡張子が .pack.gz または .gz である要求された JAR ファイルを検索し、検索したファイルを返します。サーバーは送信しているファイルのタイプに応じて、応答ヘッダーの Content-Encoding (CE) フィールドに pack200-gzip、gzip、または null を設定し、オプションとして Content-Type (CT) をアプリケーションまたは Java アーカイブとして設定します。したがって、要求側のアプリケーションでは、CE フィールドを検査することで、対応する変換を実行し、元の JAR ファイルに復元します。
この例は、単純なサーブレットまたはサーバーモジュールと HTTP 1.1 準拠の Web サーバーを使用して実現できます。ファイルを稼働時に圧縮すると、Pack200 を使用する場合は特にサーバーのパフォーマンスが低下するため、推奨されません。
Tomcat サーブレットの例:
/**
* A simple HTTP Compression Servlet
*/
import java.util.*;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.util.zip.*;
import java.net.*;
/**
* The servlet class.
*/
public class ContentType extends HttpServlet {
private static final String JNLP_MIME_TYPE = "application/x-java-jnlp-file";
private static final String JAR_MIME_TYPE = "application/x-java-archive";
private static final String PACK200_MIME_TYPE = "application/x-java-pack200";
// HTTP Compression RFC 2616 : Standard headers
public static final String ACCEPT_ENCODING = "accept-encoding";
public static final String CONTENT_TYPE = "content-type";
public static final String CONTENT_ENCODING = "content-encoding";
// HTTP Compression RFC 2616 : Standard header for HTTP/Pack200 Compression
public static final String GZIP_ENCODING = "gzip";
public static final String PACK200_GZIP_ENCODING = "pack200-gzip";
private void sendHtml(HttpServletResponse response, String s)
throws IOException {
PrintWriter out = response.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>ContentType</title>");
out.println("</head>");
out.println("<body>");
out.println(s);
out.println("</body>");
out.println("</html>");
}
/*
* Copy the inputStream to output ,
*/
private void sendOut(InputStream in, OutputStream ostream)
throws IOException {
byte buf[] = new byte[8192];
int n = in.read(buf);
while (n > 0 ) {
ostream.write(buf,0,n);
n = in.read(buf);
}
ostream.close();
in.close();
}
boolean doFile(String name, HttpServletResponse response) {
File f = new File(name);
if (f.exists()) {
getServletContext().log("Found file " + name);
response.setContentLength(Integer.parseInt(
Long.toString(f.length())));
response.setDateHeader("Last-Modified",f.lastModified());
return true;
}
getServletContext().log("File not found " + name);
return false;
}
/** Called when someone accesses the servlet. */
public void doGet(HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
String encoding = request.getHeader(ACCEPT_ENCODING);
String pathInfo = request.getPathInfo();
String pathInfoEx = request.getPathTranslated();
String contentType = request.getContentType();
StringBuffer requestURL = request.getRequestURL();
String requestName = pathInfo;
ServletContext sc = getServletContext();
sc.log("----------------------------");
sc.log("pathInfo="+pathInfo);
sc.log("pathInfoEx="+pathInfoEx);
sc.log("Accept-Encoding="+encoding);
sc.log("Content-Type="+contentType);
sc.log("requestURL="+requestURL);
if (pathInfoEx == null) {
response.sendError(response.SC_NOT_FOUND);
return;
}
String outFile = pathInfo;
boolean found = false;
String contentEncoding = null;
// Pack200 Compression
if (encoding != null && contentType != null &&
contentType.compareTo(JAR_MIME_TYPE) == 0 &&
encoding.toLowerCase().indexOf(PACK200_GZIP_ENCODING) > -1){
contentEncoding = PACK200_GZIP_ENCODING;
if (doFile(pathInfoEx.concat(".pack.gz"),response)) {
outFile = pathInfo.concat(".pack.gz") ;
found = true;
} else {
// Pack/Compress and transmit, not very efficient.
found = false;
}
}
// HTTP Compression
if (found == false && encoding != null &&
contentType != null &&
contentType.compareTo(JAR_MIME_TYPE) == 0 &&
encoding.toLowerCase().indexOf("gzip") > -1) {
contentEncoding = GZIP_ENCODING;
if (doFile(pathInfoEx.concat(".gz"),response)) {
outFile = pathInfo.concat(".gz");
found = true;
}
}
// No Compression
if (found == false) { // just send the file
contentEncoding = null;
sc.log(CONTENT_ENCODING + "=" + "null");
doFile(pathInfoEx,response);
outFile = pathInfo;
}
response.setHeader(CONTENT_ENCODING, contentEncoding);
sc.log(CONTENT_ENCODING + "=" + contentEncoding +
" : outFile="+outFile);
if (sc.getMimeType(pathInfo) != null) {
response.setContentType(sc.getMimeType(pathInfo));
}
InputStream in = sc.getResourceAsStream(outFile);
OutputStream out = response.getOutputStream();
if (in != null) {
try {
sendOut(in,out);
} catch (IOException ioe) {
if (ioe.getMessage().compareTo("Broken pipe") == 0) {
sc.log("Broken Pipe while writing");
return;
} else throw ioe;
}
} else response.sendError(response.SC_NOT_FOUND);
}
}
Pack200 では、JAR 内のクラスファイルの密度とサイズに応じて、サイズの大きいファイルを非常に効率よく圧縮できます。JAR ファイルに含まれているのがクラスファイルだけで、その大きさが数 MB 程度しかなければ、9 分の 1 に圧縮することも可能です。
前述の例と同じ jar ファイルを使用します。
Notepad.jar 46.25kb
Notepad.jar.pack.gz 22.58kb
この場合同じ jar ファイルのサイズは 50% 削減されます。
注: 大きなサイズの jar ファイルを署名すると、セキュリティーエラーによりステップ 5 が失敗することがあります。このエラーの多くは、バグ 5078608 が原因です。リリースノートに説明されている回避方法を実行してください。
Pack200 は Java クラスファイルに対して、もっとも効率のよい方法です。次のような複数の技術を使用して、効果的に JAR ファイルのサイズを小さくします。
Pack200 は、SDK または JRE ディレクトリの bin ディレクトリにある、コマンド行インタフェース pack200(1)、unpack200(1) によって使用できます。
Pack200 インタフェースは、Java からプログラムを使って呼び出すことができます。Java.util.jar.Pack200 の API リファレンス、および JavaDoc リファレンスを参照してください。
1. JAR ファイルのサイズ、JAR ファイルの内容、および対象とするユーザーの帯域幅を検討します。
これらの要因すべてを考慮して圧縮技術を選択します。unpack200 は、可能なかぎり効率的な動作をするように設計されていて、また短時間で元のファイルを復元します。JAR ファイルのサイズが大きく (2MB 以上)、そのほとんどがクラスファイルである場合は、Pack200 が推奨されます。そのほとんどがリソースファイル (JPEG、GIF、データなど) である大きい JAR ファイルの場合は、gzip が推奨されます。
2. Pack200 セグメント機能。
Pack200 では、パックファイル全体をメモリーにロードします。しかし、ターゲットシステムのメモリーとリソースに制限がある場合、Pack200.Packer.SEGMENT_LIMIT を小さな値に設定することで、パックおよびアンパックで必要となるメモリー量を減らすことができます。Pack200.Packer.SEGMENT_LIMIT=-1 に設定すると、生成するセグメントを強制的に 1 つにするためサイズの削減に効果がありますが、パックおよびアンパックするシステムでは、Java ヒープを多く使用します。これらのパックされたセグメントのいくつかは、1 つのパックされたファイルを生成するために連結されることに注意してください。
3. JAR ファイルの署名。
Pack200 では、生成される JAR ファイルの内容を再配置します。jarsigner では、クラスファイルの内容をハッシュ化し、マニフェスト内の暗号化されたダイジェストにハッシュを格納します。アンパックアプリケーションをパックされたファイルに対して実行すると、クラスの内容が再配置されるため、署名が無効になります。そのため、pack200 および unpack200 を使用して先に JAR ファイルを正規化してから、署名する必要があります。
(このように行える理由: パックプログラムが実行するクラスファイル構造の再配置はべき等であるため、2 回目のパックでは、最初のパックで生成された順序が変更されない。またアンパックプログラムでは、どのようなアーカイブ要素の伝送順序に対しても特定のバイトイメージを生成することが、JSR 200 仕様で保証されている。)
HelloWorld.jar を使用するとします。
ステップ 1: jar ファイルを正規化するためにファイルを再パックします。再パックでは、パックの実行とファイルのアンパックが 1 回のステップで行われます。
% pack200 --repack HelloWorld.jar
ステップ 2: 再パックを使用して正規化したあとに、jar ファイルに署名します。
% jarsigner -keystore myKeystore HelloWorld.jar ksrini
注: 再パックしたファイルには、元の JAR ファイルの構築に使用したものと同じ鍵で署名する必要があります。あるいは、再パック、再署名、および検証を行う前に、META-INF ディレクトリにあるすべての署名ファイルを削除します。署名ファイルの名前は MANIFEST.MF、*.DSA、および *.SF です。
署名した jar ファイルを検証して署名されていることを確認します。
% jarsigner -verify HelloWorld.jar jar verified.
ファイルが実行可能であることを確認します。
% Java -jar HelloWorld.jar HelloWorld
ステップ 3: ファイルをパックします
% pack200 HelloWorld.jar.pack.gz HelloWorld.jar
ステップ 4: ファイルをアンパックします
% unpack200 HelloWorld.jar.pack.gz HelloT1.jar
ステップ 5: jar を検証します
% jarsigner -verify HelloT1.jar jar verified.
jar をテストします ...
% Java -jar HelloT1.jar HelloWorld
検証後、圧縮されたパックファイル HelloWorld.jar.pack.gz が配備されます。
4. サイズ削減のテクニック
Pack200.Packer.MODIFICATION_TIME="LATEST" を設定できます。こうすることで、パックファイルで転送される更新時間はセグメントごとに 1 つになります。最新時間は、そのセグメント内すべてのエントリでもっとも新しく更新された時間になります。 例:
pack200 --modification-time=latest --deflate-hint="true" tools-md.jar.pack.gz tools.jar
注:上記の最適化は、何千ものエントリを含む JAR ファイルでは効果が顕著になります。
Pack200.Packer.STRIP_DEBUG=true. を指定して破棄できます。これにより、パックされたファイルのサイズは、一般的に約 10% 減ります。
例:
pack200 --strip-debug tools-stripped.jar.pack.gz tools.jar
5. 未知の属性の処理
Pack200 は、Java 仮想マシン仕様により定義される標準的な属性に対応しますが、コンパイラはカスタム属性の影響を受けません。カスタム属性が存在すると、デフォルトでは、Pack200 はクラスを通過して警告メッセージを発行します。これらの、「通過」するクラスファイルにより、パックしたファイルのサイズが大きくなる場合があります。JAR ファイルのクラス内に、未知の属性が多く使われている場合、パックされた出力のサイズが非常に大きくなる場合があります。その場合は、次の方法を検討してください。
属性が実行時に不要であると判断される場合、属性を取り除きます。これはプロパティー Pack200.Packer.UNKNOWN_ATTRIBUTE=STRIP または次を設定することにより実現されます
pack200 --unknown-attribute=strip foo.pack.gz foo.jar属性が実行時に必要で、拡大の原因になる場合、警告メッセージから属性を特定し、適切な配置を行います。この方法については、Pack200 JSR 200 の仕様、および Java API リファレンスの Pack200.Packer に関するセクションで説明されています。
コンパイラが、Pack200 のレイアウト仕様内で実装されない属性を定義することも可能ですが、パックの際に障害の元となる場合があります。その場合、クラスファイル名をリソースのようにすることで、クラスファイル全体を「通過」させることができます。次のように指定します。
pack200 --pass-file="com/acme/foo/bar/baz.class" foo.pack.gz foo.jar
または、ディレクトリとその内容全体を次のように指定します。
pack200 --pass-file="com/acme/foo/bar/" foo.pack.gz foo.jar
6. インストーラ
インストールプログラムにおいて Pack200 テクノロジの機能を利用するためには、製品の JAR ファイルは、Pack200 を使用して圧縮し、インストールのときに圧縮解除する必要があります。インストールに JRE または SDK が含められている場合、配布された「bin」ディレクトリにある unpack200 (Unix) または unpack200.exe (Windows) を自由に使用することができます。この実装は純粋な C++ アプリケーションなので、実行するために Java ランタイムは必要はありません。
Windows: インストーラは、GZIP よりも優れたアルゴリズムを使用してエントリを圧縮します。次のように pack200 を使用すれば、インストーラ自体の圧縮アルゴリズムによって、よりよい圧縮効果を得られます。
pack200 --no-gzip foo.jar.pack foo.jar
これにより、gzip として圧縮された出力ファイルになることを防ぐことができます。
unpack200 は、Windows コンソールのアプリケーションです。特に、インストール中には MS-DOS ウィンドウを表示します。これを防ぐには次に示すように、このウィンドウを非表示にする WinMain による起動ツールを使用します。
サンプルコード:
#include "windows.h"
#include <stdio.h>
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow) {
STARTUPINFO si;
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
PROCESS_INFORMATION pi;
memset(&pi, 0, sizeof(pi));
//Test
//lpCmdLine = "c:/build/windows-i586/bin/unpack200 -l c:/Temp/log c:/Temp/rt.pack c:/Temp/rt.jar";
int ret = CreateProcess(NULL, /* Exec. name */
lpCmdLine, /* cmd line */
NULL, /* proc. sec. attr. */
NULL, /* thread sec. attr */
TRUE, /* inherit file handle */
CREATE_NO_WINDOW | DETACHED_PROCESS, /* detach the process/suppress console */
NULL, /* env block */
NULL, /* inherit cwd */
&si, /* startup info */
&pi); /* process info */
if ( ret == 0) ExitProcess(255);
// Wait until child process exits.
WaitForSingleObject( pi.hProcess, INFINITE );
DWORD exit_val;
// Be conservative and return
if (GetExitCodeProcess(pi.hProcess, &exit_val) == 0) ExitProcess(255);
ExitProcess(exit_val); // Return the error code of the child process
return -1;
}
パックされているものもアンパックされているものも含め、すべての JAR ファイルは、アプリケーションのテスト合格基準に沿って問題がないことをテストする必要があります。コマンド行インタフェース pack200 を使用すると、gzip とデフォルト設定を使用して出力ファイルが圧縮されます。単なるパックファイルを作成して、圧縮にはユーザーが指定したオプションで gzip を使用したり、別の圧縮プログラムを使用したりすることが可能です。
詳細については、「Java 配備ツール」の pack200 および unpack200 を参照してください。
Java SE 6 では、Java クラスファイル形式が更新されました。詳細については、「JSR 202: Java Class File Specification Update」を参照してください。JSR 202 により、次の理由から、Pack200 エンジンを適宜更新する必要があります。
変更点を最小限に抑え、ユーザーに意識させないようにするために、圧縮プログラムは入力クラスファイルのバージョンに基づき、適切なバージョンのパックファイルを生成します。
また、下位互換性を保持するために、入力 JAR ファイルが JDK 1.5 以前のクラスファイルで統一して構成されている場合、1.5 互換のパックファイルが生成されます。それ以外の場合は、Java SE 6 互換の pack200 ファイルが生成されます。詳細については、Pack200 のマニュアルページを参照してください。