2. 依赖类型详解:compileOnly、runtimeOnly、testImplementation、annotationProcessor、provided

Gradle 的依赖配置(dependencies 块)通过声明不同的“配置”控制依赖在构建生命周期中的可见范围。理解每种配置的作用域是管理版本冲突、减小构建产物体积、避免运行时错误的基石。本章逐一拆解 compileOnly、runtimeOnly、testImplementation、annotationProcessor 以及 provided(Gradle 中的等效配置),并给出实际选择指南。

依赖类型作用域对比图 依赖配置与类路径作用域 编译类路径 运行时类路径 测试类路径 compileOnly ✓ ✗ ✗ runtimeOnly ✗ ✓ ✓ testImplementation ✗ ✗ ✓ annotationProcessor A ✗ A providedCompile ✓ ✗ ✓ 说明:✓ = 包含;✗ = 不包含;A = 仅参与注解处理阶段(编译期) providedCompile 在 Gradle war 插件中可用,等效于 compileOnly + 参与测试编译

2.1 compileOnly — 编译期可见,运行时排除

原理: 依赖仅添加到编译类路径,不会打包到最终产物(JAR/WAR)中,也不会出现在运行时类路径。常用于 Servlet API、Lombok 注解处理器、编译期库。

配置示例:

dependencies {
    compileOnly 'javax.servlet:javax.servlet-api:4.0.1'
    compileOnly 'org.projectlombok:lombok:1.18.30'
}
使用场景: 当运行时容器或框架已经提供了该库的版本(例如 Tomcat 自带 Servlet API),不应重复打包,否则可能导致类路径冲突。
常见错误: 误将 compileOnly 用于必须在运行时存在的依赖,例如数据库驱动。编译通过但运行时报 ClassNotFoundException。

2.2 runtimeOnly — 运行时可见,编译不可见

原理: 依赖不参与编译,仅在运行时(以及测试运行时)包含。典型代表是日志实现(如 Logback)、JDBC 驱动。编译时只需接口(如 SLF4J API),运行时才需要具体实现。

dependencies {
    implementation 'org.slf4j:slf4j-api:2.0.9'
    runtimeOnly 'ch.qos.logback:logback-classic:1.4.14'
}
调试提示: 运行 gradle dependencies --configuration runtimeClasspath 可查看运行时所有依赖树,确认是否有遗漏的 runtimeOnly 依赖。

2.3 testImplementation — 测试专属依赖

原理: 依赖仅在编译和执行测试时生效,不影响生产代码的编译和运行。用于 JUnit、Mockito、AssertJ 等测试框架。

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
    testImplementation 'org.mockito:mockito-core:5.4.0'
}

注意: 测试类路径继承自 implementation 和 compileOnly(取决于具体配置链),但 testImplementation 不会反向传播到生产代码。

2.4 annotationProcessor — 注解处理器专用配置

原理: Gradle 从 4.6 开始引入独立配置,将注解处理器(如 Lombok、MapStruct、Dagger、Glide 等)与其依赖分离,避免处理器 jar 被意外打包进最终产物。它仅在编译期生效,参与注解处理流程。

dependencies {
    annotationProcessor 'org.projectlombok:lombok:1.18.30'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
    implementation 'org.mapstruct:mapstruct:1.5.5.Final'
}
性能优化: 将注解处理器放在独立的 annotationProcessor 配置中,可避免增量编译时重新处理全部类;Gradle 增量处理器会缓存处理器类路径。

2.5 provided(War 插件中的 providedCompile)

原理: 在纯 Java 库中,provided 不是标准配置。但在 War 插件中,providedCompile 等效于 compileOnly + testCompileClasspath,即编译和测试阶段可用,但不会打包到 WAR 的 WEB-INF/lib。在普通 Java 插件中若要达到“编译可见、运行排除”效果,直接使用 compileOnly。

plugins {
    id 'war'
}

dependencies {
    providedCompile 'javax.servlet:javax.servlet-api:4.0.1'
    // 等价于 compileOnly + testImplementation(仅测试编译)
}
迁移注意: 从旧版 Gradle(<4.7)迁移的项目中可能存在 provided 配置(由某些插件提供),应统一改为 compileOnly 或 providedCompile(若使用 War 插件),并清理冗余依赖。

2.6 选择矩阵与常见陷阱

配置 编译类路径 运行时类路径 测试类路径 产物打包 典型场景
compileOnly ✓ ✗ ✗ ✗ Servlet API、Lombok
runtimeOnly ✗ ✓ ✓ ✓ 日志实现、JDBC 驱动
testImplementation ✗ ✗ ✓ ✗ JUnit、Mockito
annotationProcessor ✓ (仅处理阶段) ✗ ✓ (仅处理阶段) ✗ Lombok 处理器
providedCompile (war) ✓ ✗ ✓ ✗ Servlet API + 测试需要

2.7 调试与验证方法

  1. 查看依赖报告
    gradle dependencies --configuration compileClasspath
    gradle dependencies --configuration runtimeClasspath
  2. 确认是否打入产物
    jar tf build/libs/your-app.jar | grep -i 'driver\|lombok\|servlet'
  3. 运行时测试:编写一个使用 compileOnly 依赖的测试,在生产环境部署并启动,观察是否缺失类。
  4. 注解处理器失效:若 Lombok 未生效,检查是否错误地将 annotationProcessor 写成了 compileOnly 或 implementation。
最佳实践: 在 build.gradle 顶部添加注释块,明确声明每类依赖的作用域,便于团队维护。例如:
// ===== 编译期提供,运行时由容器提供 =====
compileOnly 'javax.servlet:javax.servlet-api:4.0.1'

// ===== 运行时驱动,编译时仅用接口 =====
implementation 'org.springframework.boot:spring-boot-starter-data-jpa:3.1.0'
runtimeOnly 'com.mysql:mysql-connector-j:8.1.0'

// ===== 测试框架 =====
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'

// ===== 注解处理器 =====
annotationProcessor 'org.projectlombok:lombok:1.18.30'

性能/稳定性提示: 避免在 compileOnly 中引入过大 jar(如完整 Spring Boot starter),否则会拖慢编译速度且无运行时价值。使用 runtimeOnly 时注意版本冲突,若多个 runtimeOnly 依赖传递了不同版本的同一库,运行时可能产生 NoSuchMethodError,建议统一管理版本(参考第 5 章版本对齐策略)。