mike-neckのブログ

Java or Groovy or Swift or Golang

GradleのJVM component modelスタイルのプロジェクトをJigsawでビルドする #gradle #java #jigsaw

f:id:mike_neck:20151108133321p:plain

前回のエントリーでGradleにおけるJVM component modelの実践的な内容を紹介した。

mike-neck.hatenadiary.com

ここで紹介したプロジェクトのビルド成果物はこちらのエントリーでも紹介した、Javaモジュールシステム的な考えが採用された成果物である。つまり、強いカプセル化によって、外部APIと内部用のクラスが厳格に切り分けられた成果物である。

さて、今回のエントリーでは前回のエントリーで使用したプロジェクトをJigsawでビルドする試みである。


まず、前回のおさらい。なお、括弧内の表記はGradleでの用語である。

プロジェクト(コンポーネント)の構成

プロジェクト(コンポーネント)は次のモジュール(ライブラリー)によって構成されている。

  • exceptionsモジュール(ライブラリー)
  • functionsモジュール(ライブラリー)
  • dataモジュール(ライブラリー)
  • test.frameworkモジュール(ライブラリー)
  • data.testモジュール(ライブラリー)

これらのモジュールは次のような依存関係にある。

f:id:mike_neck:20151126014918p:plain

module-info.javaの記述

依存関係を考慮して次のように記述している。

exceptionsモジュール
module exceptions {
    exports com.sample;
}
functionsモジュール
module functions {
    requires public exceptions;
    exports com.sample.func.api;
}
dataモジュール
module data {
    exports com.sample.data.api;
    requires public functions;
}
test.frameworkモジュール
module test.framework {
    exports test;
}
data.testモジュール
module data.test {
    requires data;
    requires test.framework;
}

コンパイル

コンパイラーはJDK9 Early Access with Project Jigsawのページからダウンロードする。

ダウンロードしたものを解凍する(Mac OS X版の場合)と、jdk1.9.0というディレクトリーが作成され、その中のContents/Home/binの中にjavacやらjarやらビルドに必要なものが含まれている。

今回はGradleをJava9で動かすことが目的ではないので、これらをPATHには通さずに、コマンドラインから叩いて行く予定だったが、一つだけ致命的な問題があり、Gradleから叩いていくことにした。

致命的な問題というのは、次のようなものである。

Maven/Gradleスタイルのディレクトリー構成(Javaソースコードの位置がsrc/main/javaにあるタイプ)、もしくはGradleのJVM component modelスタイルのディレクトリー構成(Javaソースコードsrc/lib.name/javaにある)を取る場合に、モジュール名を解決できないためにコンパイルに失敗する。

もちろん、build.gradleを書き直すことで、Jigsawが求めるディレクトリー構成にすることもできなくはない。

model {
  components {
    data(JvmLibrarySpec) {
      def libName = delegate.name
      targetPlatform 'java8'
      api {
        exports 'com.sample.data.api'
        dependencies {
          library 'functions'
        }
      }
      sources {
        java {
          source {
            srcDir "src/${libName}"
          }
        }
      }
    }
  }
}

モジュール(ライブラリー)が一つだけなら、まだなんとかなるかもしれないが、プロジェクト(コンポーネント)には5つのモジュール(ライブラリー)が含まれている。これをすべてに適用するととたんにメンテナンスができないビルドスクリプトが出来上がってしまうことは想像に難くない。

そういうわけで、(1)Gradleで一度ソースコードをJigsawスタイルのディレクトリー構成にコピーするタスクを実行してから、(2)javacを叩くタスクを起動することにした。

(1)と(2)のタスクの定義は次のとおり

