乐趣区

关于阿里云:高效使用Java构建工具|Maven篇|云效工程师指北

大家好,我是胡晓宇,目前在云效次要负责 Flow 流水线编排、任务调度与执行引擎相干的工作。

作为一个有多年 Java 开发测试工具链开发教训的 CRUD 专家,应用过所有支流的 Java 构建工具,对于如何高效应用 Java 构建工具积淀了一套办法。家喻户晓,以后最支流的 Java 构建工具为 Maven/Gradle/Bazel,针对每一个工具,我将别离从日常工作中常见的场景问题切入,例如依赖治理、构建减速、灵便开发、高效迁徙等,针对性地介绍如何高效灵便地用好这 3 个工具。

Java 构建工具的前世今生

在上古时代,Java 的构建都在应用 make,编写 makefile 来进行 Java 构建有十分多顺当与不便的中央。

紧接着 Apache Ant 诞生了,Ant 能够灵便的定义清理编译测试打包等过程,然而因为没有依赖治理的性能,以及须要编写简单的 xml,还是存在着诸多的不便。

随后 Apache Maven 诞生了,Maven 是一个依赖项治理和构建自动化工具,遵循着约定大于配置的规定。尽管也须要编写 xml,然而对于简单工程更加容易治理,有着标准化的工程构造,清晰的依赖治理。此外,因为 Maven 实质上是一个插件执行框架,也提供了肯定的开放性的能力,咱们能够通过 Maven 的插件开发,为构建形成发明肯定的灵活性。

然而因为采纳约定大于配置的形式,丢失了肯定的灵活性,同时因为采纳 xml 治理构建过程与依赖,随着工程的收缩,配置管理还是会带来不小的复杂度,在这个背景下,汇合了 Ant 与 Maven 各自劣势的 Gradle 诞生了。

Gradle 也是一个汇合了依赖治理与构建自动化的工具。首要的他不再应用 XML 而是基于 Groovy 的 DSL 来形容工作串联起整个构建过程,同时也反对插件提供相似于 Maven 基于约定的构建。除了在构建依赖治理上的诸多劣势之外,Gradle 在构建速度上也更具劣势,提供了弱小的缓存与增量构建的能力。

除了以上 Java 构建工具之外,Google 在 2015 年开源了一款弱小,但上手难度较大的分布式构建工具 Bazel,具备多语言、跨平台、牢靠增量构建的特点,在构建上能够成倍进步构建速度,因为它只从新编译须要从新编译的文件。Bazel 也提供了分布式近程构建和近程构建缓存两种形式来帮忙晋升构建速度。

目前业内应用 Ant 的人曾经比拟少,次要都在用 Maven、Gradle 和 Bazel,如何真正基于这三款工具的特点施展出他们最大的效用,是这个系列文章要帮大家解决的问题。先从 Maven 说起。

优雅高效地用好 Maven

当咱们正在保护一个 Maven 工程时,关注以下三个问题,能够帮忙咱们更好的应用 Maven。

● 如何优雅的治理依赖
● 如何减速咱们的构建测试过程
● 如何扩大咱们本人的插件

优雅的依赖治理

在依赖治理中,有以下几个实际准则,能够帮忙咱们优雅高效的实现不同场景下的依赖治理。

● 在父模块中应用 dependencyManagement,配置依赖
● 在子模块中应用 dependencies,应用依赖
● 应用 profiles,进行多环境治理

以我在日常开发中保护的一个规范的 spring-boot 多模块 Maven 工程为例。

工程内各个 module 之间的依赖关系如下,通常这也是规范的 spring-boot restful api 多模块工程的构造。

便捷的依赖降级

通常咱们在依赖降级的时候会遇到以下问题:

● 多个依赖关联降级
● 多个模块须要一起降级

在父模块的 pom.xml 中,咱们配置了根底的 spring-boot 依赖,也配置了日志输入须要的 logback 依赖,能够看出,咱们遵循了以下的准则:

(1)在所有子模块的父模块中的 pom 中配置 dependencyManagement,对立治理依赖版本。在子模块中间接配置依赖,不必再纠缠于具体的版本,防止潜在的依赖版本抵触。
(2)把 groupId 雷同的依赖,配置在一起,比方 groupId 为 org.springframework.boot,咱们配置在了一起。
(3)把 groupId 雷同,然而须要一组依赖独特提供性能的 artifactId,配置在一起,同时将版本号抽取成变量,便于后续一组性能独特的版本升级。比方 spring-boot 依赖的版本抽取成了 spring-boot.version。


