快捷搜索:

被逼出来的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