LoginSignup
17
17

More than 5 years have passed since last update.

GradleでEclipseのWTPプロジェクトをマルチプロジェクトで作成したら、WTPプロジェクトが依存するプロジェクトでprovided指定したはずのjarもWEB-INF/libの下に配置されてとても困ったときの話

Last updated at Posted at 2014-04-17

環境

Gradle

1.11

Eclipse

Juno

発生した問題

以下のような構成のマルチプロジェクトを作ったとする。

フォルダ構成
├─build.gradle
├─settings.gradle
├─app\
└─web\
settings.gradle
include 'web', 'app'
build.gradle
subprojects {
    apply plugin: 'java'
    apply plugin: 'eclipse-wtp'

    sourceCompatibility = '1.7'
    targetCompatibility = '1.7'

    repositories {
        mavenCentral()
    }

    configurations {
        provided
    }

    sourceSets {
        main.compileClasspath += configurations.provided
        test {
            compileClasspath += configurations.provided
            runtimeClasspath += configurations.provided
        }
    }

    eclipse {
        classpath {
            plusConfigurations += configurations.provided
            noExportConfigurations += configurations.provided
        }
    }

    dependencies {
        compile 'org.apache.commons:commons-lang3:3.2.1'

        provided 'javax:javaee-api:7.0'

        testCompile 'junit:junit:4.11', {
            transitive = false
        }
        testCompile 'org.hamcrest:hamcrest-all:1.3'
    }
}

project(':app') {
    dependencies {
        compile 'org.apache.commons:commons-io:1.3.2'
    }

    eclipse {
        project {
            name = 'app'
        }
    }
}

project(':web') {
    apply plugin: 'war'

    archivesBaseName = 'web'

    dependencies {
        compile project(':app')
    }

    eclipse {
        project {
            name = 'web'
        }

        wtp {
            component {
                contextPath = 'web'
            }
            facet {
                facet name: 'java', version: '1.7'
                facet name: 'jst.web', version: '3.0'
            }
        }
    }
}

build.gradle で設定している依存関係をまとめると、以下のようになる。

プロジェクト コンフィグレーション 依存対象(ライブラリ、プロジェクト)
app compile commons-lang 3.2.1
commons-io 1.3.2
provided javaee-api 7.0
testCompile junit 4.11
hamcrest-all 1.3
web compile commons-lang 3.2.1
app ( project )
provided javaee-api 7.0
testCompile junit 4.11
hamcrest-all 1.3

この設定で eclipse タスクを実行して Eclipse プロジェクトを生成し、 Eclipse にインポートする。

そして、 Eclipse から web プロジェクトを AP サーバー(GlassFish)にデプロイする。

GlassFish のドメインフォルダを開き、 Eclipse がデプロイしたアプリケーションの WEB-INF/lib の下を見ると、以下のようになっている。

webinflib.jpg

provided で指定している奴はおろか、 testCompile で指定している jar まで配備されてしまっている。

設定についての補足

provided について

「WAR プラグイン」を使用していれば、 providedCompile を指定できる。

しかし、上記構成の場合、 WAR プロジェクトではない app プロジェクトも、実行環境が提供する API (javaee-api 7.0)を使用している。
このため、 app プロジェクトには「WAR プラグイン」を使用せずに providedCompile 相当の設定をしなければならない。

ところが Gradle は、標準では providedCompile 相当の設定を提供していない。

代わりに、以下のように設定することで providedCompile を実現できる。

自前providedCompile
configurations {
    provided
}

sourceSets {
    main.compileClasspath += configurations.provided
    test {
        compileClasspath += configurations.provided
        runtimeClasspath += configurations.provided
    }
}

eclipse {
    classpath {
        plusConfigurations += configurations.provided
    }
}

dependencies {
    provided 'javax:javaee-api:7.0'
}

app プロジェクトにも eclipse-wtp プラグインを適用している点について

