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

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

javaのビルドをgradleで行う:実践編4:あれどうやるの?の疑問と自己解決

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


f:id:treeapps:20180426142529p:plain

今回は「antのあのコマンドってどうやんの?」「bashのあのコマンドってどうやんの?」といった「あれどうやるの?」系についてまとめていきます。

部分的にまとめるのではなく、実際のプロジェクト構成に沿った内容にしています。
実践できないサンプルは参考にならない(なりにくい)ので、実際に私が構築中のシステムでやっている事についての話題を取り上げます。

※ まだまだ調査中の身なのでとんちんかんな事をしている可能性があるのでご注意下さい

マルチプロジェクトのタスクの定義場所は?

私が最初に思った疑問です。
マルチプロジェクトの場合、subprojectsを定義します。ではどこにタスクを定義するんでしょう。
subprojects内?allprojects内?グローバル?

解答

基本的にsubprojects内にタスクを書きます。
理由はスコープです。

マルチプロジェクトの場合、基本的にhoge-parent等といったビルド専用プロジェクトを作るかと思います。
allprojects・又はグローバルにタスクを定義してしまうと不要なプロジェクトにまでタスクが追加されるからです。
subprojects内であれば、親プロジェクト等を除外する事ができます。

ビルドする前のscmのcheckoutはどうやるの?

antの時はbashでsvn export -> ant -f build.xmlという手順でビルドしてました。
ではgradleではどうでしょうか?今だとjenkinsを使う場合が多いので、jenkinを使った場合のフローはどうでしょう。

解答

jenkinsでscm checkout(svnの場合はsvn update)、gradleプラグインをインストールしておき、gradleのコマンドを実行、というフローになります。bashでcheckoutしてもいいですが、jenkinsで簡単に実現できます。

Document Baseがsrc/main/webapp以外の場合は?

通常tomcatのDocument Baseは src/main/webapp かと思います。
しかし階層が深くなるので、 WebContent としている方もいるでしょう。(私です)
ではどうやってやるんでしょうか。

解答

ずばり以下です。

subprojects {
    webAppDirName = 'WebContent'
}

不要jarを一括excludeしたい

mavenだと1jarにつき複数excludeする非生産的な作業が必要ですね。
gradleはそんな悩みが解消できます。

解答

実は過去の記事でまとめているのでこちらを参照して下さい。
javaのビルドをgradleで行う:調査編2:依存から1行で特定jarを除外! - 文系プログラマによるTIPSブログ

特定プロジェクトにだけ適用したいタスクがある

マルチプロジェクトの場合、warにしたくないプロジェクトってありますね。
例えばビルド用プロジェクト、共通の親プロジェクト、などです。
これらを除外してwarタスクを実行できないでしょうか。

解答

onlyIfを使いましょう

subprojects {
    def warProjects = [
        'hoge-web',
        'hoge-search'
    ]
    war.onlyIf { warProjects.contains(project.name)}
    war {
        // jspのtoken置換
        def now = new Date().getTime().toString()
        from(webAppDir) {
            include '**/*.jsp'
            filter(org.apache.tools.ant.filters.ReplaceTokens, tokens:['FILE_TIMESTAMP': now])
        }
    }
}

↑この例は過去記事で話題にしたjsp内のトークン置換をしています。
javaのビルドをgradleで行う:実践編3:jspの文字列置換(ReplaceTokens) - 文系プログラマによるTIPSブログ

htdocsやsolrのconfをzip圧縮したい

htdocs、solrのconf、apacheのconf、tomcatのconf、などなど、warではなく部分的にzip圧縮したい場合ってありますよね。
どうやってやるんでしょうか。

解答

Zipタスクを使います。

