返回

Gradle Java 版本冲突?轻松解决子模块依赖问题

java

搞定 Gradle 子项目中的 Java 版本差异

我们有时会碰到这样的情况:一个 Gradle 项目,主工程想用最新的 Java 21 特性,但里面又依赖了一个需要兼容旧环境(比如 Java 11)的子模块。配置好各自的 sourceCompatibility 后,满心欢喜地运行构建,结果 Gradle 却“啪”地一下,甩给你一个依赖解析错误。这究竟是咋回事?又该怎么解决呢?

问题来了:主项目 Java 21,子模块 Java 11,Gradle 不干了

想象一下这个场景:

你的主项目 build.gradle.kts (或者 build.gradle) 里写着:

// 主项目 build.gradle.kts
plugins {
    id("java")
    // 可能还有 application, kotlin("jvm") 等
}

java {
    sourceCompatibility = JavaVersion.VERSION_21
    // targetCompatibility 默认可能跟随 sourceCompatibility 或更高
}

// ... 其他配置和依赖
dependencies {
    implementation(project(":models"))
}

然后你有一个子模块,叫 models。这个模块比较特殊,它既要被主项目依赖,也可能要打成一个独立的 Jar 包给其他只支持 Java 11 的系统用。所以,在 models/build.gradle.kts 里,你这样配置:

// models/build.gradle.kts
plugins {
    id("java-library") // 或者 "java"
    kotlin("jvm") // 假设也用了 Kotlin
}

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

kotlin {
    compilerOptions {
        jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
    }
}

// ... 其他配置

看起来逻辑很清晰对吧?主项目用 Java 21 编译,models 模块用 Java 11 编译。按理说,用 Java 11 编译出来的字节码在 Java 21 的 JVM 上跑是完全没问题的。

但 Gradle 偏偏不这么认为,构建时可能会报类似下面的错误:

FAILURE: Build failed with an exception.

* What went wrong:
Could not determine the dependencies of task ':compileJava'.
> Could not resolve all dependencies for configuration ':compileClasspath'.
   > Could not resolve project :models.
     Required by:
         project :
      > No matching variant of project :models was found. The consumer was configured to find a library for use during compile time, compatible with Java 21, packaged as a jar, preferably optimized for standard JVMs, and its dependencies declared externally, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'jvm' but:
          - Variant 'apiElements' capability your.group:models:unspecified declares compatibility with Java 11:
              - Incompatible because this component declares compatibility with Java 11 and the consumer needed compatibility with Java 21
              - Other attributes:
                  - Found org.gradle.category 'library' but wasn't required.
                  - Found org.gradle.dependency.bundling 'external' but wasn't required.
                  - Found org.gradle.jvm.version '11' but wasn't required. // 注意这里
                  - Found org.gradle.libraryelements 'jar' but wasn't required.
                  - Found org.gradle.usage 'java-api' but wasn't required.
                  - Found org.jetbrains.kotlin.platform.type 'jvm' but wasn't required.
          - Variant 'runtimeElements' capability your.group:models:unspecified declares compatibility with Java 11:
              - Incompatible because this component declares compatibility with Java 11 and the consumer needed compatibility with Java 21
              - Other attributes:
                  // ... 类似上面 ...

错误信息很长,关键在于 Gradle 说找不到 :models 项目的匹配变体 (matching variant)。消费者(主项目)需要一个兼容 Java 21 的库,但找到的变体(models 提供的)明确声明了自己兼容 Java 11。即使我们知道 Java 11 的代码能在 Java 21 上跑,Gradle 的依赖解析机制默认情况下还是会认为它们不“精确匹配”。

为啥 Gradle 会报错?探究根源

这个问题的核心在于 Gradle 的 Variant Aware Dependency Management (变体感知依赖管理) 机制。

从 Gradle 5.x 开始,为了更精确地管理依赖,特别是在处理不同平台(如 JVM、Android)、不同构建类型(Debug/Release)或不同特性集时,Gradle 引入了“变体”的概念。一个项目(或库)可以发布多个“变体”,每个变体都带有其能力的“属性”(Attributes)。

当我们声明 java { sourceCompatibility = ... }targetCompatibility = ... 时,Gradle 的 Java 插件(或者 Kotlin 插件的 jvmTarget)会自动为这个项目发布的变体(比如 apiElements, runtimeElements)添加一个叫做 org.gradle.jvm.version 的属性,其值就是你设置的 Java 版本号(例如 11 或 21)。

