Gradle における Java Platform Module System (JPMS) の運用

f:id:Naotsugu:20200726205049p:plain


はじめに

Java 9 で導入されたモジュールシステムの Gradle における扱い方について見ていきます。

Java Platform Module System については以下を参照してください。

blog1.mammb.com


Gradle でのモジュールシステムの運用

Gradle 6.4 よりモジュールのビルドが直接サポートされています。

以下の3つの条件を満たす場合に、classpath ではなくmodule-path に依存 Jar を配置します。

  • modularity.inferModulePath が有効になっている
  • module-info.java によりモジュールが定義されている module-path に配置
    • または MANIFEST.MF の Automatic-Module-Name 属性が定義されている
  • モジュールが依存している Jar がモジュールである場合に依存 Jar を module-path に配置

modularity.inferModulePath は以下のように有効化できます。

java {
    modularity.inferModulePath = true
}


モジュールの依存関係の定義

build.gradle で宣言した依存関係と、module-info.java で宣言したモジュールの依存関係は直接関係があります。以下の表のように宣言が同期していることが望ましい形となります。

  • implementation => requires モジュール定義
  • api > requires transitive モジュール定義
  • runtimeOnly => requires static モジュール定義

現在の Gradle は、上記の依存関係の宣言が同期しているかどうかを自動的にチェックしていません。


非モジュール・ライブラリの利用

各種依存ライブラリのモジュール化対応はまちまちです。

com.google.code.gson:gson:2.8.6 は既にモジュール記述子を持つ完全なモジュールとして提供されています。

org.apache.commons:commons-lang3:3.10 などは MANIFEST.MF の Automatic-Module-Name 属性が定義されており、自動モジュールとして利用できます。

commons-cli:commons-cli:1.4 などはモジュール情報を全く提供しない伝統的なライブラリです。

これらは build.gradle では以下のように定義します。

dependencies {
    implementation 'com.google.code.gson:gson:2.8.6'       // real module
    implementation 'org.apache.commons:commons-lang3:3.10' // automatic module
    implementation 'commons-cli:commons-cli:1.4'           // plain library
}

モジュール宣言 module-info.java では以下のような定義にします。

module org.gradle.sample.app {
    requires com.google.gson;          // real module
    requires org.apache.commons.lang3; // automatic module
    // commons-cli-1.4.jar はモジュールではないため定義できない
}


上記依存があるプロジェクトを以下のような build.gradle とした場合、モジュール化された(named module)アプリケーションから 'commons-cli:commons-cli:1.4' を参照することができません。

plugins {
    id 'java'
    id 'application'
}

repositories {
    jcenter()
}

dependencies {
    implementation 'com.google.code.gson:gson:2.8.6'
    implementation 'org.apache.commons:commons-lang3:3.10'
    implementation 'commons-cli:commons-cli:1.4'
}

application {
    mainClassName = 'org.gradle.sample.app.App'
}

java {
    modularity.inferModulePath = true
}

plain library であるライブラリは classpath に配置され module-path に配備されません。ライブラリは無名モジュールとして扱われるため、自動モジュールからは参照できても、モジュール(named module)からは参照できないためです。

このような場合は、build.gradle に以下を追加することで対応できます。

compileJava {
    doFirst {
        options.compilerArgs = [
            "--module-path", classpath.asPath
        ]
        classpath = files()
    }
}

classpath のライブラリをモジュールをmodule-path に Automatic Module として扱うことができます(IDE 側の設定は別途必要となります)。


その他の対応方法としては、プロジェクトの一部として対象のライブラリを自動モジュールでラップしたり、アーティファクト変換を使って既存ライブラリにモジュール記述子を追加してモジュール化するなどがあります。サンプルとして artifact transformsが参考になります。


しかし、プラグインにて対応するのが現実的でしょう。

gradle-modules-plugin を使えば以下のようにシンプルに対応が可能です。

plugins {
    id 'java'
    id 'application'
    id 'org.javamodularity.moduleplugin' version '1.7.0'
}

repositories {
    jcenter()
}

dependencies {
    implementation 'com.google.code.gson:gson:2.8.6'       // real module
    implementation 'org.apache.commons:commons-lang3:3.10' // automatic module
    implementation 'commons-cli:commons-cli:1.4'           // plain library
}

application {
    mainClassName = 'org.gradle.sample.app.App'
}


複数モジュールから構成されるアプリケーションの例

application モジュールと utilities モジュールと list モジュール の3つのモジュールがあり、application -> utilities -> list のような依存関係であったとします。

これらを Gradle のマルチモジュールプロジェクトとして定義します。

settings.gradle は通常のマルチモジュールプロジェクトと同様に以下のように定義します。

rootProject.name = 'modules-multi-project'
include 'application', 'utilities', 'list'

build.gradle では以下のように modularity.inferModulePath を有効にします。

subprojects {
    version = '1.0.0'
    group = 'org.gradle.sample'

    repositories {
        jcenter()
    }

    plugins.withType(JavaPlugin).configureEach {
        java {
            modularity.inferModulePath = true
        }
    }
    
    tasks.withType(Test).configureEach {
        useJUnitPlatform()
    }
}

プロジェクト構成は以下のようなレイアウトになります。

f:id:Naotsugu:20200726170309p:plain

list モジュール

list モジュールの build.gradle は以下のようになります。

plugins {
    id 'java-library'
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.1'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}

list モジュールの module-info.java は以下のようになります。

module org.gradle.sample.list {
    exports org.gradle.sample.list;
}

外部から利用するパッケージを公開します。

utilities モジュール

utilities モジュールの build.gradle は以下のようになります。

plugins {
    id 'java-library'
}

dependencies {
    api project(':list')
}

utilities モジュールの module-info.java は以下のようになります。

module org.gradle.sample.utilities {
    requires transitive org.gradle.sample.list;
    exports org.gradle.sample.utilities;
}

list モジュールへの依存を定義するとともに、外部から利用するパッケージを公開します。 list モジュールを requires transitive としているため、list モジュールの依存先も推移的に utilities モジュールで利用可能となります。

application モジュール

application モジュールの build.gradle は以下のようになります。

plugins {
    id 'application'
}

dependencies {
    implementation project(':utilities')
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.1'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}

application {
    mainModule = 'org.gradle.sample.app'
    mainClass = 'org.gradle.sample.app.Main'
}

application モジュールの module-info.java は以下のようになります。

module org.gradle.sample.app {
    exports org.gradle.sample.app;
    requires org.gradle.sample.utilities;
}

utilities モジュールへの依存を定義しています。


まとめ

Gradle における JPMS の扱い方について説明しました。

まだまだモジュール化されたライブラリなどの整備などが追いついていない状況ではありますが、準備は整えておきたいものです。