ext {
  jigsawBuild = 'Jigsaw Build'
  jigsaw = 'jigsaw'
  copySrcTask = 'copyAsJigsawStructure'
  compileJigsaw = 'compileJigsaw'
  jdkBin = "${projectDir}/jdk1.9.0.jdk/Contents/Home/bin"
}
model {
  components {
    // library の定義
  }
  tasks {
    // ソースのコピー
    $.components.each {lib ->
      create("${lib.name}Copy", Copy) {
        into "${buildDir}/${jigsaw}/src/${lib.name}"
        lib.sources.java.source.srcDirs.each {
          from fileTree(it)
        }
      }
    }
    // ソースコピーの同期
    create(copySrcTask) {
      group = jigsawBuild
      $.components.each {lib ->
        dependsOn "${lib.name}Copy"
      }
    }
    // コンパイル
    create(compileJigsaw, Exec) {
      // コンパイルしたソースの保存先
      def destDir = file("${buildDir}/${jigsaw}/tmp")

      // ソースコピー完了後にコンパイル実行
      dependsOn copySrcTask
      group = jigsawBuild

      // 出力先の設定
      outputs.dir destDir

      // コマンド実行ディレクトリー
      workingDir "${buildDir}/${jigsaw}"

      // javacコマンドの指定
      def javac = "${jdkBin}/javac"

      // 実行コマンドの指定
      def commands = [javac,'-d', 'tmp', '-modulesourcepath', 'src']
      commands += $.components.collect {lib ->
        lib.sources.java.source.srcDirs.collect {
          project.fileTree(it).files
        }
      }.flatten().collect {
        it.absolutePath.replace("${projectDir}/", '').replace('/java/', '/')
      }

      commandLine commands

      // コンパイル結果ディレクトリーを事前に作成しておく
      doFirst {
        if (!destDir.exists()) {
          destDir.mkdirs()
        }
      }
  }
}

なお、Jigsawによるコンパイルのやり方はこちらのドキュメントを参考にしました。

これらのタスクは

  1. 各ライブラリーのソースコードを一度build/jigsaw/src/にモジュールごとにコピーする
  2. コピーしたソースコードをモジュールを認識させつつ、コンパイルしてbuild/jigsaw/tmpに保存する

というものです。

上記スクリプトの中で、$というオブジェクトが登場していますが、これはmodel{}ブロック内でのみ利用できるmodelオブジェクトです。既に値が確定した後のオブジェクトを利用することができます。この機能はGradle2.9より登場しています。

コンパイルの実行

上記のタスクにより、Jigsawでのクラスファイル生成を行うことができます。

$ gradle compileJigsaw
:data.testCopy
:dataCopy
:exceptionsCopy
:functionsCopy
:test.frameworkCopy
:copyAsJigsawStructure
:compileJigsaw
src/data.test/com/sample/Main.java:16: 警告: package exists in another module: exceptions
package com.sample;
^
src/exceptions/com/sample/EvaluationException.java:16: 警告: package exists in another module: data.test
package com.sample;
^
src/exceptions/com/sample/ExecutionException.java:16: 警告: package exists in another module: data.test
package com.sample;
^
警告3個

BUILD SUCCESSFUL

Total time: 3.505 secs
$ tree build/jigsaw/
build/jigsaw/
├── src
│   ├── data
│   │   ├── com
│   │   │   └── sample
│   │   │       └── data
│   │   │           ├── api
│   │   │           │   └── Maybe.java
│   │   │           └── internal
│   │   │               ├── MaybeBase.java
│   │   │               ├── Nothing.java
│   │   │               └── Some.java
│   │   └── module-info.java
│   ├── data.test
│   │   ├── com
│   │   │   └── sample
│   │   │       ├── Main.java
│   │   │       └── tests
│   │   │           ├── NothingTest.java
│   │   │           └── SomeTest.java
│   │   └── module-info.java
│   ├── exceptions
│   │   ├── com
│   │   │   └── sample
│   │   │       ├── EvaluationException.java
│   │   │       └── ExecutionException.java
│   │   └── module-info.java
│   ├── functions
│   │   ├── com
│   │   │   └── sample
│   │   │       └── func
│   │   │           ├── api
│   │   │           │   ├── Condition.java
│   │   │           │   ├── ExCondition.java
│   │   │           │   ├── ExFunction.java
│   │   │           │   ├── ExOperation.java
│   │   │           │   ├── Function.java
│   │   │           │   ├── Operation.java
│   │   │           │   ├── Synthesis.java
│   │   │           │   └── Verifications.java
│   │   │           └── internal
│   │   │               └── Operation.java
│   │   └── module-info.java
│   └── test.framework
│       ├── module-info.java
│       └── test
│           ├── Execute.java
│           ├── Test.java
│           ├── TestSuite.java
│           ├── exception
│           │   ├── Difference.java
│           │   ├── TestExecutionException.java
│           │   └── TestFailureException.java
│           ├── exec
│           │   ├── ExecutionManager.java
│           │   ├── Sorter.java
│           │   ├── TestCases.java
│           │   ├── TestExecutor.java
│           │   ├── TestResults.java
│           │   └── print
│           │       ├── ColorPrinter.java
│           │       └── ResultPrinter.java
│           └── result
│               ├── Accident.java
│               ├── Failure.java
│               ├── Panic.java
│               ├── Result.java
│               ├── Statistics.java
│               └── Success.java
└── tmp
    ├── data
    │   ├── com
    │   │   └── sample
    │   │       └── data
    │   │           ├── api
    │   │           │   └── Maybe.class
    │   │           └── internal
    │   │               ├── MaybeBase.class
    │   │               ├── Nothing.class
    │   │               └── Some.class
    │   └── module-info.class
    ├── data.test
    │   ├── com
    │   │   └── sample
    │   │       ├── Main.class
    │   │       └── tests
    │   │           ├── NothingTest$1.class
    │   │           ├── NothingTest$SomethingWrong.class
    │   │           ├── NothingTest.class
    │   │           └── SomeTest.class
    │   └── module-info.class
    ├── exceptions
    │   ├── com
    │   │   └── sample
    │   │       ├── EvaluationException.class
    │   │       └── ExecutionException.class
    │   └── module-info.class
    ├── functions
    │   ├── com
    │   │   └── sample
    │   │       └── func
    │   │           ├── api
    │   │           │   ├── Condition.class
    │   │           │   ├── ExCondition.class
    │   │           │   ├── ExFunction.class
    │   │           │   ├── ExOperation.class
    │   │           │   ├── Function.class
    │   │           │   ├── Operation.class
    │   │           │   ├── Synthesis.class
    │   │           │   └── Verifications.class
    │   │           └── internal
    │   │               └── Operation.class
    │   └── module-info.class
    └── test.framework
        ├── module-info.class
        └── test
            ├── Execute.class
            ├── Test$1.class
            ├── Test$Actual.class
            ├── Test$Asserting.class
            ├── Test$ExSupplier.class
            ├── Test$Step.class
            ├── Test.class
            ├── TestSuite.class
            ├── exception
            │   ├── Difference.class
            │   ├── TestExecutionException.class
            │   └── TestFailureException.class
            ├── exec
            │   ├── ExecutionManager$1.class
            │   ├── ExecutionManager.class
            │   ├── Sorter.class
            │   ├── TestCases$1.class
            │   ├── TestCases$ResultCollector.class
            │   ├── TestCases.class
            │   ├── TestExecutor.class
            │   ├── TestResults.class
            │   └── print
            │       ├── ColorPrinter.class
            │       ├── ResultPrinter$AbstractPrinter.class
            │       ├── ResultPrinter$FailType.class
            │       ├── ResultPrinter$FailurePrinter.class
            │       ├── ResultPrinter$Printer.class
            │       ├── ResultPrinter$SuccessPrinter.class
            │       └── ResultPrinter.class
            └── result
                ├── Accident.class
                ├── Failure.class
                ├── Panic.class
                ├── Result.class
                ├── Statistics$1.class
                ├── Statistics$Builder.class
                ├── Statistics$StatisticsCollector.class
                ├── Statistics.class
                └── Success.class

52 directories, 102 files

JDK9 ea b-86の問題

ここまできたらパッケージングなのだが、jdk 1.9.0-ea-jigsaw-nightly-h3660-20151022-b86という僕が使った(2015/11/25入手できた)バージョンのJigsaw入のJDK9ではjarコマンドが壊れていて、パッケージングができなかった。

パッケージング方法は先のドキュメントに記載されている。

$ cd build
$ cd jigsaw
$ ../../jdk1.9.0.jdk/Contents/Home/bin/jar --create --file=libs/exceptions-0.1.jar --module-version 0.1 -C tmp/exceptions/
jar: You must specify one of -ctxui options.
Try `jar --help' for more information.
$ ../../jdk1.9.0.jdk/Contents/Home/bin/jar -c --file=libs/exceptions-0.1.jar --module-version 0.1 -C tmp/exceptions/
jar: You must specify one of -ctxui options.
Try `jar --help' for more information.

というわけで、現在(2015/11/25)の時点のJigsawではまだパッケージングができないようである。

したがって、唐突ではあるがこのエントリーもここで中断せざるを得ない。


結論

Javaモジュールシステムの完成度において、

と言える。


Gradle3におけるJavaプロジェクトのビルド入門(4) -- 実践編 #gradle

f:id:mike_neck:20151108133321p:plain

Gradle3におけるJavaプロジェクトのビルド入門の一連の記事ではJVM component modelによるJavaプロジェクトのビルド方法について紹介してきた。その過程でJDK9より導入されるJigsawとの関連も指摘しておいた。

この記事では実践的なJVM component modelによるJavaプロジェクトのビルドについて、(簡単ではあるが)若干複雑な例を紹介していく。例題としてはJava8で随分とおなじみになっているOptionalの再実装をおこなう。


コンポーネントの設計

Optionalを使う場合、主に使われるのが次のような機能群である。

  1. 例外(ライブラリー名:exceptions) - Javaなので、完全に関数型スタイルでのプログラミングができないため、Optional自体にmapfmapなどのメソッドを定義することになる。その際にnullを渡してしまった場合などの例外を定義してある。また、Java8のOptionalは検査例外を投げる関数などを引数に取ることができないが、そのような関数を引数にとる場合にRuntimeExceptionにラップした例外なども定義してある。
  2. 関数(ライブラリー名 : functions) - Optionalの各種メソッドmapfmapfilterなどに渡すための関数型インターフェースを定義してある。また、検査例外を投げる関数型インターフェースおよび、それをRuntimeExceptionにラップしてしまう変換のためのメソッド群も定義してある。ラップした例外を送出するためにAPIとしてexceptionsライブラリーに依存する。
  3. データ(ライブラリー名 : data) - Optionalを定義しているライブラリー。実際はOptionalという名前ではなく、Maybeという名前なのだが…。また、Maybeは単なるインターフェースであり、それらの実装クラスとしてSomeNothingを定義してある。これらは関数を引数に取るためにAPIとしてfunctionsライブラリーに依存している他、実際に例外を送出するためにexceptionsライブラリーにもAPI的に依存している
  4. テストフレームワーク(ライブラリー名 : test.framework) - Gradle2.9では外部ライブラリーを解決できないために、仕方なくつくった簡単で機能がほとんどないユニットテストライブラリー。詳しくはこちらを参照
  5. テスト(ライブラリー名 : data.test) - 最終目的であるライブラリーdataを検証するためのテストを記述しているライブラリー。当然だが、dataライブラリー、test.frameworkライブラリーに依存している。

文章で書くと、丁寧に説明できる分、理解に時間がかかるし、僕の文章は語彙が貧弱なためもあるし、気取った文章も書けないので、上記の依存関係を簡単な図に表しておく。google図形描画で描いたので、綺麗な図ではないが、別に理解の妨げになるとは思わない。

f:id:mike_neck:20151126014918p:plain


各ライブラリーのDSL

exceptionsライブラリー

これは特に依存するライブラリーもないので、最も簡単に記述できる。なお、別にJava8にこだわる必要もなかったので、あえてJava6でコンパイルしている。もちろんJava6でなくてJava8でもよい。

model {
  components {
    exceptions(JvmLibrarySpec) {
      targetPlatform 'java6'
    }
  }
}

なお、ソースコードは以下のディレクトリーに格納していく。

  • javaソース : src/exceptions/java
  • resources : src/exceptions/resources

functionsライブラリー

これは以下の仕様を満たさなければならないライブラリーである。

  • exceptionsライブラリーに依存する。
  • 関数合成をおこなうためにインターフェースにdefaultメソッドを付与したかったためJava8でコンパイルする必要がある
  • その他の(モノイド的な)関数合成や、関数のnullチェックのためのクラスを定義しており、それらのクラスのインスタンス化を防ぐために内部のみで使われるクラスを持っている。そのため、APIexports指定が行う必要がある。

以上の仕様を満たすためにDSLは次のようになっている。

model {
  components {
    functions(JvmLibrarySpec) {
      targetPlatform 'java8'
      api {
        exports 'com.sample.func.api'
        dependencies {
          library 'exceptions'
        }
      }
    }
  }
}

ソースコードの格納ディレクトリーは下記の通り

  • javaソース : src/functions/java
  • resources : src/functions/resources

dataライブラリー

これは次のような仕様をもつライブラリーである。

  • Maybeの実装クラスであるSomeNothingを極力簡単にしたいため、Maybeインターフェースにstaticメソッドを実装するためにtargetPlatformはJava8である必要がある(まあ、別にMaybeを抽象クラスにしても構わないのだが…)。
  • 関数を引数としてとるためにfunctionsライブラリーにAPI的に依存している。
  • 関数にnullを渡された場合にexceptionsライブラリーで容易した例外を送出するが、GradleではAPIのtransitive dependencyの制御はできないので、functionsライブラリーにAPI的に依存した結果、exceptionsライブラリーもAPI的に依存しているため、特に記述を追加する必要はない。
  • SomeNothingも実装クラスは別にユーザーから見えても仕方ないので、これらはexportsしない。

以上の条件を満たすために、dataライブラリーのDSLは次のようになっている

model {
  components {
    data(JvmLibrarySpec) {
      targetPlatform 'java8'
      api {
        exports 'com.sample.data.api'
        dependencies {
          library 'functions'
        }
      }
    }
  }
}

ソースコードの格納ディレクトリーは下記の通り

  • javaソース : src/data/java
  • resources : src/data/resources

test.frameworkライブラリー

もともとこれはtest-frameworkライブラリーとして定義していたのだが、Jigsawで-(ハイフン)をモジュール名として利用できないことが後でわかったために、-.に変更してtest.frameworkとして定義することになった。

これは特に何かに依存するライブラリーでもないが、以前の記事に記述したようにラムダ式を使ってテストデータの変換を行っていくためにJava8で記述している。したがって、そのDSLはこのような形になっている。

model {
  components {
    'test.framework'(JvmLibrarySpec) {
      targetPlatform 'java8'
    }
  }
}

ソースコードの格納ディレクトリーは下記の通り

  • javaソース : src/test.framework/java
  • resources : src/test.framework/resources

data.testライブラリー

これは単にテストを実行するだけのライブラリーである。したがって、何らかのライブラリーをexportsする必要もないので、api{}ブロックを使わず、dataライブラリー、test.frameworkライブラリーへの依存関係を記述している。

model {
  components {
    'data.test'(JvmLibrarySpec) {
      targetPlatform 'java8'
      sources {
        java {
          dependencies {
            library 'data'
            library 'test.framework'
          }
        }
      }
    }
  }
}

ソースコードの格納ディレクトリーは下記の通り

  • javaソース : src/data.test/java
  • resources : src/data.test/resources

以上、個々のライブラリーのDSLを書くためにいちいちcomponents{model{}}の記述をとったが、これらはひとまとまりにして構わない。したがって、modelの記述は次のようになっている。

model {
  components {
    exceptions(JvmLibrarySpec) {
      targetPlatform 'java6'
    }
    functions(JvmLibrarySpec) {
      targetPlatform 'java8'
      api {
        exports 'com.sample.func.api'
        dependencies {
          library 'exceptions'
        }
      }
    }
    data(JvmLibrarySpec) {
      targetPlatform 'java8'
      api {
        exports 'com.sample.data.api'
        dependencies {
          library 'functions'
        }
      }
    }
    'test.framework'(JvmLibrarySpec) {
      targetPlatform 'java8'
    }
    'data.test'(JvmLibrarySpec) {
      targetPlatform 'java8'
      sources {
        java {
          dependencies {
            library 'data'
            library 'test.framework'
          }
        }
      }
    }
  }
}

ビルド

各ライブラリーのJarへのパッケージングはassembleタスクのdependsOnに指定されているので、assembleタスクを実行すればよい。また、buildタスクはassembleタスクに依存しているので、buildタスクの実行でも良い。

各ライブラリーは前回のAPIの説明にもあった通り、apiJarと内部用のjarの二種類が生成される。それらはbuild/jarsディレクトリーの下に生成される。それらも含めて確認してみたい。

$ gradle assemble
:compileExceptionsJarExceptionsJava
警告: [options] ブートストラップ・クラスパスが-source 1.6と一緒に設定されていません
警告1個
:createExceptionsJar
:createExceptionsApiJar
:exceptionsJar
:compileFunctionsJarFunctionsJava
:createFunctionsJar
:createFunctionsApiJar
:functionsJar
:compileDataJarDataJava
:createDataJar
:createDataApiJar
:dataJar
:compileTest.frameworkJarTest.frameworkJava
:createTest.frameworkJar
:createTest.frameworkApiJar
:test.frameworkJar
:compileData.testJarData.testJava
:createData.testJar
:createData.testApiJar
:data.testJar
:assemble

BUILD SUCCESSFUL

Total time: 2.247 secs
$ tree build/jars
build/jars/
├── data.testApiJar
│   └── data.test.jar
├── data.testJar
│   └── data.test.jar
├── dataApiJar
│   └── data.jar
├── dataJar
│   └── data.jar
├── exceptionsApiJar
│   └── exceptions.jar
├── exceptionsJar
│   └── exceptions.jar
├── functionsApiJar
│   └── functions.jar
├── functionsJar
│   └── functions.jar
├── test.frameworkApiJar
│   └── test.framework.jar
└── test.frameworkJar
    └── test.framework.jar

10 directories, 10 files

以上から明らかなようにライブラリー1つにつき、APIのjarとinternal jarが1つずつ作成されている。基本的には参照するjarの管理はGradleの方で行ってくれるが、別のコンポーネントを作成する場合はfooApiJarの中に入っている方のjarファイルを参照する。

例えば、dataApiJarおよびdataJarを解凍した場合、次のようなファイル構成になっている。

$ mkdir -p build/files/data/api build/files/data/internal
$ unzip -q build/jars/dataApiJar/data.jar -d build/files/data/api
$ unzip -q build/jars/dataJar/data.jar -d build/files/data/internal
$ tree build/files/data/
build/files/data/
├── api
│   ├── META-INF
│   │   └── MANIFEST.MF
│   └── com
│       └── sample
│           └── data
│               └── api
│                   └── Maybe.class
└── internal
    ├── META-INF
    │   └── MANIFEST.MF
    └── com
        └── sample
            └── data
                ├── api
                │   └── Maybe.class
                └── internal
                    ├── MaybeBase.class
                    ├── Nothing.class
                    └── Some.class

13 directories, 7 files

dataApiJarではない方のjarファイル(dataJar/data.jar)を参照して次のコンポーネントを作成した場合、dependency hellに陥ることは目に見えている。

テストの実行

せっかくtest.frameworkを作ったことだし、data.testライブラリーがあるので、テストを実行しましょう。

現在のGradleにはリンカーもないし、Main属性を与えるDSLもないため、Gradleの知識を総動員して、テスト実行タスクを作成します。利用できる条件は次のとおりです。

  • ライブラリーのjarパッケージングタスクはassembledependsOn対象になっている
  • jarパッケージングタスクは調査の結果、JarBinarySpecインスタンス
  • 生成される二つのjarファイルはタスクのgetJarFile()getApiJarFile()メソッドで取得できる

これで必要なjarを集めることが可能で、mainメソッドを持つクラスも判明しているので、テスト実行タスクは次のように書くことができます。

task dataTestExec(type: JavaExec, dependsOn: 'assemble') {
  def jarFiles = tasks.assemble.dependsOn.findAll {
    it instanceof JarBinarySpec
  }.collect {
    [it.jarFile, it.apiJarFile]
  }.flatten()
  classpath = files(jarFiles)
  main = 'com.sample.Main'
}

テストの実行の結果は以前のエントリーにも掲載した通りです。

f:id:mike_neck:20151121231945p:plain

よくあるハマりやすいこと

非公開パッケージで抽象クラスを作成して、それを継承したクラスを公開パッケージにいれるパターン

package com.foo.api;
import com.foo.internal.AbstractInternalType;
public class ApiType extends AbstractInternalType {
  // 実装省略
}

これはコンパイル時にAbstractInternalTypeを解決しようとすると非公開パッケージに到達して、アクセスできなくなるために、コンパイルエラーとなる。これは推測ではあるが、API jarは、そのAPI jarが依存する別のAPI jarのみを参照して、非公開パッケージの方のjarファイルを参照せずにコンパイルしているためだと考えられる。しかし、--infoを用いて出力される情報を見てみたが、JDKのCompiler APIをいじったコンパイラーを利用しているようなので、具体的な内容についてはわからなかった。


結論

以上がGradle3におけるJVM component model によるJavaプロジェクトのビルドである。

なお、この機能はまだincubatingが外されていない不安定なAPIとされているため、今後に変更が入るかもしれない。特にmainのあるクラスのMANIFEST.MFをいじることができないことや、外部依存性解決機能がないことなど、機能がまだまだ不十分である(そのためにテスティングフレームワークも自作せざるをえなかった)。外部依存性解決機能についてはGradle2.10にて導入されるという話を伺ってはいるが、実際にそれが使い物になるかどうかはまだわからない。また、Jigsawにあるrequire publicのようなtransitive dependencyの選択機能も取り込まれるかもしれない。

とはいえ、ここまでで紹介した基本的な機能で大分安定してビルドが可能なので、ここまでで紹介したDSLが変更されることもないと思われる。実際に、Gradleのコミットログを追っていると、このあたりに関する大きな変更がないためである。

次のエントリーではこのプロジェクトをJigsawでコンパイルすることを試みたいと思う。

なお、今回のプロジェクトの内容は以下のレポジトリーから入手可能である。

github.com


おわり


Gradle3におけるJavaプロジェクトのビルド入門(3) -- 用語の再確認とJigsawとの関連 #gradle

f:id:mike_neck:20151108133321p:plain

今回はJVM component modelにおける用語について整理しておく。


前回、前々回とGradle3におけるJavaプロジェクトのビルドにおけるJVM component modelの入門を行っていった。

mike-neck.hatenadiary.com

mike-neck.hatenadiary.com

また、JVM component modelに関連する知識としてJigsawについて簡単なまとめを行った。

mike-neck.hatenadiary.com

以上のエントリーの中で、僕は公式の(ただし今後変更される可能性のある)用語を幾つか混同していたので、それらをまとめておきたいと思う。また、公式のリンクは今後のリリースにともなって変更される可能性があるので、注意されたい。


用語

まず、基本的な用語を抑えておく

コンポーネント(component)

A component is a general concept for a piece of software that might be deliverable. Example of components are a standalone application, a web application, a library, etc. A component is often composed of other components.

コンポーネントは配布可能なソフトウェアの一部分を意味する。コンポーネントの例としてはスタンドアローンアプリケーション、ウェッブアプリケーション、一般的にライブラリーと呼ばれるものなどである。コンポーネントは他のコンポーネントとともに構成される。

以上が定義である。したがって、プロジェクトで成果物として作成されるものとして指定されるのがコンポーネントとなる。

ライブラリー(library)

A library is a buildable component. In the Java world, a library is often assimilated to a Jar file, but while a Jar file represents an output, a library is the description of how the output is build. A library is defined by the combination of its source sets and variants.

ライブラリーはビルド可能なコンポーネントである。一般的にライブラリーはjarファイルと同等視されている。しかしGradle3ではjarファイルは単なる出力であり、ライブラリーは成果物のビルド方法の定義である。ライブラリーは後に説明するソースセットとバリアントの組み合わせによって定義される。

ソースセット(source set)

A source set represents a logical group of source files in a component. Ass such, a source set is often an input to a single compilation task, which will produce an output(classes, compiled CSS, etc). A library may consist of multiple source sets.

ソースセットが表すのはコンポーネント内部のソースファイルの論理的グループである。したがって、ソースセットは一つのコンパイルタスクの入力である。なお、コンパイルタスクは出力(クラスファイルやコンパイルされたCSSなど)を生成する。ライブラリーは複数のソースセットで構成される。

バリアント(variant)

A variant represents a modulation of a component. A library, for example, might target Java 6 and Java 7, effectively producing two distinct outputs: a Java 6 jar and a Java 7 jar. In this case, the target platform is an example of a variant dimension. Custom library types may define their own variant dimensions, which will participate in dependency resolution.

バリアントが表すのはコンポーネントの種類である。ライブラリーは、例えば、Java6およびJava7を対象としており、結果として二つの異なる出力を生成する。Java6用のjarとJava7用のjarである。この例では対象プラットフォームのバリアント値を表している。ライブラリーによっては異なるバリアント値を取ることがあり、その値は依存性解決に利用されることがある。

バイナリー(binary)

A binary represents the output of a library. Given a combination of variants, a library may produce multiple binaries. A binary is often a consumable artifact of other components.

バイナリーはライブラリーの出力を表す。バリアントの組み合わせによってはライブラリーは複数のバイナリーを生成する。バイナリーは他のコンポーネントによって利用される成果物である。


また、基本的な用語に加え、次の一般的にも用いられている用語も利用する

API

  • An API is a set of classes, interfaces, methods that are exposed to a consumer.
  • APIはクラス、インターフェース、メソッドなどの集合であり、利用者に公開するものである
  • An API specification is the specification of classes, interfaces or methods that belong to an API. It can be found in various forms, like module-info.java in Jigsaw, or the api{...} block that Gradle defines as part of those stories. Usually, we can simplify this to a list of packages, called exported packages.
  • API仕様はAPIに含まれているクラス、インターフェース、メソッドの仕様である。これは様々な方法で定義されうる。Jigsawであればmodule-info.javaにて定義する。一方Gradleではapi{}ブロックを利用する。通常、公開パッケージというパッケージのリストを指定することで簡略化する。
  • A runtime jar consists of API classes and non-API classes used at execution time. There can be multiple runtime jars depending on combinations of the variant dimensions: target platform, hardware infrastructure, target application server, ...
  • ランタイムjarはAPIクラスと実行時にのみ使われる非APIクラスによって構成される。バリアント値にの組み合わせによっては複数のランタイムjarが存在しうる。ターゲットプラットフォーム、ハードウェア構成、ターゲットアプリケーションサーバーなど
  • API classes are classes of a variant which match the API specification
  • APIクラスはAPI仕様に一致するクラスである
  • Non-API classes are classes of a variant which do not match the API Specification. They can be referred to as internal classes.
  • APIクラスはAPI仕様に一致しないクラスである。内部クラスとして参照されるクラスである。
  • A stubbed API class is an API class for which its implementation and non public members have been removed. It is meant to be used when a consumer is going to be compiled against an API.
  • スタブAPIクラスはAPIクラスであり、その実装クラスおよび非publicメンバーを取り除いたクラスである。これは利用者がAPIを用いてコンパイルを行い際に用いられる。
  • An API jar is a collection of API classes. There can be multiple API jars depending on the combinations of variant dimensions.
  • API jarはAPIクラスを集めたものである。バリアント値の組み合わせにより複数API jarが存在しうる。
  • A stubbed API jar is a collection of stubbed API classes. There can be multiple stubbed API jars depending on the combinations of variant dimensions.
  • スタブAPI jarはスタブAPIクラスを集めたものである。バリアント値の組み合わせにより複数API jarが存在しうる。
  • An ABI(application binary interface) corresponds to the public signature of an API, that is to say the set of stubbed API classes that it exposes(and their API visible members).
  • ABI(アプリケーション・バイナリー・インターフェース)はAPIの公開署名にも対応している。つまり、公開されたスタブAPIクラスである。

Jigsawとの関連

次にJigsawとの関連を引用し、Jigsawの用語とJVM component modelの用語の対応を確認したい。

Gradleの公式ドキュメントでJigsawへの言及があるのは70.10 Enforcing API boundaries at compile time(API境界の強制をコンパイル時に行う)である。

Often a library will contain many types that -- despite having public visibility -- are intended for internal use only within that library. JDK 9 will introduce Jigsaw, the reference implementation of the Java Module System, which will provide both compile-time and run-time enforcement of strong encapusulation at the library (aka: module) level. This means that packages not intended for public consumption can remain private to the library, regardless whether the types within those packages have public visibility.

Gradle anticipates the arrival of JDK 9 and the Java Module System with an approach to specifying and enforcing strong encapsulation at compile-time. However, users need not wait for the arrival of JDK 9 to use it; this feature can be used on any version of the Java platform that Gradle supports.

--たとえアクセス修飾子がpublicであるにもかかわらず--ライブラリー内部だけでのみ使うように意図された型がライブラリーに含まれることはよくある。JDK 9が導入するJigsawは、Javaモジュールシステムの参照実装であるが、コンパイル時および実行時に強いカプセル化をライブラリー(モジュールとして知られている)に提供する。これによって、利用者に使われたくないパッケージ、たとえpublicにアクセス可能なパッケージであっても、それをライブラリー内だけで使えるようにすることができる。

GradleではJDK 9および、コンパイル時に強いカプセル化をおこなうアプローチのJavaモジュールシステムの到来を予想している。もちろんユーザーはJDK9のリリースを待つ必要はない。この機能(コンパイル時に強いカプセル化をおこなうこと)はGradleがサポートするすべてのJavaプラットフォームで利用できる。

ここで「強いカプセル化をライブラリー(モジュールとして知られている)」に着目すると、Gradle3のJVM component modelとJigsawの関係性が見えてくるようになる。

- Gradle Jigsaw
コンパイルの単位 library module
公開パッケージの指定 api{}ブロックでのexports(String)メソッド exports パッケージ名
依存ライブラリ(モジュール)の指定 api.dependencies{}ブロックでのlibrary(String)メソッド requires パッケージ名
推移的依存ライブラリ(モジュール)の指定 なし(すべて依存ライブラリーに推移的依存が適用される) requires public パッケージ名
サービスローダー なし ユーザーモジュール : uses FQCN名
提供モジュール : provides FQCN名 with FQCN(実装)名
リンカー なし jlinkコマンド
ソースセット java{}ブロックのsourceSet{}ブロックで指定 概念がない
バリアント targetPlatform(String)メソッド 概念がないし、Java9固定

かなり中途半端な終わり方になるが、Gradle3 JVM component modelによるビルドとJigsawの関連が少し見えてきたのではないかと思う。また、自己中心的な考えだが、Gradleでの用語の確認ができたところが心強い。

次回は同一のプロジェクトをJigsawとGradle(2.9)とでビルドしてみたいと思う。

おわり


簡単な特徴もないJava用のテスティングフレームワークつくった

諸事情により、JUnitが使えない状況でコードを書いていたのですが、書いたコードが動くかどうか確認したい衝動に駆られて、簡単な特徴もないテスティングフレームワークを作りました。

仕様

最初に書いた仕様がこれ。

github.com

public class MaybeTest extends Test {
    @Examination
    public void mapStringLengthRemainsSome() {
        setup(() -> some("stringLength=15"))
                .when(maybe -> maybe.map(String::length))
                .then(equalsTo(15));
    }
}

そのときはこんなイメージでした。

  • setup(Supplier<? extends T>)でテスト対象のオブジェクトを生成する
  • when(Function<? super T, ? extends N>)でテスト対象のオブジェクトに操作を加えていく
  • thenでアサートの準備
  • equalsTo(V)で比較を実施

なお、作っていく最中でthenメソッドthen(Function<? super T, ? extends V>)に変更して、実際の値を取得するように変更して、その後のequalsTo(V)で比較をする形に変更しました。

テストスイート

あと、テストクラスをスキャンしていくのは面倒だったし、作りこみたくなかったので、

TestSuite.run(Class<? extends Test>...)

というメソッドでテストしたいクラスだけ指定してテストするようにしました。

テスト結果の表示

ある程度出来上がった所で、テスト結果くらいは見栄え良く表示したいなと思い、

mike-neck.hatenadiary.com

これでコンソールの文字列の色の変え方を勉強しつつ、表示用のクラスの仕様を適当に書いておきました。

github.com

で、ある程度、テスト表示用のクラスを書いた所で、最後に表示の仕方を間違えないように次のようなコメントを書いてから実装しました。

    private static class SuccessPrinter extends AbstractPrinter {
        @Override
        public void print() {
            // クラス名(緑)
            // 以下繰り返し
            //     - テスト名(緑)
        }
    }

    private static class FailurePrinter extends AbstractPrinter {
        @Override
        public void print() {
            // クラス名(赤)
            // 以下繰り返し
            // Successの場合
            //     - テスト名(緑)
            // Failureの場合
            //     - テスト名(赤)
            // diff(赤)
            // Accidentの場合
            //     - テスト名(黄)
            // explanation(normal)
            // Panicの場合
            //     - テスト名(黄)
            // cause(normal)
        }
    }

感謝

テストの実行自体をExecutorServiceでクラスごとに並行して実行させたくて、かつ終了したら次の処理(テスト結果表示→テスト結果集計→集計結果表示)をやろうとして、CompletableFuture.allOf(CompletableFuture<?>...)を使おうとしたわけですが、StreamRunnableをボコボコ生成して、CompletableFuture.runAsyncに渡してCompletableFuture<Void>を生成してて、これを配列化しようとしていた所、new CompletableFuture<Void>[size]をしようとすると、Generic Array creationにひっかかってしまって、コンパイルエラーになるという事態が発生。

対策としてclass VoidFuture extends CompletableFuture<Void>{}というクラスを作って、new VoidFuture[size]をやりましたが、こんどはArrayStoreExceptionが発生(VoidFutureCompletableFuture<Void>ではないので、配列に詰め込むことができない)。

テンパッて壁を殴って4個くらい穴を開けながらツイートしていた所、

@makingさんうらがみさんから、いろいろと助言をいただきました。

結果的に、CompletableFuture.allOf(CompletableFuture<?>...)に対して次のようにすることで、すべてのテスト結果の同期を図ることができました。

    public CompletableFuture<Void> run() {
        return CompletableFuture.allOf(tests.stream()
                .map(createCases)
                .<Runnable>map(runners(queue))
                .map(r -> CompletableFuture.runAsync(r, exec))
                .collect(toList()).toArray(new CompletableFuture<?>[tests.size()]));
    }

new CompletableFuture<?>[size]だとGeneric array creationに引っかからないんですね…

まあ、お二方ともご協力ありがとうございます。

できた代物

で、こんな感じのテストができました。

テストスイートにテストを指定する
public class Main {
    public static void main(String[] args) {
        TestSuite.run(NothingTest.class, SomeTest.class);
    }
}
テストクラスそのもの
import static com.sample.data.api.Maybe.some;
import static com.sample.data.api.Maybe.nothing;

public class SomeTest extends Test {
    @Execute
    public void someIsSomeReturnsTrue() {
        setup(() -> some(20))
                .then(Maybe::isSome)
                .equalsTo(true);
    }
    @Execute
    public void mapperReturnsNullOnMapItBecomesNothing() {
        setup(() -> some("foo"))
                .when(m -> m.map(s -> null))
                .then(Maybe::isSome)
                .equalsTo(false);
    }
    @Execute
    public void someFmapedToNothingThenItBecomesNothing() {
        setup(() -> some(-5))
                .when(m -> m.fmap(i -> i < 0 ? nothing() : some(i)))
                .then(Maybe::isSome)
                .equalsTo(false);
    }
    @Execute
    public void someNotFilteredRemainsSome() {
        setup(() -> some("someNotFiltered"))
                .when(m -> m.filter(s -> s.startsWith("some")))
                .then(Maybe::isSome)
                .equalsTo(true);
    }
    @Execute
    public void someOrReturnsOriginalValue() {
        setup(() -> some("gene kelly"))
                .when(m -> m.map(CAPITALIZE))
                .then(m -> m.or("Frank Sinatra"))
                .equalsTo("Gene Kelly");
    }

    private static final Function<String, String> CAPITALIZE = s -> {
        boolean toUpper = true;
        char space = ' ';
        StringBuilder sb = new StringBuilder();
        for (char c : s.toCharArray()) {
            sb.append(toUpper ? Character.toUpperCase(c) : c);
            toUpper = c == space;
        }
        return sb.toString();
    };
}

テスト実行

これ、JUnitを使えない事情の原因は、このGradleプロジェクトがJVM components modelを採用していて、現状のGradle2.9ではExternal dependencyを解決できないことだったわけです。

というわけで、この簡単な特徴もないテスティングフレームワークJVM component modelのコンポーネントの一つとして実装しています。

で、実行するためには、JVM components modelで作成される大量のjarファイルを集めてclasspathに指定しないといけないわけで、Gradleのカスタムタスクを作成して実行するように出来ています。

task dataTestExec(type: JavaExec, dependsOn: 'assemble') {
    classpath = files(tasks.assemble.dependsOn.findAll {it instanceof BaseBinarySpec}.collect {
        [it.jarFile, it.apiJarFile]
    }.flatten().collect {it.absolutePath})
    main = 'com.sample.Main'
}

で、これを実行すると次のようにテストが実行されます。

f:id:mike_neck:20151121231945p:plain

ちなみに、上の例では全部のテストが通っていますが、テスト失敗するとこんな表示になります。

f:id:mike_neck:20151121232715p:plain

と、まあ、簡単で(欝でやる気でないので2日かかりましたが…)至って何の特徴もないテスティングフレームワークが完成しました。

こいつの公開等は、JVM components modelのpublishingがまだ未実装っぽいので、できても先の話になるでしょうし、はっきり言ってJUnitTestNG、Spockの方が明らかに高機能なので、公開するつもりはありません。

リポジトリー

ここ

github.com


おわり