在解决依赖时,消费者(这里是主项目)会声明它需要哪些属性。因为主项目配置了 sourceCompatibility = JavaVersion.VERSION_21,所以它在查找 :models 依赖时,会要求找到一个 org.gradle.jvm.version 属性为 21 的变体。

而我们的 :models 项目,因为它设置了 sourceCompatibility = JavaVersion.VERSION_11targetCompatibility = JavaVersion.VERSION_11,它发布的变体的 org.gradle.jvm.version 属性值是 11。

默认情况下,Gradle 在匹配这个 org.gradle.jvm.version 属性时,执行的是精确匹配或要求生产者版本低于等于消费者版本(但具体规则可能比较复杂且会演变)。在这个场景下,它看到消费者要 21,生产者提供 11,即使逻辑上兼容,属性值却不直接匹配,导致了 “No matching variant” 的错误。Gradle 认为它不能确定地知道这个 Java 11 的库在 Java 21 环境下是否真的没问题(虽然我们通常认为可以)。

解决方案来了!让不同版本的 Java 和谐共处

别担心,有几种方法可以解决这个问题,让不同 Java 版本的模块和平共处。

方案一:Gradle Toolchains - 官方推荐,省心省力

这是目前 Gradle 官方最推荐的方式,用来管理项目编译和运行所需的 JDK 版本。Toolchains 可以让你声明每个项目(或任务)应该用哪个版本的 JDK 来编译和测试,而不需要修改你系统全局的 JAVA_HOME 或者你启动 Gradle 时使用的 JDK。

原理:

Toolchains 将 “运行 Gradle 本身所用的 JDK” 和 “编译/运行/测试你的项目代码所用的 JDK” 解耦。Gradle 会根据你的配置自动检测或下载所需的 JDK 版本。它不仅设置了编译时的 -source-target(或者等效的 --release)参数,还能确保执行编译、测试等任务时使用的是对应版本的 javacjava 命令。这从根本上解决了环境不一致的问题。

操作步骤:

  1. settings.gradle(.kts) 中启用并配置 Toolchains (可选,但推荐)

    如果需要 Gradle 自动下载 JDK,可以在 settings.gradle(.kts) 里配置仓库。

    // settings.gradle.kts
    toolchainManagement {
        jvm {
            javaRepositories {
                repository("foojay") { // 使用 Foojay 作为 JDK 发现服务
                    vendorSelector.set(matching(provider { "adoptopenjdk" })) // 或者其他你想用的提供商,比如 "temurin"
                }
            }
        }
    }
    
  2. 在根项目 build.gradle(.kts) 配置主项目的 Toolchain

    // 根项目 build.gradle.kts
    plugins {
        id("java")
        // ...
    }
    
    java {
        // sourceCompatibility = JavaVersion.VERSION_21 // Toolchain 会自动处理兼容性
        toolchain {
            languageVersion.set(JavaLanguageVersion.of(21))
            // 你还可以指定 JDK 厂商和实现,如果需要的话
            // vendor.set(JvmVendorSpec.ADOPTIUM)
            // implementation.set(JvmImplementation.J9)
        }
    }
    
    dependencies {
        implementation(project(":models"))
    }
    
    // 对于 Kotlin 项目,也要确保 Kotlin 插件也使用 Toolchain
    tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
        kotlinOptions.jvmTarget = "21" // 或者让它从 Java Toolchain 推断 (可能需要新版本插件)
        // 更好的方式是让 Kotlin 插件直接使用 Toolchain:
        kotlin { // 在 kotlin { } 块内 (如果还没在 plugins{} 里配置)
           jvmToolchain {
               languageVersion.set(JavaLanguageVersion.of(21))
           }
        }
    }
    
  3. models 子模块 build.gradle(.kts) 配置其 Toolchain

    // models/build.gradle.kts
    plugins {
        id("java-library")
        kotlin("jvm")
    }
    
    java {
        // sourceCompatibility = JavaVersion.VERSION_11 // Toolchain 会处理
        // targetCompatibility = JavaVersion.VERSION_11
        toolchain {
            languageVersion.set(JavaLanguageVersion.of(11))
        }
    }
    
    kotlin {
        compilerOptions {
            // jvmTarget.set(JvmTarget.JVM_11) // 让 Toolchain 来决定更佳
        }
        jvmToolchain { // Kotlin 也要使用 Toolchain
            languageVersion.set(JavaLanguageVersion.of(11))
        }
    }
    

