mike-neckのブログ

Java or Groovy or Swift or Golang

Gradle3におけるJavaプロジェクトのビルド入門(2) #gradle

f:id:mike_neck:20151108133321p:plain

前回は、JVM component modelによるJavaプロジェクトのビルドの入門をおこなった。

mike-neck.hatenadiary.com

これまでのjavaプラグインに比べると記述量が増えてしまうが、遜色ないビルドができることは示せたと思う。


複数ライブラリー(コンポーネント)

前回、簡単にするためにmainというライブラリーだけに限ってプロジェクトのビルドを行いましたが、複数のライブラリー(コンポーネント)を指定することも可能でした。そこで、今回はまず複数のライブラリーを作成するプロジェクトを構築してみたいと思います。

前回用いたMainクラスとResクラスをそれぞれ別のライブラリーに分けることにします。

クラス ライブラリー
sample.Main main
sample.Res res

また、resources/app.propertiesファイルはmainライブラリーに含めるものとします。

まず、mainというライブラリーとresというライブラリーの二つのコンポーネントを持ちますので、build.gradleDSLは次のように書くことができます。

build.gradle

plugins {
    id 'jvm-component'
    id 'java-lang'
}

model {
    components {
        main(JvmLibrarySpec)
        res(JvmLibrarySpec)
    }
}

この状態で、componentsタスクを実行すると、前回に実行した時のように二つのライブラリーを持つコンポーネントが準備されていることがわかります。

$ gradle components
:components

------------------------------------------------------------
Root project
------------------------------------------------------------

JVM library 'main'
------------------

Source sets
    Java source 'main:java'
        srcDir: src/main/java
    JVM resources 'main:resources'
        srcDir: src/main/resources

Binaries
    Jar 'mainJar'
        build using task: :mainJar
        targetPlatform: Java SE 8
        tool chain: JDK 8 (1.8)
        Jar file: build/jars/mainJar/main.jar

JVM library 'res'
-----------------

Source sets
    Java source 'res:java'
        srcDir: src/res/java
    JVM resources 'res:resources'
        srcDir: src/res/resources

Binaries
    Jar 'resJar'
        build using task: :resJar
        targetPlatform: Java SE 8
        tool chain: JDK 8 (1.8)
        Jar file: build/jars/resJar/res.jar

Note: currently not all plugins register their components, so some components may not be visible here.

BUILD SUCCESSFUL

Total time: 0.756 secs
$

また、このコンポーネント構成を満たすためのディレクトリーは次のようになります。

$ tree -d src
src
├── main
│   ├── java
│   └── resources
└── res
    ├── java
    └── resources

6 directories
$

Javaファイルの作成

前回作成したファイルをそれぞれ次の位置に配置し直します。

元のファイル 新しいファイル
src/main/java/sample/Main.java 変更なし
src/main/java/sample/Res.java src/res/java/sample/Res.java
src/main/resources/app.properties 変更なし

この時のディレクトリー構成は次のようになります。

$ tree
src
├── main
│   ├── java
│   │   └── sample
│   │       └── Main.java
│   └── resources
│       └── app.properties
└── res
    ├── java
    │   └── sample
    │       └── Res.java
    └── resources

8 directories, 3 files
$

ビルドの実行

ではビルドを実行します。

$ gradle build
:compileMainJarMainJava
/Users/mike/IdeaProjects/gradle-projects/gradle-self-training/g3-java-intro/src/main/java/sample/Main.java:23: エラー: シンボルを見つけられません
        Res res = new Res();
        ^
  シンボル:   クラス Res
  場所: クラス Main
/Users/mike/IdeaProjects/gradle-projects/gradle-self-training/g3-java-intro/src/main/java/sample/Main.java:23: エラー: シンボルを見つけられません
        Res res = new Res();
                      ^
  シンボル:   クラス Res
  場所: クラス Main
エラー2個
:compileMainJarMainJava FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':compileMainJarMainJava'.
> Compilation failed; see the compiler error output for details.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 0.848 secs
$

