面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月27日 23:32

Gradle 如何实现构建变体和多环境配置?

构建变体是 Android Gradle 构建系统的核心机制,面试中经常围绕 buildTypes、productFlavors、flavorDimensions 和 sourceSet 合并优先级展开。理解它的关键在于:构建变体 = 构建类型 × 产品风味,是多维度组合的结果。构建变体是什么构建变体(Build Variant)是 buildTypes 和 productFlavors 的交叉组合产物。每增加一个构建类型或产品风味,变体数量都会成倍增长。假设配置了 2 个 buildTypes 和 3 个 productFlavors,最终会生成 2 × 3 = 6 个构建变体。变体的命名规则是:产品风味名 + 构建类型名(驼峰拼接),例如 freeDebug、paidRelease。buildTypes 和 productFlavors 各负责什么buildTypes 定义构建的行为特征——是否混淆、是否可调试、签名配置等。它回答"这个包怎么构建"。productFlavors 定义产品的业务差异——包名、应用名、API 地址、功能模块等。它回答"这个包是什么版本"。两者职责不同,组合使用才能覆盖"多版本 × 多环境"的完整需求。flavorDimensions 的作用和顺序当存在多个产品风味维度时,必须用 flavorDimensions 声明维度名称和顺序:android { flavorDimensions "version", "environment" productFlavors { free { dimension "version" applicationIdSuffix ".free" } paid { dimension "version" applicationIdSuffix ".paid" } dev { dimension "environment" buildConfigField "String", "API_URL", "\"https://dev.api.example.com\"" } staging { dimension "environment" buildConfigField "String", "API_URL", "\"https://staging.api.example.com\"" } prod { dimension "environment" buildConfigField "String", "API_URL", "\"https://api.example.com\"" } }}维度顺序决定 sourceSet 合并优先级——排在前面的维度优先级更高。上面的配置中 "version" 优先于 "environment",意味着 src/free/ 中的资源会覆盖 src/dev/ 中的同名资源。这个配置会生成 2 × 3 × 2 = 12 个变体:freeDevDebug、freeDevRelease、freeStagingDebug … paidProdRelease。sourceSet 合并优先级规则Gradle 合并资源时按以下优先级从高到低查找:src/freeDevDebug/ — 构建变体源集(最具体,优先级最高)src/debug/ — 构建类型源集src/free/ — 产品风味源集(高优先级维度)src/dev/ — 产品风味源集(低优先级维度)src/main/ — 主源集(最低优先级)优先级高的源集中的同名资源会覆盖优先级低的。需要注意:不同源集中不能存在同名的 Java/Kotlin 类,否则构建报错;但资源文件(strings、drawables)可以覆盖。变体过滤:剔除无意义组合多维度组合容易产生不需要的变体,用 variantFilter 过滤:android { variantFilter { variant -> def names = variant.flavors*.name // 不需要 paid + dev 的组合 if (names.contains("paid") && names.contains("dev")) { variant.setIgnore(true) } }}过滤后,paidDevDebug 和 paidDevRelease 不会出现在构建变体列表中,减少干扰。多环境配置的常见写法实际项目中,多环境通常这样组织:android { buildTypes { debug { applicationIdSuffix ".debug" debuggable true minifyEnabled false } release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release } } flavorDimensions "environment" productFlavors { dev { dimension "environment" applicationIdSuffix ".dev" resValue "string", "app_name", "MyApp-Dev" buildConfigField "String", "BASE_URL", "\"https://dev.example.com/api\"" manifestPlaceholders = [ENV_NAME: "dev"] } staging { dimension "environment" applicationIdSuffix ".staging" resValue "string", "app_name", "MyApp-Staging" buildConfigField "String", "BASE_URL", "\"https://staging.example.com/api\"" manifestPlaceholders = [ENV_NAME: "staging"] } prod { dimension "environment" resValue "string", "app_name", "MyApp" buildConfigField "String", "BASE_URL", "\"https://api.example.com\"" manifestPlaceholders = [ENV_NAME: "prod"] } }}关键配置项说明:applicationIdSuffix:让不同环境可共存在同一设备上resValue:在构建时生成资源,代码中通过 R.string.app_name 引用buildConfigField:在 BuildConfig 中生成常量,代码中通过 BuildConfig.BASE_URL 引用manifestPlaceholders:向 AndroidManifest.xml 注入占位符变量,常用于配置不同环境的 meta-data变体特定依赖Gradle 支持按变体维度声明依赖:dependencies { // 构建类型维度 debugImplementation 'com.facebook.stetho:stetho:1.6.0' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' // 产品风味维度 devImplementation 'com.example:dev-tools:1.0.0' prodImplementation 'com.example:prod-analytics:1.0.0' // 构建变体维度(最细粒度) freeDevImplementation 'com.example:free-dev-helper:1.0.0'}依赖粒度从粗到细:implementation → buildTypeImplementation → flavorImplementation → variantImplementation。只在需要的变体中引入依赖,可以减小无用包体积。常见踩坑变体爆炸:3 个 flavorDimensions 各 3 个风味 + 2 个 buildTypes = 54 个变体。务必用 variantFilter 过滤无用组合,否则构建时间变长、AS 变体选择器卡顿。源集类冲突:src/free/ 和 src/paid/ 中不能有同名 Java 类。如果需要不同实现,要么放在各自的源集中,要么用接口 + 工厂模式在 main 中统一调度。资源覆盖陷阱:高优先级源集中的 strings.xml 不是整个文件覆盖,实际上资源合并是条目级别的,同名 key 会被覆盖,不同 key 会合并。但 Java/Kotlin 类不行,同包同类名直接报错。manifestPlaceholders 未生效:确保在 productFlavors 中赋值时用的是 = 而非闭包语法,且 AndroidManifest.xml 中用 ${ENV_NAME} 引用。
服务端阅读 05月27日 23:30

Gradle 有哪些常用命令?构建速度慢怎么优化?

Gradle 是 Android 和 Java 生态的主流构建工具,掌握它的常用命令和构建优化手段是开发者的基本功,也是面试高频考点。最常用的构建命令日常开发中,这几个命令用得最多:# 编译并打包项目./gradlew build# 清理上次构建产物后重新构建./gradlew clean build# 跳过测试加快构建./gradlew build -x test# 只构建 Debug 变体(Android 项目)./gradlew assembleDebug# 只构建 Release 变体./gradlew assembleReleasebuild 会执行编译、测试、打包全流程。如果只想产出 APK/AAB,用 assembleDebug 或 assembleRelease 更快,因为跳过了测试和校验步骤。依赖查看与分析依赖冲突是 Gradle 项目最常见的坑,这几个命令能帮你定位问题:# 查看项目完整依赖树./gradlew dependencies# 只看某个配置的依赖(如 implementation)./gradlew dependencies --configuration implementation# 查看特定模块的依赖./gradlew :app:dependencies# 深入分析某个依赖的来源./gradlew dependencyInsight --dependency gsondependencyInsight 比 dependencies 更实用——它直接告诉你某个库是从哪条路径引入的,在排查版本冲突时效率很高。任务查看与调试# 列出所有可用任务./gradlew tasks# 包括隐藏任务在内全部列出./gradlew tasks --all# 预览任务执行顺序但不真正执行./gradlew build --dry-run# 强制重新执行所有任务(忽略缓存)./gradlew build --rerun-tasks# 查看项目结构./gradlew projects# 查看项目属性./gradlew properties--dry-run 在排查任务依赖关系时很有用,能看到哪些任务会被触发但不会真的执行。构建优化:为什么构建这么慢?Gradle 构建慢,根本原因通常有三个:配置阶段重复执行、任务没有利用缓存、多模块没有并行。下面逐个解决。1. 启用构建缓存构建缓存让 Gradle 跳过输入未变化的任务,直接复用上次的输出:# 命令行临时启用./gradlew build --build-cache永久生效,在 gradle.properties 中配置:org.gradle.caching=true原理:每个任务根据输入内容的哈希值生成缓存键,输入没变就直接取缓存结果。修改一行代码不会导致整个项目重新编译。2. 启用并行执行多模块项目默认串行构建,开启并行后独立模块可以同时编译:# gradle.propertiesorg.gradle.parallel=true# 控制最大并行线程数(默认等于 CPU 核心数)org.gradle.workers.max=4命令行方式:./gradlew build --parallel --max-workers=4注意:只有模块间没有依赖关系的任务才能并行。如果你的模块是线性的依赖链,并行效果有限。3. 启用配置缓存这是 Gradle 8.x 之后最重要的优化。正常每次构建都要执行配置阶段(解析 build.gradle),配置缓存可以在构建脚本没变时直接跳过这一步:# 首次尝试启用(会报告不兼容的地方)./gradlew build --configuration-cache永久配置:org.gradle.configuration-cache=true迁移阶段建议先用 warn 模式:org.gradle.configuration-cache=warn这样构建不会中断,但会在日志里提示哪些代码需要修改才能兼容。Gradle 9.0 已将配置缓存作为默认行为。4. 启用按需配置只配置当前任务涉及的模块,跳过无关模块的配置阶段:org.gradle.configureondemand=true对大型多模块项目(10+ 模块)效果显著,小项目差别不大。5. 调整 JVM 内存Gradle 本身跑在 JVM 上,默认内存可能不够:# gradle.propertiesorg.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:+UseParallelGC-Xmx4096m:堆内存,大型项目建议 4GB 以上-XX:MaxMetaspaceSize=1024m:类元数据空间,避免 Metaspace OOM-XX:+UseParallelGC:并行垃圾回收,降低 GC 暂停6. 利用 Gradle DaemonDaemon 是常驻后台的 Gradle 进程,避免每次构建都启动新 JVM:# Daemon 默认已启用,确认状态./gradlew --status# 停止所有 Daemon(出问题时重启)./gradlew --stopDaemon 默认就是开启的,不需要额外配置。如果构建行为异常,先试试 --stop 重启 Daemon。诊断构建瓶颈优化之前先定位瓶颈在哪里:# 生成构建性能报告(HTML)./gradlew build --profile# 生成更详细的 Build Scan(上传到 Gradle 服务器)./gradlew build --scan--profile 会在 build/reports/profile/ 下生成 HTML 报告,按耗时排列各阶段和任务,一眼就能看出哪个任务最耗时。--scan 生成更全面的 Build Scan,包含依赖解析时间、缓存命中率和配置阶段耗时,适合深度排查。一份推荐的 gradle.properties 配置把上面的优化汇总成一份配置,直接复制到项目根目录的 gradle.properties:# 并行构建org.gradle.parallel=true# 构建缓存org.gradle.caching=true# 配置缓存(迁移阶段用 warn)org.gradle.configuration-cache=true# 按需配置org.gradle.configureondemand=true# JVM 内存和 GC 优化org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:+UseParallelGC# 非 Android 项目可选:开启文件系统监视(加速增量构建)org.gradle.vfs.watch=true对于 Android 项目,还可以在 build.gradle 中开启以下优化:android { // 只构建当前变体,跳过其他变体 variants.all { variant -> if (variant.name != 'debug') { variant.ignore = true } }}常见构建问题与排查命令# 依赖冲突:查看某个库的所有引入路径./gradlew dependencyInsight --dependency 'com.google.code.gson:gson'# 构建失败:查看完整堆栈./gradlew build --stacktrace# 更详细的调试信息./gradlew build --info# 强制刷新依赖(解决缓存损坏)./gradlew build --refresh-dependencies# 离线构建(只用本地缓存,不访问远程仓库)./gradlew build --offline# 持续构建模式(文件变化后自动重新构建)./gradlew build --continuous面试追问方向配置缓存和构建缓存有什么区别? 配置缓存跳过配置阶段(解析 build.gradle),构建缓存跳过执行阶段(任务的输入输出哈希匹配)。两者作用在不同阶段,互不冲突,可以同时开启。并行执行有什么限制? 只有不存在依赖关系的任务才能并行。如果模块 A 依赖模块 B,B 必须先完成。串行依赖链越长,并行收益越低。Gradle Daemon 会不会导致内存泄漏? 长时间运行的 Daemon 确实可能积累内存,Gradle 会在闲置 3 小时后自动停止 Daemon。如果遇到问题,手动 ./gradlew --stop 即可。
服务端阅读 05月27日 23:21

Gradle 是什么?它有哪些核心概念和优势?

Gradle 是一个基于 JVM 的构建自动化工具,融合了 Ant 的灵活性与 Maven 的约定优于配置理念,使用 Groovy 或 Kotlin DSL 编写构建脚本。它的核心概念围绕 Project、Task、Plugin 三层展开。Project每个 Gradle 构建由一个或多个 Project 组成。单模块项目只有一个 Project,多模块项目在 settings.gradle.kts 中声明子项目。每个 Project 对应一个 build.gradle.kts 脚本,是配置的顶级容器。TaskTask 是 Gradle 执行的最小单元,如编译、测试、打包。Task 之间通过 dependsOn 声明依赖关系,Gradle 据此生成有向无环图(DAG),按拓扑排序执行:tasks.register("copyFiles") { dependsOn("processResources") doLast { copy { from("build/resources") into("dist") } }}关键点:Task 分为配置阶段和执行阶段,doFirst / doLast 中的逻辑只在执行阶段运行。PluginPlugin 是功能扩展的核心机制。二进制插件通过 plugin ID 应用:plugins { id("org.springframework.boot") version "3.2.0"}插件内部通过注册 Task、扩展 DSL、配置依赖来改变构建行为。Java Plugin、Android Plugin 都是这个模式。构建生命周期Gradle 构建分三个阶段:初始化:执行 settings.gradle.kts,确定参与构建的 Project配置:执行所有 build.gradle.kts,构建 Task 依赖图执行:按 DAG 顺序执行目标 Task 及其依赖理解生命周期是排查构建问题的关键——配置阶段的代码每次构建都会执行,即使只运行一个 Task。依赖管理Gradle 支持 Maven、Ivy 仓库和本地文件依赖,提供 resolutionStrategy 处理版本冲突,默认取最新版本。implementation 与 api 的区别是常见追问点:implementation 依赖不传递,api 依赖会传递到上层模块。Gradle Wrapper项目自带 gradlew 脚本,自动下载指定版本的 Gradle,保证团队构建环境一致。无需手动安装 Gradle 即可构建项目。核心优势增量构建:只重新执行输入输出有变化的 Task构建缓存:跨项目复用相同 Task 的输出并行执行:多模块并行构建,利用多核 CPU这三项机制叠加,使 Gradle 在大型项目中的构建速度远超 Maven。常见追问Gradle 和 Maven 的核心区别? Maven 基于 XML 声明式配置,逻辑固定在插件中;Gradle 用 DSL 编程式定义构建逻辑,灵活度更高,且增量构建和缓存机制使其速度更快。implementation 和 api 有什么区别? implementation 的依赖对上层模块不可见,修改时不触发上层重新编译;api 的依赖会传递,适合暴露给消费者的公共 API 依赖。
服务端阅读 05月27日 23:20

Gradle 中的 Task 是什么?如何创建和配置 Task?

核心答案Task 是 Gradle 构建的最小执行单元,代表一个原子操作(编译、打包、测试等)。每个 Task 包含名称、类型、动作(Action)、依赖关系和输入/输出。创建 Task 有三种方式,推荐使用 tasks.register() 实现延迟初始化,避免配置阶段不必要的开销。创建 Taskregister vs create vs task 关键字// 推荐:延迟创建,仅在实际需要时实例化tasks.register('myTask') { doLast { println 'Executing myTask' }}// 立即创建,配置阶段就会实例化tasks.create('myTask') { doLast { println 'Executing myTask' }}// DSL 快捷方式,等同 createtask myTask { doLast { println 'Executing myTask' }}register 与 create 的核心区别在于:register 返回 TaskProvider,Task 实例在首次被引用时才创建。大型项目中大量使用 create 会导致配置阶段耗时增加,register 可显著改善构建性能。指定 Task 类型tasks.register('copyFiles', Copy) { from 'src/main/resources' into 'build/resources'}继承内置类型(Copy、Delete、Exec 等)可以复用已有的文件操作逻辑,不必从零实现。配置 Tasktasks.register('myTask') { group = 'Custom Tasks' // Gradle 面板中的分组 description = '自定义任务描述' // gradle tasks 时的说明 dependsOn 'clean', 'compileJava' // 依赖其他任务 mustRunAfter 'test' // 硬排序约束 shouldRunAfter 'build' // 软排序约束(可被 Gradle 忽略) inputs.file('config.properties') // 声明输入 outputs.dir('build/output') // 声明输出 doFirst { println '执行前' } doLast { println '执行后' }}声明 inputs/outputs 的意义在于启用增量构建:Gradle 检测到输入输出未变化时会跳过 Task 执行,这是提升构建速度的关键机制。依赖与排序dependsOn:声明必须先完成的依赖,Gradle 据此构建有向无环图(DAG)决定执行顺序mustRunAfter:硬约束,两个任务同时被调度时保证先后顺序,但不引入依赖shouldRunAfter:软约束,Gradle 在并行执行优化时可以忽略finalizedBy:无论当前任务成功或失败,都会执行收尾任务(如资源清理)tasks.register('taskA') { finalizedBy 'cleanup' doLast { println 'Task A' }}tasks.register('cleanup') { doLast { println 'Cleanup' }}执行控制// 条件执行tasks.register('conditionalTask') { onlyIf { project.hasProperty('runConditional') } doLast { println '条件满足才执行' }}// 禁用任务tasks.register('disabledTask') { enabled = false}// 抛出 StopExecutionException 跳过当前动作doFirst { if (!project.hasProperty('force')) { throw new StopExecutionException() }}追问register 创建的 Task 如何在其他地方引用? 使用 tasks.named('myTask') 获取 TaskProvider,再通过 .configure { } 追加配置。不要对 register 的 Task 直接用 tasks.myTask,那会触发立即实例化,失去延迟初始化的优势。mustRunAfter 和 dependsOn 有什么区别? dependsOn 会建立真正的依赖关系,被依赖的任务一定会执行;mustRunAfter 只是排序约束,仅当两个任务都在本次构建的执行计划中时才生效,不会把对方拉入执行图。为什么不要在 Task 体中直接写逻辑而要用 doFirst/doLast? Task 闭包中的代码在配置阶段执行,而 doFirst/doLast 中的代码在执行阶段执行。配置阶段写逻辑会导致每次运行任何 Task 都会执行这些代码,产生副作用和性能浪费。
服务端阅读 05月27日 23:19

Gradle 的增量构建是如何工作的?

核心机制Gradle 增量构建通过跟踪任务的输入(inputs)和输出(outputs)的哈希值来决定是否跳过执行。当输入未变化且输出存在时,任务标记为 UP-TO-DATE 并跳过;只有输入变化或输出缺失时才重新执行。关键点:首次构建记录所有输入输出的快照(哈希值)后续构建比较当前快照与历史快照增量任务可进一步获取文件级变更细节(新增/修改/删除),只处理变化部分输入输出声明没有正确声明输入输出的任务无法参与增量构建:abstract class ProcessTask extends DefaultTask { @InputDirectory abstract DirectoryProperty getInputDir() @OutputDirectory abstract DirectoryProperty getOutputDir() @TaskAction void execute(InputChanges changes) { if (changes.incremental) { changes.getFileChanges(inputDir).each { change -> // change.changeType: ADDED / MODIFIED / REMOVED // 只处理变化的文件 } } else { // 全量处理 } }}运行时 API 方式(无需自定义 Task 类):tasks.register('processFiles') { inputs.dir('src/main/resources') outputs.dir('build/processed') doLast { /* ... */ }}构建性能优化手段1. 启用增量编译与构建缓存# gradle.propertiesorg.gradle.caching=trueorg.gradle.parallel=true2. 远程构建缓存(CI 环境)buildCache { remote(HttpBuildCache) { url = 'https://cache.example.com/cache/' push = System.getenv('CI') != null }}3. 配置缓存(Configuration Cache):缓存项目配置阶段结果,避免每次构建重新解析 build.gradle,Gradle 8.x 已趋于稳定。4. 避免配置阶段耗时操作:将逻辑移入 doLast / doFirst,而非任务配置闭包中。常见增量构建失效的原因输入输出未声明或声明不全 → 任务每次都执行输出被外部进程修改 → 缓存校验失败任务存在非确定性输出(如含时间戳) → 同样输入产生不同输出outputs.upToDateWhen 中包含非确定性逻辑调试命令:./gradlew build --info # 查看 UP-TO-DATE 原因./gradlew build --rerun-tasks # 强制全量执行./gradlew build --scan # 生成构建分析报告追问Q: 增量构建和构建缓存有什么区别?增量构建在同一项目内比较输入输出快照跳过任务;构建缓存可以跨项目、跨机器复用任务输出,存储在本地或远程。两者互补:增量构建避免本地重复执行,构建缓存避免跨环境重复执行。Q: 输入归一化(Input Normalization)是什么?Gradle 比较输入时会对 classpath 做归一化处理——忽略 JAR 中的时间戳和顺序差异,只比较实际内容。可通过 @Classpath 和 @CompileClasspath 注解控制归一化策略,减少不必要的任务重新执行。Q: 配置缓存对增量构建有什么影响?配置缓存缓存的是配置阶段的结果(任务图、属性值),增量构建缓存的是执行阶段的输入输出快照。两者作用于不同阶段,同时启用效果叠加。但配置缓存要求构建脚本无副作用,部分插件可能不兼容。
服务端阅读 05月27日 23:18

Gradle 插件有哪些类型?如何创建和使用自定义插件?

核心回答Gradle 插件分两类:脚本插件和二进制插件。创建自定义插件有三种方式:在 build script 中直接编写、在 buildSrc 模块中组织、以及创建独立项目发布。插件类型脚本插件是额外的 .gradle 文件,通过 apply from 引入:apply from: 'gradle/checkstyle.gradle'只能在本项目复用,不推荐用于复杂逻辑。二进制插件实现了 Plugin<Project> 接口,打包为 JAR 后可跨项目共享:plugins { id 'java' // 核心插件,无需版本 id 'org.springframework.boot' version '3.0.0' // 社区插件,需指定版本}推荐使用 plugins DSL 而非旧式 apply plugin:。创建自定义插件的三种方式1. Build Script 内联直接在 build.gradle 中编写,适合一次性逻辑:class GreetingPlugin implements Plugin<Project> { void apply(Project project) { project.tasks.register('greet') { doLast { println 'Hello from plugin' } } }}apply plugin: GreetingPlugin无法在其他项目复用。2. buildSrc 模块在项目根目录创建 buildSrc/src/main/groovy/ 目录,插件类放于此。整个项目可见,可编写测试,但不能跨项目共享。3. 独立项目(推荐)创建独立的 Gradle 插件项目,发布到 Maven 仓库供任意项目使用:// 插件项目 build.gradleplugins { id 'java-gradle-plugin' id 'maven-publish'}gradlePlugin { plugins { customPlugin { id = 'com.example.custom-plugin' implementationClass = 'com.example.CustomPlugin' } }}插件实现类:package com.exampleclass CustomPlugin implements Plugin<Project> { void apply(Project project) { def ext = project.extensions.create('customConfig', CustomExtension) project.tasks.register('customTask') { group = 'Custom' doLast { println "Config: ${ext.name}" } } }}class CustomExtension { String name = 'default'}注册插件 ID(META-INF/gradle-plugins/com.example.custom-plugin.properties):implementation-class=com.example.CustomPlugin使用方引入:plugins { id 'com.example.custom-plugin'}customConfig { name = 'MyApp'}追问plugins DSL 与 apply 有什么区别? plugins DSL 在配置阶段解析,支持类型安全和版本管理;apply 在脚本执行时才应用,无法提前校验。自定义插件如何测试? 使用 Gradle TestKit 编写功能测试,在临时项目中运行构建验证行为。为什么推荐用 Java/Kotlin 而非 Groovy 实现插件? 静态类型减少二进制不兼容风险,Kotlin DSL 还能获得 IDE 补全支持。
服务端阅读 05月27日 23:17

Gradle 如何实现多项目构建?项目间依赖怎么配置?

Gradle 通过 settings.gradle 声明子项目,通过 project() 依赖建立项目间引用,配合 allprojects/subprojects 共享配置,实现多项目构建。核心机制多项目构建由一个根项目和若干子项目组成。settings.gradle 定义了哪些子项目参与构建,Gradle 据此建立项目间的依赖图并决定构建顺序。// settings.gradlerootProject.name = 'my-project'include 'app', 'library', 'common'// 嵌套项目include ':data:repository'project(':data:repository').projectDir = new File(rootDir, 'modules/data/repository')项目间依赖配置子项目之间通过 project() 函数声明依赖,Gradle 自动保证被依赖项目先构建:// app/build.gradledependencies { implementation project(':library') implementation project(':common') // 指定配置 implementation project(path: ':library', configuration: 'runtimeClasspath')}// library/build.gradledependencies { api project(':common') // api 会传递依赖给上层}implementation 与 api 的区别在多项目中尤为关键:api 暴露依赖给消费方,implementation 则不传递。共享配置的三种方式1. allprojects / subprojects 注入// 根 build.gradleallprojects { group = 'com.example' version = '1.0.0' repositories { mavenCentral() }}subprojects { apply plugin: 'java' java { sourceCompatibility = JavaVersion.VERSION_17 }}2. 约定插件(Convention Plugin)在 buildSrc 或独立插件项目中定义,比 subprojects 更灵活、可测试:// buildSrc/src/main/groovy/my-java-convention.gradleplugins { id 'java-library'}java { sourceCompatibility = JavaVersion.VERSION_17}// 子项目使用// library/build.gradleplugins { id 'my-java-convention' }3. 版本目录(Version Catalog)# gradle/libs.versions.toml[versions]spring-boot = "3.0.0"[libraries]spring-boot-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }// 子项目中使用dependencies { implementation libs.spring.boot.web}构建执行./gradlew projects # 查看项目结构./gradlew :app:build # 构建指定项目(自动构建其依赖项)./gradlew build --parallel # 并行构建并行构建需要在 gradle.properties 中启用:org.gradle.parallel=trueorg.gradle.caching=trueorg.gradle.configureondemand=true循环依赖问题项目 A 依赖 B,B 又依赖 A,Gradle 构建时会报错。解决方式是抽取公共部分到第三个模块,或者用 compileOnly/testImplementation 打断传递链。可通过 ./gradlew :app:dependencies --configuration runtimeClasspath 排查依赖链路。追问allprojects 和 subprojects 有什么区别?——allprojects 包含根项目,subprojects 只作用于子项目。约定插件相比 subprojects 闭包有什么优势?——可复用、可测试、支持按需引入,避免全局污染。如何让子项目独立构建而不触发其他项目?——configureondemand 配合按需配置,Gradle 6+ 默认部分启用。
服务端阅读 05月27日 23:16

Gradle 依赖管理有哪些配置类型?如何解决冲突?

Gradle 通过 configurations(依赖配置)和 repositories(仓库)两大机制管理依赖。configurations 定义依赖的可见范围和生命周期,repositories 定义依赖的获取来源。核心依赖配置类型| 配置 | 编译时 | 运行时 | 传递给消费者 | 典型场景 ||------|--------|--------|--------------|----------|| implementation | 可见 | 可见 | 不可见 | 默认选择,大多数依赖用这个 || api | 可见 | 可见 | 可见 | 库模块需要暴露给下游的 API 依赖 || compileOnly | 可见 | 不可见 | 不可见 | Lombok 等仅编译期需要的依赖 || runtimeOnly | 不可见 | 可见 | 不可见 | JDBC 驱动等仅运行时需要的依赖 || annotationProcessor | 可见 | 不可见 | 不可见 | 注解处理器 |dependencies { implementation 'org.apache.commons:commons-lang3:3.12.0' api 'org.apache.commons:commons-math3:3.6.1' // 消费者也能访问 compileOnly 'org.projectlombok:lombok:1.18.24' // 编译后丢弃 runtimeOnly 'mysql:mysql-connector-java:8.0.28' // 运行时才加载 annotationProcessor 'org.projectlombok:lombok:1.18.24'}implementation 与 api 的关键区别: implementation 的依赖不会出现在消费者的编译类路径中,改动后只重新编译当前模块;api 会传递,改动后消费者也要重新编译。优先用 implementation 可以显著缩短构建时间。版本管理推荐使用版本目录(Version Catalog),在 gradle/libs.versions.toml 中集中声明:[versions]spring-boot = "3.0.0"[libraries]spring-boot-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }dependencies { implementation libs.spring.boot.web}多模块项目也可用 BOM 统一版本:dependencies { implementation platform('org.springframework.boot:spring-boot-dependencies:3.0.0') implementation 'org.springframework.boot:spring-boot-starter-web' // 版本由 BOM 控制}依赖冲突与排除Gradle 默认取最高版本。当冲突不可自动解决时,可强制版本或排除:// 排除特定传递依赖implementation('org.springframework.boot:spring-boot-starter-web:3.0.0') { exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'}// 强制版本configurations.all { resolutionStrategy { force 'org.apache.commons:commons-lang3:3.12.0' }}查看依赖树排查冲突:./gradlew dependencies --configuration implementation追问Q: 为什么 implementation 改了不会触发下游模块重编译?A: implementation 的依赖被隔离在当前模块的编译类路径内,消费者编译时看不到这些依赖,因此改动不影响消费者的编译产物。这是 Gradle 3.4 引入 implementation 替代 compile 的核心动机。Q: 什么时候必须用 api 而不能用 implementation?A: 当模块的公开 API 方法签名中用到了某个依赖的类型时(如返回值或参数类型来自该依赖),必须用 api,否则消费者编译时会找不到类。Q: 动态版本(如 3.+)有什么问题?A: 构建不可复现——同一份代码不同时间构建可能拉到不同版本,导致线上行为不一致。生产环境必须锁定版本。
服务端阅读 05月27日 23:16

Gradle 的生命周期包括哪些阶段?

三个阶段:初始化 → 配置 → 执行Gradle 构建生命周期分为 初始化(Initialization)、配置(Configuration)、执行(Execution) 三个阶段,依次完成项目确定、任务图构建和任务执行。初始化阶段读取 settings.gradle,确定参与构建的项目并为每个项目创建 Project 实例:// settings.gradlerootProject.name = 'my-app'include 'app', 'core', 'utils'钩子:gradle.projectsLoaded配置阶段执行所有项目的 build.gradle,配置插件、依赖和任务,建立任务依赖图。无论最终执行哪个任务,所有项目的构建脚本都会被执行——这是性能问题的常见来源。// 延迟注册任务,避免配置阶段不必要的开销tasks.register('myTask') { onlyIf { project.hasProperty('enableMyTask') }}钩子:gradle.beforeProject / gradle.afterProject执行阶段按依赖图顺序执行目标任务及其依赖。支持增量构建(仅处理变更文件)和并行执行(--parallel)。钩子:gradle.taskGraph.whenReady / gradle.taskGraph.beforeTask / gradle.taskGraph.afterTask常见追问Q: 为什么配置阶段会成为性能瓶颈?每个项目的 build.gradle 都会执行,即使只跑一个任务。大量 I/O 或复杂逻辑放在配置阶段会拖慢整个构建。应使用 tasks.register() 延迟创建、启用 --configuration-cache 缓存配置结果。Q: afterEvaluate 的作用?在项目配置完成后回调,常用于在插件配置完成后再添加依赖或修改属性,确保顺序正确。Q: 增量构建的原理?Gradle 为每个 Task 维护输入输出的哈希,若输入输出均未变化则跳过执行(标记 UP-TO-DATE),而非真正重新运行。
服务端阅读 02月21日 19:11

Gradle 和 Maven 有什么区别?如何选择?

Gradle 与 Maven 是两个最流行的 Java 构建工具,它们各有优缺点。以下是两者的详细对比:历史背景Maven发布时间:2004年开发者:Apache Software Foundation设计理念:约定优于配置(Convention over Configuration)配置文件:XML(pom.xml)Gradle发布时间:2008年开发者:Gradle Inc.设计理念:结合 Ant 的灵活性和 Maven 的约定配置文件:Groovy/Kotlin DSL(build.gradle)核心差异对比1. 配置方式Maven (XML)<!-- pom.xml --><project> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>my-app</artifactId> <version>1.0.0</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.0.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>17</source> <target>17</target> </configuration> </plugin> </plugins> </build></project>Gradle (Groovy DSL)// build.gradleplugins { id 'java' id 'org.springframework.boot' version '3.0.0'}group = 'com.example'version = '1.0.0'java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17}dependencies { implementation 'org.springframework.boot:spring-boot-starter-web:3.0.0'}2. 构建性能Maven首次构建:较慢,需要下载依赖增量构建:基本支持,但不如 Gradle 高效并行构建:支持,但配置复杂构建缓存:有限支持Gradle首次构建:较快,增量构建优化增量构建:非常高效,只处理变更的文件并行构建:原生支持,配置简单构建缓存:强大的本地和远程缓存支持3. 依赖管理Maven<dependencies> <!-- 固定版本 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.0.0</version> </dependency> <!-- 使用属性 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>${spring.boot.version}</version> </dependency> <!-- 依赖管理(BOM) --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>3.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement></dependencies>Gradledependencies { // 固定版本 implementation 'org.springframework.boot:spring-boot-starter-web:3.0.0' // 使用版本目录(推荐) implementation libs.spring.boot.web // 使用 BOM implementation platform('org.springframework.boot:spring-boot-dependencies:3.0.0') implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa'}4. 生命周期Maven# Maven 生命周期cleanvalidatecompiletestpackageverifyinstalldeploy# 执行命令mvn clean installmvn clean packageGradle# Gradle 任务(无固定生命周期)cleanbuildtestjarinstallpublish# 执行命令./gradlew clean build./gradlew clean jar5. 插件系统Maven<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>17</source> <target>17</target> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>3.0.0</version> </plugin> </plugins></build>Gradleplugins { // 二进制插件 id 'java' id 'org.springframework.boot' version '3.0.0' // 脚本插件 apply from: 'gradle/checkstyle.gradle'}// 插件配置java { sourceCompatibility = JavaVersion.VERSION_17}springBoot { mainClass = 'com.example.Application'}优缺点对比Maven 优点标准化:严格遵循约定,项目结构统一稳定性:成熟稳定,生态系统完善学习曲线:相对简单,易于上手IDE 支持:所有主流 IDE 都有良好支持文档:文档丰富,社区活跃Maven 缺点灵活性差:XML 配置冗长,难以自定义构建速度:大型项目构建速度较慢依赖管理:版本冲突处理不够灵活扩展性:插件开发相对复杂Gradle 优点灵活性高:Groovy/Kotlin DSL 表达力强构建速度快:增量构建和缓存机制优秀依赖管理:灵活的依赖解析和版本管理多语言支持:支持 Java、Kotlin、Groovy、Scala 等可扩展性:插件开发简单,易于扩展Gradle 缺点学习曲线:需要学习 Groovy/Kotlin DSL复杂性:灵活性可能导致配置复杂标准化:不如 Maven 严格,可能导致项目结构不一致文档:虽然文档丰富,但相对分散适用场景适合使用 Maven 的场景传统企业项目:需要严格遵循标准简单项目:不需要复杂的构建逻辑团队熟悉度:团队已经熟悉 MavenCI/CD 集成:需要与现有 Maven 生态系统集成依赖管理简单:不需要复杂的依赖处理适合使用 Gradle 的场景大型项目:需要高效的增量构建复杂构建逻辑:需要自定义构建流程多模块项目:需要灵活的模块间依赖Android 开发:Android Studio 默认使用 Gradle性能要求高:需要快速构建和缓存迁移指南从 Maven 迁移到 Gradle# 使用 Gradle 的 Maven 插件生成初始配置./gradlew init --type pom# 或使用在线工具# https://start.spring.io/ 可以生成 Gradle 项目从 Gradle 迁移到 Maven# 使用 Maven 的 Gradle 插件mvn com.github.ekryd.sortpom:sortpom-maven-plugin:3.3.0:sort性能对比数据构建时间对比(基于相同项目)| 操作 | Maven | Gradle | 差异 ||------|-------|--------|------|| 首次构建 | 120s | 90s | Gradle 快 25% || 增量构建(无变更) | 45s | 5s | Gradle 快 89% || 增量构建(少量变更) | 60s | 15s | Gradle 快 75% || 清理构建 | 30s | 20s | Gradle 快 33% |最佳实践建议选择 Maven 如果项目结构简单,不需要复杂构建逻辑团队已经熟悉 Maven需要与现有 Maven 生态系统集成重视标准化和一致性选择 Gradle 如果项目规模大,需要高性能构建需要自定义构建逻辑团队愿意学习新技术需要支持多种语言或平台结论Maven 和 Gradle 都是优秀的构建工具,选择哪一个取决于项目需求和团队情况:Maven:适合传统、标准化、简单的项目Gradle:适合现代、高性能、复杂的项目两者可以共存,Gradle 可以导入 Maven 项目,Maven 也可以使用 Gradle 插件。选择时应该基于实际需求,而不是盲目追求新技术。