猫好きモバイルアプリケーション開発者記録

Gradleで陥りやすい問題点の解決策TIPS集

| Comments

今回はGradleでよくハマるであろうポイントを集めたTIPSを11個紹介します。

01. 依存関係のバージョンが指定されたものにならない

MavenからGradleへ移行した場合、おそらく誰もが最初に陥る問題かと思います。 端的に言うと、Mavenの pom.xml で指定したままの依存関係の設定をそのまま build.gradle へ移したとしても 最終的に取得される依存関係は殆どのケースで同じにはなりません。 これは「推移的依存関係」によって同じライブラリが存在した場合に優先されるバージョンがMavenとGradleでは異なるために起こります。

推移的依存関係とは簡単に説明すると、ある依存関係がさらに依存する関係のことを言います。 MavenでもGradleでもそれらを自動的に取得しようとしますが、 それらの中で使っている依存関係のgroupIdもartifactIdも同じだがバージョンが異なる場合、 Javaでは同じライブラリの異なるバージョンを同じアプリ内へ共存させることが出来ませんから、 どちらかを優先させる必要が出てきます。

Mavenの場合は推移的依存関係における階層が高い方が優先されます。 しかしGradleでは、バージョンが一番高いものが優先されます。 そのため、Gradleでは利用しない依存関係をひたすら exclude 指定で除外していく必要が出てきます。

MavenからGradleへ移行する場合に依存関係のバージョンを含めて全く同じにしたい場合は、 以下のように現在の依存関係の階層構造を出力してそれを比較して合わせていけばよいです。

<Mavenで依存関係階層構造を出力>

1
2
# 対象となるプロジェクトの pom.xml のあるディレクトリへ移動して実行
mvn dependency:tree

<Gradleで依存関係階層構造を出力>

1
2
# baseプロジェクトの依存関係階層構造を出力する
gradle :base:dependencies

とはいえ、これがとにかく面倒です。 例えば自身で dependencies ブロックへ指定した依存関係が他の依存関係の推移的依存関係になっている場合、 exclude指定を行っても一見除去されないように見えたり(or 指定したバージョンにならなかったり)と、結構ハマります。 根気よく exclude 指定で不要なバージョンを除去して合わせていきましょう。

02. EclipseのWTPで起動した場合に他のプロジェクトを参照しているとその参照しているプロジェクトの依存関係を読み込まない

これも人によってはハマるポイントかと思います。 Eclipse上ではコンパイルエラーが出ていないのに、WTPでTomcatを起動すると参照しているプロジェクトの依存関係が無いと言われてしまう(ClassNotFoundExceptionが発生してしまう)現象です。 これは eclipse-wtp のプラグインの読み込み指定を行っていない場合に起こります。 どういうことなのかと言いますと、この例が起こるのは以下のように build.gradle を記述したときに起こります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/**
 * Gradleによるプロジェクトビルドスクリプト。
 *
 * @author Kou
 */
apply plugin: 'eclipse'
apply plugin: 'idea'

/**
 * ベースロジック
 */
project(':base') {

    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'idea'

    dependencies {

        // Struts
        compile(
                'taglibs:standard:1.1.2',
                'org.apache.struts:struts-core:1.3.10',
                'org.apache.struts:struts-tiles:1.3.10',
                'org.apache.struts:struts-taglib:1.3.10',
                'org.apache.struts:struts-el:1.3.10',
                'org.apache.struts:struts-extras:1.3.10',
        )

        // Solrj
        compile(
                'org.slf4j:slf4j-api:1.6.1',
                'org.slf4j:slf4j-log4j12:1.6.1',
                'org.apache.solr:solr-solrj:4.4.0',
        )

        // Solr Core
        compile('org.apache.solr:solr-core:4.4.0') {
            exclude group: 'org.slf4j', module: 'slf4j-jdk14'
        }

        // Test関係
        testCompile(
                'junit:junit:4.1.1',
        )

    }

}


/**
 * 管理サイト
 */
project(':admin') {

    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'idea'
    apply plugin: 'eclipse-wtp'
    apply plugin: 'war'

    // WARファイル名
    archivesBaseName = 'admin'

    dependencies {

        compile project(':base')

    }

    eclipse {

        // Dynamic Web Projectの設定
        wtp {

            component {

                // EclipseのWTPで起動する場合のコンテキストパス
                contextPath = '/admin'

            }

            facet {

                facet name: 'java', version: getProperty('java.version')
                facet name: 'jst.web', version: getProperty('java.servlet.version')

            }

        }

    }

}

