サイトアイコン Omomuki Tech

java.util.jar 完全ガイド:JARファイルの作成、読み込み、署名をマスターする

この記事から得られる知識

  • java.util.jar パッケージの全体像と、Java開発におけるその中心的な役割を理解できます。
  • JARファイルの基本的な構造、特に META-INF/MANIFEST.MF ファイルの重要性について深く学べます。
  • JarFile, JarOutputStream, Manifest などの主要クラスを駆使して、プログラムからJARファイルを自在に操作(作成、読み込み、更新)する具体的な手法を習得できます。
  • Main-Class 属性を指定し、コマンドラインから直接実行可能なJARファイルを作成する方法がわかります。
  • jarsigner ツールを用いたJARファイルへのデジタル署名と、その署名をプログラムから検証する基本的なセキュリティ対策を学べます。

はじめに:なぜ今、java.util.jar を学ぶのか?

Java開発において、JAR (Java Archive) ファイルは、コンパイルされたクラスファイル、関連するメタデータ、リソース(画像、設定ファイルなど)を単一のファイルに集約するための標準的なフォーマットです。 このJARファイルをプログラム的に操作するためのAPIが、java.util.jar パッケージとして提供されています。 ライブラリの配布、アプリケーションのデプロイ、プラグイン機構の実現など、Javaエコシステムの根幹を支える技術であり、その理解はすべてのJava開発者にとって不可欠です。

このパッケージは、単にファイルをまとめるだけでなく、マニフェストファイルによるメタデータの管理、デジタル署名によるセキュリティの確保、Service Provider Interface (SPI) による拡張性の提供など、高度な機能を持っています。 本記事では、java.util.jar パッケージの基本から応用までを、具体的なコード例を交えながら徹底的に解説していきます。

第1章: JARファイルとは?その心臓部「マニフェスト」を理解する

JARファイルは、その実体はZIPファイル形式に基づいています。 そのため、拡張子を .zip に変更すれば、多くのZIP展開ツールで中身を閲覧することが可能です。 しかし、JARファイルを単なるZIPファイル以上のものにしているのが、META-INF ディレクトリ、特にその中にある MANIFEST.MF ファイルの存在です。

1.1 META-INF/MANIFEST.MF の役割

マニフェストファイルは、JARファイル全体に関するメタデータを「キー: 値」のペアで記述するテキストファイルです。 このファイルはJARアーカイブの「目次」や「設定ファイル」のような役割を果たし、Javaランタイムや各種ツールがJARファイルの情報を解釈するために利用します。

主な役割は以下の通りです。

  • 実行可能JARの定義: Main-Class 属性でエントリーポイントとなるクラスを指定し、java -jar コマンドで直接実行できるようにします。
  • バージョン管理: Specification-VersionImplementation-Version といった属性で、ライブラリのバージョン情報を明記します。
  • パッケージのシーリング: 特定のパッケージがこのJARファイル内でのみ定義されていることを保証し、意図しないクラスの上書きを防ぎます。
  • 署名情報: JARファイルがデジタル署名されている場合、各ファイルエントリのダイジェスト値がここに記録され、改ざん検知の基礎となります。

1.2 マニフェストファイルの書式

マニフェストファイルはシンプルなテキスト形式ですが、いくつかのルールがあります。

マニフェストファイルの書式ルール

  • 各行は キー: 値 の形式で記述されます。コロンの後にはスペースが必要です。
  • ファイルはUTF-8でエンコードする必要があります。
  • 行の長さは72バイト(改行コード含む)に制限されています。長い値は次の行に継続され、その行はスペースで始めます。
  • ファイルは空行で終わる必要があります。空行がない場合、最後の行が正しく解釈されないことがあります。

以下はマニフェストファイルの典型的な例です。

Manifest-Version: 1.0
Created-By: 11.0.11 (AdoptOpenJDK)
Main-Class: com.example.MyApplication
Implementation-Title: My Awesome App
Implementation-Version: 1.2.3
Implementation-Vendor: Example Inc.

