第 2 章:Gradle 项目构建实战

本章目标

本章重点从"能看懂 Gradle"升级到"能维护真实项目构建"。学完后你应该能:

  • 创建和维护单模块、多模块 Gradle 工程。
  • 正确使用 implementationapiruntimeOnlytestImplementation
  • 处理依赖冲突、版本统一、BOM、排除、替换。
  • 理解 Fat Jar、Manifest、sourceSets 扩展。
  • 配置测试覆盖率收集和测试过滤。
  • 理解 Java、Kotlin、Spring Boot、Android 项目中 Gradle 配置的共同点。

1. 单模块项目构建

最小 Java 应用通常需要两个插件:

groovy 复制代码
plugins {
    id 'java'
    id 'application'
}

java 负责编译、测试、打包,application 负责定义入口类并提供 run task。

groovy 复制代码
application {
    mainClass = 'com.example.Main'
}

常用命令:

bash 复制代码
gradle clean            # 清理构建输出
gradle compileJava      # 仅编译
gradle test             # 执行测试
gradle run              # 运行应用
gradle build            # 完整构建(编译+测试+打包)
gradle jar              # 生成 jar(不含依赖)
gradle installDist      # 生成可分发包
gradle distZip          # 生成 zip 分发包

2. 标准源码目录与 sourceSets 扩展

标准目录

Gradle Java 插件默认使用 Maven 风格目录:

text 复制代码
src/main/java       生产 Java 代码
src/main/resources  生产资源
src/test/java       测试 Java 代码
src/test/resources  测试资源

修改默认源码目录

如果项目目录不标准,也可以显式配置:

groovy 复制代码
sourceSets {
    main {
        java {
            srcDirs = ['source/java']
        }
        resources {
            srcDirs = ['source/resources']
        }
    }
}

新增额外源码集(SourceSet)

新增一个 integration 测试集(集成测试和单元测试分开):

groovy 复制代码
sourceSets {
    integration {
        java.srcDir 'src/integration/java'
        resources.srcDir 'src/integration/resources'
        compileClasspath += sourceSets.main.output + configurations.testRuntimeClasspath
        runtimeClasspath += output + compileClasspath
    }
}

tasks.register('integrationTest', Test) {
    description = 'Runs integration tests.'
    group = 'verification'
    testClassesDirs = sourceSets.integration.output.classesDirs
    classpath = sourceSets.integration.runtimeClasspath
    useJUnitPlatform()
    shouldRunAfter test
}

check.dependsOn integrationTest

这样单元测试和集成测试可以分开执行:

bash 复制代码
gradle test              # 只跑单元测试
gradle integrationTest   # 只跑集成测试
gradle check             # 全部跑

3. 多模块项目

多模块项目通过根目录 settings.gradle 注册模块:

groovy 复制代码
rootProject.name = 'company-platform'
include 'app', 'service', 'common'

也可以嵌套模块:

groovy 复制代码
include 'app'
include 'service:user-service'
include 'service:order-service'
include 'common'

典型依赖关系:

text 复制代码
app -> service -> common

模块依赖写法:

groovy 复制代码
dependencies {
    implementation project(':service')
}

4. implementationapi 的区别

implementation 不把依赖暴露给下游模块,能减少编译污染并提升增量编译效率。

api 会把依赖暴露给下游模块,适合公共接口签名中出现的类型。

使用 api 需要 java-library 插件:

groovy 复制代码
plugins {
    id 'java-library'
}

示例:

java 复制代码
// common 模块
public interface JsonCodec {
    com.fasterxml.jackson.databind.JsonNode parse(String text);
}

因为接口方法签名暴露了 Jackson 类型,所以 Jackson 应该用 api。如果 Jackson 只在方法内部使用,则应该用 implementation

判断规则:

场景 推荐依赖范围
依赖类型出现在 public API 中 api
只在模块内部实现使用 implementation
只在测试中使用 testImplementation
运行时才需要(驱动、日志实现) runtimeOnly
编译需要但不打包(注解、API Stub) compileOnly