上記の例では、base プロジェクトに eclipse-wtp の指定がないことがわかります。 EclipseのWTPで起動するのは WAR プロジェクトのみです。 そのため、base プロジェクトのような通常のJavaプロジェクトは eclipse-wtp とは関係ないだろうと思いがちなのですが、 ここが落とし穴で、依存関係となるプロジェクトも含めて eclipse-wtp のプラグイン指定を行わないと、 たとえEclipse上でコンパイルエラーが出ていなくてもWTP起動時に読み込みの対象とならないのです。 なので、EclipseのWTPで起動させる場合は必ず WAR プロジェクト以外のプロジェクトにも eclipse-wtp のプラグインを読み込むよう指定してください。 (subprojects のブロックで指定すれば各プロジェクトごとにいちいち指定する必要がなくなるので記述量が減ります)

03. プロパティの参照が出来ない場合がある

主にGradleで利用するプロパティは gradle.properties に記述する方法や、 コマンド実行時に -P オプションで指定するのが一般的です。 そのプロパティをスクリプト内で参照する場合、以下の 2 つの参照方法があります。

1. プロパティ名の先頭に $ をつけてダブルクォーテーションで囲む
2. getPropertyメソッドにプロパティ名を指定して取得する

記述が短いので $ をつけて参照する方法を選んでしまいがちですが、 以下のようにドットやハイフンを含むプロパティ名の場合は参照できません。

× java.version=1.6
× java-version=1.6
○ java_version=1.6

これは Bash をはじめとしたシェルスクリプトの変数と同じ仕様です。 Javaのpropertiesファイルは慣例的にドット区切りでプロパティ名を記述する傾向があります。 なので少なからずこの問題に陥る方もいらっしゃるかと思います。

じゃあ、ドットやハイフンを使った名前は利用できないのか?というと、そういうわけではありません。 上記の 2 の方法である getPropertyメソッドを利用すればドットやハイフンを含んだプロパティ名を参照できます。

そのため、私が普段スクリプトを記述する場合は、 コマンド実行時に -P オプションで指定したプロパティは 1 の方法で参照するようにし、 gradle.properties で指定されたプロパティは 2 の方法で参照するように区別しています。 その方がスクリプトを見たときにどこから指定されたプロパティを参照しようとしているのかが判断しやすいです。

04. hasPropertyメソッドが常に false を返してしまう

これもGradle初心者は陥りやすいかと思います。 -P オプションでプロパティを指定している場合で、 かつそのプロパティが必須ではない場合、プロパティの有無をスクリプト内で判断しないとエラーになってしまいます。 そこで、以下のように書いてみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def     test

// 使用するプロファイル名が指定されている場合
if (hasProperty('propertyTest')) {

    // 使用するプロファイル名を設定する
    test = "$propertyTest"

} else {

    // デフォルトのプロファイル名を設定する
    test = 'defaultTest'

}

この場合、常に hasProperty メソッドは失敗してしまいます。 hasPropertyメソッドを何も指定せずに呼び出すと TaskのhasPropertyメソッドがコールされてしまい、 実際にプロジェクトに設定されたプロパティを判断できなくなる場合があります。 そこで、以下のように指定すると正常に判定することが出来ます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def     test

// 使用するプロファイル名が指定されている場合
if (project.hasProperty('propertyTest')) {

    // 使用するプロファイル名を設定する
    test = "$propertyTest"

} else {

    // デフォルトのプロファイル名を設定する
    test = 'defaultTest'

}

上記のように「project.hasProperty」をコールすることで正常にプロパティ有無を判定することが出来るようになります。

05. 環境ごとにリソースファイルを切り替えたい

Mavenでは使用するリソースファイルを複数指定することが可能で、 更にはビルド時にプロファイルを指定することでそれを環境ごとに切り替えることが出来ます。 これをGradleで実現するには様々な方法がありますが、ここでは私が普段利用しているスクリプト例を紹介します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
 * Gradleによるプロジェクトビルドスクリプト。
 *
 * @author Kou
 */
apply plugin: 'eclipse'
apply plugin: 'idea'


/**
 * サブプロジェクト全体の共通設定。
 *
 */
subprojects {

    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'eclipse-wtp'
    apply plugin: 'idea'

    sourceSets {

        def     useProfileName     // 指定環境名称

        // 使用するプロファイル名が指定されている場合
        if (project.hasProperty('profileName')) {

            // 使用するプロファイル名を設定する
            useProfileName = "$profileName"

        } else {

            // デフォルトのプロファイル名を設定する
            useProfileName = 'local'

        }


        // メインリソースの指定
        main.resources {

            srcDirs 'src/main/resources'
            srcDirs "src/profile.$useProfileName/resources"

        }

    }

    eclipse {

        classpath.defaultOutputDir = file('/target/classes')                // クラスファイル出力先

    }

}