Name: com/example/secure/Secret.class
SHA-256-Digest: (ここにダイジェスト値)

最初のセクションは「メイン属性」と呼ばれ、JAR全体に適用されます。 空行を挟んで続くセクションは「個別エントリ属性」と呼ばれ、Name 属性で指定されたファイルエントリにのみ適用されます。


第2章: `java.util.jar` パッケージの主要クラス群

java.util.jar パッケージには、JARファイルを扱うための便利なクラスが多数用意されています。 ここでは、特に重要なクラスの役割と概要を解説します。

クラス名 説明
JarFile 既存のJARファイルを読み込むための中心的なクラス。ファイルシステム上のJARファイルに対してランダムアクセスが可能です。マニフェスト情報を取得したり、個々のエントリを効率的に読み取ったりするのに使用します。
JarEntry JARファイル内の個々のエントリ(ファイルやディレクトリ)を表します。java.util.zip.ZipEntry を継承しており、そのエントリに関連付けられた属性や証明書を取得する機能が追加されています。
JarInputStream ストリームベースでJARファイルを読み込むためのクラス。ファイルシステム上だけでなく、ネットワーク経由など任意の入力ストリームからJARデータを読み込めます。シーケンシャルアクセスに適しています。
JarOutputStream プログラムで新しいJARファイルを生成するためのクラス。クラスファイルやリソースファイルをストリームとして書き込み、最終的にJARファイルを構築します。マニフェストを最初のエントリとして書き込む機能も持ちます。
Manifest マニフェストファイル (MANIFEST.MF) の内容をメモリ上で表現するクラスです。 メイン属性や個別エントリの属性をプログラムから読み書きできます。
Attributes マニフェスト内の属性(キーと値のペア)を保持するMapのようなクラスです。Attributes.Name クラスが標準的な属性キー(例: MAIN_CLASS, SPECIFICATION_VERSION)を定数として提供しています。

第3章: 実践!JARファイルのプログラマティックな操作

それでは、実際にこれらのクラスを使ってJARファイルを操作するコードを見ていきましょう。

3.1 新しいJARファイルの作成 (`JarOutputStream`)

ここでは、いくつかのファイルをまとめて新しいJARファイルを作成し、同時に実行可能なJARにするためにマニフェストも生成します。

<?xml version="1.0" encoding="UTF-8"?>
import java.io.*;
import java.util.jar.*;

public class CreateJarExample {
    public static void main(String[] args) throws IOException {
        String jarFilePath = "MyExecutableApp.jar";
        String mainClassName = "com.example.Main";
        String[] sourceFiles = {"com/example/Main.class", "com/example/Util.class"};
        
        // 1. マニフェストオブジェクトの作成
        Manifest manifest = new Manifest();
        Attributes mainAttributes = manifest.getMainAttributes();
        mainAttributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
        mainAttributes.put(Attributes.Name.MAIN_CLASS, mainClassName);

        // 2. JarOutputStreamの準備 (try-with-resourcesで自動クローズ)
        try (FileOutputStream fos = new FileOutputStream(jarFilePath);
             JarOutputStream jos = new JarOutputStream(fos, manifest)) {

            System.out.println("マニフェストを書き込みました。");

            // 3. 各ファイルをエントリとして追加
            for (String filePath : sourceFiles) {
                // ここではダミーファイルを作成して追加
                File fileToAdd = createDummyFile(filePath);
                
                System.out.println(filePath + " をJARに追加中...");
                JarEntry entry = new JarEntry(filePath);
                jos.putNextEntry(entry);
                
                // ファイルの内容を読み込んで書き込む
                try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileToAdd))) {
                    byte[] buffer = new byte;
                    int bytesRead;
                    while ((bytesRead = bis.read(buffer)) != -1) {
                        jos.write(buffer, 0, bytesRead);
                    }
                }
                jos.closeEntry();
                fileToAdd.delete(); // ダミーファイルを削除
                new File(fileToAdd.getParent()).delete(); // ダミーディレクトリを削除
            }
            System.out.println("JARファイルの作成が完了しました: " + jarFilePath);
        }
    }

    // テスト用のダミーファイルを作成するヘルパーメソッド
    private static File createDummyFile(String path) throws IOException {
        File file = new File(path);
        file.getParentFile().mkdirs();
        try (FileWriter writer = new FileWriter(file)) {
            writer.write("This is a dummy file for " + path);
        }
        return file;
    }
}

