Hatena::ブログ(Diary)

seraphyの日記 このページをアンテナに追加 RSSフィード

2013-08-10 JavaアプリをExe化するLaunch4jの使い方と仕組み

JavaアプリをExe化するLaunch4jの使い方と仕組み JavaアプリをExe化するLaunch4jの使い方と仕組みを含むブックマーク

Launch4jとは?

Launch4jとは、JavaアプリケーションのExeラッパーを作成するツールである。

以下よりダウンロードできる。

http://launch4j.sourceforge.net/


このLaunch4jを使うことで、実行可能JarをExeファイルとして起動できるようにラップすることができる。

しかし、生成されたExeは単純なラッパーにすぎず、Exe化したといってもJavaランタイムが不要になるわけではない。

これは、端的にいえば、「Javaアプリケーションを起動するためのexe」を作るものだと考えてよい。


であれば、単純な「実行可能なjar」と比較して何が嬉しいのか?

機能的には以下のようなアドバンテージがある。

Launch4jでExe化するメリット
  • Exeなので、これがアプリケーションであることが一目でわかる
    • Javaに詳しくない人はjarファイルを見ても扱い方がわからない
    • ユーザーによってはアーカイバの拡張子の関連付けによって、jarファイルをダブルクリックするとアーカイバが開く場合もある。(jarファイルそのものはzip形式で作られているため。)
  • Exeなのでリソースを使ってアプリケーションアイコンがつけられる。
    • 実行可能Jarは、どのアプリケーションも同じアイコンになり見分けがつかない。
  • Exeなのでマニフェストがつけられる
    • つまり、Vista以降のUACによる昇格を制御できる。
    • 管理者権限でJavaアプリを起動することが可能になる。
  • Javaがなくても、すくなくともスタブは起動することはできる
    • Javaランタイムを入れてほしいというメッセージを出せる。(Javaランタイムをバンドルすることも可能)
    • 実行可能Jarだと、Javaがなければ、そもそも何もできない。
  • Exeなので、ドキュメント類のファイルをExeアイコン上にドロップできる。
    • ドロップされたファイルはmainメソッドのargsとして受け取れる。
    • 実行可能Jarの上にファイルをドロップすることはできない。
  • Exeなので、アプリケーションに署名をつけられる(と思われる。)
    • UACの昇格時にアプリケーション名に署名によるものを表示できる(と思われる)。
  • よくあるExeと同様に、iniファイルで設定ファイルを持たせることができる。
    • Javaのシステムプロパティなどをiniファイルで記述できるようになる。
Launch4jのライセンス

ライセンスは、Launch4jのJavaで書かれたツール部分はBSDライセンスであり、Exe化した場合のスタブ部分となるCで書かれた部分についてはMITライセンスとなっている。

どちらもオープンソースであり、且つ、商用利用可能な非常に緩いライセンスであり、Launch4jを使ってExe化しても元のソフトウェアのライセンスに制限を加えるようなことにはならない。

Launch4jの仕組み

JavaアプリケーションをExe化するツールには大きく分けて3種類ある。

  1. Javaをネイティブに翻訳するもの。
  2. 起動したEXE上からJNIの機能をつかってJava仮想マシンを自プロセス内に作成し、そこで実行する。(Tomcatのサービスのようなものなど。)
  3. 起動したExeから別プロセスとしてjava.exe, javaw.exeをCreateProcess APIで起動する

このうち、Launch4jが使っているのは、3番目のjava.exe, javaw.exeを呼び出す方法である。


この方法では、以下のようなメリットがある。

  • 普通にjava.exe, javaw.exeを起動しているので、通常のJavaでのアプリ起動と何ら挙動が変わることはない。
    • JNIを使ってアプリケーション内からJava仮想マシンを作成する場合には、アプリケーションの作りでJava仮想マシンに影響を与える可能性が高い。
  • OSが64ビットでも32ビットでもどちらでも正しく動作する。
    • CreateProcessでjava.exe, javaw.exeを呼び出すだけの仕組みであるため、ラップされたExe自身は32ビットであるが、64ビットJavaVMを起動できる