在子模块 build-engine-api 的 pom.xml 中,因为在父 pom 中配置了 dependencyManagement 中依赖的 spring-boot 相干依赖的版本,因而在子模块的 pom 中,只须要在 dependencies 中间接申明依赖,确保了依赖版本的一致性。

正当的依赖范畴

Maven 依赖有依赖范畴(scope)的定义,compile/provieded/runtime/test/system/import,原则上,只依照理论状况配置依赖的范畴,在必要的阶段,只引入必要的依赖。

90% 的 Java 程序员应该都应用过 org.projectlombok:lombok 来简化咱们的代码,其原理就是在编译过程中将注解转化为 Java 实现。因而该依赖的 scope 为 provided,也就是编译时须要,但在构建出最终产物时又须要被排除。

当你的代码须要应用 jdbc 连贯一个 mysql 数据库,通常咱们会心愿针对规范 JDBC 形象进行编码,而不是间接谬误的应用 MySQL driver 实现。这个时候依赖的 scope 就须要设置为 runtime。这意味着咱们在编译时无奈应用该依赖,该依赖会被蕴含在最终的产物中,在程序最终执行时能够在 classpath 下找到它。

在子模块 dao 中,咱们有对 sql 进行测试的场景,须要引入内存数据库 h2。

因而,咱们将 h2 的 scope 设置为 test,这样咱们在测试编译和执行时能够应用,同时防止其呈现在最终的产物中。

更多对于 scope 的应用,能够参考官网帮忙文档。

多环境反对

举个简略的例子,当咱们的服务在私有云部署时,咱们应用了一个云上版本为 8.0 的 MySQL,而当咱们要进行专有云部署时,用户提供一个自运维的版本为 5.7 的 MySQL。因而,咱们在不同的环境中应用不同的 mysql:mysql-connector-java 版本。

相似的,在我的项目理论的开发过程中,咱们常常会面临同一套代码。在多套环境中部署,存在局部依赖不统一的状况。

对于 profiles 的更多用法,能够参考官网帮忙文档

依赖纠错

如果你曾经在父 pom 中应用 dependencyManagement 来锁定依赖版本,大概率的,你简直很少会碰到依赖抵触的状况。

然而当你还是意外的看到了 NoSuchMethodError,ClassNotFoundException 这两个异样的时候,有以下两个办法能够疾速的帮你纠错。

(1)通过依赖剖析找到抵触的依赖

(2)通过增加 stdout 代码找到抵触的类理论是从哪个依赖中查找的

通过具体的门路中对应的版本信息,找到对应的版本并校对。

当然这个办法也能够纠出一些依赖被谬误的加载到 classpath 下,非工程自身依赖配置引起的抵触。

测试构建过程减速

作为一个开发者,总会心愿咱们的工程无论在什么状况下,执行的又快又稳,那么在 Maven 的应用过程中,须要遵循以下准则。

● 尽可能复用缓存
● 尽可能的并行构建或测试

依赖下载减速

通常状况下,依据 Maven 配置文件 ${user.home}/.m2/settings.xml 中的配置,默认状况下是缓存在 ${user.home}/.m2/repository/。

通常在构建过程中,依赖的下载往往会成为比拟耗时的局部,然而通过一些简略的设置,咱们能够无效的缩小依赖的下载与更新。

● 优化 updatePolicy 设置
updatePolicy 指定了尝试更新的频率。Maven 会将本地 POM 的工夫戳(存储在存储库的 maven-metadata 文件中)与近程进行比拟。选项包含:always(总是)、daily(每天,默认值)、interval:X(其中 X 是以分钟为单位的整数)、never(从不)。

● 应用离线构建
除此之外,如果构建环境曾经存在缓存,能够应用 Maven 的 offline 模式进行构建,防止依赖或插件的下载更新。

直观的,日志中将不会呈现相似如下 Downloading 相干的信息。

构建过程减速

在默认状况下,Maven 构建的过程并不会充沛的应用你的硬件的全副能力,他会程序的构建你的 maven 工程的每一个模块。这个时候,如果能够应用并行构建,那么将有机会晋升构建速度。

以上是并行构建的两个命令,能够依据理论的 cpu 状况来抉择对应的命令。然而如果你发现构建工夫并没有失去缩小,那么你的 maven 模块间可能存在相似的依赖,模块之间只是一个简略的传递。

那么并行构建对你来说并不实用,如果你的模块间依赖关系存在并行的可能,那么应用上述命令进行构建,能力使并行构建施展成果。