3.2 既存のJARファイルの読み込みと内容の展開 (`JarFile`)

次に、既存のJARファイルを開き、その中に含まれるエントリの一覧を表示し、特定のエントリの内容を読み取る方法を見てみましょう。

<?xml version="1.0" encoding="UTF-8"?>
import java.io.*;
import java.util.jar.*;
import java.util.Enumeration;

public class ReadJarExample {
    public static void main(String[] args) {
        String jarFilePath = "MyExecutableApp.jar"; // 先ほど作成したJARファイルを指定

        // JarFileはリソースを解放する必要があるため、try-with-resourcesを使用
        try (JarFile jarFile = new JarFile(jarFilePath)) {
            
            // 1. マニフェストの読み込みと表示
            System.out.println("--- マニフェスト情報 ---");
            Manifest manifest = jarFile.getManifest();
            if (manifest != null) {
                Attributes mainAttributes = manifest.getMainAttributes();
                mainAttributes.forEach((key, value) -> 
                    System.out.println(key + ": " + value)
                );
            } else {
                System.out.println("マニフェストファイルが見つかりません。");
            }
            
            System.out.println("\n--- JARファイル内のエントリ一覧 ---");
            
            // 2. 全エントリの列挙
            Enumeration<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                System.out.println("エントリ名: " + entry.getName() + ", サイズ: " + entry.getSize() + " bytes");
                
                // 3. 特定のエントリの内容を読み込む (例: Main.class)
                if (entry.getName().endsWith("Main.class")) {
                    System.out.println("  -> " + entry.getName() + " の内容を読み込みます...");
                    try (InputStream is = jarFile.getInputStream(entry)) {
                        // ここでは最初の数バイトを読み込んで表示するだけ
                        byte[] buffer = new byte;
                        int bytesRead = is.read(buffer);
                        System.out.print("     先頭バイト: ");
                        for(int i = 0; i < bytesRead; i++) {
                             System.out.printf("%02X ", buffer[i]);
                        }
                        System.out.println();
                    }
                }
            }
        } catch (FileNotFoundException e) {
            System.err.println("エラー: 指定されたJARファイルが見つかりません。 " + jarFilePath);
        } catch (IOException e) {
            System.err.println("JARファイルの読み込み中にエラーが発生しました。");
            e.printStackTrace();
        }
    }
}

コードのポイント:

  • JarFile クラスは、ファイルシステム上のJARファイルを効率的に読み取るのに最適化されています。リソースの解放が必要なため try-with-resources 文の使用が推奨されます。
  • getManifest() メソッドで、JARファイルのマニフェストを直接 Manifest オブジェクトとして取得できます。
  • entries() メソッドは、JARファイル内の全エントリを Enumeration<JarEntry> として返します。これを使ってループ処理が可能です。
  • 特定のエントリの内容を取得するには、getInputStream(JarEntry) メソッドを使います。これにより、そのエントリに対応する入力ストリームが得られます。

第4章: JARファイルのセキュリティ – 署名と検証

JARファイルは、配布と実行を容易にする一方で、悪意のある第三者による改ざんのリスクも伴います。 このリスクに対処するため、JARファイルにはデジタル署名を行う機能が備わっています。

4.1 なぜ署名が必要か?

JARファイルに署名する主な目的は2つです。

  1. 完全性 (Integrity) の保証: 署名されたJARファイルは、配布中に内容が1ビットでも変更されると、検証時にエラーとなります。これにより、ファイルが改ざんされていないことを保証します。
  2. 提供元の認証 (Authentication): 署名は、信頼できる認証局(CA)によって発行されたデジタル証明書に基づいて行われます。これにより、ユーザーはそのJARファイルが確かに信頼できる提供元から来たものであることを確認できます。