また、Launch4jでjarファイルをEXE化すると、1個のEXEファイルができあがるが、それはExeの中にリソースとして結合するなどのことはしてなくて、もっと単純である。

その実体は、java.exe/javaw.exeを起動するためのスタブとしてのEXEと、その後ろに、まるまるjarファイルが結合された形式なのである。


なぜ、このような単純な構造でOKかというと、JarファイルがZIP形式であるためである。


ZIP形式はファイルの末尾の「ファイルセントラル」というエリアで識別され、

この末尾にあるファイルセントラルに、アーカイブされているファイルエントリ一覧があり、個々のエントリへのアクセス用に相対位置で記録されている。

[参考] Wikipedia: ZIP (ファイルフォーマット)


そのため、EXEのようなファイルの後ろにZIPファイルをまるごとくっつけても、末尾からの相対位置はかわらないため、ZIPとしては何ら支障なく機能する、というわけである。


この性質を利用して、Launch4jは、java.exe, javaw.exeに実行するjarとして、自分自身(Exe)のファイルパスを渡しているだけの単純な構造になっている。

実際、このEXEは、ZIPを伸張できるソフトで中身を見ることができるし、java.exe -jar foo.exe のようにEXEファイルをJarのように指定しても、ちゃんと起動してくれる。


Launch4jの基本的な使い方

準備するもの

Launch4jでExe化する手順としては、

  1. まず実行可能jarを作成して、
  2. それに対してExe化する

ということになるため、まず実行可能jarを作成すること、つぎにExeで必要となるWindows用のアイコンやExeにつけるマニフェストの準備が必要である。

対象のアプリケーションの作成

Launch4jは、あくまでも実行可能jarを起動するためのexeを作るツールであるため、まずは単体で実行可能なjarを事前に作っておく。

(実行可能jarではなく、jarファイルを指定してメインクラスを指定することもできるが、目的を考えれば実行可能jarを先に作っておいたほうが良いと思われる。)


もしライブラリとして別のjarを必要とするならば、jarファイルのMETA-INF/manifest.mfClass-Path指定で

lib/foo.jar lib/bar.jar … のように指定しておく。


以下、今回の実験に用いるJavaソースコードを示す。

package jp.seraphyware.smplejarapp;

import java.awt.Container;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Map;
import java.util.prefs.Preferences;

import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.GroupLayout;
import javax.swing.GroupLayout.Alignment;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;

/**
 * 単純な実行可能jarとしてのJavaアプリケーション例
 * @author seraphy
 */
public class SimpleJarApp extends JFrame {

    private static final long serialVersionUID = -417644051755960867L;

    public SimpleJarApp() {
        try {
            setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            initComponent();

        } catch (RuntimeException ex) {
            dispose();
            throw ex;
        }
    }

    /**
     * レジストリキーにあるPreferencesへのアクセス用
     */
    Preferences pref = Preferences.systemNodeForPackage(this.getClass());

    /**
     * メッセージ用
     */
    private JLabel lblMessage;

    /**
     * キー値
     */
    private JTextField txtKey;

    /**
     * 設定値
     */
    private JTextField txtValue;

