由于项目需要,我最近学习了一点关于 Gradle 的入门知识,本篇以一个含有多个子项目的 Java SpringBoot 工程为例,总结分享一下Gradle 的基本原理和正确使用方式。
写这篇文章,主要因为网上查阅到的 Gradle 相关博客和中文文档几乎都已经过时了,Gradle 官方文档很靠谱但是是英文的,我读了一些文档,结合自己的理解尽量写的深入浅出一些,适合 Java 开发看。但是要系统学习 Gradle 还得看官方文档。
##Gradle 从陌生到入门七问
Gradle 是什么?
Gradle 是一个以 Groovy/Kotlin 作为 DSL 的灵活的,可扩展的,高性能构建工具。广泛应用于多种语言和框架的项目构建,比如 Android 项目、Java 类库和后端项目、多编程语言的项目等等
Gradle 与 Maven 最本质的区别?
Maven 的目的是用 XML 描述项目的项目管理工具,而 Gradle 是用 DSL 描述 Build Task 的自动化构建工具。虽然功能有重合的部分,但根本上它俩的思想和机制是完全不一样的。
为什么 Java 项目推荐用 Gradle 替换 Maven?
- Gradle 比 Maven快;
- Gradle 默认使用的 DSL 是定制的 Groovy,比 XML简洁;
- Gradle 的插件机制比 Maven 更方便灵活;
- Gradle 的生态比 Maven 更加广泛,支持的编程语言和技术平台非常多样。
Gradle Wrapper 是什么?
由于 Gradle 的版本迭代很快,为了能更方便管理 Gradle 自身的版本,并且让不同项目之间的 Gradle 版本隔离开不会互相影响,Gradle 项目最好使用 Wrapper 模式。
使用 Wrapper 模式时,项目根目录有一个 CMD/Shell脚本(gradlew)用来启动 Gradle Wrapper,根据gradle-wrapper.properties的定义,wrapper 从 CDN 下载对应的 Gradle 版本并执行构建,免去了开发人员自己去下载和配置 Gradle 环境变量的负担。
如果在当前项目中升级 Gradle 的版本,执行下面这行命令即可:
|
|
使用 Wrapper 之后,本地也不需要事先安装和配置 gradle 命令,直接用项目文件中的 gradlew 命令即可。
Gradle 的基本概念有哪些?
- 三级数据模型:Build,Project,Task。一个 Build 可以包括 1 到 N 个 Project,一个 Project 包括 1-N 个 Task;
- Task 之间的关系是有向无环图(DAG),构建过程就是按照 DAG 依次执行其中的 Task,Gradle 可以看成指挥官,是Task 的调度器;
- Plugin 是底下真正干活的,不同的 Plugin实现不同功能的 Task,添加 Plugin 就会在 Build 中预定义 Task 和 Configuration。
下图即是一个常规的 Java 项目 Build 过程经历的 Task,像 jar/classes 等等这些 Task 的实现就是Java Plugin提供的。
Gradle 的命令怎么用?
|
|
在线的 Scan Report,可以看到每个 Task 的耗时、依赖树、单元测试失败的栈轨迹等等,花钱买 Gradle Enterprise 可解锁更强的功能哦。
Gradle 的原理和核心流程是什么?
这个问题我们用示例来说明,我们创建一个简单的 SpringBoot Web 工程,包含 3 个子项目:
- application-api:是导出给其他服务用于 RPC 调用的模块;
- application-core:是这个 Web 工程的核心实现模块,包括 controller/service/model 等等 MVC 结构的常用包;
- application-boot:是 SpringBoot 的启动模块,包括启动类和 Spring 的 JavaConfig 配置类等等。
其目录结构如下(build.gradle 里面具体内容在最后一节贴出详细代码来分析):
├─build.gradle # 描述根项目的依赖和构建的核心文件 ├─settings.gradle # 描述根目录和子项目的关键信息 ├─gradlew # Wrapper 的 shell script ├─gradle.bat # Wrapper 的 cmd script ├─application-api │ ├─build.gradle # 描述子项目的依赖和构建等信息 │ └─src │ └─main │ └─java │ └─com.example.x ├─application-boot │ ├─build.gradle # 描述子项目的依赖和构建等信息 │ └─src │ ├─main │ │ ├─java │ │ │ └─com.example.x │ │ └─resources │ └─test │ └─java │ └─com.example.x ├─application-core │ ├─build.gradle # 描述子项目的依赖和构建等信息 │ └─src │ └─main │ └─java │ └─com.example.x └─gradle └─wrapper ├─gradle-wrapper.jar # 一个很轻量的可执行 jar,用于下载 Gradle └─gradle-wrapper.properties # 描述 gradle 版本和下载相关配置 |
项目我们建好了,当我们执行gradlew clean build -x test就可以构建出来一个可执行的 SpringBoot 工程的 jar 包了。那么执行这行命令时,到底发生了什么?
- gradlew用 Java 命令启动gradle-wrapper.jar,gradle 尝试复用或创建一个指定版本的 gradle,下载解压后,启动gradle daemon后台服务;
- gradle 扫描当前目录下的gradle.properties,用户目录(~/.gradle)下的gradle.properties等配置参数;
- 寻找当前目录下的settings.gradle,build.gradle 等文件,分析并魔改 Groovy/Kotlin 文件的抽象语法树,构建 Task 执行的步骤;
- 如果 settings.gradle 文件include了其他子项目,继续找到对应的目录里的build.gradle等文件并分析内容,在这里就是 application-boot/application-core/application-api 三个子项目;
- 如果依赖的特定版本的插件或库缺失,会到 gradle/maven 中心仓库或自定义仓库,下载缺少的Plugin和Dependency;
- 开始按照流程图执行 ‘clean’ Task,成功后再执行 ‘build’ Task,由于参数-x 指定了跳过’test’ Task,DAG 中的 ‘test’ Task 的子节点全部被排除无需执行,下面即是我们这个示例中的 Task 顺序图。
:build +— :assemble | +— :bootJar | | +— :classes | | | +— :compileJava | | | | --- :application-boot:compileJava <br>| | | | +--- :application-api:compileJava <br>| | | | — :application-core:compileJava | | | | --- :application-api:compileJava <br>| | | — :processResources | | +— :application-api:jar | | +— :application-boot:jar | | --- :application-core:jar <br>| — :jar | --- :classes <br>| +--- :compileJava <br>| — :processResources | ……. # 省略了一些输出 --- :check <br> — :test # 未被其他 Task 使用的子节点已经被排除 |
注:上面的执行流程也是org.barfuin.gradle.taskinfo这个 Gradle Plugin 提供的 ‘tiTree’ Task 生成的,另外,由于我们使用了 SpringBoot Plugin,assemble Task 中执行的是 ‘bootJar’ Task,而不是一节图中的 ‘jar’ Task。
Java + Gradle 项目实战指南
加依赖的正确方式
对于开发人员来说,不管是 Maven 还是 Gradle,最常用的功能就是二方/三方库的依赖管理,我们从最简单的场景开始。大多数情况下,Gradle 定义依赖只需要在dependencies 闭包里写一行,比如这样的:
|
|
注意:compile 指令早在 Gradle 3.x 版本就已经弃用,在 Java 项目中使用 compile 会导致依赖传递(Transitive Dependency),也就是在 A 中定义的 compile 依赖,会传递到依赖了 A 的 B 项目编译期间的 Classpath,污染了 B 项目。
我们再考虑一些复杂的情况:有时候,我们又想要依赖传递出去;有时候,我们想某个库对于依赖方不是必须的;有时候只想一些依赖在单元测试的时候编译进去…
- api 指令: 声明一个传递依赖,等价于之前的 compile,也等价于 Maven 的compile scope,慎用;
- runtimeOnly 指令: 相当于 Maven 的runtime scope,仅运行时需要的 jar,比如 MySQL Connector, Logback/Log4j 等;
- compileOnly 指令: 仅添加到编译时的 classpath 中,类似于 Maven 的provided scope,不会打包到最终产物中, 比如lombok适合用 compileOnly;
- annotationProcessor 指令: JSR-269 引入了编译期注解处理器, 在 Java 1.6 之后提供了这个修改源码 AST 的机会,我们熟悉的 lombok 用的就是这个原理,需要搭配 compileOnly 使用;
- testXXX 指令: 仅 Test Task 执行的指令,类似 Maven 的test scope, Gradle 对 test 依赖的定义更加细致,包括testImplementation, testCompileOnly等细分指令。
注:有一些 Java 相关的添加依赖指令,比如 api、annotationProcessor 等,是Java Plugin实现的,因此需要事先在 build.gradle 声明:
|
|
指定仓库
指定仓库也是常见操作,比如添加企业内部 Maven 仓库。在单项目工程中,可以直接在build.gradle以及buildscript里声明中心仓库。
|
|
对于多项目工程/Monorepo,可以在allprojects/subprojects闭包中声明 repositories,也可以把这段脚本抽到单独的文件,使用apply from的方式引入到所有需要的地方:
|
|
解决依赖冲突
依赖冲突问题想必大家都遇到过,解决这类问题,首先需要分析依赖树,Gradle 提供了相应的 Task:
|
|
结果中可以看到整个项目的依赖树,如果需要定点查看某个库的依赖情况,执行dependencyInsight命令,需要注意–configuration 参数的区别。
|
|
下面是我们对 slf4j-api 的依赖分析的截取结果,第一行的含义就是即使 logback-classic 库依赖的是1.7.25版本的 slft4j-api,由于其他库依赖了1.7.30这个更高版本的 slf4j-api,最终使用的是1.7.30。
|
|
用 dependency/dependencyInsight 找到根因后,解决依赖问题通常的做法,是排除某些有问题的依赖,或者强制指定某个库的版本。
- 全局强制排除依赖,在 build.gradle 中直接写全局的以来排除 configuration,如果是多项目工程可以在对应的build.gradle或者allprojects/subprojects闭包下写这段
|
|
- 单点排除依赖
|
|
- 强制某个依赖使用某版本
|
|
最后,验证问题是否已经解决,可以从下面几个方面入手:
- 再次执行依赖分析的命令;
- 从最终构建产物直接分析,对于 SpringBoot 工程,把最终构建的 jar 解压,在 BOOT-INF/lib 下找到三方库,确定某个库打包版本是否正确;
- 在部署运行后,使用 arthas 等诊断工具寻找运行期间加载的类。
可选依赖
Gradle 没有 Maven 中的 Optional Dependency, 因为 Maven 的 Optional 是一个有问题的设计,比如 A 声明需要 Optional 的库 C,这时 C 不会传递给依赖 A 的项目 B,B 项目就很容易出现各种找不到类、找不到方法的报错,写 Java 的同学一定经历过这类依赖问题导致的报错。
这个场景中,问题的本质在于:难道要让 B 去了解三方库 A 里,有哪些依赖是需要手动加到自己项目的,哪些是不需要手动加的?强迫依赖方 B 去了解三方库 A 的实现细节,学过软件工程的人都知道这不是一个正确的设计。
Gradle 设计了一个语义化更强的 Feature+Compability 方案,参考文档:https://blog.gradle.org/optional-dependencies。大概流程是这样的:
- 类库 A 的 gradle 文件中声明提供 Feature C: java { registerFeature(‘featureC’) {}
- A 的 dependencies 闭包这样写:dependencies { featureCImplementation(‘some-lib-required-for-feature-c’) }
- 依赖方 B 项目在依赖时声明是否需要该 Feature:implementation(‘some-group:lib-A:1.0.0’) { capabilities { requireCapability(‘featureC’) } }
发布到 Maven 仓库
Maven-Publish Plugin 可以实现发布 jar 到 Maven 仓库,具体参考文档:https://docs.gradle.org/current/userguide/publishing_maven.html。
全套组件的统一依赖管理
在 Maven 中有MavenBOM(Bill of Materials),pom.xml 的 dependencyManagement 声明整套组件的 BOM,所有相关组件都无需写特定的版本号,对于 Spring 全家桶,SpringBoot 全家桶,SpringCloud 全家桶这类生态型技术平台的依赖管理非常有用。Gradle 有没有类似的功能呢?网上搜索的资料大多是这样的:
|
|
这是已经过时的用法,新项目就别再用这种方式了!Gradle 5.0 之后支持的原生的platform指令更加简洁。
|
|
如果是在中大型企业中,架构组可能会自己做技术中台,比如开发一套内部生态组件,对于生态系统的开发方,如何确保多个组件之间的依赖约束呢?
答案是使用Java-Platform Plugin,具体使用方式这里不再展开,参考文档:https://docs.gradle.org/current/userguide/java_platform_plugin.html
SpringBoot/SpringCloud 组件的依赖约束
上面已经提到,对于全家桶式的一套组件,应该使用 Gradle 的Platform特性,具体代码如下:
|
|
SpringBoot Plugin 的原理
SpringBoot Plugin 会给 Gradle 加一个bootJar Task, 在打 jar 包时把设置的 mainClass、SpringBoot JarLauncher 等信息到META-INF/MANIFEST.MF中。
|
|
最终构建结果中的 META-INF/MANIFEST.MF
|
|
示例:多模块 SpringBoot 项目
####项目结构
完整的 Gradle 代码
./settings.gradle
|
|
./build.gradle
|
|
./release.gradle
|
|
./application-api/build.gradle
|
|
./application-core/build.gradle
|
|
./application-boot/build.gradle
|
|