KDP(電子出版)のメモ 急急如律令

Amazon Kindleダイレクト・パブリッシングでの電子出版や電子書籍の作成販売について、文章やイラストの作成や編集方法について書いています。

Windowsインストーラー版とjar版を同じコードで動かす

 Windowsインストーラー版とjar版とで一部コードを変えることで同じように動かしていた。ただ、ビルドするたびに毎回修正するのが面倒なので自動で切り分けたい。

99nyorituryo.hatenablog.com

あなた:

iniで設定をjarファイルと同じフォルダーに保存しているJavaアプリケーションがある。Windowsのインストールしてprogramfilesにiniファイルが保存できなくなる。その場合の対処方法はどうすればいいのか。

ChatGPT:

とても良い質問です。 Windows環境で「Program Files」配下にインストールされたアプリケーションは 通常ユーザー権限では書き込みが禁止 されているため、 jarファイルと同じ場所(例:C:\Program Files\MyApp\app.jar)に ini を保存しようとすると確実に失敗します。

  • Program Files 以下は 管理者権限が必要。
  • 通常のユーザー実行では IOException(アクセス拒否)などが発生。
  • これは Windows の UAC(ユーザーアカウント制御) による保護の一環。

Program Files 以下は 管理者権限が必要。

通常のユーザー実行では IOException(アクセス拒否)などが発生。

これは Windows の UAC(ユーザーアカウント制御) による保護の一環。

アプリ設定などの可変データは、ユーザーごとの書き込み可能な場所に保存するのが標準的な方法です。

たとえば次のいずれか:

種類 保存先例 Javaでの取得方法
ユーザー設定(推奨) C:\Users\<ユーザー名>\AppData\Roaming\MyApp\config.ini System.getenv("APPDATA")
一時データ C:\Users\<ユーザー名>\AppData\Local\MyApp\config.ini System.getenv("LOCALAPPDATA")
クロスプラットフォーム対応 ユーザーホーム ~/.myapp/config.ini System.getProperty("user.home")
import java.io.*;
import java.nio.file.*;

public class ConfigPath {
    public static Path getConfigFilePath() {
        String appData = System.getenv("APPDATA"); // Windows
        if (appData == null) {
            appData = System.getProperty("user.home"); // Linux/macOS fallback
        }
        Path configDir = Paths.get(appData, "MyApp");
        try {
            Files.createDirectories(configDir); // 存在しなければ作成
        } catch (IOException e) {
            e.printStackTrace();
        }
        return configDir.resolve("config.ini");
    }
}

import java.io.*;
import java.nio.file.*;

public class ConfigPath {
    public static Path getConfigFilePath() {
        String appData = System.getenv("APPDATA"); // Windows
        if (appData == null) {
            appData = System.getProperty("user.home"); // Linux/macOS fallback
        }
        Path configDir = Paths.get(appData, "MyApp");
        try {
            Files.createDirectories(configDir); // 存在しなければ作成
        } catch (IOException e) {
            e.printStackTrace();
        }
        return configDir.resolve("config.ini");
    }
}

こうしておけば、インストール場所が Program Files でも問題なく動作します。

Program Files に書き込みたい場合、アプリを常に管理者として実行すれば保存可能です。 しかしこれは:

  • セキュリティ的に望ましくない
  • ショートカットや .manifest に設定が必要
  • UAC ダイアログが毎回出る

セキュリティ的に望ましくない

ショートカットや .manifest に設定が必要

UAC ダイアログが毎回出る

という理由から、通常は避けるべきです。

アプリをインストーラ(Inno SetupやNSISなど)で配布する場合、 次のように構成するのもよくある手です:

C:\Program Files\MyApp\app.jar        ← 本体(読み取り専用)
C:\ProgramData\MyApp\config.ini       ← 全ユーザー共通設定
C:\Users\<user>\AppData\Roaming\MyApp\user.ini ← ユーザー別設定

C:\Program Files\MyApp\app.jar        ← 本体(読み取り専用)
C:\ProgramData\MyApp\config.ini       ← 全ユーザー共通設定
C:\Users\<user>\AppData\Roaming\MyApp\user.ini ← ユーザー別設定

ProgramData は管理者権限なしでも書き込み可能(ただし全ユーザー共有)です。

Javaではこう取得できます:

String programData = System.getenv("PROGRAMDATA"); // usually C:\ProgramData
Path configPath = Paths.get(programData, "MyApp", "config.ini");

String programData = System.getenv("PROGRAMDATA"); // usually C:\ProgramData
Path configPath = Paths.get(programData, "MyApp", "config.ini");
方法 権限 推奨度 用途
AppData\Roaming に保存 不要 ★★★★★ 通常ユーザー設定
ProgramData に保存 不要 ★★★★☆ 全ユーザー共有設定
Program Files に保存 要管理者 ★☆☆☆☆ 原則禁止

もしあなたのアプリが「ポータブル版(USBなどで使う)」であるなら、 jar と同じフォルダに保存する方式でも構いませんが、 書き込み権限がある場所(例:C:\MyAppやDocuments)に置くよう案内する必要があります。

ご希望があれば:

  • 上記を自動で切り替えるJavaコード(「Program FilesならAppDataに切り替える」) も示せます。 実際の運用に合わせて出力しますか?

上記を自動で切り替えるJavaコード(「Program FilesならAppDataに切り替える」) も示せます。 実際の運用に合わせて出力しますか?

あなた:

jarファイルで配布しているバージョンとインストーラーとも互換性を保ちたい

ChatGPT:

非常に良い方針です。 「jar単体配布版(ポータブル)」と「インストーラー版(Program Filesに配置)」の設定ファイル互換性を保つのは現実的な課題です。

以下に、両者を自動的に切り替えつつ同じini形式で互換性を保つ構成を解説します。

  • app.jar と同じフォルダにある config.ini を優先的に使う(ポータブルモード)
  • Program Files にインストールされた場合などで書き込みできないときは 自動的に AppData に保存する(通常インストールモード)
  • 両者で同じini形式を使用(読み書きできる)