    private void initComponent() {
        setTitle("SimpleJarApp");

        Container contentPane = getContentPane();
        GroupLayout layout = new GroupLayout(contentPane);
        contentPane.setLayout(layout);

        // アイコンのロード
        try {
            Image iconImage = ImageIO.read(getClass().getResource("icon.png"));
            setIconImage(iconImage);

        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }


        layout.setAutoCreateGaps(true);
        layout.setAutoCreateContainerGaps(true);

        JLabel lblKey = new JLabel("Key");
        JLabel lblValue = new JLabel("Value");

        this.lblMessage = new JLabel();
        this.txtKey = new JTextField("sample-key");
        this.txtValue = new JTextField("sample-val");

        this.lblMessage.setText(pref.absolutePath());

        JButton btnInfo = new JButton(new AbstractAction("Info") {
            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent e) {
                showInfo();
            }
        });
        JButton btnLoad = new JButton(new AbstractAction("Load") {
            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent e) {
                load();
            }
        });
        JButton btnRegister = new JButton(new AbstractAction("Register") {
            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent e) {
                register();
            }
        });

        Dimension dim = txtKey.getPreferredSize();
        dim.width = 200;
        txtKey.setPreferredSize(dim);

        GroupLayout.SequentialGroup hGroup = layout.createSequentialGroup();
        hGroup.addGroup(layout.createParallelGroup()
                .addComponent(lblKey)
                .addComponent(lblValue)
                );
        hGroup.addGroup(layout.createParallelGroup(Alignment.TRAILING)
                .addComponent(lblMessage, GroupLayout.DEFAULT_SIZE, 238, Short.MAX_VALUE)
                .addComponent(txtKey)
                .addComponent(txtValue)
                .addGroup(layout.createSequentialGroup()
                        .addComponent(btnInfo)
                        .addComponent(btnLoad)
                        .addComponent(btnRegister)));
        layout.setHorizontalGroup(hGroup);

        GroupLayout.SequentialGroup vGroup = layout.createSequentialGroup();
        vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE)
                .addComponent(lblMessage));
        vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE)
                .addComponent(lblKey)
                .addComponent(txtKey));
        vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE)
                .addComponent(lblValue)
                .addComponent(txtValue));
        vGroup.addGroup(layout.createParallelGroup()
                .addComponent(btnInfo)
                .addComponent(btnLoad)
                .addComponent(btnRegister));
        layout.setVerticalGroup(vGroup);

        // レイアウト確定
        pack();

        // レジストリを読み取る
        load();
    }

    /**
     * システムの情報を表示する.
     */
    protected void showInfo() {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);

        pw.println("[System Properties]");
        System.getProperties().list(pw);

        pw.println();
        pw.println("[Environment]");
        for (Map.Entry<String, String> entry : System.getenv().entrySet()) {
            String key = entry.getKey();
            String val = entry.getValue();
            pw.println(key + "=" + val);
        }

        JTextArea textArea = new JTextArea();
        textArea.setText(sw.toString());

        JScrollPane scr = new JScrollPane(textArea);
        scr.setPreferredSize(new Dimension(600, 400));
        JOptionPane.showMessageDialog(this, scr);
    }

    /**
     * レジストリから指定されたキーの値を読み取りテキストフィールドに設定する.
     */
    protected void load() {
        String key = txtKey.getText();
        String val = pref.get(key, "");
        // 以下のレジストリキーの読み込み
        // \HKLM\Software\JavaSoft\Prefs\jp\seraphyware\simplejarapp
        txtValue.setText(val);
    }

    /**
     * レジストリの指定されたキーにテキストフィールドの値を設定する.
     */
    protected void register() {
        String key = txtKey.getText();
        String val = txtValue.getText();
        // 以下のレジストリキーの書き込み
        // \HKLM\Software\JavaSoft\Prefs\jp\seraphyware\simplejarapp
        // Windows Vista以降の場合、管理者権限があってもUACが有効でなければ動作しない
        // (この場合は、失敗しても例外は発生せず、コンソール上に警告が表示されるのみ。)
        try {
            pref.put(key, val);

        } catch (Exception ex) {
            ex.printStackTrace(System.out);
            JOptionPane.showMessageDialog(
                    this, ex.toString(), "ERROR", JOptionPane.ERROR_MESSAGE);
        }
    }

    /**
     * エントリポイント
     * @param args
     * @throws Exception
     */
    public static void main(final String[] args) throws Exception {
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());

        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                if (args.length > 0) {
                    // 起動引数があれば、それをメッセージボックスに表示する.
                    StringBuilder buf = new StringBuilder();
                    for (String arg : args) {
                        buf.append(arg);
                        buf.append("\r\n");
                    }
                    JOptionPane.showMessageDialog(null, buf.toString());
                }

                // フレームを作成して表示する.
                SimpleJarApp app = new SimpleJarApp();
                app.setLocationByPlatform(true);
                app.setVisible(true);
            }
        });
    }
}

