#navi_header|Java| 実行可能なJavaプログラムをjarファイルで配布する場合、その形態として次の2方式をよく見かけます。 - "-jar ほげほげ.jar"で実行可能な単一jarファイルでの配布 : GUIのJavaアプリで時々見かけます。OSによってはjarファイルをダブルクリックするだけで実行できるので非常にお手軽です。 - 依存jarと、それらをclasspathに自動的に追加して実行してくれる起動スクリプト(.bat, .sh)がzipやtar.gzで一緒に配布される : 依存jarが存在する、Tomcatなどサーバ系の配布でよく目にします。 本記事ではこれらの配布と実行方式の実現方法について調べてみました。 - サンプルコード: -- https://github.com/msakamoto-sf/jar-deployment-examples - JDK 1.7(64bit), Maven 3.0.4, Win7SP1(日本語版)64bit, MacOSX(10.7)64bit にてコンパイル・動作確認しています。 #more|| ---- #outline|| ---- * step1 : MANIFEST.MFを使って"-jar"で実行可能なjarファイルを作成する(依存jar無しの超シンプル版) まずは一番シンプルな形態として、依存jarが無い状態で"-jar"で実行可能なjarファイルを作成してみます。 ** シンプルなJavaプロジェクト(.javaを手動でコンパイル、jarファイルも手動で作成) Hello.java: #pre||> public class Hello { public static void main(String[] args) { System.out.println("Hello"); } } ||< コンパイル&実行: $ javac Hello.java $ java Hello Hello 単純にjarファイルを作成して実行してみます: #pre||> $ 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"属性を指定してみる MavenプロジェクトでMain-Class属性を指定してみます。 quickstartを使ってMavenプロジェクトを生成: #pre||> $ 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の"" -> "" -> ""に指定します。 pom.xml抜粋: #pre||> ... org.apache.maven.plugins maven-jar-plugin step1.App ... ||< ビルド&実行: $ mvn package $ java -jar target/step1-mf-mvn-1.0-SNAPSHOT.jar Hello World! 実際のMANIFEST.MFファイルの中身: #pre||> $ 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 ||< * step2 : MANIFEST.MFを使って"-jar"で実行可能なjarファイルを作成する(依存jar有り版) step1では依存jarが無い、一番シンプルな形で確認しました。step2では、依存jarが存在する、より現実的な場合での対応方法を試してみます。 ** マニフェスト・ファイルののClass-Path属性の基本 マニフェスト・ファイルでは、"Class-Path"属性を使ってjarファイルの配置場所からの相対パスでclasspathに通したいjarを指定することが可能です。 サンプル: #pre||> 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"に依存しています。 #code|java|> 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属性の指定と依存jarの集約 Mavenでもマニフェスト・ファイルのClass-Path属性を設定できます。"mainClass"を設定した時と同様、Maven Archiverの提供する"addClasspath"設定を使うことで、Maven側で依存しているjarを自動的にClass-Path属性にまとめてくれます。 step2-mvn1/pom.xml: #pre||> ... commons-logging commons-logging 1.1.1 commons-lang commons-lang 2.6 junit junit 3.8.1 test org.apache.maven.plugins maven-jar-plugin step2.App true lib ||< src/main/java/step2/App.java: #code|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! うまく動作してくれました。 ** maven-assembly-pluginを使った依存jarのパッケージング 上で紹介した "dependency:copy-dependencies" はあくまでもmvnコマンドが実行できる開発環境に限定されます。実際に配布する場合はzipやtar.gzなどにパッケージングすることになり、Mavenの場合ではmaven-assembly-pluginを使って依存jarのパッケージングを自動化できます。 step2-mvn1のMavenプロジェクトを "step2-mvn2" にコピーして、pom.xmlを以下のようにします。 step2-mvn2/pom.xml: #pre||> 4.0.0 step2-mvn2 step2-mvn2 jar 1.0-SNAPSHOT step2-mvn2 http://maven.apache.org commons-logging commons-logging 1.1.1 commons-lang commons-lang 2.6 junit junit 3.8.1 test org.apache.maven.plugins maven-jar-plugin step2.App true maven-assembly-plugin 2.3 src/assemble/bin.xml make-assembly package single ||< maven-assembly-pluginは配布用のパッケージの"assemble"に利用されます。設定が複雑になりがちなこともあり、詳細な設定を外部の"descriptor" XMLファイルに記載し、pom.xmlではdescriptor XMLファイルの場所だけを指定しています。 step2-mvn2/src/assemble/bin.xml: #pre||> bin tar.gz tar.bz2 zip / *:jar ||< $ 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 実行してみます。 #pre||> $ 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も含めて配布できるようになりました。 * コーヒーブレイク:step1-2までの参考資料 - JAR ファイルの仕様 -- http://docs.oracle.com/javase/jp/6/technotes/guides/jar/jar.html - Jarファイルメモ(Hishidama's java-archive Memo) -- http://www.ne.jp/asahi/hishidama/home/tech/java/jar.html - [Maven2][Java]Maven2で作ったjarプロジェクトを簡単に実行する - SOSOG -- http://d.hatena.ne.jp/yosisa/20110106 - Maven Archiver -- http://maven.apache.org/shared/maven-archiver/ --- maven-jar-pluginなど、jarにアーカイブするときに使われているコンポーネントで、マニフェスト・ファイルの設定方法などはこちらにまとめられています。 - Set Up The Classpath -- http://maven.apache.org/shared/maven-archiver/examples/classpath.html --- maven-jar-pluginを使ってClass-Path属性を設定する基本的なサンプルが紹介されています。 - Maven JAR Plugin -- http://maven.apache.org/plugins/maven-jar-plugin/ - 今まで知らなかった 5 つの事項: JAR -- http://www.ibm.com/developerworks/jp/java/library/j-5things6.html - JAR files revealed -- http://www.ibm.com/developerworks/library/j-jar/index.html - MANIFEST.MFを使って、jarファイルをダブルクリックで実行 -- http://www.searchman.info/tips/2130.html - Java Tip 127: See JAR run - JavaWorld -- http://www.javaworld.com/javaworld/javatips/jw-javatip127.html - MANIFEST.MFとjarファイル - 役立たずのプログラマーブログ -- http://blog.goo.ne.jp/linux_ekyu/e/d9fa610a5a513fe80491f2ecb99cc129 - jarファイルの作り方(ytp.ne.jp) -- http://www.ytp.ne.jp/tech/java/sineruka/jarhowto.html - maven-assembly-plugin で実行可能な jar ファイルを作る - Think Different - はてな版 -- http://d.hatena.ne.jp/nanasess/20090330/1238422438 * step3 : batやshでjarファイルのリストを取り出しclasspathを調整する ここまでは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することで以下のように配置されます。 #pre||> echoes-act9-1.0.0/ lib/*.jar ea9_*.bat ea9_*.sh echoes-act9.ini 他、ライセンスやNTサービス登録用のbatファイルなど ||< ** Windowsのbatでclasspathを調整する Windowsでclasspathを調整しているのは ea9_env.bat と ea9_addclp.bat の2つになります。 ea9_env.bat: #pre||> 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のsh(bash)でclasspathを調整する 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を調整するサンプルの紹介でした。 * step4 : Maven Application Assemblerを使う 参考: - Mavenで配布用zipファイルを作成する - Sacrificed & Exploited -- http://d.hatena.ne.jp/cnaos/20100102/1262430319 - Maven Application Assembler - Mojo Appassembler -- http://mojo.codehaus.org/appassembler/ 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を以下のように修正します。 - ポイント1 : appassemblerはあくまでもシェルスクリプトおよび依存jarの収集までを担当するだけです。実際のzip/tarボールへのパッケージングには引き続きmaven-assembly-pluginが必要です。 - ポイント2 : appassemblerのデフォルトでは、Mavenリポジトリのディレクトリ構成で依存jarが収集されてしまい、それに合わせたclasspathが、appassemblerの生成したbat/sh中にハードコードされます。それだとCLASSPATH環境変数の文字数が、サブディレクトリに区切られる影響でやたらと長くなってしまいますので、今回はjarの収集はassembly側のdependencySetで"/lib"以下にflatに収集させ、appassembler側では"repositoryLayout"と"repositoryName"を"flat"と"lib"にして、assembly側で収集した"/lib"以下に合わせました。同時に、jar収集はassemblyのdependencySetで実施するため、appassembler側は"generateRepository"にfalseを指定してjar収集をしないようにします。 #pre||> org.codehaus.mojo appassembler-maven-plugin 1.3 false lib flat echoes.act9.server.EchoesServer ea9_server echoes.act9.client.EchoesShutdown ea9_shutdown echoes.act9.client.EchoesClient ea9_client package assemble maven-assembly-plugin 2.3 src/assemble/bin.xml make-assembly package single ||< src/bin/以下のsh, batは今回は使いませんので、バッサリディレクトリごと削除します。 続いてassemblyプラグインのdescriptorである src/assemble/bin.xml ファイルを以下のように修正します。 src/assemble/bin.xml: #pre||> bin tar.gz tar.bz2 zip ${project.basedir} / *.ini LICENSE* target/appassembler/bin /bin README.txt / true /lib *:jar ||< ビルドしてみます。 #pre||> $ 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 ||< 実行結果については省略します。 注意点: - unix用shellscriptについては実行権限がつかないため、展開後に手動でchmodするか"sh bin/xxxx"とする必要があります。もしかしたら設定で修正できるかも? - step3と異なり、echoes-act9.iniをコマンドラインから手動で指定する必要があります。このあたりは、javaコマンドに引数を設定することが可能ですので、設定次第で省略出来るかもしれません。 * stepX(おまけ) : 複数のjarを1つのjarに統合する各種技術 One-JARやfat-jar、あるいはmaven-assembly-pluginの"jar-with-dependencies" descriptorでは、依存するjarを1つのjarに統合するアプローチを採用しています。その中でもさらに以下の2つのアプローチがあるようです。 + One-JARが採用している、jarの中にjarを入れ子にして、専用のクラスローダを埋め込む。 + 単純に依存jarを全部展開して、1つのjarにアーカイブし直す。(maven-assembly-pluginの"jar-with-dependencies" descriptorがこのアプローチ・・・っぽい。多分。) 確かに単純に実行の容易さだけを考えるとCoolでSmartに見えなくもないのですが、本来は別個で扱われるべきアーカイブ内容を、無理やり1つに統合してしまうので、デメリットも存在します。例えば個別のjarファイル中のマニフェスト情報はロストしてしまう可能性が高いです。これらの情報を扱うようなアプリでは致命的な問題になります。One-JARが採用する、専用のクラスローダによるjarの入れ子の場合、jar中に埋め込んだ設定ファイルを読み出すようなプログラムではクラスローダ周辺のトラブルが発生する可能性が高くなります。 便利ではありますが、落とし穴も大きい可能性がありますので、採用する場合は事前に入念な下調べが必要になると思われます。時間の都合上、本記事では参考URLを紹介するのみに止めます。 - One-JARでアプリケーションの配布を単純化 -- http://www.ibm.com/developerworks/jp/java/library/j-onejar/ - Deliver Your Java Application in One-JAR™ ! -- http://one-jar.sourceforge.net/ - Maven Fat Jar - @//メモ -- http://hondou.homedns.org/pukiwiki/index.php?Maven%20Fat%20Jar - Leo's Chronicle: JARファイルの中にJARを埋め込む -- http://leoclock.blogspot.jp/2008/08/jarjar.html - Leo's Chronicle: Fat Jar: Jarファイルの中にJarを埋め込む -- http://leoclock.blogspot.jp/2007/02/fat-jar-jarjar.html - jarを一本化するonejar-maven-pluginを使ってみた - Sacrificed & Exploited -- http://d.hatena.ne.jp/cnaos/20100509/1273399163 #navi_footer|Java|