ブラウザ上で実行されるJavaアプレットやJava Web Startアプリケーションでは、署名されていないコードはサンドボックス内で厳しく制限されますが、信頼できる証明書で署名されたコードにはより多くの権限が与えられます。

4.2 `jarsigner` ツールによる署名と検証

JARファイルの署名と検証は、主にJDKに付属する jarsigner というコマンドラインツールを使って行います。 署名には、事前に keytool ツールで作成したキーストアと秘密鍵が必要です。

署名と検証のコマンド例

署名コマンド:

jarsigner -keystore mykeystore.jks -storepass mystorepass MyExecutableApp.jar mykeyalias
  • -keystore: キーストアファイルを指定します。
  • -storepass: キーストアのパスワードを指定します。
  • MyExecutableApp.jar: 署名対象のJARファイルです。
  • mykeyalias: キーストア内で使用する鍵のエイリアスです。

検証コマンド:

jarsigner -verify -verbose MyExecutableApp.jar
  • -verify: 検証モードで実行します。
  • -verbose: 詳細な検証結果を表示します。

署名を行うと、META-INF ディレクトリに .SF (Signature File) と .DSA または .RSA (Signature Block File) といったファイルが自動的に生成されます。 これらのファイルに署名情報と各ファイルのダイジェストが含まれています。

4.3 プログラムによる署名情報の検証

プログラムからJARファイルの署名情報を確認することも可能です。 これにより、アプリケーションが動的にロードするJARファイルの信頼性を実行時にチェックできます。

<?xml version="1.0" encoding="UTF-8"?>
import java.io.IOException;
import java.util.jar.JarFile;
import java.util.jar.JarEntry;
import java.security.cert.Certificate;
import java.util.Enumeration;
import java.io.InputStream;

public class VerifyJarSignatureExample {
    public static void main(String[] args) throws IOException {
        String signedJarPath = "SignedApp.jar"; // 署名済みのJARファイルを指定

        try (JarFile jarFile = new JarFile(signedJarPath)) {
            System.out.println(signedJarPath + " の署名情報を検証します...");

            // マニフェストエントリをまず読み込む必要がある
            JarEntry manifestEntry = jarFile.getJarEntry("META-INF/MANIFEST.MF");
            if (manifestEntry == null) {
                System.out.println("マニフェストが見つかりません。署名されていません。");
                return;
            }
            readEntry(jarFile, manifestEntry); // エントリを読み込むことで検証がトリガーされる

            Enumeration<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                if (entry.isDirectory()) {
                    continue;
                }
                
                System.out.println("エントリ: " + entry.getName());

                try {
                    // エントリの内容を読み込むことで、そのエントリの署名が検証される
                    readEntry(jarFile, entry);
                    
                    // 署名者証明書を取得
                    Certificate[] certs = entry.getCertificates();
                    if (certs != null && certs.length > 0) {
                        System.out.println("  署名者: " + ((java.security.cert.X509Certificate)certs).getSubjectX500Principal());
                    } else {
                        // META-INF内のファイルなど、署名対象外のエントリは証明書を持たない
                        if (!entry.getName().startsWith("META-INF")) {
                           System.out.println("  警告: このエントリは署名されていません!");
                        }
                    }
                } catch (SecurityException e) {
                    System.err.println("  セキュリティ例外: " + entry.getName() + " の署名検証に失敗しました! 改ざんの可能性があります。");
                    System.err.println("    " + e.getMessage());
                }
            }
            System.out.println("\n検証が完了しました。");

        }
    }
    
    // エントリの全内容を読み込むヘルパーメソッド
    private static void readEntry(JarFile jarFile, JarEntry entry) throws IOException {
        try (InputStream is = jarFile.getInputStream(entry)) {
            byte[] buffer = new byte;
            while (is.read(buffer) != -1) {
                // 内容は読み捨てる
            }
        }
    }
}