上記の「sourceSets」のブロック内がリソースを環境ごとに切り替える処理になります。 この build.gradle のプロジェクト設定は前回も紹介したとおりIDEでも利用される設定となります。 そのため、ここでは環境指定が無い場合はローカル環境の設定であるものと見なしています。

では、他の環境指定を行うにはいつ行えばいいのかというと、これはビルド時に指定します。 WARやJARのビルドを行うときに -P オプションでプロパティを与えて読み込む環境を指定します。

1
gradle --daemon -PprofileName=development war

こうすることで、「src/main/resources」「src/profile.development/resources」ディレクトリにあるリソースファイルが WARに追加されるようになります。

06. testCompileで依存関係となるプロジェクトを指定した場合にそのプロジェクトの testCompile を参照してくれない

これはどうやら仕様(?)のようで、 compileの場合は「compile project(‘:base’)」といったようにプロジェクトを指定するとその指定したプロジェクトの依存関係を継承しますが、 testCompileの場合は「testCompile project(‘:base’)」と指定しても、どういうわけか継承しないようです。 この問題に対するスマートな解決方法というのは現時点ではあまりないのですが、 強いてあげるとすれば、以下のように各プロジェクトごとに testCompile の依存関係を指定してあげることで ある程度管理しやすくなります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
 * Gradleによるプロジェクトビルドスクリプト。
 *
 * @author Kou
 */
apply plugin: 'eclipse'
apply plugin: 'idea'

/**
 * 依存関係を定義する
 */
ext {

    libs = {

        // テストで利用する依存関係
        testCompile: [
            'junit:junit:4.4',
            'org.easymock:easymock:3.2',
            'org.hamcrest:hamcrest-all:1.3',
        ],

    }

}


/**
 * ベースロジック
 */
project(':base') {

    dependencies {

        // Test関係
        testCompile libs.testCompile

    }

}


/**
 * 管理サイト
 */
project(':admin') {

    apply plugin: 'war'

    // WARファイル名
    archivesBaseName = 'admin'

    dependencies {

        compile project(':base')

        // Test関係
        testCompile libs.testCompile

    }

}

このように、拡張プロパティでテストの依存関係をまとめた変数を宣言し、 そこに利用する依存関係を全て記述すれば比較的管理のしやすい記述ができます。 これについてはもっとスマートな記述方法が今後登場しましたらそのときに再度紹介させていただきます。

07. EclipseのWTPを利用している場合に provided や providedCompile の指定が効かない

これは以前紹介させて頂いたEclipseのGradleプラグインである「Gradle Integration for Eclipse」のプラグインの仕様のようで、 このプラグインは自前で作成した provided の指定やWARプラグインを指定した場合のみ利用できる providedCompile の指定をしたとしても、 WTPでAPサーバを起動したときに WEB-INF/lib のディレクトリへ強制的に全ての依存関係ライブラリがコピーされてしまいます。要は provided であることを考慮してくれません。 この問題に対する公式な解決方法としては、Eclipseの設定から「Gradle」→「WTP」を選択し、そこに除去するプラグインを記述すれば WEB-INF/lib ディレクトリへコピーされなくなる、とのことです。 せっかくGradleを利用しているのに手動で設定するという点からもなんとも腑に落ちないですが…現状このプラグインを利用している以上はこうするしかないようです。

もう1つの解決策としては、「Gradle Integration for Eclipse」プラグインでインポートせず、 「gradle eclipse」コマンドを自前で実行してEclipseのプロジェクト設定ファイルを作成し、 作成後のプロジェクトをインポートするという方法です。 この方法であれば provided も考慮してプロジェクト設定を作成してくれます。 ただし、手動でコマンドを実行しなければならないという点がなんともスマートではないことと、 build.gradle を変更した場合(依存関係などを追加した場合)に再度コマンドを実行し直さなければなりません。 「Gradle Integration for Eclipse」プラグインを利用している場合はこのあたりを自動でやってくれます。

provided で除去するのは殆どの場合 servlet-api や jsp-api ですので、 腑に落ちない気持ちはありつつも、「Gradle Integration for Eclipse」プラグインを使用し、 Eclipseの設定で除去してしまう方法で利用していく方が無難かと思われます。

08. 複数のMavenリポジトリを定義する

参照するMavenリポジトリは repositories ブロック内で以下のように定義します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * サブプロジェクト全体の共通設定。
 *
 */
