被逼出来的Gradle学习总结
Gradle是一个构建工具,核心功能是解决“库”的依赖问题。表面看起来gradle似乎是配置文件,但底层是groovy语言,掌握Gradle实际上相当于学习Groovy语言、Gradle框架、在Gradle上开发或利用别人的插件,所以学习曲线异常陡峭。
背景
绝大多数软件的编写,都需要大量依赖第三方的专业的库,无论是C++、Rust、Java还是Python。比如Python中的numpy用于数据值计算、Rust中的fastrand的随机数产生、Java中的'org.apache.spark:spark-core_2.13:3.4.1'用于离线大数据计算、C++中的boost库。
- C++1983年发布,到40年后的今天,还没有一个统一的构建工具。各个大厂都有自己内部方案,目前Google开源的bazel用得比较多。
- Java 1995年发布,通常是Maven/Gradle两个构建工具, 其中maven 2001年发布, gradle 2012年发布, 都支持通过网络下载第三方的依赖Jar包。
- Python 有pip/conda包管理工具,且支持构建一个虚拟环境
- Rust/Go 原生自带构建工具。
无论是否开源,本质上都是将各个版本及编译的源码或二进制在远程保存(比如可以是https, 也可以是git的仓库),编译时通过配置,拉取对应的源码编译,做得好的话,可以分布式编译,并构建缓存,加快编译速度。
本文重点是Gradle, 大数据处理,基本离不开Java, 虽然Maven和Gradle比较难用,但硬着头皮选一个,只能gradle了, 受不了xml的臃肿。
Gradle
构建工具,依赖管理是重要的的一个目标:
- 如何管理依赖?尤其是网络依赖项,现代的构建工具都已经支持自动下载。
- 依赖冲突如何解决?一个软件的依赖,可以表示成一张DAG, 如果这张图中出现了多个不兼容的多个版本的第三方软件,需要解决依赖冲突。
Java中的Jar包,使得问题变得更复杂。比如待运行任务的机器,已经自带Flink的一些内置依赖包,打包时不需要将相关的包打进去,但是本地编译调试时,又需要依赖。
Gradle设计时,将各种特定场景抽象成了不同的sourceSet, 每个sourceSet生成不同的Task, 每个任务通过不同的Configuration, 来表示不同用途的Dependency。
- sourceSet: 逻辑上来看,代码可分为应用代码、单元测试代码等。每类代码有各自的依赖,Java将其分为main/test两类src/main: 此目录存main sourceSetscr/test: 此目录存test sourceSet
- task: gradle任务,比如build、check、jar、run等
- configuration: 一组依赖dependency,用于某种特定的用途。比如compileOnly, runtimeOnly, implementation
- dependency: 编译、测试或执行时,依赖于外部的模块,从仓库Repository中根据GAV座标获取远程依赖
Gradle将代码分成不同的sourceSet, 针对每种sourceSet, 生成特定的任务,比如compile{sourceSet}Java/process{sourceSet}Resoures, 其中每个任务通过Configuration(比如{sourceSet}compileOnly, {sourceSet}implementation)来配置Dependency。比如:
- compile{sourceSet}Java (task){sourceSet}compileOnly (configuration){sourceSet}implementation (configuration){sourceSet}runtimeonly (configuration)
- process{sourceSet}Resoures (task)
task
gradle是基于任务的构建,比如gradle build, 其中build就是一个内置任务。
Figure 1: 常见task依赖图
可以通过com.dorongold.task-tree插件,在命令行打印任务树:
gradle jar taskTree
gradle build taskTree
gradle tasks --all
可以编写脚本,自定义任务。比如新增info任务:
tasks.register("info") {
FileCollection compileClasspath = configurations.compileClasspath
FileCollection runtimeClasspath = configurations.runtimeClasspath
dependsOn compileClasspath
dependsOn runtimeClasspath
doLast ({
println "project.version = ${project.getVersion()}"
println "project.name = ${project.name}"
println "project.group = ${project.group}"
println "project.description = ${project.description}"
println "compiletime_classpath = ${compileClasspath.collect { File file -> file.name }}\n"
println "runtime_classpath = ${runtimeClasspath.collect { File file -> file.name }}\n"
})
}
dependency
Gralde 依赖管理的整体架构,注意有本地的cache, 可以支持增量构建,加速编译。
Figure 2: dependency management for JVM
严格上来讲,有两种依赖:
- 直接依赖(direct depencencies): 当前的代码,直接依赖于某个模块,比如你的代码用到了Guava, Guava就是当前项目的直接依赖
- 间接依赖(transitive depencencies): 当前的代码,直接依赖于模块A, 但A又依赖于Y, 我们说Y是当前项目的间接依赖, 这种是由传递性导致的依赖
举例:
- 任务 A 依赖于任务 B,
- 而任务 B 依赖于任务 C,后者生成了一个特定文件作为任务 A 所需的输入。
- 任务 B 的所有者确定不再需要依赖于任务 C,删除依赖C, 但会导致任务 A 失败。
下面代码中, httpclient是直接依赖, commons-codec是间接依赖,仅当commons-codecs作为间接依赖时,才生效。
dependencies {
implementation 'org.apache.httpcomponents:httpclient:4.5.3'
constraints {
implementation('commons-codec:commons-codec:1.11') {
because 'version 1.9 pulled from httpclient has bugs affecting this application'
}
}
}
configuration
通过不同的配置Configuration, 来声明特定用途的依赖。
Figure 3: Configurations use declared dependencies for specific purposes
sourceSet main的配置依赖关系:
Figure 4: Java plugin - main source set dependency configurations
sourceSet test的配置依赖关系:
Figure 5: Java plugin - test source set dependency configurations
- compileOnly: for dependencies that are necessary to compile your production code but shouldn’t be part of the runtime classpath 仅编译时需要,不会参与打包我希望别人来导入我需要的库,如果你不导入我要的库,运行时我就报错给你看,所以使用者要包含complieOnly的依赖spark/flink中,服务器的运行环境已经内置一些jar包,这些包不需要用户提供一般依赖公共库,由使用者指定,保证整个项目统一 (但兼容性两方需要保持一致)
- implementation (supersedes compile) — used for compilation and runtime我不准别人使用我依赖的库。你想用,自己去依赖。 你可以不提供我依赖的库并且我保证运行正常依赖库只能当前module使用,是内部实现, 会参与打包
- api:我允许别人使用我依赖的库会把依赖库传给使用者,使用者的项目External Libraries 中能够看的见。可以在其他module使用,会参与打包。
- runtimeOnly (supersedes runtime) — only used at runtime, not for compilation
- testCompileOnly — same as compileOnly except it’s for the tests
- testImplementation — test equivalent of implementation
- testRuntimeOnly — test equivalent of runtimeOnly
配置可以通过继承复用。
configurations {
smokeTest.extendsFrom testImplementation
}
dependencies {
testImplementation 'junit:junit:4.13'
smokeTest 'org.apache.httpcomponents:httpclient:4.5.5'
}
下面的代码中, task shadowJar 通过属性configurations配置这个任务的依赖,依赖于flinkShadowJar Configuration, 而flinkShadowJar在configurations代码块中排除了一些依赖,在depencencies中配置了一些依赖。
configurations {
flinkShadowJar
flinkShadowJar.exclude group: 'org.apache.flink', module: 'force-shading'
}
dependencies {
implementation("org.apache.flink:flink-streaming-java:${flinkVersion}") {because("DataStream API")}
flinkShadowJar("org.apache.flink:flink-walkthrough-common:${flinkVersion}")
runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl:${log4jVersion}")
}
shadowJar {
// configruations是shadowJar的属性
configurations = [project.configurations.flinkShadowJar]
}
常用命令
# dependency
gradle dependency
gradle dependencyInsight
gradle -q dependencies --configuration testRuntimeClasspath
gradle -q dependencies --configuration tRC
gradle dependencyInsight
gradle -q dependencyInsight --dependency flink-file-sink-common --configuration compileClasspath
# configuration
gradle resolvableConfigurations
# task
gradle build --dry-run
gradle build
# 查看jar的文件列表
jar -tf hello-0.1.0-all.jar |grep class
依赖冲突
思路: 通过插件shadow将有冲突的Jar包重命名(relocation)实现不兼容的多版本共存的问题。
举例:
大数据项目需要,依赖于hive-exec, hive-exec依赖于protobuf-java:2.6.1, 而我们想使用protobuf 3.23.2的版本。
- org.apache.hive:hive-exec:2.3.6com.google.protobuf:protobuf-java:2.6.1
- com.google.protobuf:protobuf-java:3.23.2
此时可通过shadow插件,将org.apache.hive中的2.6版本的protobuf重命名解决。此时两个版本可共存,内部已经为两个不同名称的包,认为是两个不同的jar.
plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '7.1.2'
}
ext {
hiveVersion = '2.3.6'
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(8)
}
}
dependencies {
implementation "org.apache.hive:hive-exec:${hiveVersion}"
}
shadowJar {
relocate 'com.google.protobuf', 'com.xxx.shaded.google.protobuf'
}
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
- 新建立一个项目, 用上面的内容作为build.gradle,执行gradle shadowJar生成一个shaded.jar
- 原项目新增一个shaded.jar依赖即可(此时shaded.jar内部含com.xxx.shaded.google.protobuf:2.6.1, 不会和com.google.protobuf:protobuf-java:3.23.2冲突)
点评
java 目前的两个依赖, maven太臃肿,gradle 学习曲线陡峭,不知道java程序员怎么忍受了这么多年
JVM语言太多了:
- java: JDK 21都发布了,但现状是: 新版任你发,我用Java8
- scala: 我为了学Spark, 学了Scala;
- groovy: 我为了学习gradle,学了groovy; 这两种语言语法糖太华丽,怎么写都对,一运行都错;
- koltin: 这个貌似是为了取代Java
gradle:除了Java, 我还可以编译C++; bazel: 我可以编译Java、C++、Go、Android、iOS 以及其他许多语言。那我是不是都要学,这么多坑……
参考
- https://docs.gradle.org/current/userguide/dependency_management_terminology.html
- https://bazel.build/basics?hl=zh-cn
- https://mp.weixin.qq.com/s/4AI7H428oSc4fWgcK3KOpQ
[注:本文部分图片来自互联网!未经授权,不得转载!每天跟着我们读更多的书]
互推传媒文章转载自第三方或本站原创生产,如需转载,请联系版权方授权,如有内容如侵犯了你的权益,请联系我们进行删除!
如若转载,请注明出处:http://www.hfwlcm.com/info/253733.html