微服务开发系列:为什么用 gradle 构建

源码地址

在该微服务架构中,并没有使用常见的 maven 作为管理工具,而是使用了 gradle。

我在使用 maven 搭建这个架构完成了大部分的工作之后,决定全面转向 gradle,花了四天的时间才全面熟悉与替换。

总结一下这个框架中为什么使用 gradle,以及使用 gradle 需要遵守的规则。

1 maven 与 gradle

两者相比较思想是一样的,都是管理 jar 的依赖版本,定义了一个项目从编译到打包中间的各个阶段。

在使用下来之后发现,对于小型或者单个项目,使用 maven 与 gradle 实际上没有什么差别,甚至 小型项目 maven 更加方便一些,因为配置都是提前定义好的,不需要做过多的配置,就能直接使用,而 gradle 想要用起来配置稍微麻烦一些。

但是对于多模块项目,两层甚至三层项目结构时,使用 gradle 绝对是必需的,使用 maven 常常不能达到的目的,在 gradle 里面轻松就能够实现。

两者根本的区别是,maven 是配置型的,配置的设计依赖于配置的设计者是否留有修改的接口,gradle 是脚本型的,如何配置绝大部分取决于使用者如何设计。

一个主动权在 maven 插件作者,一个主动权在 gradle 的使用者。

1.1 maven

maven 的配置是固定的,不可修改的,只能基于配置上做定制化的改造,稍微超出配置之外的操作,就要通过插件扩展来完成。

比如说我希望打包的时候将 git sha1 作为版本号,你必须要安装一个 git-commit-id-plugin 插件。

在单层结构下没什么,完全可以达到我使用的目的,两层结构也勉强够用,三层结构貌似也没什么问题。

但是当我使用希望把一个项目提供给其它所有项目作为依赖时,问题就来了

 \--- server
    +---framework
    +---gateway
    \---business
        +---business-foundation
        +---business-web

framework 就是我将要提供给所有项目作为基本依赖。在这个需求里面,maven 有两个问题解决不了:

  1. git-commit-id-plugin 插件提供的变量 git.commit.id.abbrev 没法传递 dependency,像下面的方式,就无法实现,因为插件在处理依赖时是不生效的,只有在编译打包的时候才能生效,因此变量也就无法提供。

     
         cn.server
         framework
         ${git.commit.id.abbrev}
     
  2. maven 无法解决项目之间的循环依赖,如果希望各个项目不用自己手动引用 framework ,那么我就要在顶层去引用,但是 framework 也在这个框架之中,parent 已经被指定顶层项目是 server,当然不指定 parent 是 server,能够很轻易的解决这个问题,但是在 parent 中的其它引用,都要在 framework 中被重新引用一遍,并且还不能设置 framework 的版本变量。

这两个问题困扰了我非常久,直到使用 gradle 替换了之后。

1.2 gradle

 \--- server:build.gradle.kts
    +---framework:build.gradle.kts
    +---gateway:build.gradle.kts
    \---business:build.gradle.kts
        +---business-foundation:build.gradle.kts
        +---business-web:build.gradle.kts

gradle 是使用脚本去管理项目的,脚本的类型一般分为 groovy 与 kotlin(架构中使用的是 kotlin)。

它把编译、构建、打包、测试等阶段,都看做 task 对象,你可以在脚本中写代码动态的定义变量,比如上述第一个问题,解决起来非常简单,直接写代码去 .git 文件夹下去获取。

def getCheckedOutGitCommitHash() {
    def gitFolder = "$projectDir/.git/"
    def takeFromHash = 12
    def head = new File(gitFolder + "HEAD").text.split(":")
    def isCommit = head.length == 1

    if(isCommit) return head[0].trim().take(takeFromHash)

    def refHead = new File(gitFolder + head[1].trim())
    refHead.text.trim().take takeFromHash
}

但是在这个框架里,还是使用了 com.palantir.git-version ,因为专业的插件考虑问题更加全面。

gradle 还可以定义每个阶段去做什么,你还可以在依赖中去写代码,去判断什么模块需要依赖什么。

再比如上述第二个问题,使用五行代码就可以解决,思路就是当我判断模块不是 framework 就添加依赖,不是就不添加,简单易懂。

因为顶层的配置在 subprojects 中都是继承的,不论是几层的结构,都能够使用。

allprojects.forEach { project ->
    if (project.name != "framework") {
        implementation(project(":framework"))
    }
}

2 依赖版本准则

不论是使用 maven 还是 gradle,子项目都不允许自己选择依赖的版本,必须有上一级项目或者顶级项目选择版本。后面只讨论使用 gradle 的情况。

在顶层项目中定义的依赖分两种

  1. dependencyManagement 确定的依赖版本,实际上不直接引入依赖
  2. dependencies 中依赖,在这里定义的依赖,即为全局依赖,既确定了版本,又给所有项目提供了依赖使用