5. 依赖版本统一

直接声明版本

小项目可以直接在 dependencies 中写版本:

groovy 复制代码
implementation 'com.google.guava:guava:33.2.1-jre'

Version Catalog(推荐)

多模块项目建议使用 Version Catalog(gradle/libs.versions.toml):

toml 复制代码
[versions]
guava = "33.2.1-jre"
junit = "5.10.2"
jackson = "2.17.1"
mockito = "5.11.0"

[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }

[plugins]
spring-boot = { id = "org.springframework.boot", version = "3.3.0" }

使用:

groovy 复制代码
dependencies {
    implementation libs.guava
    implementation libs.jackson.databind
    testImplementation libs.junit.jupiter
    testImplementation libs.mockito.core
}

好处:

  • 版本集中管理。
  • 多模块不会各写各的版本。
  • 升级依赖时更容易评估影响面。
  • IDE 可以跳转到版本定义。

BOM(Bill of Materials)平台依赖

BOM 是一个只声明版本的 POM,引入后可以不写具体版本:

groovy 复制代码
dependencies {
    // 引入 Spring Boot BOM,版本由 BOM 统一管理
    implementation platform('org.springframework.boot:spring-boot-dependencies:3.3.0')

    // 不需要写版本号,由 BOM 决定
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

自定义平台模块(类似 BOM):

groovy 复制代码
// platform 模块的 build.gradle
plugins {
    id 'java-platform'
}

javaPlatform {
    allowDependencies()
}

dependencies {
    api platform('org.springframework.boot:spring-boot-dependencies:3.3.0')
    constraints {
        api 'com.google.guava:guava:33.2.1-jre'
        api 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
    }
}

其他模块引用:

groovy 复制代码
dependencies {
    implementation platform(project(':platform'))
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

6. 依赖冲突排查

查看依赖树:

bash 复制代码
gradle :app:dependencies --configuration runtimeClasspath

定位某个依赖为什么被引入:

bash 复制代码
gradle :app:dependencyInsight --dependency guava --configuration runtimeClasspath

排除传递依赖

groovy 复制代码
dependencies {
    implementation('org.example:library:1.0') {
        exclude group: 'com.google.guava', module: 'guava'
    }
}

全局排除(所有配置):

groovy 复制代码
configurations.configureEach {
    exclude group: 'commons-logging', module: 'commons-logging'
}

强制版本(force)

groovy 复制代码
configurations.configureEach {
    resolutionStrategy {
        force 'com.google.guava:guava:33.2.1-jre'
    }
}

依赖替换(substitution)

将某个依赖替换为另一个(常用于本地 stub 或组件迁移):

groovy 复制代码
configurations.configureEach {
    resolutionStrategy.dependencySubstitution {
        // 用本地模块替换外部依赖(本地联调时有用)
        substitute module('com.example:common') using project(':common')
        // 替换坐标(组件改名时)
        substitute module('javax.servlet:servlet-api') using module('jakarta.servlet:jakarta.servlet-api:6.0.0')
    }
}

依赖约束(推荐方式)

groovy 复制代码
dependencies {
    constraints {
        implementation('com.google.guava:guava:33.2.1-jre') {
            because '统一 Guava 版本,避免传递依赖引入旧版本'
        }
    }
}

7. 测试配置

JUnit 5 基础配置

groovy 复制代码
dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

test {
    useJUnitPlatform()
}

显示测试日志

groovy 复制代码
test {
    useJUnitPlatform()
    testLogging {
        events 'passed', 'skipped', 'failed'
        showStandardStreams = false
        exceptionFormat = 'full'
    }
}

参数化测试示例(Java)

java 复制代码
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;

class CalculatorTest {
    @ParameterizedTest
    @CsvSource({
        "1, 1, 2",
        "2, 3, 5",
        "10, -5, 5"
    })
    void add(int a, int b, int expected) {
        assertEquals(expected, a + b);
    }
}

测试过滤

bash 复制代码
# 执行单模块测试
gradle :service:test

# 执行单个测试类
gradle :service:test --tests "com.example.UserServiceTest"

# 执行单个测试方法
gradle :service:test --tests "com.example.UserServiceTest.shouldReturnUserById"

# 执行名称匹配的所有测试
gradle test --tests "*Service*"

测试超时控制

groovy 复制代码
test {
    useJUnitPlatform()
    timeout = Duration.ofMinutes(5)  // 单个测试套件超时
}

8. 打包和 Fat Jar

标准 Jar(不含依赖)

bash 复制代码
gradle :app:jar

生成在 app/build/libs/app-1.0.0.jar,不含依赖,通常需要配合 classpath 运行。

配置 Manifest

groovy 复制代码
jar {
    manifest {
        attributes(
            'Main-Class': 'com.example.Main',
            'Implementation-Title': project.name,
            'Implementation-Version': project.version,
            'Built-By': System.getProperty('user.name'),
            'Build-Jdk': System.getProperty('java.version')
        )
    }
}

Fat Jar(手写,不用插件)

把所有依赖打进一个 jar:

groovy 复制代码
tasks.register('fatJar', Jar) {
    group = 'build'
    description = 'Build a fat jar containing all runtime dependencies'
    archiveClassifier = 'fat'
    manifest {
        attributes 'Main-Class': 'com.example.gradledemo.app.App'
    }
    from {
        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
    with jar
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

执行:

bash 复制代码
gradle fatJar
java -jar app/build/libs/app-1.0.0-fat.jar

可分发包(Application 插件)

bash 复制代码
gradle installDist    # 生成可运行目录(含 bin/ 和 lib/)
gradle distZip        # 生成 zip 包
gradle distTar        # 生成 tar 包

输出:

text 复制代码
app/build/install/app/
├── bin/
│   ├── app       # Unix 启动脚本
│   └── app.bat   # Windows 启动脚本
└── lib/
    ├── app-1.0.0.jar
    └── *.jar     # 所有依赖

9. Spring Boot 项目构建要点

Spring Boot 常见配置:

groovy 复制代码
plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.0'
    id 'io.spring.dependency-management' version '1.1.5'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.h2database:h2'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

常用 task:

bash 复制代码
gradle bootRun                  # 直接运行应用
gradle bootJar                  # 生成可执行 fat jar
gradle bootBuildImage           # 构建 Docker 镜像(需要 Docker)
gradle dependencies             # 查看所有依赖

要点:

  • bootJar 生成可执行 jar,包含所有依赖。
  • jar 是普通 jar,Spring Boot 插件会禁用它(可配置恢复)。
  • io.spring.dependency-management 导入 Spring BOM,无需写版本。

多环境配置

groovy 复制代码
bootRun {
    args = ['--spring.profiles.active=dev']
    jvmArgs = ['-Xmx512m']
}

10. Kotlin 项目构建

Kotlin JVM 项目构建:

groovy 复制代码
plugins {
    id 'org.jetbrains.kotlin.jvm' version '2.0.0'
}

kotlin {
    jvmToolchain(17)
}

dependencies {
    implementation 'org.jetbrains.kotlin:kotlin-stdlib'
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
    testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5'
}

test {
    useJUnitPlatform()
}

Kotlin DSL 写法(build.gradle.kts):

kotlin 复制代码
plugins {
    kotlin("jvm") version "2.0.0"
}

kotlin {
    jvmToolchain(17)
}

dependencies {
    implementation(kotlin("stdlib"))
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
    testImplementation(kotlin("test-junit5"))
}

tasks.test {
    useJUnitPlatform()
}

Kotlin + Spring Boot:

groovy 复制代码
plugins {
    id 'org.jetbrains.kotlin.jvm' version '2.0.0'
    id 'org.jetbrains.kotlin.plugin.spring' version '2.0.0'
    id 'org.springframework.boot' version '3.3.0'
    id 'io.spring.dependency-management' version '1.1.5'
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

11. Android Gradle 项目要点

Android 项目通常使用 Android Gradle Plugin:

groovy 复制代码
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android' version '2.0.0'
}

核心配置:

groovy 复制代码
android {
    namespace 'com.example.app'
    compileSdk 35

    defaultConfig {
        applicationId 'com.example.app'
        minSdk 23
        targetSdk 35
        versionCode 1
        versionName '1.0'
    }

    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }

    kotlinOptions {
        jvmTarget = '17'
    }
}

dependencies {
    implementation 'androidx.core:core-ktx:1.13.1'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.2.1'
}

Android 项目和 Java 项目的共同点是:仍然由插件创建 task、由依赖配置管理三方库、由 Gradle 负责构建图和执行。


12. 实操:给 demo 验证模块依赖关系

目标:理解 app -> service -> common 的模块关系。

步骤:

bash 复制代码
cd demo/gradle-multi-module-demo
gradle :app:dependencies --configuration runtimeClasspath

验证点:

  • 能看到 project :service
  • 能看到 project :common
  • 能看到外部依赖 guava

运行:

bash 复制代码
gradle :app:run

预期输出包含:

text 复制代码
Gradle Demo | <当前日期>
Hello, Gradle Learner. Multi-module build is working.

依赖 insight 查询:

bash 复制代码
gradle :app:dependencyInsight --dependency guava --configuration runtimeClasspath

验证点:

  • 能看到 guava 是通过 :common 传递进来的。
  • 能看到版本号 33.2.1-jre

13. 常见问题

问题 1:多模块依赖写错导致找不到类

先检查 settings.gradle 是否 include 了模块,再检查依赖方向是否正确。

错误示例:

groovy 复制代码
implementation project('service')

正确示例:

groovy 复制代码
implementation project(':service')

问题 2:为什么 api 用多了会变慢

api 会扩大模块对外暴露的编译 classpath。公共 API 变化时,下游模块更容易被迫重新编译。默认优先使用 implementation,只有类型出现在公共签名中才使用 api

问题 3:为什么本地能构建,CI 失败

常见原因:

  • 本地依赖了缓存,CI 是干净环境。
  • JDK 版本不一致。
  • 本地用了未提交的文件。
  • 没有使用 Wrapper 固定 Gradle 版本。
  • 私服或代理配置只在本机存在。

排查顺序:

bash 复制代码
gradle clean build --refresh-dependencies
gradle --version
git status

问题 4:BOM 和 Version Catalog 能同时用吗

可以。BOM 管理来自外部框架(如 Spring)的版本,Version Catalog 管理你自己声明的其他依赖版本,两者互不冲突。

问题 5:Fat Jar 打包时报 duplicate files 错误

设置去重策略:

groovy 复制代码
tasks.register('fatJar', Jar) {
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    // ...
}

或者选择保留 META-INF/services(服务加载所需):

groovy 复制代码
duplicatesStrategy = DuplicatesStrategy.INCLUDE  // 保留所有(可能导致冲突)

更好的做法是使用 Shadow 插件(com.github.johnrengelman.shadow),它能正确合并 META-INF/services

相关推荐
空中海9 小时前
第 1 章:Gradle 入门基础
gradle
空中海12 小时前
第 4 章:Gradle 专家级实践
gradle
黄林晴1 天前
警惕!AGP 9.2 别只改版本号,R8 规则与构建链路全线收紧
android·gradle
followYouself7 天前
Gradle、AGP、Plugin插件基本知识
android·gradle·plugin·agp
千码君201611 天前
Flutter:在win10上第一次安装和尝试开发记录
flutter·gradle·android-studio·安卓模拟器
Ww.xh20 天前
Flutter配置Gradle完整教程
flutter·gradle·android studio
vortex520 天前
Gradle 从入门到实战
java·gradle
蜡台24 天前
Android Studio Gradlew JDK配置
java·gradle·android studio·intellij-idea
spencer_tseng25 天前
java.net.SocketTimeoutException: Connect timed out
gradle