You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Kotlin Gradle实现Java程序付费/免费版本的Monorepo架构求助

解决方案:Monorepo下Kotlin Gradle构建多版本应用

1. 调整Monorepo模块结构

确保项目层级清晰,模块职责明确:

monorepo-app/
├── settings.gradle.kts
├── build.gradle.kts (根项目可配置全局依赖或留空)
├── common/
│   ├── build.gradle.kts
│   └── src/main/
│       ├── kotlin/ (通用业务代码,含可被覆盖的类)
│       └── resources/ (通用资源文件)
├── paid/
│   ├── build.gradle.kts
│   └── src/main/
│       ├── kotlin/ (付费专属代码+覆盖common的类)
│       └── resources/ (付费专属资源)
└── free/
    ├── build.gradle.kts
    └── src/main/
        ├── kotlin/ (免费专属代码+覆盖common的类)
        └── resources/ (免费专属资源)

2. 配置Common模块

作为基础库提供通用代码和依赖,不打包为可执行Jar:

// common/build.gradle.kts
plugins {
    `java-library`
    kotlin("jvm") version "1.9.20"
}

group = "com.example"
version = "1.0.0"

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.slf4j:slf4j-api:2.0.9") // 示例通用依赖
}

tasks.withType<JavaCompile> {
    sourceCompatibility = "17"
    targetCompatibility = "17"
}

kotlin {
    jvmToolchain(17)
}

3. 配置Paid/Free模块(核心实现)

这两个模块需打包包含所有依赖的可执行Jar,同时实现类覆盖与版本标识区分。

3.1 基础插件与依赖配置

使用application定义应用入口,shadow插件打包全量依赖Jar:

// paid/build.gradle.kts(free模块仅需修改Jar名称和版本标识)
plugins {
    application
    kotlin("jvm") version "1.9.20"
    id("com.github.johnrengelman.shadow") version "8.1.1"
}

group = "com.example"
version = "1.0.0"

repositories {
    mavenCentral()
}

dependencies {
    implementation(project(":common")) // 依赖common模块
}

// 指定应用主类路径
application {
    mainClass.set("com.example.app.Main")
}

3.2 实现类覆盖

通过调整源码优先级,让模块自身代码覆盖common中的同名类:

// 继续在paid/build.gradle.kts中添加
sourceSets.main {
    // 先声明自身源码目录,优先级高于common
    kotlin.srcDirs("src/main/kotlin")
    java.srcDirs("src/main/java")
    // 再添加common的源码目录,作为通用代码补充
    kotlin.srcDir(project(":common").sourceSets.main.get().kotlin.srcDirs)
    java.srcDir(project(":common").sourceSets.main.get().java.srcDirs)
}

shadowJar {
    archiveBaseName.set("paid-app") // free模块改为"free-app"
    // 排除默认common依赖,手动控制类加载顺序
    dependencies {
        exclude(dependency(project(":common")))
    }
    // 先添加common编译输出,再添加自身编译输出,确保自身类覆盖common
    from(project(":common").sourceSets.main.get().output)
    from(sourceSets.main.get().output)
    // 打包所有运行时依赖库
    configurations = listOf(project.configurations.runtimeClasspath.get())
}

// 让assemble任务自动触发shadowJar打包
tasks.assemble {
    dependsOn(tasks.shadowJar)
}

3.3 版本标识实现

推荐用资源文件方式区分版本,无需编译参数:

  • 在common模块添加版本读取工具类:
// common/src/main/kotlin/com/example/AppEdition.kt
import java.util.Properties

object AppEdition {
    val current: String = run {
        val props = Properties()
        AppEdition::class.java.classLoader.getResourceAsStream("edition.properties")?.use {
            props.load(it)
        }
        props.getProperty("edition", "unknown")
    }
}
  • 在paid模块src/main/resources/edition.properties中添加:
edition=paid
  • 在free模块src/main/resources/edition.properties中添加:
edition=free

4. 验证方案有效性

  1. 类覆盖验证:在common中创建com.example.feature.FeatureService实现默认逻辑,在free模块创建同包同名类实现免费版逻辑,运行free的shadowJar检查输出是否符合预期。
  2. Jar内容验证:解压生成的shadowJar,确认包含common所有类、依赖库及模块自身代码/资源。
  3. 版本标识验证:在主类中打印AppEdition.current,运行两个版本的Jar,确认分别输出paid和free。

5. 常见问题排查

  • 类未覆盖:检查覆盖类的包路径与common完全一致;确认sourceSets中自身源码目录在common之前。
  • Jar缺失依赖:确认shadowJar配置中configurations指定了runtimeClasspath;检查common模块依赖用implementation声明。
  • 编译报错:确保Kotlin、Gradle、Shadow插件版本兼容;统一项目JDK版本。

内容的提问来源于stack exchange,提问作者Xemor

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 21:15:03