子项目中使用依赖,不可盲目的添加,要准守下面的原则:

  1. 引入依赖之前,检查所需功能项目中是否已经有其它依赖提供,比如一系列的工具类,95% 都已经包含在 hutool-all 的依赖中;再比如,如果你需要使用 rpc 功能,先调研,你会发现 redis 客户端 redisson 已经做到了,不需要再添加额外的依赖;再比如分布式超时缓存,redisson 同样已经有了。你能想到的东西,优秀的开源项目早就已经考虑到了。
  2. 添加依赖以及依赖版本需要做好调研,版本是否已经被某些依赖定义,如果你是使用 spring 体系中的官网依赖,那么大部分已经定义好了,比如 spring-boot-starter-tomcat 就已经被 spring-boot-dependencies 提供了, spring-cloud-dependencies 和 spring-cloud-alibaba-dependencies 都是同样的思路。这样做的同时也能防止依赖之间的版本不一致问题
  3. 依赖要使用最小化的原则,无用的依赖及时清理,有用的依赖注意只取必需的,举个例子,javacpp 提供了很多 Java 中使用 C 的库,涉及到多个平台,只取需要的平台使用,不能一股脑的都添加进来,这样打出来的版本要几个 G 的大小
  4. 不允许自己私自修改依赖的版本,任何依赖的升级,都应该只会项目管理者,做统一修改,应该由某个或者某些人,去找到一种合适的依赖升级方式,盲目的升级,只会破坏项目结构的稳定

2.1 修改父级依赖版本

上面提到过,添加依赖以及依赖版本需要做好调研,版本是否已经被某些依赖定义。

但是项目中肯定有需要升级某些依赖,但不升级其它关联依赖的情况,常见于修复某些依赖的漏洞。

于是,框架中也提供了修改方式。

server:build.gradle.kts 中,引入了插件 io.spring.dependency-management,它能够让你用下面的方式,覆盖依赖版本的变量内容。

ext["elasticsearch.version"] = elasticsearchVersion

3 项目打包

在框架中,提供了三种打包方式 warjarbootJar

具体的行为模式,都定义在 server:build.gradle.kts 中。

3.1 war

war 包是提供给 tomcat 或者 weblogic 或者其它项目运行使用的。

在 tomcat 下运行需要使用 web.xml,weblogic 下运行需要 weblogic.xml,打包时如果有这两个文件,都会打到包里面。

3.2 jar

这里的 jar 是为了方便 war 的更新,里面只有一个项目的 classes 打包,没有任何额外的依赖库,可以直接替换 war 包中解压出来的项目 jar。

这样更新起来就较为方便,一个项目可能达到上百兆,只改代码不修改依赖的情况下,只需要更新几十 k 的 jar 包即可。

3.3 bootJar

bootJar 就是 spring boot 自带的打包打出来的,可以直接运行,使用起来比较方便,在部署并不复杂的系统时候,简单使用一下。

3.4 模块自主选择打包格式

框架里面提供了自主选择打包格式的配置,如果有特殊的需要,也能够在里面做自定义的配置,比如拷贝特殊的文件等,具体怎么使用的参考顶层 server:build.gradle.kts 里面中的例子。

tasks {
    bootJar {
        enabled = true
    }

    jar {
        enabled = true
    }

    "war"(War::class) {
        enabled = true
    }
}

如果是一个父节点,不需要参与构建打包,指定即可。

build {
    enabled = false
}

4 打包方式

使用命令 gradle 或者 项目里面带的 gradlew 可执行命令。

gradlew 简单解释一下就是为了避免不同的 gradle 版本差异过大导出出现问题,相当于把项目的 gradle 版本固定了。

多说一嘴就是 gradle 进化太快,从 1.x 进化到 6.x,很多地方不兼容,maven 同样也有这个问题,但是现在 maven3 已经是主流,并且使用方式十分固定,所以较少遇见过特殊情况。

框架中提供了 buildAll 这个指令,能够执行三种方式打包,方法定义在 server:build.gradle.kts > subprojects > tasks> register(name = "buildAll") 中。

命令使用:
gradle buildAll

级联打包所有模块的所有类型包。

gradle business:buildAll

级联打包 business 下所有模块的所有类型包。

gradle business:business-web:buildAll

指定项目去打包。

gradle buildAll -Ppack=bootJar

级联打包选择 bootJar 打包格式。

gradle buildAll -Ppack=bootJar,jar

级联打包选择 bootJar 和 jar 打包格式。

5 在 Maven 中使用

很多地方生产环境有可能出现只支持 maven 的情况,这种极端情况也不代表 gradle 就没法使用了。

框架中在项目根目录里面增加了一个 pom.xml,能够在执行 maven package 的情况下,自动调用 gradle buildAll

也可以自定义其它命令去执行。

6 注意点

从我使用 gradle 解决一些问题的过程来看。

gradle 并不是一个特别容易使用的框架,从头驾驭它需要大量的时间,以及对开发本身了解的要相对深入。

从我的角度来看,是因为 gradle 同时支持了 groovy 和 kotlin 构建脚本的方式,以及 gradle 的版本变化太快。

我经常在网上搜索一些 gradle 中某些需求的实现方式,比如打包时排除某些或包含文件,其搜索结果五花八门。

并不是说这些结果大部分都是无效的,而是很难判断一种解决方案是否符合自己的要求,只能不断试错。

很多解决方案要么是无效的,要么根本找不到对应的方法。

拿处理子模块依赖时排除来自父模块的依赖为例,我查找了网上大量的解决方案,在网上搜索内容是 gradle exclude parent dependency,结果是解决方案很多,但是没有一种是我能够使用的,但我不清楚问题的来源是因为我的 gradle 版本问题,还是因为我使用的是 kotlin 构建脚本的问题。

最终还是通过自己的摸索找到了解决方案。

configurations.implementation.get().exclude(group = "org.springframework.session")
本文参与了思否技术征文,欢迎正在阅读的你也加入。

你可能感兴趣的:(微服务开发系列:为什么用 gradle 构建)