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

Gradleでリソースファイルをフィルタリングする場合の注意点

| Comments

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
/**
 * ベースロジック
 */
project(':base') {

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


    processResources {

        from(sourceSets.main.resources.srcDirs) {

            // トークンを置換する
            filter(
                org.apache.tools.ant.filters.ReplaceTokens,
                tokens: [
                    'version': '1.0.0',
                    'db_ip': '127.0.0.1'
                ]
            )

        }

    }


}

上記のスクリプトは、baseプロジェクトに含まれるリソースファイル(JARやWARに含まれるソースコード以外のファイル)に対して 「@version@」と「@db_ip@」の指定が含まれていた場合にそれぞれを指定の値に置換するという指定です。
一見これは上手く動作しますし、問題ないケースもあります。

しかし、この指定は見ての通りリソースファイル全てに対して行われれます
これはどういうことなのかというと、リソースファイルがテキストファイルであれば何ら問題はないのですが、 バイナリファイルが含まれているとそれすらも置換対象となってしまうのです!

例えばWebサイトシステムでは、ExcelファイルのテンプレートとなるExcelを予めJARやWARに含めて、 それをプログラム内で利用するケースがよくあるかと思います。 その場合、リソースファイルであるExcelファイルすらも置換対象となり、ファイルが盛大に壊れます。 Mavenでフィルタリングを行うときはバイナリファイルは対象外となるため、それを期待して利用するととんでもない目に遭うわけです。

解決方法

解決策としては、以下のように対象となる拡張子を指定することで回避します。 gradle.propertiesに拡張子を定義しておくと管理しやすいです。

[gradle.properties]

1
2
# カンマ区切りで定義しておく
replace.extensions=xml,conf,dicon

[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
/**
 * ベースロジック
 */
project(':base') {

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


    processResources {

        // フィルタ対象リソース拡張子分繰り返す
        getProperty('replace.extensions').tokenize(',').each {

            // 対象ファイルの場合
            filesMatching("**/*.$it") {

                // トークンを置換する
                filter(
                    org.apache.tools.ant.filters.ReplaceTokens,
                    tokens: [
                        'version': '1.0.0',
                        'db_ip': '127.0.0.1'
                    ]
                )

            }

        }

    }

}

filesMatchingを利用するのがミソになります。 拡張子を 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
/**
 * ベースロジック
 */
project(':base') {

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


    processResources {

        from(sourceSets.main.resources.srcDirs) {

            // 対象拡張子のみコピー
            include 'xml'
            include 'conf'
            include 'dicon'

            // トークンを置換する
            filter(
                org.apache.tools.ant.filters.ReplaceTokens,
                tokens: [
                    'version': '1.0.0',
                    'db_ip': '127.0.0.1'
                ]
            )

        }

        // 残りのファイルをコピー
        from(sourceSets.main.resources.srcDirs) {

            exclude 'xml'
            exclude 'conf'
            exclude 'dicon'

        }

    }

}

ただ、この書き方は推奨いたしません。 なぜなら、上記の拡張子指定を動的記述すると上手く動作しないためです。 せっかくなので紹介しておきます。

悪い例

※ replace.extensions が gradle.properties に定義されていることとする

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
/**
 * ベースロジック
 */
project(':base') {

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


    processResources {

        from(sourceSets.main.resources.srcDirs) {

            // フィルタ対象リソース拡張子分繰り返す
            getProperty('replace.extensions').tokenize(',').each {

                // 対象拡張子のみコピー
                include it

            }

            // トークンを置換する
            filter(
                org.apache.tools.ant.filters.ReplaceTokens,
                tokens: [
                    'version': '1.0.0',
                    'db_ip': '127.0.0.1'
                ]
            )

        }

        // 残りのファイルをコピー
        from(sourceSets.main.resources.srcDirs) {

            // フィルタ対象リソース拡張子分繰り返す
            getProperty('replace.extensions').tokenize(',').each {

                // 対象拡張子のみ除外
                exclude it

            }

        }

    }

}

これだと同じ拡張子を指定しているにも関わらず、上手く動作しません。
どうやら processResources において from 指定を行う場合、 同じパスに対して include と exclude の指定が被ると上手く動作せず、リソースファイルが全てJARやWARから消失します。 (ただし、直接記述すると何故か期待通りに動作する) おそらく動的記述した場合と静的記述した場合でスクリプトの解釈順に違いが出るのだと思いますが、 無理に考えても仕方ないのでこの書き方はやめましょう。

まとめ

Gradleは便利ですが、Mavenのときと同じ挙動を期待するとこのような思わぬ落とし穴があります。 processResourcesでフィルタリングを行う場合は filesMatching、またはfilesNotMatching を利用し、対象ファイルの拡張子を限定してからフィルタリングするようにしましょう。

Comments