Java 项目构建选型:Maven、Gradle 与模块化实践
针对 Java 项目构建选型,按项目类型给出判断:内部服务、Java 版本统一、CI 流程固定的项目宜用 Maven,单体仓库、模块多、需要增量构建与缓存的场景宜用 Gradle,JPMS 模块化两者皆可但 Gradle 处理 module path 更省心,并给出两者最小可用的多模块骨架、常用构建命令、依赖版本统一方式,以及拆模块须保持依赖单向、避免循环依赖的实践要点。
学完这篇,你能在半小时内为自己的 Java 项目选对 Maven 或 Gradle,并把单模块工程拆成能编译、能复用、不踩循环依赖坑的多模块结构。
第一步:先分清你的项目属于哪一类
选型不是比谁先进,而是比谁匹配:
- 公司内部服务、团队 Java 版本统一、CI 流程固定 → Maven,声明式、报错好查、新人零学习成本。
- 单体仓库、模块多、要求增量构建和构建缓存(Android、Kotlin、大型微服务群)→ Gradle。
- 需要 JDK 9+ 的模块化(JPMS)或 jlink 裁剪运行时 → 两者都能做,但 Gradle 对 module path 的处理更省心。
注意:不要用「Gradle 比 Maven 快」当唯一理由。小项目两者差距在秒级,而 Gradle 的构建脚本是代码,出错时排查成本更高。
第二步:Maven 的最小可用多模块骨架
根 pom.xml 用 pom 打包,只做聚合和版本管理:
<packaging>pom</packaging>
<modules>
<module>common</module>
<module>service</module>
<module>web</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.demo</groupId>
<artifactId>common</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
子模块里引用兄弟模块时不要写版本号,交给 dependencyManagement 统一。常用命令:
mvn -T 1C clean install # 并行构建(1 线程/核)
mvn -pl web -am package # 只构建 web 及其依赖的模块
mvn dependency:tree -Dincludes=com.demo
注意:子模块继承父 POM 靠
<parent>的relativePath,默认../pom.xml。如果目录层级不是标准的,必须显式写对,否则会去本地仓库找父 POM 然后报「找不到」。
第三步:Gradle 的最小可用多模块骨架
settings.gradle.kts 声明模块:
rootProject.name = "demo"
include("common", "service", "web")
根 build.gradle.kts 用 subprojects 或 allprojects 统一插件和 Java 版本,子模块只声明自己的依赖:
plugins { java }
java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }
dependencies {
implementation(project(":common")) // 不对外暴露
api(project(":service")) // 对外暴露
}
命令用 Wrapper,别用本机 gradle:
./gradlew :web:build --configuration-cache
./gradlew dependencies --configuration runtimeClasspath
版本统一建议用 Version Catalog(gradle/libs.versions.toml),比在根脚本里拼变量好维护。
注意:
implementation和api的区别直接决定依赖是否传递。选错会导致「本地能编译、CI 编译不过」——因为 CI 是干净构建,缺的传递依赖才暴露出来。
第四步:多模块拆分与 JPMS 落地
拆模块只守一条规则:依赖方向单向,不允许环。常见分层是 common(工具与 DTO)→ service(业务)→ web(控制器与启动类)。发现 A 依赖 B、B 又依赖 A 时,把共用部分下沉到 common,而不是互相引用。
要真正用 JPMS,在 src/main/java 根目录放 module-info.java:
module com.demo.common {
exports com.demo.common.api; // 只导出对外 API
requires transitive java.logging; // 传递依赖
opens com.demo.common.entity to com.fasterxml.jackson.databind; // 反射框架需要
}
Gradle 中 java.modularity.inferModulePath 默认开启,模块化项目会自动走 module path;Maven 用 maven-compiler-plugin 即可,模块描述符放在各模块源码根。
注意:Spring Boot 的可执行 fat jar 与 JPMS 不兼容——
module-info.java会和 Boot 的类加载器冲突。用 Boot 打包就别加模块描述符,把模块化留给内部库;真要做模块化运行时,考虑用 jlink 自己组装。
校验依赖是否合理:
jdeps --module-path build/libs --print-module-deps com.demo.web
第五步:构建速度与依赖治理
Maven 侧:mvn -T 1C 并行、-o 离线、避免 install 跑全量,CI 上用 -pl ... -am 只构建受影响模块。
Gradle 侧:开构建缓存(org.gradle.caching=true)、配置缓存(--configuration-cache)、--build-cache 复用他人产物;少用 allprojects,它会让配置阶段无法缓存。
两者都要做版本收敛:Maven 用 BOM(<scope>import</scope>)统一第三方版本,Gradle 用 platform() 引 BOM。别再靠 exclusions 一个个排除冲突。
小结
- 选型看项目形态:标准化团队选 Maven,重增量与缓存选 Gradle,别看热闹。
- 多模块靠父 POM 的
dependencyManagement或 Gradle 的 Version Catalog 统一版本,子模块不写版本号。 - 模块拆分守单向依赖,环依赖只能靠下沉共用代码解决。
- Gradle 里分清
implementation与api,Maven 里善用-pl -am增量构建。 - 用 Spring Boot fat jar 时不要加
module-info.java,模块化留给内部库或 jlink 场景。
转载请注明出处,版权归原作者所有。
正式会员
认证极客