subprojects {

    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'eclipse-wtp'
    apply plugin: 'idea'

    repositories {

        maven {

            url getProperty('project.maven.repository.url.01')

        }
        mavenCentral()

    }

}

任意のリポジトリを複数定義する場合は、mavenブロックに url パラメータを複数記述するかと思いがちですが、 そのような記述は出来ません。 以下のように記述します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
 * サブプロジェクト全体の共通設定。
 *
 */
subprojects {

    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'eclipse-wtp'
    apply plugin: 'idea'

    repositories {

        maven {

            url getProperty('project.maven.repository.url.01')

        }
        maven {

            url getProperty('project.maven.repository.url.02')

        }
        mavenCentral()

    }

}

上記のように、mavenブロックを複数宣言することでURLを複数指定できます。

09. アーカイブタスクで exclude 指定が効かない

これも人によってはAntやMavenのときの記述に慣れてしまっているが故に勘違いしてしまいがちなのですが、 Zipなどのアーカイブタスクで exclude 指定をした場合に効かないように見えてしまう場合が有ります。

例として、前回紹介したbatchのパッケージを作成するタスクにおいて一部のファイルをZIPファイルへ含めないようにしたいと思います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * バッチ
 */
project(':batch') {

    /**
     * リリース用パッケージの作成を行う。
     *
     */
    task distribution(dependsOn: 'jar', type: Zip) {

        into ('batch/bin') {

            from 'bin'
            exclude 'bin/test.sh'   // test.sh を除去する指定

        }


        into ('batch/libs') {

            from 'build/libs/batch.jar'
            from configurations.compile

        }


    }

}

上記では、binディレクトリの中身にある test.sh のコピーを除去する想定で記述しました。 しかし、実際にこの状態でアーカイブを作成すると、 test.sh ファイルはZIPの中に含まれてしまいます。 AntやMavenの場合だと、exclude の指定を行えばそれだけで対象となるパスのファイルが除去されましたが、 Gradleの場合だと、以下のように指定する必要があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
 * バッチ
 */
project(':batch') {

    /**
     * リリース用パッケージの作成を行う。
     *
     */
    task distribution(dependsOn: 'jar', type: Zip) {

        into ('batch/bin') {

            from ('bin') {

                exclude 'test.sh'   // test.sh を除去する指定

            }

        }


        into ('batch/libs') {

            from 'build/libs/batch.jar'
            from configurations.compile

        }


    }

}

上記のように記述することで bin/test.sh ファイルをコピーしないようになります。 Gradleのアーカイブタスクにおける exclude 指定は、このようにコピーする対象となるディレクトリの from ブロック内に exclude 指定を記述する必要があります。

10. ブランチ切り替えを頻繁に行った場合に他のブランチのクラスファイルが残る場合がある

これは主にJenkinsでJavaビルドを行う場合に起こることが多いのですが、JenkinsではジョブのSCMのブランチ切り替えを頻繁に行うかと思います。 そうなると、例えばあるブランチにはあって、あるブランチにはないクラスファイルというものが当然のように出てきます。 そして、そのクラスファイルの一部が残ることでビルドエラーが発生する場合があります。 これを回避するには、以下のように「clean」タスクをビルド前に実行するように指定することで回避することが出来ます。(尤も、これはGradleに限らずMavenでも同じです)

1
gradle --daemon clean war

clean タスクを実行すると更新する必要がないクラスも再コンパイルするようになってしまいますので、 ブランチ切り替えを行わないような例では開発スピードをあげるために、あえて clean を指定しないというのも手かと思います。 とはいえ、本番環境へリリースするWARやJARをビルドする場合は、万が一のことを考えて clean タスクを指定してビルドすることをお勧めします。

11. 突然動作がおかしくなった場合にキャッシュをクリアする方法

例えば、MavenリポジトリのURLを変更したなど、 さまざまな理由でキャッシュが残った場合に動作がおかしくなってエラーが発生する場合が有ります。 その場合、Gradleの作業用ディレクトリを削除すれば正常に動作する場合が有ります。 ユーザのホームディレクトリにある「.gradle」というディレクトリを削除することでキャッシュをクリアすることが出来ます。 ただしこの場合、Mavenの依存関係を一から取得し直しになるために、初回のビルド時に時間がかかります。 あくまで原因不明のエラー回復のための最終手段と思ってください。

まとめ

今回は私がGradleを利用した上でハマるであろうと考えたポイントを 11 個紹介させていただきました。 AntやMavenに慣れた人たちからすると、Gradleは最初のうちは少々取っ付きにくいものであるため、 このようにいくつかハマってしまうポイントが発生します。 Gradleをこれから使ってみようと思う皆様の参考になれば幸いです。

Comments