このソースコードは、以下の手順で、実行可能Jarにするものとする。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project basedir="." default="build" name="SimpleJarApp">
    <property name="jarName" value="SimpleJarApp.jar"/>   
    <target name="build">
        <delete dir="work" />
        <mkdir dir="work" />
        <javac
            srcdir="src"
            encoding="UTF-8"
            target="1.6"
            debug="true"
            destdir="work" />
        <copy toDir="work">
            <fileset dir="src">
                <exclude name="**/*.java"/>
            </fileset>
        </copy>
        <jar 
            basedir="work"
            destfile="${jarName}">
            <manifest>
                <attribute
                    name="Main-Class"
                    value="jp.seraphyware.smplejarapp.SimpleJarApp" />
            </manifest>
        </jar>
        <delete dir="work" />
    </target>
</project>

ソースコード一式をGithubに格納しています。

実行例

実行すると以下のような小さなフレームが開かれる。

f:id:seraphy:20130811013622p:image

レジストリに対してKeyで指定したキー名で、指定したValueの値を設定したり、あるいは読み込んだりするだけの簡単なツールである。

(Infoボタンでは、システムプロパティや環境変数の一覧を表示する。)


JavaアプリケーションでのUACの昇格

今回は、Launch4jでExeするにあたり、ただ実行可能JarをExeでラップしただけでは面白くないので、UACによる昇格可能なJavaアプリケーションの作成をしてみる。


このアプリケーションはソースコード上のコメントにあるように、JavaのPreferences APIを用いてユーザー共通の情報の読み書きを行っている。

このAPIはWindowsの場合はレジストリに対する読み書きを行っており、#systemNodeForPackageメソッドを用いた場合は、対応するレジストリキーとして「\HKLM\Software\JavaSoft\Prefs\...」が用いられる。

これはユーザー共通のレジストリ項目であるため、どのユーザーからのリードアクセス可能であるが、書き込みは管理者権限が必要である。


ところが、Windows Vista/7以降ではUACという仕組みが導入され、管理者権限をもっているユーザーでも通常は特権を有効にされておらず、管理者権限が必要な場合には、プロセスの開始時にあらかじめ「昇格」させておく必要がある。

このため、通常の状態では、JavaPreferences#systemNodeForPackageメソッドは、たとえ管理者が実行としたとしても、あらかじめjava.exeプロセスを昇格した状態で起動していないかぎり利用することができなくなっている。


したがって、本アプリケーションを実行するためにはUACの昇格が必要となる。


UACを昇格するために必要なのは、単にExeファイルに対するマニフェストに、管理者権限への昇格を指示するだけである。(昇格はプロセスの起動時にのみ行われるため。)

Exeから呼び出されるjava.exe、javaw.exeは、CreateProcess APIによって起動される。

そのため、呼び出し元のプロセスの権限状態を受け継ぐため、もし、Exeが管理者権限として昇格しているのであれば、呼び出されたjava.exe, javaw.exeも昇格した状態を引き継ぐことになる。

この結果、JavaアプリケーションがUACにより昇格した状態で動作できるようになる。


(なお、本来、HKLMキーは書き込みこそ管理者権限が必要でも、読み込みは全ユーザー可能である。しかし、2013/8現在のOracleのJava7のsystemNodeForPackageの実装では、実行しているユーザーが管理者である場合は特権が有効にされているかどうかにかかわらずHKLMキーを「読み書きモード」で開こうとするため、結果、管理者権限があると読み込みですら失敗する状態になっているようである。一般ユーザであればsystemNodeForPackageは「読み込みモード」でHKLMを開くため、問題なく読み込みアクセスできる、逆転的な現象になっている。)


(DotNET以降の最近では、Windowsでもレジストリにアプリケーションデータを格納するのではなく、環境変数%ALLUSERSPROFILE%や、%APPDATA%が示すようなフォルダ上にファイルとして保存することが推奨されるようになっており、その意味では、Preferences APIを使う必要性は、ずいぶんと低くなったものと思われる。)


(余談) JavaアプリケーションでWindowsユーザの権限有無を判定する方法

Javaアプリケーション上でユーザーが管理者権限を持っているか、あるいはUACによって昇格しているか、を判定するにはcom.sun.security.auth.module.NTSystem#getGroupIDsでチェックすることができるようである。


