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

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

javaのビルドをgradleで行う:実践編5:マルチプロジェクト時のprovidedCompileについて

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


f:id:treeapps:20180426142529p:plain

Gradleの依存管理にはprovidedCompileという属性があります。

これは、コンパイル時にはクラスパスに含めるが、パッケージ(war・jar等)には含めない、という機能です。

しかしこのprovidedCompile、どうもサブプロジェクト構成で挙動が怪しいのです。

Gradleの環境は以下の通りです。

------------------------------------------------------------
Gradle 1.4
------------------------------------------------------------

Gradle build time: 201312834246秒 UTC
Groovy: 1.8.6
Ant: Apache Ant(TM) version 1.8.4 compiled on May 22 2012
Ivy: 2.2.0
JVM: 1.7.0_21 (Oracle Corporation 23.21-b01)
OS: Mac OS X 10.8.4 x86_64

hoge-common
 ├ hoge-search
 └ hoge-web

こんな感じでcommonを共通の親プロジェクトとし、searchとwebが小プロジェクトになります。

この時、commonとwebの両者でservlet-api.jarに依存したコードがあるとします。

すると、commonとwebの両方のクラスパスにservlet-api.jarが無いといけません。

クラスパスが通っていないと、コンパイルに失敗してしまいます。

単純にcommonとwebの両方にservlet-api.jarを以下のように追加してしまうと、クラスパス上にservlet-apiが複数存在する事になり、tomcat起動時にClassCastExceptionが発生してしまいます。

project(':hoge-common') {
    dependencies {
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-servlet-api', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-api', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-el-api', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-jasper', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-jasper-el', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-jsp-api', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-juli', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-util', version: '7.0.27'
    }
}
project(':hoge-web') {
    dependencies {
        compile project(':hoge-common')
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-servlet-api', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-api', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-el-api', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-jasper', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-jasper-el', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-jsp-api', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-juli', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-util', version: '7.0.27'
    }
}
project(':hoge-search') {
    dependencies { compile project(':hoge-common') }
}

こうするとClassCastExceptionが発生します。

親クラスはjar化されてwebとsearchに自動的にクラスパスに配備されるので、親クラスだけにservlet-api.jarがあればいいのでは?という事でやってみます。

project(':hoge-common') {
    dependencies {
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-servlet-api', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-api', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-el-api', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-jasper', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-jasper-el', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-jsp-api', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-juli', version: '7.0.27'
        providedCompile group: 'org.apache.tomcat', name: 'tomcat-util', version: '7.0.27'
    }
}
project(':hoge-web') {
    dependencies { compile project(':hoge-common') }
}
project(':hoge-search') {
    dependencies { compile project(':hoge-common') }
}

これでコンパイルエラーは出ず、tomcat起動時にClassCastExceptionは発生しません。
これで解決ですね!!!

・・・・

実は全然解決してないのです

何が問題なのかというと、war内にservlet-api.jarが入っているのです。

別の環境でGradle1.6で試しましたが、結果は同じでした。

これではtomcatが起動できません。

localhost:lib tree$ ll
・・・中略・・・
-rw-r--r--  1 tree  staff   6.7K  5 31 01:56 tomcat-api-7.0.27.jar
-rw-r--r--  1 tree  staff    45K  5 31 01:56 tomcat-el-api-7.0.27.jar
-rw-r--r--  1 tree  staff   577K  5 31 01:56 tomcat-jasper-7.0.27.jar
-rw-r--r--  1 tree  staff   120K  5 31 01:56 tomcat-jasper-el-7.0.27.jar
-rw-r--r--  1 tree  staff    87K  5 31 01:56 tomcat-jsp-api-7.0.27.jar
-rw-r--r--  1 tree  staff    37K  5 31 01:56 tomcat-juli-7.0.27.jar
-rw-r--r--  1 tree  staff   173K  5 31 01:56 tomcat-servlet-api-7.0.27.jar
-rw-r--r--  1 tree  staff    23K  5 31 01:56 tomcat-util-7.0.27.jar
-rw-r--r--  1 tree  staff    34K  6 19 00:48 hoge-base-1.0-SNAPSHOT.jar

providedCompileの仕様を考えると、war内にservlet-api.jarが残るはおかしいのでは?と思います。

このままでは困るので、webのwarから特定のjarを除外する方法を見つけました。

具体的には以下の通りです。

def tomcatJars = [
    'tomcat-servlet-api-' + tomcatVersion + '.jar',
    'tomcat-api-' + tomcatVersion + '.jar',
    'tomcat-el-api-' + tomcatVersion + '.jar',
    'tomcat-jasper-el-' + tomcatVersion + '.jar',
    'tomcat-jsp-api-' + tomcatVersion + '.jar',
    'tomcat-juli-' + tomcatVersion + '.jar',
    'tomcat-util-' + tomcatVersion + '.jar'
]
war {
    def now = new Date().getTime().toString()
    // jspのtoken置換
    from(webAppDir) {
        include '**/*.jsp'
        filter(ReplaceTokens, tokens:['TIMESTAMP': now])
    }
    classpath = classpath.filter { File file ->
        !tomcatJars.contains(file.name)
    }
}

classpathの部分です。

こうすると、配列内のjar以外を対象にクラスパスに追加する事ができます。

さて、再度warタスクを実行し、warを解凍してlibを確認します。

localhost:lib tree$ ll
・・・中略・・・
-rw-r--r--  1 tree  staff    34K  6 19 00:55 hoge-base-1.0-SNAPSHOT.jar

tomcatのjarは全て消えました。これで目標達成です。

具体的に以下の条件を満たすことができています。

  • 親・子プロジェクトが共にservlet-api.jarに依存している
  • tomcat起動時にClassCastExceptionが発生しないこと
  • warタスク実行時のコンパイルエラーが発生しないこと
  • war内のlibにtomcatのjarが混入しないこと


話は変わりますが、このprovidedCompileの挙動、なんか変なのです。

例えば以下の構成でwarタスクを実行するとどうなるでしょう。

project(':hoge-common') {
}
project(':hoge-web') {
    dependencies { providedCompile project(':hoge-common') }
}
project(':hoge-search') {
    dependencies { providedCompile project(':hoge-common') }
}

hoge-commonをprovidecCompileで依存させています。

実はこの状態だと、webのwarからjarが全て消えるのです。

この挙動を見ると、やはりサブプロジェクト下のprovidedCompileはちょっとおかしいのでは?という事が予想できます。

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