環境
Gradle
1.11
Eclipse
Juno
発生した問題
以下のような構成のマルチプロジェクトを作ったとする。
├─build.gradle
├─settings.gradle
├─app\
└─web\
include 'web', 'app'
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 の下を見ると、以下のようになっている。
provided
で指定している奴はおろか、 testCompile
で指定している jar まで配備されてしまっている。
設定についての補足
provided について
「WAR プラグイン」を使用していれば、 providedCompile
を指定できる。
しかし、上記構成の場合、 WAR プロジェクトではない app プロジェクトも、実行環境が提供する API (javaee-api 7.0
)を使用している。
このため、 app プロジェクトには「WAR プラグイン」を使用せずに providedCompile
相当の設定をしなければならない。
ところが Gradle は、標準では 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 プラグインを適用している点について
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 の設定ファイルにあると思われる。
<?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.1
は exported="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.jar
の classpathentry
タグの要素には、 attribute
タグで "org.eclipse.jst.component.dependency" が設定されている。
正確には理解していないけど、この "org.eclipse.jst.component.dependency" は「この依存関係をどこに公開するか」を設定するものっぽく、 exported
が true
でないときは設定しないものと思われる。
この "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 を書き換える
- configurations.provided with eclipse-wtp plugin
- Gradle + Eclipse: How to not export JARs that are dependencies of a dependency?
上記2つのコンボで、なんとか問題を発生させなくできるようになった。
具体的には以下のように 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つ。
-
classpath.file.withXml
でクラスパスエントリからattributes
を無理やり削除している。 -
provided
とtestCompile
をやめてnoExport
というのを宣言している。
とりあえず、これで 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="<略>/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
が残って、それ以外は除去されている。
以下、調べてて分かったこととか。
根本的な問題
調べてみると、関係してそうな問題はチラホラ見つけるも、英語力が無くて理解できず。
- WTP Plugin should add org.eclipse.jst.component.dependency to all WEB-INF/lib deps and not add them to the WTP component file
- Eclipse plugin should eliminate Eclipse warning: Classpath entry ... will not be exported or published
仕方ないのでソースを見に行ったら、 "org.eclipse.jst.component.dependency" を設定しているっぽいところを見つけた。
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" を保持している定数。
たぶん、以下のような処理をしている。
- このプロジェクトに WAR プラグインが適用されていれば、
- プロジェクトの全 classpath エントリに対して、
-
"org.eclipse.jst.component.nondependency"=""
を設定する。 - さらに、このプロジェクトが依存している他のプロジェクトに対して、
- そのプロジェクトの全 classpath エントリに対して、
-
"org.eclipse.jst.component.dependency"="../"
を設定する。
つまり、WTP と WAR の両方のプラグインが適用されているプロジェクトがあった場合、その WTP プロジェクトが依存している他のプロジェクトが持っている classpath エントリには、そのエントリが exported=true
を持つかどうかに関わらず、問答無用で "org.eclipse.jst.component.dependency"="../"
を設定しているっぽい。
(コメントの英語がまさに、「必要だから全部 WEB-INF/lib に配備するよ」って宣言してる)
Gradle のコードを修正してみる
exported
をチェックすればいいのかと以下のように実装を変更してみた。
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
は以下のようになる。
<?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
に追加してみる。
eclipse {
classpath {
plusConfigurations += configurations.provided
noExportConfigurations += [configurations.provided, configurations.testRuntime] // testRuntime も追加
}
}
.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 に配備されず、実行時にエラーが発生してしまう。
exported
を false
に切り替えているのは以下の部分。
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 // ③
}
}
}
}
- クラスパスの各エントリに対して、
- そのエントリの
declaredConfigurationName
がnoExportConfigNames
で指定した名前に含まれていたら、 -
exported
をfalse
にしている。
動作を確認したところ、 compile
を指定しているエントリも、ここの処理に来た時点で declaredConfigurationName
が testRuntime
になっていた。
このため、 noExportConfigNames
に testRuntime
を指定すると、 compile
を指定しているエントリも export
対象外になってしまった模様。
いろいろ試したけど、疲れたのでこの辺でコードを修正するのはあきらめた。