たとえば、"S-1-5-32-544"を所有していれば、管理者権限がある。

ただし、これがUACによって有効化(昇格)しているかどうかは判断できない。


加えて、"S-1-16-8192"(Medium Mandatory Level), "S-1-16-4096" (Low Mandatory Level)のいずれかがある場合は、

管理者権限はあるが、まだ昇格していない、と判断できるようである。

(定義上は、このほかにも数種類あり、本当に、この2つだけで判断できるかは不安なところである。)


なお、"S-1-16-12288"(High Mandatory Level)があればUACによる昇格がされていることを判別できるが、

Windows XPのように、そもそもUACがないか、Vista以降でもUACを無効にしている場合には出てこないSIDであるため、

このSIDだけで管理者権限が有効になっているか判断するのは不味いと考えられる。


Windows Vista/7/8にUACの制御を示すためにExeに埋め込むマニフェスト"SimpleJarApp.manifest"を追加する。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel level="highestAvailable"
          uiAccess="False" />
      </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>

requestedExecutionLevelが、UACの制御を示す部分である。

  • "highestAvailable"の場合、管理者であれば昇格してから実行となり、そうでない一般ユーザーであれば昇格されないまま実行される。
  • "requireAdministrator"の場合は、つねに管理者権限が必要であることを示しており、管理者であれば昇格となり、一般ユーザーであれば管理者ユーザーとパスワードを求められるようになる。
Exe用のアイコンを用意する

また、Exeファイルにつけるアイコン「*.ico」ファイルが必要である。

JFrameで指定したアイコン用のPNGから、Windowsで用いる透過ICOファイルに変換してくれるようなフリーソフトかサービスを使って、ICOファイルを用意しておく必要がある。

