Kotlin中的Gradle
Grandle程序
第一个Gradle程序
1.创建
左上角【File】->【New Project】->【Project】,出现以下画面,如下勾选
在配置Gradle时,需要连接网络下载资源,不然会出现配置失败的情况。
若在构建完毕后,左侧的路径中没有出现src包,可以在build.gradle文件中最外部添加如下内容(对应的是5.X版本的Gradle):
task 'create-dirs' { doLast { sourceSets*.java.srcDirs*.each { it.mkdirs() } sourceSets*.resources.srcDirs*.each { it.mkdirs() } } }
点击右侧栏进行如下操作:
2.配置
Gradle是从4.0开始支持Kotlin的,所以一般的Gradle版本要求是要在4.0以上。
默认IDEA使用的是groovy脚本构建的,若想使用Kotlin语言构建,可以将build.gradle文件重命名为build.gradle.kts,为保证修改成功,建议重启IDEA。若build.gradle.kts文件的图标变更,则说明修改成功。
此时将build.gradle.kts文件内容修改为以下内容:
plugins { application } application{ mainClassName="com.example.gradleExample.Main" }
“mainClassName”中对应的是程序入口类的绝对路径,Main类在后续会创建。
3.创建功能类
首先在src/main/java中创建与绝对路径同名的包名。
在该包下分别创建Girl和Main类。
public class Girl { public String greeting() { return "Hello"; } }
public class Main { public static void main(String[] args) { Girl girl = new Girl(); System.out.println(girl.greeting()); } }
4.运行
打开侧边栏中的Gradle,也可如图中点击选项打开。
在打开的Gradle Project中,双击run会运行刚才的Main类。
运行结果如图所示。
在运行之后,在distribution文件夹中可见一个distZip标识。
双击该标识可将该程序发布,路径位于build/distributions中。
解压该文件,会出现两个文件夹,分别是lib和bin,其中lib文件夹中有一个jar文件,是程序中的代码编译成的一个jar包;bin文件夹中有两个文件,分别是同名文件和bat文件。
同名文件是linux环境下运行需要的脚本,bat文件是Windows环境下运行需要的脚本。在cmd中,将bat文件拖入窗口回车,会运行刚才我们编写的程序并输出结果。
Java代码和Kotlin代码共存
Gradle之前是使用groovy脚本编写的,是一种基于JVM的动态语言,常见的动态语言有Python和JavaScript,动态语言是动态运行的,在编译时编译器无法识别它需要干什么。
后来出现了Kotlin,Kotlin和Java都是静态语言,在之后Gradle开始使用Kotlin进行编写。
1.配置build.gradle.kts文件
为使程序可以编写java代码又可以编写kotlin代码,可对build.gradle.kts文件做如下修改:
plugins { application kotlin("jvm") } application { mainClassName = "com.example.gradleExample.Main" } dependencies { compile(kotlin("stdlib")) } repositories { jcenter() }
使用application构建工具可以编写java代码,如果想同时编写kotlin代码,需要依赖kotlin-stdlib.jar包,这个包是kotlin的标准库。
repositories用于声明仓库,在示例中从jcenter仓库中下载依赖;
然后在dependencies节点中,通知jcenter仓库需要依赖(compile)kotlin中的stdlib.jar包;
而在application中的mainClassName声明的是程序的入口,即Main.java。
配置build.gradle.kts文件后,在src/main文件夹下创建kotlin文件夹并刷新项目,kotlin文件夹颜色变为蓝色后,该项目就可以运行kotlin文件了。
若刷新项目在jvm处报错,则需要指定版本号。
plugins { application kotlin("jvm") version "1.3.61"} application { mainClassName = "com.example.gradleExample.Main" } dependencies { compile(kotlin("stdlib")) } repositories { jcenter() }
2.创建Boy.kt
在刚刚创建的kotlin文件夹中,如同刚才在java中,创建一个与绝对路径同名的包,并在该包中创建一个Boy.kt文件。
class Boy(var name: String) { fun greeting(): String { return name + ": hello" } }
3.修改Main.java
添加代码调用Boy类中的greeting方法。
public class Main { public static void main(String[] args) { Boy boy = new Boy("Tom"); System.out.println(boy.greeting()); } }
Gradle的任务
Gradle中的project和task
Gradle本身的实体类(又称领域对象)主要有Project和Task,Project为Task提供执行的容器和上下文。Gradle之所以可以工作,是因为在Gradle脚本中将代码以任务的方式插入到了Project中,再由Project来执行这些任务,Gradle就开始工作了。
1.创建
参照前文创建项目名为ProjectAndTask的程序,在build.gradle.kts中创建一个task,代码如下:
task("HelloWorld",{ println("Hello World!") })
示例中,task方法接受了两个参数,第一个参数("HelloWorld")是任务task的名称,第二个参数(println("Hello World!"))是一个闭包,这个闭包中编写的是kotlin的代码,是实现任务的内容,在示例中表现为输出一段语句。
2.运行
当写完上述代码后,在侧边栏的Gradle中刷新以下,接着会看见other中出现了我们编写的内容,双击可以运行这个任务。
Gradle任务的依赖
依赖管理是指如果一件事没有完成,则下一件事也没法跟着完成,这两件事就被称为有依赖关系。
比如把苹果放入冰箱这一操作,就需要三个步骤:
- 打开冰箱门
- 放入苹果
- 关上冰箱门
如果冰箱门没打开,那么就没法把苹果放入冰箱,这说明步骤2是依赖于步骤1的,这种依赖关系在代码中是靠dependsOn方法来实现的。下面用示例来演示这一过程。
task("opendoor") { println("打开冰箱门") } task("putapple") { println("放入苹果") }.dependsOn("opendoor") task("closedoor"){ println("关上冰箱门") }.dependsOn("putapple")
当刷新之后,在Gradle侧边栏出现了三个新建的task(opendoor、putapple、closedoor)。在示例中,后两个task都存在依赖关系:putapple依赖opendoor,closedoor依赖putapple, 而运行这三个task的任意一个,都会得到完整的放入苹果操作。
若在Gradle输出中文出现乱码,可以进行如下操作:
在打开的文件最后一行,输入:
-Dfile.encoding=UTF-8
重启IDEA后生效。
Gradle任务的生命周期
Gradle任务的生命周期分为“扫描时”和“运行时”。
1.扫描时任务
扫描时任务的特点是扫描时所有任务都会执行,任务执行的先后顺序同build.gradle.kts文件中任务代码顺序一致,谁编写在先谁执行,与依赖顺序无关。
task("opendoor") { println("打开冰箱门") } task("putelephant") { println("放入大象") }.dependsOn("opendoor") task("closedoor"){ println("关上冰箱门") }.dependsOn("putelephant")
运行这三个任务中的任意一个,得到的结果都是相同的,这是因为在Gradle任务的生命周期在扫描阶段时,会根据任务在build.gradle.kts文件中的顺序来依次执行每个任务。
2.运行时任务
Gradle中的任务默认是扫描时任务,若想使用运行时任务,需要添加doFirst高阶函数来实现。
task("opendoor") { doFirst { println("打开冰箱门") } } task("putelephant") { doFirst { println("放入大象") } }.dependsOn("opendoor") task("closedoor") { doFirst { println("关上冰箱门") } }.dependsOn("putelephant")
可见在添加doFirst高阶函数后,运行时任务只执行指定的任务,不执行其他无关的任务。
3.运行时任务与扫描时任务相辅相成
扫描时任务一般是用于声明变量,运行时任务主要是用于执行程序的业务逻辑。
task("login") { val name = "kotlin" val pwd = "123" doFirst { if (("kotlin" == name) and ("123" == pwd)) { println("登陆成功") } else { println("登陆失败") } } }
在初始化变量时,默认进入了扫描时任务,此时从先到后声明用户名name和密码pwd;在执行task的内部逻辑时,使用了高阶函数doFirst,此时task只执行doFirst中包含的内容,不执行其他无关任务。因此,运行时任务与扫描时任务是相辅相成的。
Gradle任务集
使用task可以声明一个任务,使用tasks可以声明一个任务集。
任务集可以将所有单独的任务放在一个集合中,便于管理多个任务。
tasks{ "opendoor"{ println("打开冰箱门") } "putelephant"{ println("放入大象") } "closedoor"{ println("关上冰箱门") } }
Gradle默认属性和任务
在Gradle项目中有一些默认的属性和任务,这些默认属性存放在build.gradle.kts文件中的properties集合中,properties是项目中所有默认属性组成的一个Map集合,可以通过forEach高阶函数来遍历并获取集合中的key和value的值。
1.输出Gradle项目中的默认属性及对应的值
使用forEach来输出Gradle项目中所有默认属性以及对应的值。
task("showConfig") { val map = project.properties map.forEach { (key, value) -> println("key=$key,value=$value") } }
在Gradle中的所有默认属性比较多,因此在运行结果中只显示了部分运行结果的内容。
2.通过命令行运行任务
之前都是用Gradle侧边栏双击需要运行的任务来执行,这一操作属于图形化界面的操作方式。
在进行操作之前,需要对Gradle进行配置。
(1)配置环境变量
首先在Gradle的官网(Gradle | Releases)下载最新的gradle压缩包。
下载完成后解压到自己设定的目录里,记住这一目录。
右键此电脑,选择属性,点击左侧边栏中的“高级系统设置”。
弹出界面中,选择“环境变量”。
在弹出界面中,在下方的“系统变量”中编辑变量名为“Path”的值,在值后添加路径,该路径为先前解压gradle压缩包的路径。
在添加好之后,通过快捷键win+r,输入“cmd”回车打开命令提示符窗口,输入“gradle -version”并回车,若正确添加环境变量,将会出现如下输出。
(2)通过命令行执行任务
在IDEA的Terminal窗口输入“gradle showConfig”并回车,就可以通过命令行来执行showConfig任务。
但当使用最新版本的gradle时(本文使用的是8.3),可能会出现冲突无法执行showConfig的情况,冲突的点应该是与build.gradle.kts前文的jcenter()有关。
我的解决方法是使用了较低版本的gradle(gradle 4.1)版本,如果读者有更好的办法欢迎举例。在gradle官网往下滚动便可找到此版本,在使用新版本之后需要对环境变量的路径也进行修改。
若在下方没有找到Terminal窗口,可以在顶部栏中【View】->【Tool Windows】->【Terminal】来打开。
除了上述方式,也可以直接在build.gradle.kts文件中,通过defaultTasks设置默认的任务来执行。
task("HelloWorld") { println("Hello World!") } task("showConfig") { val map = project.properties map.forEach { (key, value) -> println("key=$key,value=$value") } } defaultTasks("HelloWorld","showConfig")
若想用命令行窗口执行Gradle项目中的所有默认任务,可以在Terminal直接输入“gradle”回车。
Gradle增量式更新任务
Gradle的增量式更新会判断哪些文件发生了改变,哪些文件没有发生改变,他只会冲i性能编译发生改变的代码,这样就提高了编译速度。
1.未使用增量式更新
plugins { java } task("updateTask") { val fileTree = fileTree("src") val infoFile = file("info.txt") infoFile.writeText("")//清空数据 fileTree.forEach { if (it.isFile) { Thread.sleep(1000) infoFile.appendText(it.absolutePath + "\r\n")//写入文件 } } }
示例中,没有使用增量式更新,每次执行updateTask任务时,都会重新遍历src目录并把目录写入info.txt文件中。
2.使用增量式更新
修改上一示例,增量式更新涉及两个新的API:inputs和outputs,分别对应输入和输出。
修改后,只有输入或输出中有改变,才会执行updateTask任务。
plugins { java } task("updateTask") { inputs.dir("src")//输入 outputs.file("info.txt")//输出 doFirst { val fileTree = fileTree("src") val infoFile = file("info.txt") infoFile.writeText("")//清空数据 fileTree.forEach { if (it.isFile) { Thread.sleep(1000) infoFile.appendText(it.absolutePath + "\r\n")//写入文件 } } } }
示例中,调用inputs中的dir方法来指定输入目录src,调用putputs中的file方法来指定输出文件info.txt。
Gradle的依赖
Gradle的依赖包管理
在开发中,一个项目可能需要依赖多个jar包,并且jar包之间可能互有关联,而这也导致这些包都需要同时引入。
例如,在使用commons-httpclient-3.1.jar时,还需要使用commons-logging.jar和commongs-codec.jar包。
而在Gradle项目中,只需要添加commons-httpclient-3.1.jar即可,与之关联的jar包会自动关联。
plugins { kotlin("jvm") version "1.3.61" } apply { plugin("kotlin") } dependencies { compile("commons-httpclient", "commons-httpclient", "3.1") } repositories { mavenCentral() }
点击Gradle侧边栏的刷新按钮后,可见左侧的Project栏,不仅新增了我们添加的jar包,还一并添加了两个jar包,而他们都是与我们添加的jar包相关联的。
公共仓库和依赖配置
公共仓库
在Gradle项目中,存放开发中常用的一些资源的仓库被称为公共仓库。常用的仓库是Maven Center公共仓库和Jcenter公共仓库,其中Maven Center是目前使用最多的公共仓库。
此处我们就以Maven Center为例,进入Maven Center官网(https://mvnrepository.com/),搜索okhttp。
选中Central,在其中寻找需要的版本。
此处我选择了最多人使用的版本,因为我们使用的是Kotlin版本的Gradle,所以需要注意选择。
plugins { kotlin("jvm") version "1.3.61" } dependencies { implementation("com.squareup.okhttp3:okhttp:4.10.0") } repositories { mavenCentral() }
示例中dependencies是用于配置依赖仓库中的jar包,repositories用于配置仓库。
依赖配置
Gradle项目的依赖配置分为两个阶段:编译时依赖和测试时依赖。
在build.gradle.kts中的compile声明的是编译时依赖,testcompile声明的是测试时依赖。
测试时依赖只在测试阶段使用,在项目打包上线不依赖,这样可使项目的体积变小。
注:compile和implementation的区别
compile被称为过时的方法,取而代之的是api和implementation两个依赖指令。
compile(api)是我们最常用的方式,使用该方式依赖的库将会参与编译和打包。
api在使用上和compile完全相同,可直接替换。
implementation特点在于,对于使用了该命令编译的依赖,对该项目有依赖的项目,将无法访问到使用该命令编译的依赖中的任何程序,也就是将该依赖隐藏在内部,而不对外部公开。
plugins { kotlin("jvm") version "1.3.61" } apply { plugin("kotlin") } dependencies { compile(kotlin("stdlib")) testCompile("junit", "junit", "4.12") } repositories { mavenCentral() }
接下来在项目的src/test/java文件夹中创建一个java类,名为Gradle。
public class Gradle { @Test//通过注解调用junit测试 public void test() { //断言:当程序执行到断言的位置时,对应的断言应该为真。 // 若断言不为真时,程序会中止执行,并给出错误信息。 Assert.assertEquals(true, 1 == 1); } }
此时测试通过,若将最后一行的判断修改为1==2,则运行结果如下图所示。
Gradle扩展
Gradle插件自定义扩展
在Gradle官网中,声明了很多系统自带的任务,这些任务有Copy、Delete、Checkstyle等。
接下来展示插件自定义扩展的使用方法。
1.扩展Copy任务,实现将src目录中的文件复制到temp目录中
task("myCopy", Copy::class) { from("src") into("temp") }
from方法用于声明源目录,into用于声明目标目录,若目标目录不存在,会自动生成这个目录。
2.扩展Delete任务,实现删除temp目录
task("myDelete", Delete::class) { setDelete("temp") }
3.扩展Jar任务,将class文件生成Jar包并存放在temp目录
import org.gradle.jvm.tasks.Jar task("myjar", Jar::class) { //将out/production/classes目录下的class文件打成jar包存放在temp目录下 from("out/production/classes") include("**/*.class") archiveName = "temp/my.jar" }
include方法主要用于设置文件的类型为class,将out/production/classes目录下的class文件生成my.jar包,并保存到temp目录下。
除了上述任务之外,系统还提供了其他任务,如用JavaCompile来编译Java源码等。开发中需要扩展哪些任务,都可以参考官方文档。
Gradle调用外部扩展
若想在build.gradle.kts文件中执行一段java代码,且这段代码已经写好并生成了一个字节码文件(在java中,被编译器预处理生成的.class文件,被称为字节码文件),此时不想再build.gradle.kts中用groovy或kotlin重写一遍这个代码,而是直接运行。这个时候,Gradle提供了一个闭包Javaexec实现直接运行字节码的功能。
在这个闭包中,main对应的是要运行的类名,classpath对应的是当前的字节码文件存放的位置。
1.生成一个Hello类的字节码
(1)先创建一个名为Order的Gradle项目,在该项目的java文件夹中创建一个Hello类
public class Hello { public static void main(String[] args){ Systerm.out.println("I am java code"); } }
(2)修改build.gradle.kts中的内容
plugins{ application } application{ mainClassName = "Hello" }
在Gradle侧边栏中的other文件夹中,双击compileJava可以运行Hello类中的main方法,运行成功后,将会在build/classes/java/main目录下生成Hello.class字节码。
2.在build.gradle.kts中运行Hello.class字节码
将生成的Hello.class文件复制到与build.gradle.kts同一目录下,在刚才的build.gradle.kts添加如下内容
task("order"){ //执行java代码 javaexec{ main = "Hello" classpath(".") } }