build.gradle一部抜粋
subprojects {
    apply plugin: 'java'
    apply plugin: 'eclipse-wtp'

(以下略)

このようになっているので、 app プロジェクトにも eclipse-wtp プラグインが適用されている。

一見すると app プロジェクトは普通の jar を生成するプロジェクトなので eclipse プラグインだけで良いように思える。

しかし、 eclipse プラグインだと問題が発生する。

具体的には、 WTP プロジェクトが依存しているプロジェクトに eclipse プラグインしか適用していないと、 WTP プロジェクトを Eclipse から AP サーバーにデプロイしたときに、依存しているプロジェクトが AP サーバーに配備されないという現象が発生する。

WTP プロジェクトが依存している他のプロジェクトにも、 eclipse-wtp プラグインを適用しないといけない。

参考

直接の原因

こんなことが起こってしまう直接の原因は、 Gradle が吐き出した Eclipse の設定ファイルにあると思われる。

appプロジェクトの.classpath
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
    <classpathentry kind="output" path="bin"/>
    <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER" exported="true"/>
    <classpathentry sourcepath="<略>/commons-lang3-3.2.1-sources.jar" kind="lib" path="<略>/commons-lang3-3.2.1.jar" exported="true">
        <attributes>
            <attribute name="org.eclipse.jst.component.dependency" value="../"/>
        </attributes>
    </classpathentry>
(中略)
    <classpathentry sourcepath="<略>/javaee-api-7.0-sources.jar" kind="lib" path="<略>/javaee-api-7.0.jar">
        <attributes>
            <attribute name="org.eclipse.jst.component.dependency" value="../"/>
        </attributes>
    </classpathentry>
(後略)
</classpath>

classpathentry タグの exported 属性が重要で、これが true だとその依存関係を公開する――要は、 jar を WEB-INF/lib の下に配備する。

commons-lang3-3.2.1exported="true" となっているが、 javaee-api-7.0.jar には exported 属性が設定されていない。
これは、最初の build.gradle で指定していた noExportConfigurations の設定が効いているため。

eclipse {
    classpath {
        plusConfigurations += configurations.provided
        noExportConfigurations += configurations.provided // ←これ
    }
}

noExportConfigurations に追加したコンフィグレーションを持つ依存関係には、 exported 属性が設定されなくなる。

ところが、 exported が設定されていないにもかかわらず、 javaee-api-7.0.jarclasspathentry タグの要素には、 attribute タグで "org.eclipse.jst.component.dependency" が設定されている。

正確には理解していないけど、この "org.eclipse.jst.component.dependency" は「この依存関係をどこに公開するか」を設定するものっぽく、 exportedtrue でないときは設定しないものと思われる。

この "org.eclipse.jst.component.dependency" の指定があるせいで、本来配備されては困る jar が WEB-INF/lib の下に配備されてしまっている模様。
(実際、この attributes をごっそり消すと、問題は発生しなくなる)

解決策

1.何もしない

例であげた jar だけなら、正直実害は無い。

この現象が起こるのはあくまで Eclipse から AP サーバーにデプロイしたときの話なので、 Gradle の war タスクで war を生成した場合は、 WEB-INF/lib の下に全 jar が配備されるなんて現象は発生しない。

ていうか、たぶん普通に使っているだけだと裏でこんな現象が発生しているなんて気付かずに使い続けると思う。
(今回これを見つけたのも他の問題の原因を調べているときで、偶然だった)

でも、実害が完全にゼロというわけでもない。

AP サーバーの持っている jar とバッティングすることでエラーが起こることも無きにしもあらずだし、なにより実際の生成物とは異なる状態で開発+動作確認をするというのは気持ち悪い話です。

2.build.gradle 上で withXml を使ってゴリゴリ .classpath を書き換える

上記2つのコンボで、なんとか問題を発生させなくできるようになった。

具体的には以下のように build.gradle を記述する。

build.gradle
subprojects {
    apply plugin: 'java'
    apply plugin: 'eclipse-wtp'

    sourceCompatibility = '1.7'
    targetCompatibility = '1.7'

    repositories {
        mavenCentral()
    }

    configurations {
        noExport
    }

    sourceSets {
        main.compileClasspath += configurations.noExport
        test {
            compileClasspath += configurations.noExport
            runtimeClasspath += configurations.noExport
        }
    }

    eclipse {
        classpath {
            plusConfigurations += configurations.noExport
            noExportConfigurations += configurations.noExport
        }

        classpath.file.withXml {
            it.asNode().classpathentry.findAll { entry ->
                entry.@kind == 'lib' && !entry.@exported
            }.each { noExportEntry ->
                def attributes = noExportEntry.children()[0]
                if (attributes != null) {
                    def dependency = attributes.attribute.find {it.@name == 'org.eclipse.jst.component.dependency'}

                    if (dependency != null) {
                        attributes.remove(dependency)
                    }
                }
            }
        }
    }

    dependencies {
        compile 'org.apache.commons:commons-lang3:3.2.1'

        noExport 'javax:javaee-api:7.0'

        noExport 'junit:junit:4.11', {
            transitive = false
        }
        noExport 'org.hamcrest:hamcrest-all:1.3'
    }
}

project(':app') {
    dependencies {
        compile 'org.apache.commons:commons-io:1.3.2'
    }

    eclipse {
        project {
            name = 'app'
        }
    }
}

project(':web') {
    apply plugin: 'war'

    archivesBaseName = 'web'

    dependencies {
        compile project(':app')
    }

    eclipse {
        project {
            name = 'web'
        }

        wtp {
            component {
                contextPath = 'web'
            }
            facet {
                facet name: 'java', version: '1.7'
                facet name: 'jst.web', version: '3.0'
            }
        }
    }
}

最初のやつとの大きな違いは以下の2つ。

  1. classpath.file.withXml でクラスパスエントリから attributes を無理やり削除している。
  2. providedtestCompile をやめて noExport というのを宣言している。

とりあえず、これで app プロジェクトの .classpath は以下のように出力されるようになり、問題は発生しなくなる。

.classpath
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
    <classpathentry kind="output" path="bin"/>
    <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER" exported="true"/>
    <classpathentry sourcepath="<略>/commons-lang3-3.2.1-sources.jar" kind="lib" path="<略>/commons-lang3-3.2.1.jar" exported="true">
        <attributes>
            <attribute name="org.eclipse.jst.component.dependency" value="../"/>
        </attributes>
    </classpathentry>
    <classpathentry sourcepath="<略>/commons-io-1.3.2-sources.jar" kind="lib" path="<略>/commons-io-1.3.2.jar" exported="true">
        <attributes>
            <attribute name="org.eclipse.jst.component.dependency" value="../"/>
        </attributes>
    </classpathentry>
    <classpathentry sourcepath="<略>/javaee-api-7.0-sources.jar" kind="lib" path="<略>/javaee-api-7.0.jar">
        <attributes/>
    </classpathentry>
    <classpathentry sourcepath="<略>/junit-4.11-sources.jar" kind="lib" path="<略>/junit-4.11.jar">
        <attributes/>
    </classpathentry>
    <classpathentry sourcepath="<略>/hamcrest-all-1.3-sources.jar" kind="lib" path="<略>/hamcrest-all-1.3.jar">
        <attributes/>
    </classpathentry>
    <classpathentry sourcepath="<略>/activation-1.1-sources.jar" kind="lib" path="<略>/activation-1.1.jar">
        <attributes/>
    </classpathentry>
    <classpathentry sourcepath="<略>/javax.mail-1.5.0-sources.jar" kind="lib" path="<略>/javax.mail-1.5.0.jar">
        <attributes/>
    </classpathentry>
</classpath>

exported="true" のものだけ attributes が残って、それ以外は除去されている。


以下、調べてて分かったこととか。

根本的な問題

調べてみると、関係してそうな問題はチラホラ見つけるも、英語力が無くて理解できず。

仕方ないのでソースを見に行ったら、 "org.eclipse.jst.component.dependency" を設定しているっぽいところを見つけた。

EclipseWtpPlugin.groovy
private void configureEclipseClasspathForWarPlugin(Project project) {
    project.plugins.withType(WarPlugin) { // ①
        project.eclipse.classpath.containers WEB_LIBS_CONTAINER

        project.eclipse.classpath.file.whenMerged { Classpath classpath ->
            for (entry in classpath.entries) { // ②
                if (entry instanceof AbstractLibrary) {
                    //this is necessary to avoid annoying warnings upon import to Eclipse
                    //the .classpath entries can be marked all as non-deployable dependencies
                    //because the wtp component file declares the deployable dependencies
                    entry.entryAttributes[AbstractClasspathEntry.COMPONENT_NON_DEPENDENCY_ATTRIBUTE] = '' // ③
                }
            }
        }

        doLaterWithEachDependedUponEclipseProject(project) { Project otherProject -> // ④
            otherProject.eclipse.classpath.file.whenMerged { Classpath classpath -> 
                for (entry in classpath.entries) { // ⑤
                    if (entry instanceof AbstractLibrary) {
                        // '../' and '/WEB-INF/lib' both seem to be correct (and equivalent) values here
                        //this is necessary so that the depended upon projects will have their dependencies
                        // deployed to WEB-INF/lib of the main project.
                        entry.entryAttributes[AbstractClasspathEntry.COMPONENT_DEPENDENCY_ATTRIBUTE] = '../' // ⑥
                    }
                }
            }
        }
    }
}

COMPONENT_DEPENDENCY_ATTRIBUTE が "org.eclipse.jst.component.dependency" を保持している定数。

たぶん、以下のような処理をしている。

  1. このプロジェクトに WAR プラグインが適用されていれば、
  2. プロジェクトの全 classpath エントリに対して、
  3. "org.eclipse.jst.component.nondependency"="" を設定する。
  4. さらに、このプロジェクトが依存している他のプロジェクトに対して、
  5. そのプロジェクトの全 classpath エントリに対して、
  6. "org.eclipse.jst.component.dependency"="../" を設定する。

つまり、WTP と WAR の両方のプラグインが適用されているプロジェクトがあった場合、その WTP プロジェクトが依存している他のプロジェクトが持っている classpath エントリには、そのエントリが exported=true を持つかどうかに関わらず、問答無用で "org.eclipse.jst.component.dependency"="../" を設定しているっぽい。
(コメントの英語がまさに、「必要だから全部 WEB-INF/lib に配備するよ」って宣言してる)

Gradle のコードを修正してみる

exported をチェックすればいいのかと以下のように実装を変更してみた。

EclipseWtpPlugin.groovy
doLaterWithEachDependedUponEclipseProject(project) { Project otherProject ->
    otherProject.eclipse.classpath.file.whenMerged { Classpath classpath ->
        for (entry in classpath.entries) {
            if (entry instanceof AbstractLibrary) {
                if (entry.exported) { // ★ exported が true のときだけ attributes を設定するように修正
                    entry.entryAttributes[AbstractClasspathEntry.COMPONENT_DEPENDENCY_ATTRIBUTE] = '../'
                }
            }
        }
    }
}

これで eclipse タスクを実行すると .classpath は以下のようになる。

.classpath
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
    <classpathentry kind="output" path="bin"/>
    <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER" exported="true"/>
    <classpathentry sourcepath="<略>/commons-lang3-3.2.1-sources.jar" kind="lib" path="<略>/commons-lang3-3.2.1.jar" exported="true">
        <attributes>
            <attribute name="org.eclipse.jst.component.dependency" value="../"/>
        </attributes>
    </classpathentry>
    <classpathentry sourcepath="<略>/junit-4.11-sources.jar" kind="lib" path="<略>/junit-4.11.jar" exported="true">
        <attributes>
            <attribute name="org.eclipse.jst.component.dependency" value="../"/>
        </attributes>
    </classpathentry>
    <classpathentry sourcepath="<略>/hamcrest-all-1.3-sources.jar" kind="lib" path="<略>/hamcrest-all-1.3.jar" exported="true">
        <attributes>
            <attribute name="org.eclipse.jst.component.dependency" value="../"/>
        </attributes>
    </classpathentry>
    <classpathentry sourcepath="<略>/commons-io-1.3.2-sources.jar" kind="lib" path="<略>/commons-io-1.3.2.jar" exported="true">
        <attributes>
            <attribute name="org.eclipse.jst.component.dependency" value="../"/>
        </attributes>
    </classpathentry>
    <classpathentry sourcepath="<略>/javaee-api-7.0-sources.jar" kind="lib" path="<略>/javaee-api-7.0.jar"/>
    <classpathentry sourcepath="<略>/activation-1.1-sources.jar" kind="lib" path="<略>/activation-1.1.jar"/>
    <classpathentry sourcepath="<略>/javax.mail-1.5.0-sources.jar" kind="lib" path="<略>/javax.mail-1.5.0.jar"/>
</classpath>

provided 指定していた依存関係は、 "org.eclipse.jst.component.dependency" がなくなったが、テストで使用するライブラリが残ってしまっている。

なので、以下のようにしてテストで使用している依存関係も noExportConfigurations に追加してみる。

build.gradle
eclipse {
    classpath {
        plusConfigurations += configurations.provided
        noExportConfigurations += [configurations.provided, configurations.testRuntime] // testRuntime も追加
    }
}

.classpath を出力すると、

.classpath
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
    <classpathentry kind="output" path="bin"/>
    <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER" exported="true"/>
    <classpathentry sourcepath="<略>/commons-lang3-3.2.1-sources.jar" kind="lib" path="<略>/commons-lang3-3.2.1.jar"/>
    <classpathentry sourcepath="<略>/junit-4.11-sources.jar" kind="lib" path="<略>/junit-4.11.jar"/>
    <classpathentry sourcepath="<略>/hamcrest-all-1.3-sources.jar" kind="lib" path="<略>/hamcrest-all-1.3.jar"/>
    <classpathentry sourcepath="<略>/commons-io-1.3.2-sources.jar" kind="lib" path="<略>/commons-io-1.3.2.jar"/>
    <classpathentry sourcepath="<略>/javaee-api-7.0-sources.jar" kind="lib" path="<略>/javaee-api-7.0.jar"/>
    <classpathentry sourcepath="<略>/activation-1.1-sources.jar" kind="lib" path="<略>/activation-1.1.jar"/>
    <classpathentry sourcepath="<略>/javax.mail-1.5.0-sources.jar" kind="lib" path="<略>/javax.mail-1.5.0.jar"/>
</classpath>

compile で指定していた分まで配備されなくなってしまった。

このままだと、 app プロジェクトが必要としている commons-io が WEB-INF/lib に配備されず、実行時にエラーが発生してしまう。

exportedfalse に切り替えているのは以下の部分。

ExportedEntriesUpdater.groovy
void updateExported(List<ClasspathEntry> classpathEntries, List<String> noExportConfigNames) {
    classpathEntries.each { // ①
        if (it instanceof AbstractLibrary || it instanceof ProjectDependency) {
            if (noExportConfigNames.contains(it.declaredConfigurationName)) { // ②
                it.exported = false // ③
            }
        }
    }
}
  1. クラスパスの各エントリに対して、
  2. そのエントリの declaredConfigurationNamenoExportConfigNames で指定した名前に含まれていたら、
  3. exportedfalse にしている。

動作を確認したところ、 compile を指定しているエントリも、ここの処理に来た時点で declaredConfigurationNametestRuntime になっていた。

このため、 noExportConfigNamestestRuntime を指定すると、 compile を指定しているエントリも export 対象外になってしまった模様。

いろいろ試したけど、疲れたのでこの辺でコードを修正するのはあきらめた。

17
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
17