ライブラリーを分けた瞬間にMainクラスが参照することのできた、Resクラスを参照することができなくなり、コンパイルエラーとなってしまいました。

コンポーネント間の依存性

これは、JVM component modelにおける、コンポーネント間のAPI依存性です。例えば、ライブラリーfooがライブラリーbarに依存する場合、fooをコンパイルするためにはbarをコンパイルする必要があるというものです。

Gradle2.8現在、コンポーネント依存性解決については以下の二つのタイプをサポートしています。

  • 同一プロジェクトのライブラリー間の依存性解決
  • 同一プロジェクトを構成するサブプロジェクトのライブラリー間の依存性解決

現在のところ、外部ライブラリー(例えばslf4jとかtwitter4jのようなライブラリー)の参照はサポートされていないようです。

外部ライブラリー依存解決についてGradle Forumに質問してみたところ、現在のコードベース(リリースされていない)においては、実装および簡単なテストはあるようですが、リリースについてはGradle2.10頃になるのではないかとのことです。もし、外部ライブラリーを利用したい方はナイトリービルドを用いるなどするとよいでしょう。

discuss.gradle.org

依存性の解決方法

依存性解決のDSLについては、SourceSetsDSLのように記述をおこないます。

今のこのサンプルにおいては、mainライブラリーがresライブラリーに依存しているために、次のように記述します。

plugins {
  id 'jvm-component'
  id 'java-lang'
}

model {
  components {
    main(JvmLibrarySpec) {
      sources {
        java {
          dependencies {
            library 'res'
          }
        }
      }
    }
    res(JvmLibrarySpec)
  }
}

では、このスクリプトの状態で、componentsタスクを実行してみます。

$ gradle components
:components

------------------------------------------------------------
Root project
------------------------------------------------------------

JVM library 'main'
------------------

Source sets
    Java source 'main:java'
        srcDir: src/main/java
        dependencies:
            library 'res'
    JVM resources 'main:resources'
        srcDir: src/main/resources

Binaries
    Jar 'mainJar'
        build using task: :mainJar
        targetPlatform: Java SE 8
        tool chain: JDK 8 (1.8)
        Jar file: build/jars/mainJar/main.jar

JVM library 'res'
-----------------

Source sets
    Java source 'res:java'
        srcDir: src/res/java
    JVM resources 'res:resources'
        srcDir: src/res/resources

Binaries
    Jar 'resJar'
        build using task: :resJar
        targetPlatform: Java SE 8
        tool chain: JDK 8 (1.8)
        Jar file: build/jars/resJar/res.jar

Note: currently not all plugins register their components, so some components may not be visible here.

BUILD SUCCESSFUL

Total time: 0.759 secs
$

mainライブラリーのコンポーネント情報のところに、resに依存するという情報が追加されました。

ちなみに、dependencies {}ブロックのdelegateオブジェクトはDependencySpecContainerであり、そこに指定できるのは単なるStringなので、次のような記述もできます。

    main(JvmLibrarySpec) {
      sources {
        java {
          dependencies {
            library 'res'
            library 'foo'
          }
        }
      }
    }

まあ、できることはできますが、指定されたfooライブラリーは存在しないので、コンパイル時にエラーになるだけです。評価フェーズにて特に検証を行っていないので、ライブラリー名のtypoには気をつけてください。

依存性解決プラスアルファ

サブプロジェクトへの依存性解決

例えばmainライブラリーが、サブプロジェクトsubのライブラリーfooに依存するライブラリーの場合は次のように記述します(これは次々回に詳しくやりましょう)。

    main(JvmLibrarySpec) {
      sources {
        java {
          dependencies {
            library 'res'
            project ':sub' library 'foo'
          }
        }
      }
    }

外部依存性の解決

