基于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. 验证方案有效性
- 类覆盖验证:在common中创建
com.example.feature.FeatureService实现默认逻辑,在free模块创建同包同名类实现免费版逻辑,运行free的shadowJar检查输出是否符合预期。 - Jar内容验证:解压生成的shadowJar,确认包含common所有类、依赖库及模块自身代码/资源。
- 版本标识验证:在主类中打印
AppEdition.current,运行两个版本的Jar,确认分别输出paid和free。
5. 常见问题排查
- 类未覆盖:检查覆盖类的包路径与common完全一致;确认
sourceSets中自身源码目录在common之前。 - Jar缺失依赖:确认
shadowJar配置中configurations指定了runtimeClasspath;检查common模块依赖用implementation声明。 - 编译报错:确保Kotlin、Gradle、Shadow插件版本兼容;统一项目JDK版本。
内容的提问来源于stack exchange,提问作者Xemor
相关产品推荐
相关产品推荐