优点:

  • 清晰分离: 构建环境 JDK 和项目目标 JDK 分开,非常干净。
  • 环境一致性: 无论在哪台机器上,只要能上网(如果需要自动下载),Gradle 都能保证使用正确的 JDK 版本进行编译和测试。
  • 官方推荐: 这是 Gradle 未来处理 Java 版本的主要方向。

进阶使用:

  • Toolchains 可以配置 vendor (如 Adoptium, Oracle, Amazon Corretto 等) 和 implementation (HotSpot, OpenJ9)。
  • Gradle 会缓存下载的 JDK,不会每次都重新下载。
  • 你可以为不同的任务(编译、测试、运行 Javadoc)设置不同的 Toolchain。

使用 Toolchains 后,Gradle 对依赖解析时的 Java 版本兼容性判断通常会更加智能和宽容,因为它知道字节码的实际目标版本,并且能保证编译环境的正确性。之前的那个 variant matching 问题很可能就自然消失了。

方案二:配置 targetCompatibility 并理解其影响

如果你暂时不想或者不能使用 Toolchains,可以尝试确保 targetCompatibility 被正确设置,并理解它与依赖解析的关系。

原理:

  • sourceCompatibility: 控制你的源代码可以用哪些 Java 语言特性。
  • targetCompatibility: 控制生成的 .class 文件兼容哪个版本的 JVM。Java 11 编译出的 class 文件理论上是可以在 Java 21 JVM 上运行的。

问题在于,即使你设置了 targetCompatibility = JavaVersion.VERSION_11,正如我们之前分析的,Gradle 的依赖解析机制仍然可能因为 org.gradle.jvm.version 属性的精确匹配失败而报错。单纯设置 source/targetCompatibility 可能不足以解决 依赖解析阶段 的问题,虽然它能保证编译出的 字节码 是兼容的。

操作步骤:

基本上就是你在问题中已经做的配置:

// 根项目 build.gradle.kts
java {
    sourceCompatibility = JavaVersion.VERSION_21
    // 如果需要明确 target,也设置 targetCompatibility = JavaVersion.VERSION_21
}

// models/build.gradle.kts
java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11 // 确保 target 也设置了
}
kotlin {
    compilerOptions {
        jvmTarget.set(JvmTarget.JVM_11) // Kotlin 的 JVM 目标也要对齐
    }
}

更推荐的做法 (替代 source/target): 使用 options.release

Java 9 引入了一个更好的编译选项 --release N,它能同时保证 API 使用和字节码版本都兼容目标 Java 版本 N。在 Gradle 中可以通过 JavaCompile 任务的 options.release 属性来设置。

// 根项目 build.gradle.kts
tasks.withType<JavaCompile>().configureEach {
    options.release.set(21)
}

// models/build.gradle.kts
tasks.withType<JavaCompile>().configureEach {
    options.release.set(11)
}
// Kotlin 的 jvmTarget 依然需要单独设置
kotlin {
    compilerOptions {
        jvmTarget.set(JvmTarget.JVM_11)
    }
}

为何此方案可能仍然失败 (如问题所述):

正如一开始分析的,即使编译配置正确,依赖解析时,Gradle 默认的属性匹配规则可能依然过于严格,导致消费 Java 21 的项目拒绝 Java 11 的变体。这表明,仅配置编译选项有时不足以解决 依赖发现 阶段的问题。

方案三:精调 Variant Attributes 和兼容性规则 (深入理解与实践)

如果 Toolchains 不适用,且仅设置 targetCompatibilityrelease 依然报错,那就需要深入一层,直接调整 Gradle 处理变体属性匹配的方式。这通常涉及两个方向:

  • 修改生产者 (models) 发布的属性: 让它声明自己也能兼容更高的 Java 版本。
  • 修改消费者 (主项目) 的匹配规则: 告诉主项目,接受 Java 11 的库也是 OK 的。

警告: 这属于比较高级的 Gradle 用法,可能会让构建逻辑变复杂,并且可能在 Gradle 版本更新后需要调整。优先考虑 Toolchains。

操作步骤 (以消费者添加兼容性规则为例,通常更推荐):

根项目build.gradle(.kts) 文件中,我们可以定义一个属性兼容性规则 (Attribute Compatibility Rule)。告诉 Gradle,当消费者请求 org.gradle.jvm.version = N 时,如果生产者提供的是 org.gradle.jvm.version = M (其中 M < N),也认为是兼容的。

// 根项目 build.gradle.kts

// ... 其他配置 ...