(たとえば、 http://www.icoconverter.com/ とか。)

Launch4jの起動と設定内容

Launch4jをインストールすると、

  • GUIツール
  • コマンドラインツール
  • Antタスク

の3種類の方法によって、Launch4jを利用することができるようになる。

GUIで使用する場合は、launch4j.exeを起動する。

ファイルの指定

実行可能jarがある場合には、単に、それをラップするだけで良いので設定項目は少ない。

f:id:seraphy:20130811014331p:image

Basicタブでは、入力するJarファイル名と、アイコン名、マニフェスト名を指定する。

また、出力するExeファイル名を設定する。

これらのパスは、この設定ファイルを保存した場所の相対パスを指定できる。


Jarファイルの指定では、実行可能jarを指定するだけで良い。

外部のライブラリファイルを使用している場合でも、マニフェストのClass-Pathで指定されているならば、ライブラリのjarファイルをLaucn4jの中で指定する必要はない。

この場合、実行可能jarがexeに置き換わったものと考えればよく、libへのディレクトリ構造等は、まったく同じとなる。


Java Download URLには、Javaランタイムがシステムになかった場合に開く、Javaのダウンロード先のURLを指定する。

JavaランタイムとJVMオプションの指定

f:id:seraphy:20130811014332p:image

JREタブでは、Javaランタイムの最低バージョンを指定する。

(最低バージョンの指定は必須である。)


また、Launch4jでは任意のJREを同梱して、そのJREを使用するように設定することもできる。


JREを同梱していない場合、もしくは指定された場所にJREが見つからなかった場合は、システムにインストール済みのJavaランタイムから条件に合致する、もっともバージョンの新しいものを用いる。

もし、JREを同梱しておらず、システムにもJavaランタイムがない場合には、最初に指定したjava.comへのダウンロードページが開くことになる。


また、Laucn4jの良いところ、特徴の1つは、ラップされたExeファイルは32ビットバイナリであるが、実行するJavaランタイムは32/64を問わない、ということである。

64ビットOS上にインストールされた最新のJavaランタイムが64ビットJavaVMであれば、Launch4jでラップされたExeから起動されるJavaは64ビットJavaVMとなる。


また、ここでヒープサイズや、JVMオプションなどを指定することもできる。

ヒープサイズの指定を省略した場合は、java.exe, java.exeでヒープサイズを省略した場合と同じである。

(最近のJavaランタイムでは、1GBを超えない搭載している物理メモリの25%が最大ヒープサイズに選ばれる。)


また、ここで指定したヒープサイズやJVMオプションの指定は、後述するiniファイルの指定が優先(上書き)される。


Launch4jの設定ファイルの保存

生成されるLaunch4jの設定ファイルは以下のようなXML形式で記録される。

一度、この設定ファイルを定義すれば以降はコマンドラインやAntタスクで使えるようになる。

<launch4jConfig>
  <dontWrapJar>false</dontWrapJar>
  <headerType>gui</headerType>
  <jar>SimpleJarApp.jar</jar>
  <outfile>SimpleJarApp.exe</outfile>
  <errTitle></errTitle>
  <cmdLine></cmdLine>
  <chdir></chdir>
  <priority>normal</priority>
  <downloadUrl>http://java.com/download</downloadUrl>
  <supportUrl></supportUrl>
  <customProcName>false</customProcName>
  <stayAlive>false</stayAlive>
  <manifest>SimpleJarApp.manifest</manifest>
  <icon>icon.ico</icon>
  <jre>
    <path></path>
    <minVersion>1.6.0_37</minVersion>
    <maxVersion></maxVersion>
    <jdkPreference>preferJre</jdkPreference>
  </jre>
</launch4jConfig>
Launch4jによって生成されたEXE

こうして実行可能JarをExe化すると、以下のようになる。

f:id:seraphy:20130811014333p:image

SimpleJarApp.exeファイルが、

  1. アプリケーションアイコンになっており、且つ、
  2. UACのシールドアイコンがオーバーレイされている。

ということがわかる。

隣の実行可能jarとアイコンを見比べてみれば、このExeがアプリケーションの実行ファイルであり、且つ、管理者権限で実行されることは一目で明らかである。

この違い、メリットは小さくはないといえる。


実行結果

管理者権限があるユーザーで実行した場合
  1. UACによる昇格確認のダイアログが表示される。
  2. キーに値を書き込める
  3. キーから値を読み込める
一般ユーザの場合
  1. UACの確認は表示されず、すぐにアプリケーション画面が開く
  2. キーから値を読み込める
  3. キーへの書き込みは例外が発生する

というような、XP時代と同等な動きをするようになっている。

なお、本アプリケーションをXPで実行した場合には、単にExeにつけられたUACのマニフェストが無視されるだけで、実行可能Jarを単体で実行するのと動きは変わらない。

ファイルのドロップのテスト

ExeファイルまたはExeファイルへのショートカットにはファイルをドロップすることができる。

f:id:seraphy:20130811014633p:image

このようにファイルをショートカット上に落とすと、

f:id:seraphy:20130811014634p:image

このようにアプリケーションが起動され、起動引数として認識することができる。


Launch4jでExeしたアプリケーションのJavaオプションの指定方法

Javaアプリケーションでは、JavaVMの起動引数によって各種パラメータを調整したり、システムプロパティを指定してライブラリやアプリケーション固有の設定を指定することが頻繁に用いられている。


Launch4jによってExe化したとしても、元がJavaアプリケーションである以上、これらのシステムプロパティの指定方法が必要になる場合がある。

このような場合にも、Launch4jではiniファイルを用いることで対応できるようにしている。


たとえば、最大ヒープサイズを512Mbytes、アプリケーション固有のシステムプロパティとして「simplejarapp.EXEDIR」「simplejarapp.PWD」というプロパティが必要だとする。


その場合、「ファイル名.l4j.ini」というIniファイルを用意する。

たとえば、「SimpleJarApp.exe」というファイルであれば「SimpleJarApp.l4j.ini」というIniファイルに設定を記述する。

-Xmx512m
-Xms512m
"-Dsimplejarapp.EXEDIR=%EXEDIR%"
"-Dsimplejarapp.PWD=%PWD%"

iniファイルでは、1つのオプションごとに1行指定する。

(オプション引数に空白を含む場合はダブルクォートで囲むなどの処置をしないと空白位置で複数オプションと認識されてしまうので注意のこと。)*1

行頭が#のものはコメントとみなされる。


指定する内容はjava.exeへのオプション同様である。


ただし、%〜%で囲まれたものは環境変数として展開される。

また、Exeのあるディレクトリを示す%EXEDIR%や、現在ディレクトリを示す%PWD%といった「特殊な変数」もある。

(特殊変数は、そのほかにいくつかある。これらについてはドキュメントには記載がないようだ。)


環境変数や、どちらの特殊変数もJavaでプログラム的に取得可能な情報であるが、

システムプロパティがEXEファイル相対位置にある場合など、システムプロパティ自身を柔軟に指定したい場合には、これらの特殊変数と環境変数の展開は役に立つと思われる。


Launch4jのカスタマイズ方法

前述のとおり、Launch4jが生成するEXEは、java.exe, javaw.exeを起動するだけのスタブであり、jarファイルは、そのスタブの後ろにまるごとくっついているだけの単純なものである。

このスタブはC言語で記述されており、Launch4jのソースに付属しているため、ちょっとした事前処理を拡張したりすることなどは比較的簡単にできる。


ここで使われている処理系はMinGW/gccである。

このスタブ部分は、Dev-C++というオープンソースのIDEで作成されている。


Launch4jのhead_srcフォルダ以下には、

  • consolehead(コンソールアプリ用 java.exe起動)
  • guihead(GUIアプリ用 javaw.exe起動)

の、それぞれのプロジェクトファイル(*.dev)があり、これをDev-C++で開くことができる。

f:id:seraphy:20130813011901p:image


スタブとしての、ほとんどの中心的機能は、両者共通の"head.c"に記述されている。

たとえばGUIアプリであったとしても、javaw.exeではなく、java.exeを呼んで、コンソールを非表示にしておく、という使い方がしたい場合には、このhead.cを修正してリコンパイルし、生成されたoファイルを入れておけば、Launch4jでExe化する場合に、このhead.oファイルで実行可能ファイルが作成されるようになる。


これらのコンパイルして得られたオブジェクト*.oファイルが、headフォルダに格納されている。


Launch4jの設定画面の「Header」を開くと、これらのoファイルがあることがわかる。

これらが、Exeのスタブ部分となるオブジェクトである。

f:id:seraphy:20130811015335p:image


Launch4jは、Exeラッパーを生成するために、

  1. まず、設定画面で指定された項目やアイコン、マニフェストといったファイルを、リソースコンパイラでリソースファイルにし、
  2. 次に、リソースと上記のコンパイル済みの*.oファイルをリンカーによってリンクしてExeファイルを作成し、
  3. 最後にExeの後ろにJarファイルを連結する、という処理を行う。

このため、Launch4jは、リソースとリンクのためにbinフォルダMinGWの一部を格納し、それを呼び出している。

Launch4j 3.0.2で使われているMinGW/gccのバージョンは、gcc version 3.4.2 (mingw-special) であり、

Exeラップ作成のためにLaunch4jから呼び出されるのは、以下のコマンドである。

  • windres.exe (リソースコンパイラ) GNU windres 2.15.91 20040904
  • ld.exe (リンカー) GNU ld version 2.15.91 20040904

これは「Dev-C++ 5.0 beta 9.2 (ver 4.9.9.2) with Mingw/GCC 3.4.2」を使うと、ちょうど良い環境となる。


※ なお、Dev-C++から派生した後継のIDEではgccのバージョンが上がっているため、もし、新しいIDEを使う場合には、古いMinGW/gcc 3.4.2の環境を準備し、それらを使えるようにしておく必要がある。

※ もし、gccのバージョンが一致しなければ不可解なエラーに悩まされると思われる。

※ また、Visual C++では処理系が異なり使用することはできない。


結論

Launch4jは、実行可能Jarを、よりWindows上での実行ファイルらしくする上で、原理が非常に単純であるため、もっとも副作用が少なく安心して使える扱いやすいツールと思われる。


Javaで実行可能Jarを作成するならばExe化する選択肢として考えておいて良いだろうし、とくに管理者権限が必要なJavaアプリケーションであれば、UACを制御するための非常に有効な手段になると思われる。


以上、メモ終わり。

*1:2014/03/13追記