コードのポイント:

  • JARエントリの署名検証は、そのエントリの入力ストリームを完全に読み込んだ時点で暗黙的に行われます。読み込み中にダイジェストが一致しない場合、SecurityException がスローされます。
  • 検証を確実に行うには、エントリのストリームを最後まで読み切る必要があります。上記の readEntry メソッドがその役割を果たします。
  • 検証が成功した後、JarEntry.getCertificates() を使って、そのエントリに署名した署名者の証明書チェーンを取得できます。これにより、誰が署名したかを確認できます。

第5章: 発展的なトピックとベストプラクティス

5.1 Service Provider Interface (SPI) との連携

java.util.jar は、Javaの強力な拡張機能メカニズムである Service Provider Interface (SPI) を支える重要な役割を担っています。SPIは、特定のインターフェースの実装を動的に発見・ロードするための仕組みです。

ライブラリの提供者は、META-INF/services/ という特別なディレクトリに、サービスインターフェースの完全修飾名をファイル名として持つ設定ファイルを作成します。 このファイルの中には、そのインターフェースを実装したクラスの完全修飾名を1行ずつ記述します。

アプリケーション側は java.util.ServiceLoader クラスを使って、クラスパス上にあるすべてのJARファイルからこの設定ファイルを検索し、実装クラスをインスタンス化できます。これにより、アプリケーションのコードを変更することなく、新しい実装(例えば、新しいデータベースドライバや画像フォーマットのデコーダ)をJARファイルとして追加するだけで機能拡張が可能になります。

5.2 Javaモジュールシステム (JPMS) との関係

Java 9で導入されたJava Platform Module System (JPMS) は、JARが抱えていたいくつかの課題(いわゆる「JAR地獄」)を解決するために設計されました。 JPMSは、より厳密なカプセル化と明示的な依存関係の定義を提供します。

モジュールは、module-info.java という記述子を含むJARファイル(モジュラーJAR)またはJMOD形式のファイルとしてパッケージングされます。 しかし、JPMSが導入されたからといって、従来のJARファイルが不要になったわけではありません。クラスパスベースで動作する膨大な数の既存ライブラリやアプリケーションとの後方互換性を保つため、非モジュラーなJARファイルは「自動モジュール」としてモジュールシステムから扱われます。

新しいプロジェクトではモジュールシステムの利用が推奨されますが、java.util.jar パッケージとJARファイル形式は、依然としてJavaエコシステムの基本的な構成要素であり続けるでしょう。

5.3 パフォーマンスに関する考慮事項

  • `JarFile` vs `JarInputStream`: ファイルシステム上のJARファイルにアクセスする場合、JarFile はランダムアクセスが可能で、内部的にインデックスを利用するため、特定のエントリを読み取る際には JarInputStream よりも高速です。JarInputStream はストリームベースであり、ネットワーク越しのデータなど、シーケンシャルな読み込みにしか利用できない場合に適しています。
  • リソースのクローズ: JarFile, JarInputStream, JarOutputStream はすべて、ネイティブのリソースを扱うため、使用後は必ず close() メソッドを呼び出す必要があります。try-with-resources 構文を使用することで、これを確実に行うことができます。

まとめ

本記事では、java.util.jar パッケージの核心に迫り、その機能と使い方を包括的に解説しました。 JARファイルの基本的な構造から始まり、マニフェストファイルの重要性、主要なAPIクラスを用いたファイルの作成・読み込み、そしてデジタル署名によるセキュリティ確保に至るまで、多岐にわたるトピックをカバーしました。

java.util.jar は、単なるアーカイブツールではなく、Javaプラットフォームのモジュール性、拡張性、セキュリティを支える基盤技術です。 Java 9以降、モジュールシステムが登場しましたが、JARファイルとその操作APIは、依然としてJava開発における普遍的なスキルセットの一部です。 この記事で得た知識を活用し、より堅牢で、配布しやすく、安全なJavaアプリケーションを構築してください。

モバイルバージョンを終了