// 在 dependencies { ... } 块之外或者内部都可以,但通常放在顶层
abstract class JvmVersionCompatibilityRule : AttributeCompatibilityRule<Int> {
    override fun execute(details: CompatibilityCheckDetails<Int>) {
        // details.consumerValue = 消费者请求的版本 (e.g., 21)
        // details.producerValue = 生产者提供的版本 (e.g., 11)
        if (details.consumerValue != null && details.producerValue != null) {
            if (details.consumerValue >= details.producerValue) {
                // 如果消费者请求的版本 >= 生产者提供的版本,则声明兼容
                details.compatible()
            }
        }
    }
}

// 应用这个规则到 'org.gradle.jvm.version' 属性上
dependencies {
    attributesSchema {
        attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE) { // 获取标准 jvm version 属性
            compatibilityRules.add(JvmVersionCompatibilityRule::class.java)
        }
    }

    implementation(project(":models")) // 依赖声明在这里或其他地方
    // ... 其他依赖 ...
}

// 确保 'org.gradle.api.attributes.java.TargetJvmVersion' 被导入
// 或者使用字符串 "org.gradle.jvm.version"

// 如果上面的 TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE 找不到
// 可以直接使用属性的 FQDN 字符串:
dependencies {
    attributesSchema {
        // Gradle 7.x+ 可以直接用
         findAttribute("org.gradle.jvm.version")?.let { jvmVersionAttribute ->
             (jvmVersionAttribute as Attribute<Int>).compatibilityRules.add(JvmVersionCompatibilityRule::class.java)
         }
        // 对于更早版本或直接使用字符串
        // attribute(Attribute.of("org.gradle.jvm.version", Int::class.javaObjectType)) {
        //    compatibilityRules.add(JvmVersionCompatibilityRule::class.java)
        //}
    }
    // ...
}

原理:

这段代码定义了一个规则 (JvmVersionCompatibilityRule),当 Gradle 在检查 org.gradle.jvm.version 这个属性是否匹配时会调用它。规则很简单:只要消费者需要的 Java 版本(比如 21)大于或等于生产者提供的版本(比如 11),就认为它们是兼容的 (details.compatible())。然后,我们把这个规则添加到 attributesSchema 中,应用到 org.gradle.jvm.version 这个属性上。

这样一来,即使 models 模块声明自己是 Java 11 (org.gradle.jvm.version = 11),当主项目(需要 Java 21)来请求时,这个自定义的兼容性规则会生效,告诉 Gradle “没问题,接受它!”,依赖解析就能顺利通过。

操作步骤 (修改生产者属性 - 不太推荐):

你也可以在 models 项目的 build.gradle(.kts) 中尝试修改它发布的 apiElementsruntimeElements 变体的属性,但这通常更复杂且可能影响其他消费者。

// models/build.gradle.kts (示例,谨慎使用)
configurations {
    // 假设 'apiElements''runtimeElements' 是 Java Library 插件创建的
    // 你需要找到实际输出的变体配置名
    listOf(apiElements, runtimeElements).forEach { conf ->
        conf.get().attributes {
            // 强制或修改 jvm version 属性
            // 这可能不是最好的方式,可能会掩盖问题
            // attribute(Attribute.of("org.gradle.jvm.version", Int::class.javaObjectType), 21) // 风险操作
        }
    }
}

这种方法风险较高,因为它可能错误地声明了兼容性,不如下面的消费者兼容性规则明确。

安全建议:

修改属性或添加兼容性规则时,一定要清楚自己在做什么。过度宽松的规则可能隐藏潜在的运行时兼容性问题(尽管 Java 向下兼容性通常很好)。记录下你添加这些规则的原因,方便未来维护。

选择哪个方案?

  1. 首选:Gradle Toolchains (方案一) 。这是最现代、最健壮、也是 Gradle 官方推荐的方式。它从根本上解决了编译环境和目标版本的匹配问题。
  2. 次选:正确配置 targetCompatibility / options.release (方案二结合方案三的兼容性规则) 。如果不能用 Toolchains,确保字节码目标版本设置正确,并辅以消费者端的兼容性规则 来解决依赖解析时的属性匹配问题。
  3. 慎用:直接修改生产者属性 (方案三的一部分) 。这通常是最后的手段,容易出错且难以维护。

通过理解 Gradle 的变体感知依赖管理和属性匹配机制,再运用 Toolchains 或精调兼容性规则,就能灵活地让不同 Java 版本的子项目在同一个 Gradle 构建中和谐共存了。