先ほど、外部依存ライブラリーの解決はサポートされていないと書きましたが、教えてもらったテストケースを読むと次のように記述するようです。ただし、incubatingな機能なので、実際にリリースされる際には変更されている可能性があります。その点についてはご容赦ください。

    main(JvmLibrarySpec) {
      sources {
        java {
          dependencies {
            library 'res'
            library 'org.slf4j:slf4j-api:1.7.13'
          }
        }
      }
    }

transitive dependencies(推移的依存性解決)

ライブラリー間の依存性解決については、transitive dependencies(推移的依存性解決)にも対応していません。したがって、次のような依存関係を持つライブラリーがあるとします。

  • fooライブラリーはbarライブラリーに依存している
  • barライブラリーはbazライブラリーに依存している

このような状況においては、fooライブラリーの依存性にbazライブラリーを加えておく必要があります。

ビルド

さて、依存性の解決も行ったことですし、ビルドをしてみましょう。

$ gradle build
:compileResJarResJava
:processResJarResResources
:createResJar
:resJar
:compileMainJarMainJava
:processMainJarMainResources
:createMainJar
:mainJar
:assemble
:check UP-TO-DATE
:build

BUILD SUCCESSFUL

Total time: 0.859 secs
$

mainライブラリーがresライブラリーに依存しているという情報を解析して、resライブラリー→mainライブラリーの順番でビルドされていることがわかります。

では、実際にjarファイルが作成されていることもあわせて確認してみましょう。

$ tree build/jars/
build/jars/
├── mainJar
│   └── main.jar
└── resJar
    └── res.jar

2 directories, 2 files
$

実行

おそらくMANIFEST.MFファイルにMain-Class属性はないものと思われますので、-cpオプションでjarを読み込んで、mainクラスを自分で指定して、実行しましょう。また、まだ、incubatingな機能ですから、クラスのリンクを書き出す機能などないでしょうし、resライブラリーの方も、-cpオプションに入れ忘れないように気をつけます。

$ java -cp build/jars/mainJar/main.jar:build/jars/resJar/res.jar sample.Main
こんにちわ
mikeさん
僕はGradle
$

前回と同じ結果が出力されました。

とはいえ、毎回、こんな長いコマンドを書くのも面倒ですね。というわけで、カスタムタスクを作ってしまいましょう。

task runApp(type: JavaExec) {
    def jar = org.gradle.jvm.tasks.Jar
    tasks.withType(jar) {
        classpath += it.outputs.files
    }
    main = 'sample.Main'
}

このタスクを実行すると次のようになります。

$ gradle runApp
:compileResJarResJava UP-TO-DATE
:processResJarResResources UP-TO-DATE
:createResJar UP-TO-DATE
:resJar UP-TO-DATE
:compileMainJarMainJava UP-TO-DATE
:processMainJarMainResources UP-TO-DATE
:createMainJar UP-TO-DATE
:runApp
こんにちわ
mikeさん
僕はGradle

BUILD SUCCESSFUL

Total time: 1.095 secs
$

見事、アプリケーションを実行してくれました。

注意

スクリプト中でJarの型をorg.gradle.jvm.tasks.Jarと書きました。Gradle2.8ではJarタイプのタスクの型はorg.gradle.api.tasks.bundling.Jarですが、ドキュメントのほうが若干先行していてorg.gradle.jvm.tasks.Jarとなっています(少し(5分くらい)ハマりました)。現在、JavaまわりのAPIが過渡期なので注意してください。


終わり

なんか、Gradle2.9rc-1のリリースノートを読み直したら、JavaOneに合わせてか、Jigsaw関連のexportモジュール関連のリリースノートが追加されていました。いや、もしかしたら、僕がJigsawわかってなかったから意識的に読まなかったのかもしれませんが…

ここまで、読んでこられて、かつ、Java9の事情にある程度詳しい皆様なら薄々感づいていらっしゃると思いますが、JVM component modelはGradleのJava9対応でもあるわけです。そのあたりのところを次回(あるのか????)やっていきたいとおもいます。