文系プログラマによるTIPSブログ

文系プログラマ脳の私が開発現場で学んだ事やプログラミングのTIPSをまとめています。

javaのビルドをgradleで行う:実践編6:高速に実用的な実行可能jarを生成する

Gradle実践シリーズその6です〜


f:id:treeapps:20180426142529p:plain

今回はExecutableJar、所謂実行可能jarを生成する方法についてです。
簡単な実行可能jarの作り方としては以下のコードがよくサンプルとして挙がります。

jar {
    from configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    manifest.mainAttributes("Main-Class" : "tree.s2.batch.SampleBatch")
}

これで実行可能jarは作成できました!やったぜ。
・・・
・・・・・
・・・・・・・
全然やってない

全然ダメです。
何がダメかというと、jarの中身がダメなのです。
ちょっとこのjarを解凍してみます。

・・・中略・・・
   creating: META-INF/maven/org.seasar.mayaa/
   creating: META-INF/maven/org.seasar.mayaa/mayaa/
  inflating: META-INF/maven/org.seasar.mayaa/mayaa/pom.properties
  inflating: META-INF/maven/org.seasar.mayaa/mayaa/pom.xml
   creating: META-INF/resource/
  inflating: META-INF/resource/JAXEN-LICENSE.txt
  inflating: META-INF/resource/LICENSE-2.0.htm
  inflating: META-INF/resource/RHINO-NOTICE.txt
  inflating: META-INF/resource/acknowledgment.txt
  inflating: META-INF/resource/mayaa_banner.gif
  inflating: META-INF/resource/release.txt
   creating: org/
   creating: org/seasar/
   creating: org/seasar/mayaa/
  inflating: org/seasar/mayaa/ContextAware.class
  inflating: org/seasar/mayaa/FactoryFactory.class
  inflating: org/seasar/mayaa/ParameterAware.class
  inflating: org/seasar/mayaa/PositionAware.class
  ・・・中略・・・

なんじゃこりゃあ!!

私が想像していたのはこう↓ですが・・・

lib
├── 1.jar
├── 2.jar
└── 3.jar

しかし実際はjarが解凍された状態で入ってるのです。
この一々解凍する処理が非常に重く、時間がかかる原因なのです。

何故わざわざjarを解凍してから再度jarにする必要があるのか・・・
あまり仕様を把握していませんが、原因は zipTree(it) と思われます。

jarが解凍されてしまうことで、resources系が全部表に出てきてしまい、同名のファイル名があった場合、上書きされてしまいます。これは使えませんね。

jarを解凍せず高速に実行可能jarを生成

前述のコードでダメな点は以下であると解りました。

  • classpath上のjarが解凍されてしまう。
  • zipTreeで解凍される処理の負荷が高く時間がかかる。
  • resourcesが上書きされてしまう。

ではこれを解消しましょう。

jar {
    copy {
        from configurations.compile
        into "distribution/lib"
    }
    def manifestClasspath = configurations.compile.collect{ 'lib/' + it.getName() }.join(' ')
    manifest {
        attributes "Main-Class" : "tree.s2.batch.SampleBatch"
        attributes 'Class-Path': manifestClasspath
    }
    from (configurations.compile.resolve().collect { it.isDirectory() ? it : fileTree(it) }) {
        exclude 'META-INF/MANIFEST.MF'
        exclude 'META-INF/*.SF'
        exclude 'META-INF/*.DSA'
        exclude 'META-INF/*.RSA'
        exclude '**/*.jar'
    }
    destinationDir = file("distribution")
    archiveName = 'batch.jar'
}

1個づつ解説します。

copy

最初にコンパイルに必要なjarをコピーしておきます。これでjarが解凍される心配もありません。
注意点として、${buildDir}にコピーすると、cleanが影響するためか、コピーがうまくできません。
従って${buildDir}以外にコピーしています。

manifestClasspath

このままだと実行可能jarがコピーしたjarを認識できません。
なので、MANIFESTの属性である「Class-Path」にjarを列挙します。

ttree-macpro:distribution tree$ cat META-INF/MANIFEST.MF
Manifest-Version: 1.0
Main-Class: tree.s2.batch.SampleBatch
Class-Path: lib/mayaa-1.1.18.jar lib/antlr-2.7.7.jar lib/aopalliance-1
 .0.jar lib/guava-14.0.1.jar lib/commons-beanutils-1.8.3.jar lib/commo
 ns-beanutils-core-1.8.3.jar lib/commons-collections-3.2.1.jar lib/com
 mons-configuration-1.9.jar lib/commons-digester-2.1.jar lib/commons-e
 l-1.0.jar lib/commons-fileupload-1.3.jar lib/commons-io-2.4.jar lib/c
  ・・・略・・・

