実行可能なJavaプログラムをjarファイルで配布する場合、その形態として次の2方式をよく見かけます。
本記事ではこれらの配布と実行方式の実現方法について調べてみました。
まずは一番シンプルな形態として、依存jarが無い状態で"-jar"で実行可能なjarファイルを作成してみます。
Hello.java:
public class Hello { public static void main(String[] args) { System.out.println("Hello"); } }
コンパイル&実行:
$ javac Hello.java $ java Hello Hello
単純にjarファイルを作成して実行してみます:
$ jar cf hello-nomf.jar -C hello . $ jar tf hello-nomf.jar META-INF/ META-INF/MANIFEST.MF Hello.class Hello.java $ java -cp hello-nomf.jar Hello Hello
このjarファイルを実行可能にするには、マニフェスト・ファイルの"Main-Class"属性にmain()を実行するクラス名を指定します。
以下のようなテキストファイルを用意します。最後の空行は必須です。
hello.mf:
Main-Class: Hello
jar作成&実行:
$ jar cmf hello.mf hello-mf.jar -C hello . $ java -jar hello-mf.jar Hello
正常に"-jar"だけで実行できました。展開して中身を確認してみます。
$ jar xf hello-mf.jar META-INF/MANIFEST.MF $ cat META-INF/MANIFEST.MF Manifest-Version: 1.0 Created-By: 1.7.0_07 (Oracle Corporation) Main-Class: Hello
MavenプロジェクトでMain-Class属性を指定してみます。
quickstartを使ってMavenプロジェクトを生成:
$ mvn archetype:generate -DarchetypeArtifactId=maven-archetype-quickstart ... Define value for property 'groupId': : step1-mf-mvn Define value for property 'artifactId': : step1-mf-mvn Define value for property 'version': 1.0-SNAPSHOT: : Define value for property 'package': step1-mf-mvn: : step1 Confirm properties configuration: groupId: step1-mf-mvn artifactId: step1-mf-mvn version: 1.0-SNAPSHOT package: step1 ... $ cat ./src/main/java/step1/App.java package step1; /** * Hello world! * */ public class App { public static void main( String[] args ) { System.out.println( "Hello World!" ); } }
Main-Class属性を指定するには、maven-jar-pluginの"<archive>" -> "<manifest>" -> "<mainClass>"に指定します。
pom.xml抜粋:
... <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifest> <mainClass>step1.App</mainClass> </manifest> </archive> </configuration> </plugin> </plugins> </build> ...
ビルド&実行:
$ mvn package $ java -jar target/step1-mf-mvn-1.0-SNAPSHOT.jar Hello World!
実際のMANIFEST.MFファイルの中身:
$ jar xf target/step1-mf-mvn-1.0-SNAPSHOT.jar META-INF/MANIFEST.MF $ cat META-INF/MANIFEST.MF Manifest-Version: 1.0 Archiver-Version: Plexus Archiver Created-By: Apache Maven Built-By: FengJing Build-Jdk: 1.7.0_07 Main-Class: step1.App
step1では依存jarが無い、一番シンプルな形で確認しました。step2では、依存jarが存在する、より現実的な場合での対応方法を試してみます。
マニフェスト・ファイルでは、"Class-Path"属性を使ってjarファイルの配置場所からの相対パスでclasspathに通したいjarを指定することが可能です。
サンプル:
step2-with-jar/ depjars-src/ depjar1/Lib1.java, Lib1.class depjar2/Lib2.java, Lib2.class depjars-lib/ depjar1.jar depjar2.jar Hello.java hello.mf
Hello.javaは以下の様な内容で、depjar1.jar中の"depjar1.Lib1"およびdepjar2.jar中の"depjar2.Lib2"に依存しています。
import depjar1.Lib1; import depjar2.Lib2; public class Hello { public static void main(String[] args) { int a = 5; int b = 3; System.out.println(Lib1.calc(a, b)); System.out.println(Lib2.calc(a, b)); } }
コンパイル:
$ cd step2-with-jar/ $ javac -cp .:depjars-src Hello.java
マニフェスト・ファイルとして以下の内容を作成し、hello.mfとして保存し、jar作成時に指定します。
hello.mf:
Main-Class: Hello Class-Path: ./depjars-lib/depjar1.jar ./depjars-lib/depjar2.jar
jarを作成し、実行します。Class-Path属性により、jarファイルからの相対パスで"depjars-lib"以下のjarファイルが参照され、正常に実行されました。
$ jar cmf hello.mf hello.jar Hello.class $ jar tf hello.jar META-INF/ META-INF/MANIFEST.MF Hello.class $ java -jar hello.jar 8 2
Mavenでもマニフェスト・ファイルのClass-Path属性を設定できます。"mainClass"を設定した時と同様、Maven Archiverの提供する"addClasspath"設定を使うことで、Maven側で依存しているjarを自動的にClass-Path属性にまとめてくれます。
step2-mvn1/pom.xml:
<project ...> ... <dependencies> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifest> <mainClass>step2.App</mainClass> <!-- Class-Path属性の自動設定 --> <addClasspath>true</addClasspath> <!-- Class-Path属性で"lib/"などprefixを付けたい時に指定 --> <classpathPrefix>lib</classpathPrefix> </manifest> </archive> </configuration> </plugin> </plugins> </build> </project>
src/main/java/step2/App.java:
package step2; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.commons.lang.WordUtils; public class App { static Log log = LogFactory.getLog(App.class); public static void main( String[] args ) { log.fatal("fatal log sample"); log.error("error log sample"); log.warn("warn log sample"); log.info("info log sample"); log.debug("debug log sample"); log.trace("trace log sample"); System.out.println(WordUtils.swapCase("Hello World!")); } }
パッケージングして、マニフェスト・ファイルを確認してみます。
$ mvn package $ jar xf target/step2-mvn1-1.0-SNAPSHOT.jar META-INF/MANIFEST.MF $ cat META-INF/MANIFEST.MF Manifest-Version: 1.0 Archiver-Version: Plexus Archiver Created-By: Apache Maven Built-By: FengJing Build-Jdk: 1.7.0_07 Main-Class: step2.App Class-Path: lib/commons-logging-1.1.1.jar lib/commons-lang-2.6.jar
commons-loggingとcommons-langの依存jarがClass-Path属性に指定されていることが確認されます。
しかし、この段階ではまだ"lib/"ディレクトリ自体がまだ存在しないので実行できいません。"lib/"ディレクトリを作成し、依存jarを集約するにはmaven-dependency-pluginのcopy-dependenciesゴールを使います。
$ mvn dependency:copy-dependencies -DoutputDirectory=target/lib -DincludeScope=compile ... "outputDirectory"で"target/lib"以下にコピー ... "includeScope"で"compile"スコープの依存jarのみをコピー
この時点で以下のようなjar構成になります。
step2-mvn1/target/ step2-mvn1-1.0-SNAPSHOT.jar lib/ commons-lang-2.6.jar commons-logging-1.1.1.jar
準備出来ましたので、実行してみます。
$ java -jar target/step2-mvn1-1.0-SNAPSHOT.jar 11 04, 2012 12:38:24 午後 step2.App main SEVERE: fatal log sample 11 04, 2012 12:38:24 午後 step2.App main SEVERE: error log sample 11 04, 2012 12:38:24 午後 step2.App main WARNING: warn log sample 11 04, 2012 12:38:24 午後 step2.App main 情報: info log sample hELLO wORLD!
うまく動作してくれました。
上で紹介した "dependency:copy-dependencies" はあくまでもmvnコマンドが実行できる開発環境に限定されます。実際に配布する場合はzipやtar.gzなどにパッケージングすることになり、Mavenの場合ではmaven-assembly-pluginを使って依存jarのパッケージングを自動化できます。
step2-mvn1のMavenプロジェクトを "step2-mvn2" にコピーして、pom.xmlを以下のようにします。
step2-mvn2/pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>step2-mvn2</groupId> <artifactId>step2-mvn2</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <name>step2-mvn2</name> <url>http://maven.apache.org</url> <dependencies> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifest> <mainClass>step2.App</mainClass> <!-- Class-Path属性を指定します。 --> <addClasspath>true</addClasspath> <!-- maven-assembly-pluginのデフォルトでは、 artifactのjar自体も依存jarと同じディレクトリに まとめられますので、あえてclasspathPrefix は指定せず、アプリのjarと同じ場所に 依存jarがあるものとします。 --> </manifest> </archive> </configuration> </plugin> <plugin> <artifactId>maven-assembly-plugin</artifactId> <version>2.3</version> <configuration> <descriptors> <descriptor>src/assemble/bin.xml</descriptor> </descriptors> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
maven-assembly-pluginは配布用のパッケージの"assemble"に利用されます。設定が複雑になりがちなこともあり、詳細な設定を外部の"descriptor" XMLファイルに記載し、pom.xmlではdescriptor XMLファイルの場所だけを指定しています。
step2-mvn2/src/assemble/bin.xml:
<assembly> <id>bin</id> <formats> <!-- 今回は"<format>"としてtar.gz, tar.bz2, zipの3種類を指定します。 --> <format>tar.gz</format> <format>tar.bz2</format> <format>zip</format> </formats> <dependencySets> <dependencySet> <!-- artifact自身のjarを含め、依存jarをパッケージの直下に配置します。 --> <outputDirectory>/</outputDirectory> <includes> <!-- 集約する対象は依存する"jar"のみにします。 --> <include>*:jar</include> </includes> </dependencySet> </dependencySets> </assembly>
$ mvn package ... [INFO] Building tar: .../ [INFO] Building tar: .../target/step2-mvn2-1.0-SNAPSHOT-bin.tar.gz [INFO] Building tar: .../target/step2-mvn2-1.0-SNAPSHOT-bin.tar.bz2 [INFO] Building zip: .../target/step2-mvn2-1.0-SNAPSHOT-bin.zip [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------
試しにtar.gzを適当なディレクトリに展開してみます。
$ tar zxf step2-mvn2-1.0-SNAPSHOT-bin.tar.gz $ find . . ./step2-mvn2-1.0-SNAPSHOT ./step2-mvn2-1.0-SNAPSHOT/commons-lang-2.6.jar ./step2-mvn2-1.0-SNAPSHOT/commons-logging-1.1.1.jar ./step2-mvn2-1.0-SNAPSHOT/step2-mvn2-1.0-SNAPSHOT.jar ./step2-mvn2-1.0-SNAPSHOT-bin.tar.gz
実行してみます。
$ cd ./step2-mvn2-1.0-SNAPSHOT $ java -jar step2-mvn2-1.0-SNAPSHOT.jar 11 04, 2012 2:37:04 午後 step2.App main SEVERE: fatal log sample 11 04, 2012 2:37:04 午後 step2.App main SEVERE: error log sample 11 04, 2012 2:37:04 午後 step2.App main WARNING: warn log sample 11 04, 2012 2:37:04 午後 step2.App main 情報: info log sample hELLO wORLD!
問題なく動作しています。これで、tarやzipなどで依存jarも含めて配布できるようになりました。
ここまではjarファイルの機能だけで実行可能にしたり依存jarの問題を対応してきました。このstep3では、Tomcatの配布パッケージで見られるようにbatやshファイルで依存関係を解決するアプローチを紹介します。
以前、Apache MINAを使ったechoサーバの実験でcodereposに"echo_samples/act9"というサンプルコードをupし、その中でこのアプローチを採用し、batやshで依存jarの一覧を取り出してclasspathに指定していますので、そちらを簡単に紹介していきます。
http://coderepos.org/share/browser/lang/java/echo_samples/act9
ポイントとなるのは、act9/src/bin/ 以下に格納したbatやshになります。これらはpackagingすることで以下のように配置されます。
echoes-act9-1.0.0/ lib/*.jar ea9_*.bat ea9_*.sh echoes-act9.ini 他、ライセンスやNTサービス登録用のbatファイルなど
Windowsでclasspathを調整しているのは ea9_env.bat と ea9_addclp.bat の2つになります。
ea9_env.bat:
set EA9_HOME=%~dp0 set EA9_SVC_NAME=EchoesAct9 set EA9_SVC_DISPLAY_NAME="Echoes Act9" set EA9_SVC_DESCRIPTION="Echoes Act9 Service" set JAVA_OPTIONS_SRV=-Xmx200m -Xloggc:"%EA9_HOME%gc.log" set CLASSPATH= for %%I in ("%EA9_HOME%lib\*.jar") do call ea9_addclp.bat "%%I"
最後のforで、"lib/*.jar" をワイルドカードで指定することで1ファイルずつ展開て "%I" に格納し、ea9_addclp.bat に渡しています。
ea9_addclp.bat:
SET CLASSPATH=%1;%CLASSPATH%
ea9_addclp.batの中では、単純にCLASSPATH環境変数に渡されてきたファイルパスを追加していきます。
これ、最初はea9_env.batの中だけで完結できるんじゃ・・・と試してたんですが、うまく行かなくて(理由は忘れた)、CLASSPATHの設定だけea9_addclp.batに分離してうまく動作しました。
実際のjava実行用batは以下のようになっていて、ea9_env.batをcallしたらそのままjavaを起動するだけです。
ea9_client.bat:
call ea9_env.bat java -classpath %CLASSPATH% echoes.act9.client.EchoesClient "%EA9_HOME%echoes-act9.ini"
unixの場合にclasspathを調整しているのは ea9_env.sh になります。調整箇所だけ抜粋します。
ea9_env.sh:
CLASSPATH= for i in `/bin/ls lib/*.jar`; do CLASSPATH=$i:$CLASSPATH ; done export CLASSPATH
lsコマンドの出力結果をCLASSPATHに追加してるだけです。・・・ちょっとlsコマンドの挙動に依存関係が発生してしまうのが気持ち悪いですが、2010年ごろのサンプル作成時は、これでうまく動いてました・・・。
実行用のshは以下のようになっていて、bashの"."でea9_env.shを展開して実行したら、javaを起動するだけです。
ea9_client.sh:
cd `dirname $0` . ea9_env.sh java -classpath $CLASSPATH echoes.act9.client.EchoesClient echoes-act9.ini
以上、batやshの機能だけでclasspathを調整するサンプルの紹介でした。
参考:
step3では手作りのshやbatファイルでclasspathを調整してみましたが、Maven Application Assemblerを使うともっとちゃんとした(?)・・・その、多分、もうちょっと依存性を薄めてちゃんと動いてくれる・・・本格的?な?batやshを作ってくれるようです。
原理的には、パッケージングの段階で依存jarを収集して、プリセットされたディレクトリ構成に配置し、それを相対パスで参照するようなclasspathを埋め込んでbat/shを生成します。ですので、bat/sh内で黒魔術を使って動的にjarを探索、classpathを構築してるわけではないです。(生成されたbat/shを見てもらえれば「あ~」と納得していただけるかと。)
ということで、step3で紹介したechoes.act9をappassemblerで構築してみます。
まずpom.xmlのpluginsを以下のように修正します。
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>appassembler-maven-plugin</artifactId> <version>1.3</version> <configuration> <!-- appassembler自身は依存jarは収集しない --> <generateRepository>false</generateRepository> <!-- assembly側のdependencySetで、"/lib" 以下に依存jarがflat構成で収集されるのに合わせる --> <repositoryName>lib</repositoryName> <repositoryLayout>flat</repositoryLayout> <programs> <program> <mainClass>echoes.act9.server.EchoesServer</mainClass> <name>ea9_server</name> </program> <program> <mainClass>echoes.act9.client.EchoesShutdown</mainClass> <name>ea9_shutdown</name> </program> <program> <mainClass>echoes.act9.client.EchoesClient</mainClass> <name>ea9_client</name> </program> </programs> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>assemble</goal> </goals> </execution> </executions> </plugin> <plugin> <artifactId>maven-assembly-plugin</artifactId> <version>2.3</version> <configuration> <descriptors> <descriptor>src/assemble/bin.xml</descriptor> </descriptors> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin>
src/bin/以下のsh, batは今回は使いませんので、バッサリディレクトリごと削除します。
続いてassemblyプラグインのdescriptorである src/assemble/bin.xml ファイルを以下のように修正します。
src/assemble/bin.xml:
<assembly> <id>bin</id> <formats> <format>tar.gz</format> <format>tar.bz2</format> <format>zip</format> </formats> <fileSets> <!-- iniファイルやLICENSEファイルは従来通りパッケージ直下に配置 --> <fileSet> <directory>${project.basedir}</directory> <outputDirectory>/</outputDirectory> <includes> <include>*.ini</include> <include>LICENSE*</include> </includes> </fileSet> <!-- appassemblerにより生成されたbat/shは"/bin"以下に配置 --> <fileSet> <directory>target/appassembler/bin</directory> <outputDirectory>/bin</outputDirectory> </fileSet> </fileSets> <files> <file> <source>README.txt</source> <outputDirectory>/</outputDirectory> <filtered>true</filtered> </file> </files> <!-- 依存jarは従来通り"/lib"以下に配置 --> <dependencySets> <dependencySet> <outputDirectory>/lib</outputDirectory> <includes> <include>*:jar</include> </includes> </dependencySet> </dependencySets> </assembly>
ビルドしてみます。
$ mvn package $ tar ztf target/echoes-act9-1.0.0-bin.tar.gz echoes-act9-1.0.0/echoes-act9.ini echoes-act9-1.0.0/LICENSE.txt echoes-act9-1.0.0/LICENSE_slf4j.txt echoes-act9-1.0.0/bin/ echoes-act9-1.0.0/bin/ea9_client echoes-act9-1.0.0/bin/ea9_client.bat echoes-act9-1.0.0/bin/ea9_server echoes-act9-1.0.0/bin/ea9_server.bat echoes-act9-1.0.0/bin/ea9_shutdown echoes-act9-1.0.0/bin/ea9_shutdown.bat echoes-act9-1.0.0/lib/mina-core-1.1.7.jar echoes-act9-1.0.0/lib/slf4j-api-1.6.1.jar echoes-act9-1.0.0/lib/mina-integration-jmx-1.1.7.jar echoes-act9-1.0.0/lib/log4j-1.2.16.jar echoes-act9-1.0.0/lib/slf4j-log4j12-1.6.1.jar echoes-act9-1.0.0/lib/echoes-act9-1.0.0.jar echoes-act9-1.0.0/README.txt
実行結果については省略します。
注意点:
One-JARやfat-jar、あるいはmaven-assembly-pluginの"jar-with-dependencies" descriptorでは、依存するjarを1つのjarに統合するアプローチを採用しています。その中でもさらに以下の2つのアプローチがあるようです。
確かに単純に実行の容易さだけを考えるとCoolでSmartに見えなくもないのですが、本来は別個で扱われるべきアーカイブ内容を、無理やり1つに統合してしまうので、デメリットも存在します。例えば個別のjarファイル中のマニフェスト情報はロストしてしまう可能性が高いです。これらの情報を扱うようなアプリでは致命的な問題になります。One-JARが採用する、専用のクラスローダによるjarの入れ子の場合、jar中に埋め込んだ設定ファイルを読み出すようなプログラムではクラスローダ周辺のトラブルが発生する可能性が高くなります。
便利ではありますが、落とし穴も大きい可能性がありますので、採用する場合は事前に入念な下調べが必要になると思われます。時間の都合上、本記事では参考URLを紹介するのみに止めます。
コメント