微服务开发系列:开篇
微服务开发系列:为什么选择 kotlin
微服务开发系列:为什么用 gradle 构建
微服务开发系列:目录结构,保持整洁的文件环境
微服务开发系列:服务发现,nacos 的小补充
微服务开发系列:怎样在框架中选择开源工具
微服务开发系列:数据库 orm 使用
微服务开发系列:如何打印好日志
微服务开发系列:鉴权
微服务开发系列:认识到序列化的重要性
微服务开发系列:设计一个统一的 http 接口内容形式
微服务开发系列:利用异常特性,把异常纳入框架管理之中
微服务开发系列:利用 knife4j,生成最适合微服务的文档
在该微服务架构中,并没有使用常见的 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 有两个问题解决不了:
git-commit-id-plugin
插件提供的变量git.commit.id.abbrev
没法传递 dependency,像下面的方式,就无法实现,因为插件在处理依赖时是不生效的,只有在编译打包的时候才能生效,因此变量也就无法提供。<dependency> <groupId>cn.server</groupId> <artifactId>framework</artifactId> <version>${git.commit.id.abbrev}</version> </dependency>
- 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 的情况。
在顶层项目中定义的依赖分两种
- dependencyManagement 确定的依赖版本,实际上不直接引入依赖
- dependencies 中依赖,在这里定义的依赖,即为全局依赖,既确定了版本,又给所有项目提供了依赖使用
子项目中使用依赖,不可盲目的添加,要准守下面的原则:
- 引入依赖之前,检查所需功能项目中是否已经有其它依赖提供,比如一系列的工具类,95% 都已经包含在 hutool-all 的依赖中;再比如,如果你需要使用 rpc 功能,先调研,你会发现 redis 客户端 redisson 已经做到了,不需要再添加额外的依赖;再比如分布式超时缓存,redisson 同样已经有了。你能想到的东西,优秀的开源项目早就已经考虑到了。
- 添加依赖以及依赖版本需要做好调研,版本是否已经被某些依赖定义,如果你是使用 spring 体系中的官网依赖,那么大部分已经定义好了,比如
spring-boot-starter-tomcat
就已经被spring-boot-dependencies
提供了,spring-cloud-dependencies
和spring-cloud-alibaba-dependencies
都是同样的思路。这样做的同时也能防止依赖之间的版本不一致问题 - 依赖要使用最小化的原则,无用的依赖及时清理,有用的依赖注意只取必需的,举个例子,javacpp 提供了很多 Java 中使用 C 的库,涉及到多个平台,只取需要的平台使用,不能一股脑的都添加进来,这样打出来的版本要几个 G 的大小
- 不允许自己私自修改依赖的版本,任何依赖的升级,都应该只会项目管理者,做统一修改,应该由某个或者某些人,去找到一种合适的依赖升级方式,盲目的升级,只会破坏项目结构的稳定
2.1 修改父级依赖版本
上面提到过,添加依赖以及依赖版本需要做好调研,版本是否已经被某些依赖定义。
但是项目中肯定有需要升级某些依赖,但不升级其它关联依赖的情况,常见于修复某些依赖的漏洞。
于是,框架中也提供了修改方式。
在 server:build.gradle.kts
中,引入了插件 io.spring.dependency-management
,它能够让你用下面的方式,覆盖依赖版本的变量内容。
ext["elasticsearch.version"] = elasticsearchVersion
3 项目打包
在框架中,提供了三种打包方式 war
、jar
、bootJar
。
具体的行为模式,都定义在 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")
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。