测试过程减速

当咱们尝试减速 maven 工程测试用例的局部,那么就不得不提到一个插件,maven-surefire-plugin。

当你在执行 mvn test 的时候,默认状况下就是 surefire 插件在工作。如果咱们想在测试中应用并行的能力,能够作如下配置。

<p style=”text-align:center”></p>

然而须要留神不失当的应用并行能力进行测试,反而可能带来副作用。比方当 parallel 配置为 methods,然而因为某些起因测试用例的执行之间存在程序要求,反而会呈现因为用例办法并行执行,导致用例失败,因而也倒逼咱们,如果想取得更快的测试速度,case 的编写也须要独立且高效。

更多对于 surefire 插件的应用,能够参考这篇文档。

Maven 插件开发

maven 实质上是一个插件执行框架,所有的执行过程,都是由一个一个插件独立实现的。对于 maven 的外围插件能够参考这篇文档。

maven 默认为咱们提供的这些插件比方 maven-install-plugin/mvn-surefire-plugin/mvn-deploy-plugin 外,还有一些三方提供的插件,单测覆盖率插件 mvn-jacoco-plugin,生成 api 文档的 swagger-maven-plugin 等等。

在日常工作的过程中,我碰到了这样一个问题:有个存在显著问题的 sql 被公布到了预公布环境,同时因为预发与生产应用的是同一个 db 实例,因为 sql 的性能问题,影响了线上。

除了通过必要的 code review 准入,来防止相似的问题,更简略的,咱们能够本人入手实现一个代码中 sql 扫描的插件,让代码在 CI 时间接失败掉,自动化的防止此类问题的产生。于是咱们开发了一个 maven 插件,应用办法和成果如下:

在工程中引入咱们开发并部署好的插件 com.aliyun.yunxiao:mybatis-sql-scan。

执行以下命令,或其余蕴含 validate 阶段执行的命令。

咱们将会在日志中看到如下插件执行的信息

在扫描出缺点时,build 失败,并会在日志中呈现对应的信息:

在 GlobalLockMapper.java 这个文件中,咱们有一条全表扫描的 sql 语句可能存在危险,

同时 build 失败。

接下来我会从如何开发这个异样 sql 扫描的 maven 插件动手,帮忙大家理解插件开发的过程。

1、创立工程

生成的 sample 工程如下,

其中 MyMojo.java 定义了插件的入口实现,

此外在根 pom.xml 中能够看到,

● packaging 为“maven-plugin”。
● 依赖配置中,依赖了一些插件开发的根底二方库。
● 插件节点下,依赖了 maven-plugin-plugin 帮助咱们实现插件的构建。


2、Mojo 实现

在开始实现咱们的 Mojo 之前,咱们须要做如下剖析:

● 插件在 maven 的哪个生命周期执行
● 插件在执行时须要哪些入口参数
● 插件执行实现后怎么退出

因为咱们要实现的插件是要做 mybatis annotation 扫描比方 @Update/@Select,判断是否有异样的 sql,比方是否存在全表扫描的 sql,是否存在全表更新的 sql 等,对于此种场景下,

● 因为须要扫描特定的源码,须要晓得工程源码的所在目录,以及扫描哪些文件
● 插件扫描出异样时,只有报错即可,不必产出任何报告
● 心愿在后续执行 mvn validate 时触发扫描

那么预期中的插件是这样的,

那么,

● @Mojo(name = “check”) 定义了 goal
● @Parameter
○ @Parameter(defaultValue = “${project}”, readonly = true) 参数绑定了工程的根目录,project.getCompileSourceRoots()便能够获取到源代码的根门路
○ 咱们定义了 mapperFiles,用来负责扫描哪些文件的通配,excludeFiles 用来负责排除哪些文件
● execute()
○ 有了以上的根底,在 execute 办法中咱们便能够实现对应的逻辑,当扫描结出异样的 sql 时,抛出 MojoFailureException 异样,插件便会失败终止。

以上,咱们便实现了一个插件的根本能力的开发。

3、插件的打包与上传

插件开发实现后,咱们能够通过配置 distributionManagement,而后执行 mvn deploy,实现插件的构建与公布。

心愿通过我的介绍,可能帮忙大家更好的应用 maven,下一篇咱们讲 Gradle,欢送继续关注咱们。

点击下方链接,即可收费体验云效流水线 Flow。

https://www.aliyun.com/product/yunxiao/flow?channel=yy_practice

退出移动版