app.jar と同じフォルダにある config.ini を優先的に使う(ポータブルモード)

Program Files にインストールされた場合などで書き込みできないときは 自動的に AppData に保存する(通常インストールモード)

両者で同じini形式を使用(読み書きできる)

モード 実行パス例 設定ファイルの場所 備考
ポータブル D:\tools\MyApp\app.jar D:\tools\MyApp\config.ini 同一フォルダ内
インストール版 C:\Program Files\MyApp\app.jar C:\Users\\AppData\Roaming\MyApp\config.ini 自動切替
import java.io.*;
import java.nio.file.*;

public class ConfigManager {
    private static final String CONFIG_NAME = "config.ini";
    private static final String APP_NAME = "MyApp";

    /** 設定ファイルのパスを自動で決定する */
    public static Path getConfigPath() {
        try {
            // jarと同じフォルダを取得
            Path jarPath = Paths.get(ConfigManager.class.getProtectionDomain()
                    .getCodeSource().getLocation().toURI()).getParent();
            Path portableConfig = jarPath.resolve(CONFIG_NAME);

            // 書き込みできるかチェック(存在しない場合は親フォルダに対して)
            if (Files.isWritable(jarPath)) {
                return portableConfig; // ポータブルモード
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

        // 書き込み不可(Program Filesなど)→ AppDataに退避
        String appData = System.getenv("APPDATA");
        if (appData == null) {
            appData = System.getProperty("user.home");
        }
        Path configDir = Paths.get(appData, APP_NAME);
        try {
            Files.createDirectories(configDir);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return configDir.resolve(CONFIG_NAME);
    }

    /** 設定ファイルを読み込む例 */
    public static void loadConfig() {
        Path configPath = getConfigPath();
        System.out.println("Using config file: " + configPath);
        if (Files.exists(configPath)) {
            // ここでiniファイルを読み込む処理
        } else {
            // デフォルト設定で新規作成など
        }
    }

    /** 設定ファイルを書き込む例 */
    public static void saveConfig(String content) {
        Path configPath = getConfigPath();
        try {
            Files.writeString(configPath, content);
            System.out.println("Saved to: " + configPath);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

import java.io.*;
import java.nio.file.*;

public class ConfigManager {
    private static final String CONFIG_NAME = "config.ini";
    private static final String APP_NAME = "MyApp";

    /** 設定ファイルのパスを自動で決定する */
    public static Path getConfigPath() {
        try {
            // jarと同じフォルダを取得
            Path jarPath = Paths.get(ConfigManager.class.getProtectionDomain()
                    .getCodeSource().getLocation().toURI()).getParent();
            Path portableConfig = jarPath.resolve(CONFIG_NAME);

            // 書き込みできるかチェック(存在しない場合は親フォルダに対して)
            if (Files.isWritable(jarPath)) {
                return portableConfig; // ポータブルモード
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

        // 書き込み不可(Program Filesなど)→ AppDataに退避
        String appData = System.getenv("APPDATA");
        if (appData == null) {
            appData = System.getProperty("user.home");
        }
        Path configDir = Paths.get(appData, APP_NAME);
        try {
            Files.createDirectories(configDir);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return configDir.resolve(CONFIG_NAME);
    }

    /** 設定ファイルを読み込む例 */
    public static void loadConfig() {
        Path configPath = getConfigPath();
        System.out.println("Using config file: " + configPath);
        if (Files.exists(configPath)) {
            // ここでiniファイルを読み込む処理
        } else {
            // デフォルト設定で新規作成など
        }
    }

    /** 設定ファイルを書き込む例 */
    public static void saveConfig(String content) {
        Path configPath = getConfigPath();
        try {
            Files.writeString(configPath, content);
            System.out.println("Saved to: " + configPath);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  1. アプリ起動時、app.jar と同じ場所に config.ini があるか確認。
  2. そのフォルダに書き込み権限があれば → 同じ場所を使う。
  3. 書き込み権限がなければ → AppData\MyApp\config.ini を使用。
  4. 両方の形式は共通のINIフォーマットで互換性を維持。

アプリ起動時、app.jar と同じ場所に config.ini があるか確認。

そのフォルダに書き込み権限があれば → 同じ場所を使う。

書き込み権限がなければ → AppData\MyApp\config.ini を使用。

両方の形式は共通のINIフォーマットで互換性を維持。

初回起動時、もしProgram Files内に古いconfig.iniが残っていたら AppData に自動コピーして移行することもできます。

if (!Files.exists(appDataConfig) && Files.exists(programFilesConfig)) {
    Files.copy(programFilesConfig, appDataConfig);
}

if (!Files.exists(appDataConfig) && Files.exists(programFilesConfig)) {
    Files.copy(programFilesConfig, appDataConfig);
}

同フォルダに portable.flag ファイルがあれば強制的にポータブル扱い、なども可能。

項目 内容
ファイル形式 どちらも同じ config.ini
優先順位 JARと同フォルダ > AppData
権限問題 自動判定で回避
互換性 保持可能(両方で読み書き可)
拡張性 移行・ポータブルフラグにも対応可能

ご希望があれば:

  • 上記コードに実際のini読み書き実装(java.util.Properties 版 or 独自iniパーサ版)を組み込んだ完全サンプル を出すこともできます。 どちらの方法(Properties or INI形式)で保存していますか?

上記コードに実際のini読み書き実装(java.util.Properties 版 or 独自iniパーサ版)を組み込んだ完全サンプル を出すこともできます。 どちらの方法(Properties or INI形式)で保存していますか?

あなた:

jarから各OS向けのインストーラーを作成する方法

ChatGPT:

とても良い質問です。 Javaアプリ(app.jar)を Windows / macOS / Linux それぞれで動くインストーラー付きアプリにする方法はいくつかあります。 ここでは、配布目的(エンドユーザーが簡単に使える形)を前提に、現実的でメンテナンスしやすい方法を整理します。

目的 手段
単体の .jar を実行可能ファイル化 jpackage(JDK公式)
Windows のインストーラー(.exe / .msi) jpackage または Inno Setup / NSIS
macOS の .app / .dmg jpackage
Linux の .deb / .rpm jpackage
Javaを含めた配布(JRE同梱) jlink + jpackage

JDK 14以降に標準搭載されている jpackage を使うと、 JARファイルを ネイティブ形式(exe, dmg, debなど) に変換し、 OS別インストーラー まで自動生成できます。

  • JDK 17以降(推奨)
  • app.jar が実行可能であること(Main-ClassがMANIFEST.MFにある)

JDK 17以降(推奨)

app.jar が実行可能であること(Main-ClassがMANIFEST.MFにある)

jpackage ^
  --input target ^
  --main-jar app.jar ^
  --name MyApp ^
  --main-class com.example.Main ^
  --type exe ^
  --icon icon.ico ^
  --win-menu ^
  --win-dir-chooser ^
  --win-shortcut ^
  --java-options "-Xmx512m"

jpackage ^
  --input target ^
  --main-jar app.jar ^
  --name MyApp ^
  --main-class com.example.Main ^
  --type exe ^
  --icon icon.ico ^
  --win-menu ^
  --win-dir-chooser ^
  --win-shortcut ^
  --java-options "-Xmx512m"

これで MyApp-1.0.exe のインストーラーが生成されます。

💡--type を変えると他OS向けにも対応:

  • Windows: exe or msi
  • macOS: pkg or dmg
  • Linux: deb or rpm

Windows: exe or msi

macOS: pkg or dmg

Linux: deb or rpm

オプション 説明
--input JARファイルを置くフォルダ
--main-jar メインのjar
--main-class 起動クラス(MANIFEST.MFがあれば省略可)
--name アプリ名
--type 出力形式 (exe, msi, pkg, dmg, deb, rpm)
--icon アイコンファイル指定(Windowsは.ico)
--java-options 起動時のJVM引数
--win-menu, --win-shortcut スタートメニューやデスクトップショートカット作成
--runtime-image jlinkで作った最小JREを指定(後述)
  1. まずアプリ専用の軽量JREを作る:

bash コードをコピーする jlink --add-modules java.base,java.desktop --output runtime

  1. そのランタイムを使ってパッケージング:

bash コードをコピーする jpackage --input target --main-jar app.jar --name MyApp --runtime-image runtime --type exe

まずアプリ専用の軽量JREを作る:

jlink --add-modules java.base,java.desktop --output runtime

jlink --add-modules java.base,java.desktop --output runtime

そのランタイムを使ってパッケージング:

jpackage --input target --main-jar app.jar --name MyApp --runtime-image runtime --type exe

jpackage --input target --main-jar app.jar --name MyApp --runtime-image runtime --type exe

→ これで「Javaが入っていなくても動く」Windowsアプリが完成します。

もし jpackage が使えない古い環境や、細かいインストール挙動を制御したい場合は、 次のような手もあります。

  1. MyApp.jar と run.bat(または .exeランチャー)を用意

bat コードをコピーする @echo off java -jar "%~dp0MyApp.jar"

  1. Inno Setup(無料のWindowsインストーラ作成ツール)でパッケージ化 https://jrsoftware.org/isinfo.php

MyApp.jar と run.bat(または .exeランチャー)を用意

@echo off
java -jar "%~dp0MyApp.jar"

@echo off
java -jar "%~dp0MyApp.jar"

Inno Setup(無料のWindowsインストーラ作成ツール)でパッケージ化 https://jrsoftware.org/isinfo.php

[Setup]
AppName=MyApp
AppVersion=1.0
DefaultDirName={pf}\MyApp
OutputBaseFilename=MyAppSetup
[Files]
Source: "MyApp.jar"; DestDir: "{app}"
Source: "run.bat"; DestDir: "{app}"
[Icons]
Name: "{group}\MyApp"; Filename: "{app}\run.bat"

[Setup]
AppName=MyApp
AppVersion=1.0
DefaultDirName={pf}\MyApp
OutputBaseFilename=MyAppSetup
[Files]
Source: "MyApp.jar"; DestDir: "{app}"
Source: "run.bat"; DestDir: "{app}"
[Icons]
Name: "{group}\MyApp"; Filename: "{app}\run.bat"

→ MyAppSetup.exe が生成されます。 Program Files へのインストールも可能。

macOS / Linux も同様に jpackage が使えます。

jpackage --input target --main-jar app.jar --name MyApp --type dmg --icon icon.icns

jpackage --input target --main-jar app.jar --name MyApp --type dmg --icon icon.icns
jpackage --input target --main-jar app.jar --name MyApp --type deb --icon icon.png

jpackage --input target --main-jar app.jar --name MyApp --type deb --icon icon.png

jpackage はOS依存のツールなので、 Windows用 .exe は Windows で、 macOS用 .dmg は macOS で作る必要があります。

ただし、自動ビルド環境(GitHub Actionsなど)で各OSごとに実行することで、 ワンクリックで全OSのインストーラーを生成可能です。

目的 ツール 特徴
簡単にインストーラー化 jpackage 公式・クロスプラットフォーム
カスタムインストーラー制御 Inno Setup / NSIS Windows専用・柔軟
Java同梱 jlink + jpackage JRE不要の配布が可能
自動化 GitHub Actions など 各OSビルドを自動実行

ご希望があれば:

  • ✅ 実際の jpackage スクリプト(Windows/macOS/Linux共通)
  • ✅ JRE同梱・自動アイコン設定・AppData保存対応済みテンプレート

✅ 実際の jpackage スクリプト(Windows/macOS/Linux共通)

✅ JRE同梱・自動アイコン設定・AppData保存対応済みテンプレート

をセットで提示できます。 どの環境向け(Windows / macOS / Linux)から作りたいですか?

あなた:

jpackageで作成したexeとjarでテンプレートファイルの読み込み時のパスが変わる

ChatGPT:

とても重要なポイントに気づかれています 👏 jpackage で作った .exe(ネイティブパッケージ)と、 手動実行する .jar では リソースファイルやテンプレートファイルのパスの扱いが異なる ため、 そのままでは「ファイルが見つからない」などの問題が起こります。

実行方法 カレントディレクトリ(相対パスの基準)
java -jar app.jar jarのあるフォルダ(例:C:\tools\MyApp\)
app.exe(jpackage) C:\Users\\AppData\Local\Programs\MyApp\(インストール先)やショートカット経由で実行時は別ディレクトリ

つまり:

new File("template/config.txt")

new File("template/config.txt")

のように相対パスで読み込むと、 .jar 実行時は見つかるが、.exe 実行時には存在しないパスを参照してしまいます。

テンプレートがアプリに同梱される固定ファイルなら、 jar内部のリソースとして埋め込み、getResourceAsStream()で読み込むのが正解です。

src/
 └─ main/
     ├─ java/
     └─ resources/
         └─ template/
             └─ default.ini

src/
 └─ main/
     ├─ java/
     └─ resources/
         └─ template/
             └─ default.ini
try (InputStream in = getClass().getResourceAsStream("/template/default.ini")) {
    if (in == null) throw new FileNotFoundException("template not found");
    String text = new String(in.readAllBytes(), StandardCharsets.UTF_8);
    System.out.println(text);
}

try (InputStream in = getClass().getResourceAsStream("/template/default.ini")) {
    if (in == null) throw new FileNotFoundException("template not found");
    String text = new String(in.readAllBytes(), StandardCharsets.UTF_8);
    System.out.println(text);
}

💡 src/main/resources に置けば、ビルド時に自動的に jar に含まれます。 この方法なら .jar でも .exe でも 同じコードで動作 します。

もしテンプレートをユーザーが編集可能にしたい場合(外部iniなど)、 「アプリの設置場所」を動的に取得して相対パスを組み立てるのが安全です。

import java.nio.file.*;

public class AppPaths {
    public static Path getAppDir() {
        try {
            // 実行中のjarまたはexeのあるフォルダを取得
            Path path = Paths.get(AppPaths.class
                .getProtectionDomain()
                .getCodeSource()
                .getLocation()
                .toURI())
                .getParent();
            return path;
        } catch (Exception e) {
            e.printStackTrace();
            return Paths.get(".").toAbsolutePath();
        }
    }

    public static void main(String[] args) {
        Path appDir = getAppDir();
        Path templatePath = appDir.resolve("template/config.txt");
        System.out.println("Template: " + templatePath);
    }
}

import java.nio.file.*;

public class AppPaths {
    public static Path getAppDir() {
        try {
            // 実行中のjarまたはexeのあるフォルダを取得
            Path path = Paths.get(AppPaths.class
                .getProtectionDomain()
                .getCodeSource()
                .getLocation()
                .toURI())
                .getParent();
            return path;
        } catch (Exception e) {
            e.printStackTrace();
            return Paths.get(".").toAbsolutePath();
        }
    }

    public static void main(String[] args) {
        Path appDir = getAppDir();
        Path templatePath = appDir.resolve("template/config.txt");
        System.out.println("Template: " + templatePath);
    }
}

これで .jar 実行時も .exe 実行時も 「自身がある場所」(例:Program Files\MyApp\template\config.txt)を取得できます。

インストーラーで配布される .exe では、 Program Files 下に配置されるため、書き込み権限がありません。

したがって、次のように「初回起動時にAppDataへコピー」する設計がよく使われます:

Path appDir = getAppDir();
Path userDir = Paths.get(System.getenv("APPDATA"), "MyApp");
Files.createDirectories(userDir);

Path templateSrc = appDir.resolve("template/config.txt");
Path templateDst = userDir.resolve("config.txt");

if (!Files.exists(templateDst)) {
    Files.copy(templateSrc, templateDst);
}

Path appDir = getAppDir();
Path userDir = Paths.get(System.getenv("APPDATA"), "MyApp");
Files.createDirectories(userDir);

Path templateSrc = appDir.resolve("template/config.txt");
Path templateDst = userDir.resolve("config.txt");

if (!Files.exists(templateDst)) {
    Files.copy(templateSrc, templateDst);
}

→ 読み取りはappDir、編集はAppData。 これで両バージョン間でも完全に整合が取れます。

System.out.println("User.dir = " + System.getProperty("user.dir"));
System.out.println("CodeSource = " +
    MyClass.class.getProtectionDomain().getCodeSource().getLocation());

System.out.println("User.dir = " + System.getProperty("user.dir"));
System.out.println("CodeSource = " +
    MyClass.class.getProtectionDomain().getCodeSource().getLocation());

これで実行環境ごとの違いを簡単に確認できます。 jpackage版では user.dir が %ProgramFiles%\MyApp ではなく、 ショートカット起動時のディレクトリになるのが分かります。

用途 解決策 特徴
jar内のテンプレート(固定) getResourceAsStream() OSや形式に依存しない
外部テンプレート(可変) getCodeSource().getLocation()で実行パス基準にする jar/exe両対応
書き込みが必要 AppDataにコピーして利用 Windows標準仕様に適合

もし「一部はjar内リソース、他は外部ini」というようなハイブリッド構成を取りたい場合、 その設計に合わせたテンプレートロードクラスを1つ用意するのがベストです。

ご希望があれば、

✅ jar/exe両対応のテンプレート読み込み+AppDataコピー済みクラス(実用コード) を提示できます。 そうしますか?

あなた:

更新インストール時にインストールフォルダー内に保存されたデータはどうなるのか

ChatGPT:

非常に良い質問です。 この点を正しく理解しておかないと、ユーザーが保存した設定やテンプレートが更新時に消えるというトラブルが起こります。

配布形式 更新インストール時の挙動 保存データへの影響
jpackage製インストーラー(MSI / EXE) 既存フォルダを 上書き再展開 🔥 削除される可能性が高い
Inno Setup / NSIS など独自インストーラー スクリプト次第 条件付きで保持可能
ポータブル版(zip配布 / jar実行) そのまま上書きしなければ保持 手動更新に依存

jpackage が作る Windows 用インストーラー(--type exe または --type msi)は、 Windows Installer(MSI)標準の動作に従います。

つまり:

更新版インストール時は旧版をアンインストールしてから新しいファイルをインストールする

という手順になります。

そのため、 インストールフォルダ(通常は C:\Program Files\MyApp)にユーザーが保存したファイルは削除されます。

たとえば:

C:\Program Files\MyApp\
 ├─ app.exe
 ├─ lib/
 ├─ template/
 └─ user_config.ini  ← ユーザーが編集した

C:\Program Files\MyApp\
 ├─ app.exe
 ├─ lib/
 ├─ template/
 └─ user_config.ini  ← ユーザーが編集した

このように Program Files 内に user_config.ini を保存していると、 更新時にアンインストール時点で削除されます。

  • MSI の「アンインストール」はフォルダごと削除
  • jpackage で特別な保持設定を入れられない(現状の仕様)
  • Program Files はアプリのバイナリ専用領域と見なされる

MSI の「アンインストール」はフォルダごと削除

jpackage で特別な保持設定を入れられない(現状の仕様)

Program Files はアプリのバイナリ専用領域と見なされる

最も安全で標準的な方法です。

保存場所 用途 Java取得方法
%APPDATA%\MyApp 設定ファイル System.getenv("APPDATA")
%LOCALAPPDATA%\MyApp 一時データ System.getenv("LOCALAPPDATA")
%USERPROFILE%\Documents\MyAppData 大きなデータ System.getProperty("user.home") + "Documents"

→ インストールや更新に関係なく、データは維持されます。

アプリ配布時にはテンプレートを Program Files\MyApp\template\ に置き、 起動時にユーザー書き込み可能な場所へコピーするのが定番。

Path appDir = getAppDir(); // app.exeやjarのあるフォルダ
Path userDir = Paths.get(System.getenv("APPDATA"), "MyApp", "template");
Files.createDirectories(userDir);

Path src = appDir.resolve("template/default.ini");
Path dst = userDir.resolve("default.ini");

// 初回のみコピー
if (!Files.exists(dst)) {
    Files.copy(src, dst);
}

Path appDir = getAppDir(); // app.exeやjarのあるフォルダ
Path userDir = Paths.get(System.getenv("APPDATA"), "MyApp", "template");
Files.createDirectories(userDir);

Path src = appDir.resolve("template/default.ini");
Path dst = userDir.resolve("default.ini");

// 初回のみコピー
if (!Files.exists(dst)) {
    Files.copy(src, dst);
}

こうしておけば、アップデートしても:

  • Program Files内のテンプレートは更新される(新しいバージョン反映)
  • AppData内のユーザー編集済みテンプレートは残る(上書きされない)

Program Files内のテンプレートは更新される(新しいバージョン反映)

AppData内のユーザー編集済みテンプレートは残る(上書きされない)

アプリ側で config.ini のバージョンを持たせ、 古い形式の場合はアップグレード処理を行うのが安全。

if (configVersion < currentVersion) {
    migrateConfig(oldConfigPath, newConfigPath);
}

if (configVersion < currentVersion) {
    migrateConfig(oldConfigPath, newConfigPath);
}
  • macOS の jpackage --type dmg → /Applications 配下に展開 → 書き込み不可。 → 同様に ~/Library/Application Support/MyApp へ保存すべき。
  • Linux の deb / rpm → /opt/MyApp 配下にインストール → 書き込み不可。 → ~/.config/MyApp に保存すべき。

macOS の jpackage --type dmg → /Applications 配下に展開 → 書き込み不可。 → 同様に ~/Library/Application Support/MyApp へ保存すべき。

Linux の deb / rpm → /opt/MyApp 配下にインストール → 書き込み不可。 → ~/.config/MyApp に保存すべき。

項目 推奨場所 更新時に保持 備考
設定ファイル %APPDATA%\MyApp 最安全
テンプレート(編集不可) Program Files\MyApp\template ⚠️ 再展開(上書き)
ユーザー編集用テンプレート %APPDATA%\MyApp\template 初回コピーで維持
実行ファイル・ライブラリ Program Files\MyApp 🔁 上書き 問題なし
  1. アプリ本体(exe, jar, template原本)は Program Files
  2. ユーザーデータ・設定は AppData
  3. 起動時に必要ファイルを AppData に自動コピー(初回のみ)
  4. 更新時は AppData をそのまま使用 → データ保持

アプリ本体(exe, jar, template原本)は Program Files

ユーザーデータ・設定は AppData

起動時に必要ファイルを AppData に自動コピー(初回のみ)

更新時は AppData をそのまま使用 → データ保持

もしよければ、

✅ 上記設計を実現する「Config&Template管理クラス」(appDirとAppDataを自動判定・コピー)

のJava実装を提示できます。 生成してみましょうか?

あなた:

jarファイルとexeファイルの位置関係。jarファイルのパスから読み込みファイルの相対パスを指定していたので、exeファイルになったときにファイルが見つからない。exeファイルの相対パスにファイルを配置すると起動した。jarからのパス設定をexeファイルの下のappフォルダー内を指定することにした。

ChatGPT:

非常に良い判断です 👍

あなたの説明のように、 jpackage で作成した .exe アプリは、実際には jarファイルを含む構造が変化 しています。

jpackage で作成した Windows 用アプリ (.exe) を展開すると、典型的にこんな構造になります:

MyApp/
├─ MyApp.exe
├─ app/
│   ├─ MyApp.jar
│   ├─ lib/
│   │   └─ (依存ライブラリたち)
│   └─ data/
│       └─ template.ini
└─ runtime/
    └─ (JREが含まれている)

MyApp/
├─ MyApp.exe
├─ app/
│   ├─ MyApp.jar
│   ├─ lib/
│   │   └─ (依存ライブラリたち)
│   └─ data/
│       └─ template.ini
└─ runtime/
    └─ (JREが含まれている)

ここで、MyApp.exe は app/MyApp.jar を起動しているだけです。 つまり、jar 内で次のようなコードを書いていた場合:

Path basePath = Paths.get("").toAbsolutePath(); // 実行ファイルのカレントディレクトリ
Path templatePath = basePath.resolve("template.ini");

Path basePath = Paths.get("").toAbsolutePath(); // 実行ファイルのカレントディレクトリ
Path templatePath = basePath.resolve("template.ini");

この場合、

  • jar単体で起動する時 → template.ini は jar と同じディレクトリにある
  • exeで起動する時 → template.ini は app/ の中にある

jar単体で起動する時 → template.ini は jar と同じディレクトリにある

exeで起動する時 → template.ini は app/ の中にある

という違いが生じます。

両方に対応するには、実行中の jarファイル自身の位置 からパスを求めるのが最も確実です。

import java.nio.file.*;

public class PathUtils {
    public static Path getAppDir() {
        try {
            // 実行中のJARまたはクラスがある場所を取得
            String path = PathUtils.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
            Path jarPath = Paths.get(path).toAbsolutePath();
            // jarのディレクトリを返す(exe内でもapp/になる)
            return jarPath.getParent();
        } catch (Exception e) {
            return Paths.get(".").toAbsolutePath(); // fallback
        }
    }

    public static void main(String[] args) {
        Path appDir = getAppDir();
        Path template = appDir.resolve("data/template.ini"); // app/data/template.ini
        System.out.println("Template path: " + template);
    }
}

import java.nio.file.*;

public class PathUtils {
    public static Path getAppDir() {
        try {
            // 実行中のJARまたはクラスがある場所を取得
            String path = PathUtils.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
            Path jarPath = Paths.get(path).toAbsolutePath();
            // jarのディレクトリを返す(exe内でもapp/になる)
            return jarPath.getParent();
        } catch (Exception e) {
            return Paths.get(".").toAbsolutePath(); // fallback
        }
    }

    public static void main(String[] args) {
        Path appDir = getAppDir();
        Path template = appDir.resolve("data/template.ini"); // app/data/template.ini
        System.out.println("Template path: " + template);
    }
}

この方法なら:

  • .jar 単体で実行 → MyApp.jar の隣のファイルを参照
  • .exe で実行 → app/MyApp.jar の下を参照

.jar 単体で実行 → MyApp.jar の隣のファイルを参照

.exe で実行 → app/MyApp.jar の下を参照

どちらでも動作します。

もしユーザー設定などを保存する場合は、 Program Files 内ではなく下記のようなパスに保存する方が安全です:

  • Windows: %APPDATA%\MyApp\config.ini
  • macOS: ~/Library/Application Support/MyApp/config.ini
  • Linux: ~/.config/MyApp/config.ini

Windows: %APPDATA%\MyApp\config.ini

macOS: ~/Library/Application Support/MyApp/config.ini

Linux: ~/.config/MyApp/config.ini

ただし、テンプレートやリソース読み込みファイルは app/ 以下に置くのが自然です。

必要であれば、上記パス取得コードを使って 「jar/exe 両対応のリソースローダー」ユーティリティクラスとして整理してお渡しすることもできます。 作成してほしいですか?

あなた:

下記コードのパスの取得方法との違いは何

this.jarPath = getClass().getClassLoader().getResource("").getFile(); this.jarPath = this.jarPath.replaceFirst("\/bin\/$", "/");

ChatGPT:

とても良い質問です。 あなたの書かれたコードと、先ほど説明した CodeSource を使う方法は、見た目が似ていても動作原理がかなり違う ので、exe化したときに挙動差が出ます。

this.jarPath = getClass().getClassLoader().getResource("").getFile();
this.jarPath = this.jarPath.replaceFirst("\\/bin\\/$", "/");

this.jarPath = getClass().getClassLoader().getResource("").getFile();
this.jarPath = this.jarPath.replaceFirst("\\/bin\\/$", "/");
  • getClass().getClassLoader().getResource("") は、 クラスローダーが読み込んでいる「クラスパスのルート」(bin/やbuild/classes/など)を返します。

getClass().getClassLoader().getResource("") は、 クラスローダーが読み込んでいる「クラスパスのルート」(bin/やbuild/classes/など)を返します。

  • Eclipseなどの開発環境で実行 → .../project/bin/
  • jarファイルで実行 → jarの中の file:/C:/path/to/app.jar!/(実ファイルパスではない)
  • jpackageのexeで実行 → null または file:/app/ に解決されないケースあり

Eclipseなどの開発環境で実行 → .../project/bin/

jarファイルで実行 → jarの中の file:/C:/path/to/app.jar!/(実ファイルパスではない)

jpackageのexeで実行 → null または file:/app/ に解決されないケースあり

  • jar内部のリソースは「実ファイルシステム上のパス」ではないため、 File や Path として扱えません。
  • exe化後にはクラスローダーのルートが異なるため、null や空文字になる場合があります。
  • replaceFirst("\/bin\/$", "/") のようにパスを加工しても、jar/exeどちらでも安定はしません。

jar内部のリソースは「実ファイルシステム上のパス」ではないため、 File や Path として扱えません。

exe化後にはクラスローダーのルートが異なるため、null や空文字になる場合があります。

replaceFirst("\/bin\/$", "/") のようにパスを加工しても、jar/exeどちらでも安定はしません。

String path = getClass().getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
Path jarPath = Paths.get(path).toAbsolutePath().getParent();

String path = getClass().getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
Path jarPath = Paths.get(path).toAbsolutePath().getParent();
  • これは 実行中のクラスを含むJAR(またはEXEが呼び出すJAR)そのものの物理的パス を取得します。
  • jar実行でもexe実行でも、実ファイルとして存在するjarファイルの場所を返すため、 その隣(app/以下など)にあるファイルに確実にアクセスできます。

これは 実行中のクラスを含むJAR(またはEXEが呼び出すJAR)そのものの物理的パス を取得します。

jar実行でもexe実行でも、実ファイルとして存在するjarファイルの場所を返すため、 その隣(app/以下など)にあるファイルに確実にアクセスできます。

実行方法 結果の例
EclipseなどIDE /C:/project/bin/(開発中)
JAR直接実行 /C:/app/MyApp/(jarと同じ場所)
jpackageのEXE /C:/Program Files/MyApp/app/(appフォルダ)
  • jar単体/exe配布版の両対応
  • ファイルシステム上の実パスとして安全に扱える
  • jar内部ではなく外部リソースを読む用途(ini, template, 画像など)に最適

jar単体/exe配布版の両対応

ファイルシステム上の実パスとして安全に扱える

jar内部ではなく外部リソースを読む用途(ini, template, 画像など)に最適

比較項目 getResource("") getProtectionDomain()
得られるパス クラスパスのルート 実際のJARファイルの場所
jar内での動作 jar内部の仮想パス jarファイル実体のパス
exeでの動作 不安定(nullになることあり) 常に安定して動作
ini/外部ファイルの読み込み 不向き 向いている
開発環境でのデバッグ OK OK

つまり:

🔸 getResource("") は「クラスパス」を対象とした開発・内部リソース向け。 🔸 getProtectionDomain() は「実ファイル位置」を対象とした外部ファイル向け。

あなたのように「jar/exeの横やappフォルダ内のiniを読む」場合は、 後者(getProtectionDomain)を使うのが正解です。

希望があれば、あなたの現在のコードをベースに 「exe/jar両対応の安全なパス取得ユーティリティ」に書き換えたサンプルを提示できます。 作成しましょうか?

あなた:

String jarPath として文字列としたときに問題はあるか

ChatGPT:

とても良い視点です。 結論から言うと:

🔸 「String jarPath にして使う」こと自体は問題ありません。 ただし、パスの扱い(文字列操作)を誤ると、環境差による不具合が起きやすくなります。

次のように「取得直後に Path に変換して使う」なら安全です。

String jarPath = getClass().getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
Path baseDir = Paths.get(jarPath).getParent(); // jarのあるフォルダ
Path iniPath = baseDir.resolve("config.ini");

String jarPath = getClass().getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
Path baseDir = Paths.get(jarPath).getParent(); // jarのあるフォルダ
Path iniPath = baseDir.resolve("config.ini");

ここでは

  • jarPath は一時的な文字列(URL形式の文字列などもあり得る)
  • 実際にファイルを扱うのは Path 経由 なので、OS差(/ vs \)やスペース、エンコードなどを吸収できます。

jarPath は一時的な文字列(URL形式の文字列などもあり得る)

実際にファイルを扱うのは Path 経由 なので、OS差(/ vs \)やスペース、エンコードなどを吸収できます。

String iniFile = jarPath + "/config.ini"; // ← NG

String iniFile = jarPath + "/config.ini"; // ← NG
  • jarPathがC:\Program Files\MyApp\app\MyApp.jarのようなパスだと、 ファイルセパレーターが混ざって壊れる場合があります。
  • mac/Linuxでは/、Windowsでは\なので統一性がありません。
  • jarPathがURL形式(例:/C:/Program%20Files/MyApp/app/)の時、 %20(スペース)やfile:/が混ざってしまいファイルアクセスに失敗します。

jarPathがC:\Program Files\MyApp\app\MyApp.jarのようなパスだと、 ファイルセパレーターが混ざって壊れる場合があります。

mac/Linuxでは/、Windowsでは\なので統一性がありません。

jarPathがURL形式(例:/C:/Program%20Files/MyApp/app/)の時、 %20(スペース)やfile:/が混ざってしまいファイルアクセスに失敗します。

toURI().getPath()で取得した文字列には、 スペースや日本語が含まれるとエンコードされた形(%20など)になる場合があります。

例:

C:\Program Files\MyApp → C:/Program%20Files/MyApp

C:\Program Files\MyApp → C:/Program%20Files/MyApp

このままではファイルアクセスに失敗します。

import java.nio.file.*;
import java.net.*;

public class PathHelper {
    public static Path getAppBaseDir() {
        try {
            URI uri = PathHelper.class.getProtectionDomain().getCodeSource().getLocation().toURI();
            Path jarPath = Paths.get(uri);
            return jarPath.getParent(); // jarやexeのappフォルダなど
        } catch (Exception e) {
            return Paths.get(".").toAbsolutePath(); // fallback
        }
    }

    public static void main(String[] args) {
        Path appDir = getAppBaseDir();
        System.out.println("Base dir = " + appDir);

        // ファイル指定も安全
        Path iniFile = appDir.resolve("config.ini");
        System.out.println("INI path = " + iniFile);
    }
}

import java.nio.file.*;
import java.net.*;

public class PathHelper {
    public static Path getAppBaseDir() {
        try {
            URI uri = PathHelper.class.getProtectionDomain().getCodeSource().getLocation().toURI();
            Path jarPath = Paths.get(uri);
            return jarPath.getParent(); // jarやexeのappフォルダなど
        } catch (Exception e) {
            return Paths.get(".").toAbsolutePath(); // fallback
        }
    }

    public static void main(String[] args) {
        Path appDir = getAppBaseDir();
        System.out.println("Base dir = " + appDir);

        // ファイル指定も安全
        Path iniFile = appDir.resolve("config.ini");
        System.out.println("INI path = " + iniFile);
    }
}
項目 String jarPath Path jarPath
単なる表示用途 OK OK
文字列連結でパスを作る ⚠ 不安定 ✅ 安全
OS依存吸収 ❌ 手動対応が必要 ✅ 自動対応
URLエンコード対策 ❌ 自分で処理 ✅ 自動で吸収
  • Stringで保持してもOKだが、Path変換してから使うのが安全。
  • もしjarPathを直接結合・加工してファイルパスを作っているなら、 Path.resolve() に置き換えるのが確実です。

Stringで保持してもOKだが、Path変換してから使うのが安全。

もしjarPathを直接結合・加工してファイルパスを作っているなら、 Path.resolve() に置き換えるのが確実です。

希望があれば、 あなたの現行コード(String jarPathベース)を、 Pathベースに安全に書き換えたバージョンを提示できます。 出してみましょうか?

あなた:

intellijで実行できない

ChatGPT:

なるほど、 getProtectionDomain().getCodeSource().getLocation() を使ったコードを IntelliJ IDEA で実行すると、パス取得がうまく動かない(または例外になる)ことがあります。

これは実はよくある落とし穴です。 原因と対策を分かりやすく整理します👇

IntelliJで実行すると、アプリは通常「クラスパス上のout/production/...」からロードされます。 つまり、実際には jar ファイルを実行していないので、 次のようなパスが返ってきます:

String path = getClass().getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
System.out.println(path);

String path = getClass().getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
System.out.println(path);

➡ 出力例:

/C:/Users/UserName/IdeaProjects/MyApp/out/production/MyApp/

/C:/Users/UserName/IdeaProjects/MyApp/out/production/MyApp/

これは「ビルドフォルダ(クラスファイルの場所)」であって、 MyApp.jar は存在しません。 したがって「jarのある場所を基準に外部ファイルを読む」コードは失敗します。

IntelliJの「実行構成(Run Configuration)」で、 デフォルトでは Working Directory(作業ディレクトリ) が プロジェクトルートや out/production に設定されています。

外部ファイルの相対パスがこの作業ディレクトリ基準で解決されるため、 想定外の場所を見に行くことになります。

実行構成(Run → Edit Configurations)で以下を設定します:

  1. Working directory を → プロジェクトのルートディレクトリ 例)

makefile コードをコピーする C:\Users\UserName\IdeaProjects\MyApp

  1. コード内では「jarモード」と「開発モード」を自動判定して切り替える。

Working directory を → プロジェクトのルートディレクトリ 例)

C:\Users\UserName\IdeaProjects\MyApp

C:\Users\UserName\IdeaProjects\MyApp

コード内では「jarモード」と「開発モード」を自動判定して切り替える。

import java.nio.file.*;
import java.net.*;

public class PathHelper {
    public static Path getAppBaseDir() {
        try {
            URI uri = PathHelper.class.getProtectionDomain().getCodeSource().getLocation().toURI();
            Path path = Paths.get(uri);
            Path dir = path.getParent();

            // IntelliJなど開発環境では "out/production" を含む場合が多い
            if (dir.toString().contains("out/production")) {
                // プロジェクトルートを返すように補正
                return Paths.get(System.getProperty("user.dir"));
            }

            return dir; // jarやappフォルダの場合
        } catch (Exception e) {
            return Paths.get(".").toAbsolutePath();
        }
    }

    public static void main(String[] args) {
        System.out.println("Base dir = " + getAppBaseDir());
    }
}

import java.nio.file.*;
import java.net.*;

public class PathHelper {
    public static Path getAppBaseDir() {
        try {
            URI uri = PathHelper.class.getProtectionDomain().getCodeSource().getLocation().toURI();
            Path path = Paths.get(uri);
            Path dir = path.getParent();

            // IntelliJなど開発環境では "out/production" を含む場合が多い
            if (dir.toString().contains("out/production")) {
                // プロジェクトルートを返すように補正
                return Paths.get(System.getProperty("user.dir"));
            }

            return dir; // jarやappフォルダの場合
        } catch (Exception e) {
            return Paths.get(".").toAbsolutePath();
        }
    }

    public static void main(String[] args) {
        System.out.println("Base dir = " + getAppBaseDir());
    }
}

これで:

  • IntelliJ上で実行 → プロジェクトルートを返す
  • jar/exe実行 → 実際のjar/appフォルダを返す

IntelliJ上で実行 → プロジェクトルートを返す

jar/exe実行 → 実際のjar/appフォルダを返す

ようになります。

もしテンプレートファイルなどを読み込むだけであれば、 src/main/resources/ に入れておくとビルド時に自動的にクラスパスへ入ります。

InputStream in = getClass().getResourceAsStream("/template.ini");

InputStream in = getClass().getResourceAsStream("/template.ini");

こうすれば IntelliJ でも jar でもパスを意識せず動作します。 (ただし、外部ファイルとして書き換える用途には向きません。)

環境 パス取得方法 動作内容
IntelliJ(開発時) out/production/... jarが存在しない
jar実行時 app/MyApp.jar OK
exe実行時 app/ フォルダ内 OK
  • IntelliJでの動作時は "out/production" を検出して補正する
  • または、Working Directoryをプロジェクトルートに変更する

IntelliJでの動作時は "out/production" を検出して補正する

または、Working Directoryをプロジェクトルートに変更する

もしよければ、 あなたの「実際のテンプレートファイル配置構造」と 「どう読み込んでいるか(コード数行)」を見せてもらえれば、 IntelliJ/jar/exeすべてで動く最適な解決版 を具体的に示せます。 貼ってもらえますか?