電卓片手に

jenkins.war みたいな実行可能な war ファイルの作成

投稿日:

Jenkins が配布している war ファイルは、サーブレットコンテナに読み込ませれば war ファイルとして機能するし、さらに以下のように単体で実行可能になっている:

java -jar jenkins.war

これを実現するための簡単なサンプルを作ったので、これを実現するための要所を 簡単に書いていきたい。

サンプルのビルド方法

ビルドには Maven 3 が必要になる。

git clone git@github.com:kui/executable-war-sample.git
cd executable-war-sample
mvn package
ls **/sample.war
java -jar winstone/target/sample.war

依存関係

winstone をサーブレットコンテナとして使う場合は依存関係にこう書く:

<project ...>
  ...
  <repositories>
    <!-- jenkins が管理してる winstone 使うためにリポジトリ追加 -->
    <repository>
      <id>repo.jenkins-ci.org</id>
      <url>http://repo.jenkins-ci.org/public/</url>
    </repository>
  </repositories>
  ...
  <dependencies>
    <dependency>
      <groupId>org.jenkins-ci</groupId>
      <artifactId>winstone</artifactId>
      <version>0.9.10-jenkins-43</version>
      <!-- scope=provided である必要は無いが、plugin 使って winstone の class
           ファイルのコピーをするので、Jar にアクセスできれば何でも良い
           compile にしてしまうと war のサイズが無駄に大きくなるので非推奨 -->
      <scope>provided</scope>
    </dependency>
  </dependencies>
  ...

本家 winstone は、最近更新がない のが気になったので、 比較的更新頻度の高い Jenkins が管理している winstone を使っている。

Main.java

java -jar sample.war を実行した時に呼ばれるメソッドを定義している。

主にやるべきことは:

の二点になる。

winston 版の例:

package jp.k_ui.sample;
import java.net.URL;
import java.util.*;
import winstone.Launcher;

public class Main {
    public static void main(String[] args) throws Exception {
        // war 自身の場所を取得
        URL warLocation = Main.class.getProtectionDomain().getCodeSource()
                .getLocation();

        // コマンドライン引数ほぼそのまま winstone に渡す。
        // war ファイルの場所だけ加える。
        List<String> argList = new ArrayList<String>(Arrays.asList(args));
        argList.add("--warfile=" + warLocation.getPath());
        System.out.println(argList);

        Launcher.main(argList.toArray(new String[0]));
    }
}

pom.xml

つまるところ、実行可能 war は、その war に梱包するファイル配置を実行可能 jar と同じようなファイル配置にすればよい。

そのファイル配置を Maven にやらせる設定をする。ただし、通常は war を 構築するだけのファイル配置になってしまうので、少々面倒な設定が必要になる。

Main-Class の指定

どのクラスの main(String[]) を呼べば良いのか、という指定をする設定:

<project ...>
  <build>
    ...
    <plugins>
      ...
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>2.3</version>
        <configuration>
          <archive>
            <manifest>
              <mainClass>jp.k_ui.sample.Main</mainClass>
            </manifest>
          </archive>
        </configuration>
      </plugin>

この状態で mvn package もできるし、java -jar sample.jar で実行も可能。 ただし失敗する。

失敗する理由は unzip -l sample.jar で中身を見てみるとわかるけれど、 呼び出すはずの jp.k_ui.sample.Main が梱包されていなため。

Main.java のコピー

上で梱包されていないことが分かった Main クラスをコピーする設定:

<project ...>
  <build>
    ...
    <plugins>
      ...
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-antrun-plugin</artifactId>
        <version>1.7</version>
        <executions>
          <execution>
            <id>main-class-placement</id>
            <phase>prepare-package</phase>
            <configuration>
              <tasks>
                <move todir="${project.build.directory}/${project.build.finalName}/">
                  <fileset dir="${project.build.directory}/classes/">
                    <include name="jp/k_ui/sample/Main.class" />
                  </fileset>
                </move>
              </tasks>
            </configuration>
            <goals>
              <goal>run</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

Apache Ant を使って、war 化する前のディレクトリにコピーしている。

この状態で java -jar sample.war を実行すると、Main 見つからないという文句は消える代わりに、 Launcher が見つからないと言ってくるはず。

先程同様、unzip -l sample.jar で中身を見てみると、サーブレットを実行するためのクラスが war に梱包されていない。

サーブレットコンテナが使うクラスのコピー

Main.java が利用しているサーブレットコンテナに関わるクラス全てを、 これから war 化する前のディレクトリにコピーする設定:

<project ...>
  <build>
    ...
    <plugins>
      ...
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-dependency-plugin</artifactId>
        <version>2.7</version>
        <executions>
          <execution>
            <id>winstone-classpath</id>
            <phase>prepare-package</phase>
            <goals>
              <goal>unpack-dependencies</goal>
            </goals>
            <configuration>
              <includeGroupIds>org.jenkins-ci</includeGroupIds>
              <includeArtifactIds>winstone</includeArtifactIds>
              <includeScope>provided</includeScope>
              <excludes></excludes>
              <outputDirectory>
                ${project.build.directory}/${project.build.finalName}
              </outputDirectory>
            </configuration>
          </execution>
        </executions>
      </plugin>
      ...

設定おわり

以上で、実行可能 war を作成するための設定が完了したことになる。

mvn clean package をしたあと、java -jar sample.war で実行できるはず。

Winstone 対 Jetty

結論から書くと Winstone のほうが向いているかなと感じました。理由としては:

実行可能 war のファイルサイズ

察しはついていたが、今回のために用意したサンプルをビルドしてみて改めて理解した。

$ git clone git@github.com:kui/executable-war-sample.git
$ cd executable-war-sample
$ mvn package
$ ls -1sh **/sample.war
1.4M jetty/target/sample.war          # Jetty 9 版
 80K non-executable/target/sample.war # 比較のために用意した単なる war
360K winstone/target/sample.war       # winstone 版

実行可能 war にした時のファイルサイズの増分は:

となり、Winstone のほうがファイルサイズが小さいことが分かった。ただし、 元の war ファイルのサイズ次第では無視できるような差分かも。

Main.java の作りやすさ

これは、実際のコードと、作られた sample.jar の出来栄えを比較するのが一番早いかと。

Winstone は Launcher にそのままコマンドライン引数を渡せば、winstone.jar として機能してくれるため、このようなお手軽 Main.java でも、ポートの指定、SSL の云々、 ログファイルの指定、--help でヘルプメッセージ出力などができてしまう。 面倒なコマンドライン引数のオプションパースをしないで済む。嬉しい。

Jetty には Launcher 相当がパッと調べた限りでは見当らない。(報告求む)

おわり

ここまで書いてしまいましたが Jenkins の Main.java は、少し違う方法をとっている:

ClassLoadernew したり、winstone.jar はバラさずに war に梱包している。 ここまで複雑な方法を取る理由がよくわからない。。。どういうことなんだろう。。。

参考

このへん参考にしたんですが、情報古かったり、設定が間違ってたり、Jetty しか使ってなかったりだったで手を加えてる。