最近嫌弃项目编译比较慢,又听说Gradle构建速度会比Maven快很多(官方的说法是至少2倍),于是萌生了将已有项目的Maven编译,迁移到Gradle编译的想法。当然,万事开头难,所以我决定先从一个项目开始试水。下面先简单的说下这个项目的架构概况:
- 该项目是一个Java项目,混编了一部分Kotlin代码,并且使用了Spock作为单元测试框架。
- 该项目是一个Maven多模块项目,父模块定义了
<dependencyManagement>
节点,统一管理了所有子模块的依赖,子模块中无需再定义依赖的版本号。 - 该项目有API模块,基于Spring Boot编写,需要利用Spring Boot Maven Plugin生成可执行的FatJar。
- 项目使用了
lombok
,需要基于lombok的注解生成器生成最终代码。 - 项目使用了QueryDSL,需要使用QueryDSL的插件,将数据库Entity类编译为QClass,并在源码中引用。
- 该项目需要发布到Nexus私服
该项目应该涵盖了大部分Maven项目需要用到的编译配置,下面,我就通过这个项目的实战经验。带大家一步步将项目从Maven构建,迁移到全新的Gradle构建。
基于以上的概况,要将该项目从使用Maven构建,迁移到Gradle构建,就需要在Gradle中实现以上同等的逻辑。那么,就开始干吧~
基于POM,自动生成Gradle配置
Gradle官方非常贴心的内置了插件,可以一键将Maven POM转换为Gradle配置。只需要在项目目录内执行:
gradle init
然后,按照提示,将该Maven项目转换为一个Gradle项目即可。Gradle会自动为我们生成根项目的build.gradle、settings.gradle和gradle wrapper文件夹,当然,还有每个子项目的build.gradle文件(太强大了,笔芯Gradle♥️)
然鹅,光是这样当然是不够的,自动生成的Gradle配置是无法通过编译的...我们还需要继续按照原有的编译配置继续调整Gradle配置文件。
另外,听说Gradle的Kotlin DSL对IDE更友好,而且是类型安全的。因此,我决定把自动生成的Groovy DSL改写为Kotlin DSL(自动转换工具目前没有提供生成Kotlin DSL的选项,遗憾)
DSL的转换不是必须的,不过这里也简单说下,过程不是很复杂,基本就是把单引号'
换成"
,另外把省略括号的地方补上括号即可。其他写法差异比较大的,基本Google就可以解决,这一步难度不大。
引入Spring Boot Bom依赖版本管理
众所周知,基于Spring Boot的项目,一定离不开Spring Boot Bom依赖管理。这个spring boot的bom文件内定义了很多依赖的版本,例如spring-boot-jpa
、spring-boot-web
等依赖的版本。这样,我们在开发时,就可以减少依赖冲突,也不用自己再额外定义版本号了。
在使用Maven构建时,引入Spring Boot Bom有两种做法,第一种就是官方教程中常用的,将它作为模块的Parent:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
</parent>
但是,这种做法必须将该bom作为模块的parent,有一定局限性。因此,我一般是将他作为依赖管理引入:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
同时,因为项目也使用了Spring Cloud组件,我也在<dependencyManagement>
中引入了spring-cloud-dependencies
bom文件。
下面,就是要解决如何在Gradle中应用上面这套,我们在Maven-Spring Boot项目中常用的逻辑了。
查阅资料(踩了很多坑)后,我发现网上最多的方案,是使用Spring Boot官方编写的io.spring.dependency-management
插件,样例代码如下:
apply(plugin = "io.spring.dependency-management")
the<DependencyManagementExtension>().apply {
imports {
mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
}
}
但是,注意,我在踩了很多坑之后,我并不推荐这个插件。更简单直接的做法是,使用Gradle官方的解决方案:
dependencies {
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
}
具体的文档,也可以参考Spring Boot提供的文档:Managing Dependencies with Gradle’s Bom Support
总之,我尝试下来,我更加推荐使用Gradle官方提供的BOM解决方案。在使用io.spring.dependency-management
插件的过程中,我遇到父模块引入了bom,子模块中没生效、无法修改bom中定义的Groovy版本等问题。当然,也可能是我的使用姿势不对,知道的小伙伴可以提点下我。
另外,如果单纯的使用platform
也会有子模块版本仍然没有被管理的问题,因此,我最后使用的是enforcedPlatform
来引入bom。代码如下:
dependencies {
implementation("io.vavr:vavr:0.9.0")
implementation(enforcedPlatform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
implementation(enforcedPlatform("org.springframework.cloud:spring-cloud-dependencies:${Version.SpringCloud}"))
compileOnly("org.projectlombok:lombok:${Version.lombok}")
annotationProcessor("org.projectlombok:lombok:${Version.lombok}")
}
以上,就没有问题了,所有子项目中也可以随便引入Spring Boot相关的依赖,并且不需要再写版本号。
如果需要自定义bom中的kotlin、groovy等版本,Spring Boot写的文档中也有提供解决方案:
configurations.all {
resolutionStrategy {
eachDependency {
if (requested.group == "org.jetbrains.kotlin") useVersion("1.3.72")
if (requested.group == "org.codehaus.groovy") useVersion("2.5.11")
}
}
}
迁移其他位于Maven依赖管理中的依赖版本定义
除了Spring Bom之外,原项目也在根模块的Maven POM中定义了很多依赖的版本:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${version.lombok}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>${version.apache.common.beanutils}</version>
</dependency>
</dependencies>
</dependencyManagement>
使用Gradle官方提供的解决方案替代即可:
configurations.all {
force(
"org.projectlombok:lombok:${version.lombok}",
"commons-beanutils:commons-beanutils:${version.beanutils}"
)
}
}
使用Nexus私服下载依赖
父模块中定义仓库配置:
// 私服和仓库配置
repositories {
// 优先查找本地仓库
mavenLocal()
// 私服配置
maven {
url = uri("http://192.168.20.99:8099/repository/maven-public/")
// 账户密码
credentials {
username = "admin"
password = "admin123"
}
}
// 备选仓库
maven {
url = uri("https://repo.maven.apache.org/maven2")
}
}
发布模块到Nexus私服
父模块中引入maven-publish
插件,并定义发布配置:
allprojects {
// 引入发布插件
apply(plugin = "maven-publish")
// 定义不需要执行发布的项目名称
val ignorePublishingProjects = setOf("xxx-api")
if (!ignorePublishingProjects.contains(project.name)){
publishing {
repositories {
maven {
val releasesRepoUrl = "http://192.168.20.99:8099/repository/maven-releases/"
val snapshotsRepoUrl = "http://192.168.20.99:8099/repository/maven-snapshots/"
// 根据版本号不同,发布到不同的Nexus仓库
url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl)
credentials {
username = "admin"
password = "admin123"
}
}
}
// 定义发布的模块信息
publications {
create<MavenPublication>("maven") {
groupId = group.toString()
artifactId = project.name
version = project.version.toString()
from(components["java"])
}
}
}
}
}
将应用打包为可执行的Spring Boot FatJar
和Maven中一样,在这我们也需要借助Spring Boot为我们提供的Gradle插件:org.springframework.boot
我们可以在父模块的plugins
配置下,定义好依赖的版本,这样在子模块中就可以直接使用插件,而不需要指定版本:
plugins {
java
`maven-publish`
// 插件版本定义
kotlin("plugin.jpa") version "1.3.72" apply false
kotlin("jvm") version "1.3.72" apply false
kotlin("plugin.spring") version "1.3.72" apply false
id("org.springframework.boot") version "2.0.5.RELEASE" apply false
}
在需要打包执行的Spring Boot应用所在模块的build.gradle.kts
配置中,引入插件:
plugins {
java
`groovy-base`
kotlin("plugin.jpa")
id("org.springframework.boot") // 引入Spring Boot插件
kotlin("jvm")
kotlin("plugin.spring")
}
就这样,就ok啦。只需要这样做,Spring Boot 插件就会自动帮我们把源文件打包为可执行的jar包。
如果想修改默认的jar包名字,例如把应用都打包为app.jar
,只需像这样配置:
tasks.withType<org.springframework.boot.gradle.tasks.bundling.BootJar>{
archiveFileName.set("app.jar")
}
混合编译Kotlin和Java代码
要让Gradle编译代码中的Kotlin代码,只须在相应模块中引入kotlin(jvm)
插件:
plugins {
java // 编译Java代码
id("org.springframework.boot") // Spring Boot插件
kotlin("jvm") // 编译JVM代码
}
当然,为了Kotlin和Spring、JPA框架能更好的一起工作,我们一般还会引入其他插件:
plugins {
java
kotlin("plugin.jpa")
id("org.springframework.boot")
kotlin("jvm")
kotlin("plugin.spring")
}
如果要配置Kotlin插件的其他参数,可以通过下面的方式配置:
val compileKotlin: KotlinCompile by tasks
compileKotlin.kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict", "-Xjvm-default=enable")
jvmTarget = "1.8"
}
val compileTestKotlin: KotlinCompile by tasks
compileTestKotlin.kotlinOptions {
jvmTarget = "1.8"
}
Kotlin插件默认也会帮我们编译源码目录下的Java代码(大善人啊~),不过,Kotlin插件默认的源码目录配置在src/main/kotlin
下,而我目前的kotlin和java代码都位于src/main/java
目录下,因此,我需要让Kotlin插件也将src/main/java
视为源码目录:
sourceSets.main {
java.srcDirs("src/main/java", "src/main/kotlin")
}
Spock单元测试
按照Spock官方文档介绍的那样,引入对应的插件和依赖即可:
引入Groovy插件:
plugins {
java
`groovy-base` // 编译Groovy
}
引入Groovy依赖和Spock依赖:
dependencies {
implementation(project(":base-project"))
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.spockframework:spock-core:1.2-groovy-2.4")
testImplementation("org.spockframework:spock-spring:1.2-groovy-2.4")
testImplementation("org.codehaus.groovy:groovy-all:2.5.11")
testImplementation("com.h2database:h2:1.4.197")
}
大功告成,顺利的话,我们运行gradle test
,就能看到Spock Test单元测试正常编译并运行了。
编译Lombok
要编译使用lombok的代码,只须在依赖中,引入lombok依赖和注解处理器:
dependencies {
compileOnly("org.projectlombok:lombok:${Version.lombok}")
annotationProcessor("org.projectlombok:lombok:${Version.lombok}")
}
使用QueryDSL
抱歉,我没解决使用Gradle编译QueryDSL的问题。
所以,我的解决方案,就是把项目里使用QueryDSL的地方都替换成了JPA Specification...
开启并行编译和缓存
Gradle相比Maven的改进还有并行编译和缓存,他们都可以大大提升编译速度。要开启他们,只需在项目的根目录,创建gradle.properties文件,内容为:
# 启用缓存
org.gradle.caching=true
# 启用并行编译
org.gradle.parallel=true
构建时间对比
迁移完成,项目也可以正常Build了。那就来对比下同一个项目使用Maven和Gradle编译的时间对比吧:
- Maven使用命令:
mvn clean package
执行结果为:
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:34 min
[INFO] Finished at: 2020-08-15T23:26:17+08:00
[INFO] ------------------------------------------------------------------------
- Gradle使用命令:
gradle clean build
执行结果为:
BUILD SUCCESSFUL in 1m 46s
啥玩意儿?竟然比Maven还慢...
不过后几次有缓存的话,就快得多,字需要在7~8s的样子。
Gradle的机制还是不太熟悉,具体还得等之后再熟练掌握Gradle了,我会再进行更科学的测试。
总结
这次折腾花了我大概两天时间,终于把项目迁移到了Gradle。在这里分享经验,也是希望帮助到和我一样想要踩坑的同学...目前来看,使用Gradle似乎没有带来比Maven更多的优势,这其中有我对Gradle不太熟悉,还有项目不算太大的缘故。目前,Spring Boot官方也把构建切换到了Gradle,而且据他们的说法,速度提升了3~4倍。期待以后随着项目变大,能更多从Gradle构建中受益。