subprojects {
    task zipHtdocs(type: Zip) {
        if (project.name == 'hoge-web') {
            from 'WebContent'
            exclude 'WEB-INF'
            baseName = 'htdocs'
            archiveName = 'htdocs.zip'
        }
    }

    task zipSolr(type: Zip) {
        if (project.name == 'hoge-search') {
            from 'solr.home'
            exclude '**/data/'
            baseName = 'search'
            archiveName = 'search.zip'
        }
    }

このコードにはちょっと注意点があります。excludeの部分ですが、末尾にスラッシュが無いとフォルダと認識されないようです。
solrにはおなじみの「data」フォルダがありますが、zip内からdataを除外します。
このとき、「exclude '**/data'」だとdataは削除されず、「exclude '**/data/'」だと削除されました。

ちょっと腑に落ちないのは、zipHtdocsの「exclude 'WEB-INF」は除外されるのに、zipSolrの「exclude '**/data'」は除外できないんです。
違いはroot直下に対象フォルダがあるかどうかです。

環境毎に設定ファイルを上書きしたい

ビルドする環境によって設定ファイルを上書きコピーする、こういう要望は必ずといっていいほどあります。どうやりましょうか。

解答

例えば以下の構成で上書きコピー用のフォルダ・ファイルを用意しておきます。
(必ずしも上書きでなくてもいいのです)

hoge-web/
└── environment
    ├── common
    │   └── logback.xml
    ├── develop
    │   └── system.properties
    ├── product
    │   └── system.properties
    └── staging
        └── system.properties

そしてコマンドラインパラメータでビルドする環境名を以下のように渡します。

gradle -daemon clean war -Penv=develop

こうするとbuild.gradle内でenvを変数として使用できます。
これを利用して以下のようにファイルを上書きコピーします。

    task overrideResources(dependsOn: processResources) {
        if (hasProperty('env')) {
            // commonをコピー後に環境毎のファイルで上書き
            copy{
                from "environment/common/"
                into "src/main/resources"
            }
            copy{
                from "environment/${env}/"
                into "src/main/resources"
            }
        }
    }

まず共通の設定ファイルであるenvironment/common/をsrc/main/resourcesにコピーし、続いて環境毎の設定ファイルであるenvironment/${env}/をsrc/main/resourcesにコピーしています。
processResourcesはjavaのプロパティファイルをコピーする組み込みタスク?です。
dependsOn: processResourcesとする事で、設定ファイルを編集する時はoverrideResourcesを実行する、としています。

複数タスクを一括実行するタスクが欲しい

以下のように個別にタスクを指定しても問題ありませんが、タスクが増えた時等のメンテが不安です。

gradle -daemon zipHtdocs zipSolr

これ、なんとかなりませんか?

解答

これは恐らく正規の書き方ではないかもしれませんが、一応動いているので晒してみます。

    task unitTasks << {
        tasks.zipBatch.execute()
        tasks.zipHtdocs.execute()
        tasks.zipSolr.execute()
    }

こうすると、以下のように実行できます。

gradle -daemon clean unitTasks

しかしこの書き方だとタスク間の依存を考慮して実行してくれないです。
dependsOnを考慮して実行してくれないのです。なので、単体実行で問題無いものを指定しましょう。

ちゃんと調べてませんが、こういう場合は本当は以下を使うのかと思います。

task distribution << {
println "We build the zip with version=$version"
}

task release(dependsOn: 'distribution') << {
println 'We release now'
}

gradle.taskGraph.whenReady {taskGraph ->
if (taskGraph.hasTask(release)) {
version = '1.0'
} else {
version = '1.0-SNAPSHOT'
}
}

http://gradle.monochromeroad.com/docs/userguide/tutorial_using_tasks.html

成果物をアップロード(scp)したい

ビルドし終わったものを各サーバにアップロードする事は必須ですね。
リファレンスを見るとuploadarchivesでapache-maven-wagonのpluginを使うしかないっぽい・・・?

解答

解りません
すみません未解決です。
wagonを使って開発機のリポジトリにアップするところまでは動作確認できています。
しかし、以下が解決できませんでした。

  • wagonがどのフォルダを対象にパッケージングしているのか解らない
  • htdocs.zipなど、war以外を対象にする際にファイル名が指定できない

他にも沢山未解決次項があったと思いますが、覚えてません。
ちなみに以下のように指定すると、個別タスクをuploadArchivesに組み込めます。
この場合のファイル名指定ができなくて困っていました。

artifacts {
    archives zipHtdocs
}

uploadArchivesと格闘すること1時間、ふと疑問に思った事があります。それは・・

mavenが嫌だからgradle使ってるのになんでmavenで悩んでるんだろう

という点です。
いつの間にかスーパー黒魔術的ツールであるmavenで悩んでいたのです。
そしてすぐにその疑問に対する解がでました。

wagon使わなきゃ万事OKじゃね?

あら簡単。解決しました。wagonを使うのをやめました。
gradleはビルドまでしか行わず、各サーバへのアップロードはbashで対話式scpを突破すればいいじゃない。
mavenで悩んでいた自分は実にアホでした。

mavenを極めた黒魔術師の方々が言っているのを見たのですが、
全部maven(gradle)で完結しようとするからドツボにはまる
この言葉をちゃんと意識しておかないと、本当にドツボにハマりますね。

実は一応解決策?と思われる案があったりします。
uploadArchivesがうまく制御できないなら、一旦全部wagonでwarのみをscpし、デプロイスクリプト実行時にwar解凍して適切な場所にmvしたりシンボリックリンクをはれば?
と思いましたが、もう完全にmavenの呪縛にハマっている気がしているのでやらないつもりです。

未解決次項

最後にまだ未解決の事項をまとめます。

providedCompileの謎

hoge-common
    └ hoge-web
こういったプロジェクト構成で、commonとweb、それぞれでservlet-apiに依存している場合、commonのdependencesにtomcatのservlt-apiをprovidedCompileで依存させています。
この場合、hoge-webをwarにするとprovidedCompileが有効になり、hoge-webからservlet-api.jarが除外されると思っていました。

しかしhoge-web内にservlet-api.jarが残っていました
providedCompileは一体どうなったのか、本来どうやって依存させるべきなのか、調査中です。

servlet-api.jarが含まれたままwarを解凍すると、tomcatに内蔵されているtomcat-servlet-apiと強豪し、tomcatが動作しないので、必ず依存から除外しないといけません。一体どうすればいいのか・・・・

一応解決できました!
javaのビルドをgradleで行う:実践編5:マルチプロジェクト時のprovidedCompileについて - 文系プログラマによるTIPSブログ

実行可能jarの謎

ネットの情報を参考に、以下のように実行可能jarは簡単に作れます。

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

しかし、なんかこのjarの中身がおかしいのです。
jarを解凍してみると、WEB-INF/lib内のjarが全部解凍されているのです。
Executable jarの仕様なんでしょうか。なぜわざわざ解凍する必要があるのか。

この解凍処理のせいで信じられない程jarタスクに時間がかかります。
hoge-batch.jarを作成し、依存jarは外出しにしてclasspathをbashで通す、という事を現在調査中です。

しょうもない罠

最後に本当にしょうもない罠にはまったので紹介します。
javadocタスク実行後は要注意です。

task javadocAll(type: Javadoc) << {
    source subprojects*.sourceSets.main.allJava
    destinationDir = new File(buildDir, 'javadoc')
    classpath = files(subprojects*.sourceSets.main.compileClasspath)
    options.encoding = defaultEncoding
}

javadocタスクはこんな感じで定義し、war等のタスクとは完全に切り離しておきます。
時間がかかる処理なので、jenkinsで単独jobとした方がいいでしょう。
で、何が問題かというと、javadocはファイル数が多く、eclipseのValidation処理が異常に時間がかかるのです。

eclipseでお馴染みのうざいvalidation処理です。
こいつがhtmlに反応して、大量のvalidationをしようとして一気にCPUとメモリを持っていかれます。。。
javadocAllした後はeclipse側でF5してリフレッシュしない事をおすすめします。


今回はここまでです。
今は未解決事項の解決のため、毎日調査と実践を繰り返しています。
解決したら随時記事に起こしていこうと思います。

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ブログ