半角スペース区切りでjarを列挙します。
jdbc.dicon等も含めれば、DB等の接続先も変える事ができそうです。

fileTree(it)

zipTreeだとjarファイルが解凍されてしまうので、fileTreeに変更します。
zipTreeと違って解凍処理が入らないので非常に高速です。

exclude '**/*.jar'

今回はfat.jarではないので、実行可能jar内からjarを除外し、libs/*.jarをMANIFESTのClass-Pathを参照しています。

最終的な構造とjarの中身

構造は以下の通りです。

distribution/
├── batch.jar
└── lib
    ├── activation-1.1.jar
    ├── antlr-2.7.7.jar
・・・略・・・
    ├── xml-apis-1.3.03.jar
    └── zookeeper-3.4.5.jar

batch.jarの中身は以下の通りです。

tree-macpro:distribution tree$ unzip batch.jar
Archive:  batch.jar
   creating: META-INF/
  inflating: META-INF/MANIFEST.MF
   creating: tree/
   creating: tree/s2/
   creating: tree/s2/batch/
  inflating: tree/s2/batch/BaseBatch.class
  inflating: tree/s2/batch/SampleBatch.class
  inflating: app.dicon
  inflating: convention.dicon
  inflating: creator.dicon
  inflating: customizer.dicon
  inflating: logback.xml
  inflating: s2container.dicon

こうして生成した実行可能jarを実行するには以下のようにします。

tree-macpro:distribution tree$ java -jar batch.jar
INFO 2013/06/24 23:07:50:716 [main] (SingletonS2ContainerFactory.log:234) - s2-frameworkのバージョンは2.4.45です。
INFO 2013/06/24 23:07:50:720 [main] (SingletonS2ContainerFactory.log:234) - s2-extensionのバージョンは2.4.45です。
INFO 2013/06/24 23:07:50:721 [main] (SingletonS2ContainerFactory.log:234) - s2-tigerのバージョンは2.4.45です。
INFO 2013/06/24 23:07:51:545 [main] (HttpClientUtil.createClient:103) - Creating new http client, config:maxConnections=128&maxConnectionsPerHost=32&followRedirects=false
INFO 2013/06/24 23:07:51:571 [main] (SingletonS2ContainerFactory.info:137) - Running on [ENV]ut, [DEPLOY MODE]Warm Deploy
WARN 2013/06/24 23:07:51:628 [main] (AopProxy.log:237) - tree.s2.batch.SampleBatchのメソッド(main)にはアスペクトを適用できない修飾子が指定されています
WARN 2013/06/24 23:07:51:629 [main] (AopProxy.log:237) - tree.s2.batch.SampleBatchのメソッド(execute)にはアスペクトを適用できない修飾子が指定されています
INFO 2013/06/24 23:07:51:630 [main] (BaseBatch.execute:94) - [サンプルバッチ] 開始。
INFO 2013/06/24 23:07:51:630 [main] (SampleBatch.run:22) - サンプル
INFO 2013/06/24 23:07:51:630 [main] (BaseBatch.execute:102) - [サンプルバッチ] 正常終了。
INFO 2013/06/24 23:07:51:631 [main] (BaseBatch.execute:115) - [サンプルバッチ] 実行時間=0秒。

この実行可能jarを生成するのは大分苦労しました。
微妙な点も多いかと思いますが、参考にはなるかと思います。

javaのビルドをgradleで行う:調査編1:maven・SBTとの違い - 文系プログラマによるTIPSブログ
javaのビルドをgradleで行う:調査編2:依存から1行で特定jarを除外! - 文系プログラマによるTIPSブログ
javaのビルドをgradleで行う:調査編3:seasar系の依存の注意点 - 文系プログラマによるTIPSブログ
javaのビルドをgradleで行う:調査編4:Groovy Librariesの罠 - 文系プログラマによるTIPSブログ



javaのビルドをgradleで行う:実践編1:jsとcssのcombineとminify - 文系プログラマによるTIPSブログ
javaのビルドをgradleで行う:実践編2:静的ファイルの圧縮 - 文系プログラマによるTIPSブログ
javaのビルドをgradleで行う:実践編3:jspの文字列置換(ReplaceTokens) - 文系プログラマによるTIPSブログ
javaのビルドをgradleで行う:実践編4:あれどうやるの?の疑問と自己解決 - 文系プログラマによるTIPSブログ
javaのビルドをgradleで行う:実践編5:マルチプロジェクト時のprovidedCompileについて - 文系プログラマによるTIPSブログ
javaのビルドをgradleで行う:実践編6:高速に実用的な実行可能jarを生成する - 文系プログラマによるTIPSブログ