前置准备
使用start.spring快速搭建一个Gradle的Web项目(https://start.spring.io/)
项目结构解析
├─build.gradle ①
├─gradlew ②
├─gradlew.bat ③
├─settings.gradle ④
├─gradle ⑤
│ └─wrapper
│ ├─ gradle-wrapper.jar
│ ├─ gradle-wrapper.properties
└─src ⑥
├─main
└─test
- 项目自动编译的时候要读取的配置文件。比如指定项目的依赖包等。
build.grade
有两个,一个是全局的,一个是在模块里面。全局的build.grade主要设置的是声明仓库源,gradle的版本号说明等。 - linux下的gradle环境脚本,可以执行gradle指令,如:./gradlew build
- windows下的gradle环境,可以执行gradle指令
- 包含必要的一些设置,例如,任务或项目之间的依懒关系等,无论有多少个子模块,该文件只会有一个,且一定在根项目中;
- 包含wrapper文件夹及其2个子文件,作用是:可以自动安装gradle环境
- 程序源码
build.gradle
基础结构
//插件声明
plugins {
id 'org.springframework.boot' version '2.7.4'
id 'io.spring.dependency-management' version '1.0.14.RELEASE'
id 'java'
}
//该项目的坐标信息
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
//仓库地址
repositories {
//这里推荐使用阿里云的镜像仓库,待会下载jar包的时候速度会很快
maven {
url 'https://maven.aliyun.com/repository/public/'
}
mavenCentral()
}
//第三方依赖jar
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
//单元测试声明
tasks.named('test') {
useJUnitPlatform()
}
打开cmd命令行,我们使用gradlew build
来编译这个项目(默认会下载一个gradle-x.x-bin.zip),并使用java -jar build/libs/demo-0.0.1-SNAPSHOT.jar
运行起来。
将项目导入到IDEA中,然后执行右侧build
再启动服务
到这里,我们前置准备工作已经就绪,接下来我们使用这个build.gradle来学习gradle的api
Gradle的Project
在gradle中,每一个build.gradle
文件对应一个Project实例,我们在build.gradle中编写的内容,就相当于Project
实例的属性或方法。
构建初始化期间,Gradle实例化的步骤如下:
- 给整个构建创建一个
Settings
实例,一个Settings
实例就是一个settings.gradle
文件 - 针对
Settings
实例的配置,按配置层次扫描并解析配置在settings.gradle
中的project
。(其中settings
中最为重要的属性是include
) - 针对每个
project
对应的build.gradle
进行初始,并创建Project实例(这里加载project的顺序是按照前面的配置层次进行,即广度扫描加载,这样可以保证父级模块加载完后,子模块才会被加载)
一个完整的project由以下几个对象组成(实际上只由属性和方法组成)
我们可以在build.gradle
文件中任意使用gradle
提供的属性或方法,如下:
//输出当前项目名称
println(project.name)
//一般在当前build.gradle中使用时,默认会省略project
println(name)
//输出project中名字为name的属性
println(project.property("name"))
//指定默认执行的task,即./gradlew不指定task时会执行该task
defaultTasks('yourTask')
属性
内置属性可以直接赋值,无需声明
group = 'com.it235' version = '1.0.0'
自定义属性可以使用groovy语法,也可以与java语法结合
//groovy定义属性 def pname = "projectName:" + project.name //java类型接收 String pname = "projectName:" + project.name
使用ext名命空间来扩展属性,定义后可以在project、task、subproject中读取和更新
ext.prop1 = "it235" ext.prop2 = "君哥聊编程"
使用案例
//REPOSITORY_HOME 和 REPOSITORY_URL apply plugin: 'maven' ext { REPOSITORY_HOME = 'http://maven.aliyun.com' REPOSITORY_URL = REPOSITORY_HOME + "/nexus/content/groups/public" } repositories { // 使用 mavenCentral()时,将远程的仓库替换为自己搭建的仓库 maven { url REPOSITORY_URL } } uploadArchives { repositories { mavenDeployer { snapshotRepository(url: REPOSITORY_HOME + "/nexus/content/repositories/snapshots/") { authentication(userName: 'xxx', password: 'xxx') } repository(url: REPOSITORY_HOME + "/nexus/content/repositories/releases/") { authentication(userName: 'xxx', password: 'xxx') } } } }
属性作用域
读写属性时,Project 会按照下面范围的顺序进行查找的,在某个范围找到属性后就会返回该属性。如果没有找到,会抛出异常。
Project
对象自身。这个范围里的属性包含Project
实现类中定义有 getters 和 setters 方法的所有属性。比如:project.getName()
方法就对应了name
属性。至于这些属性的可读写性取决于它们是否定义 getters 或者 setters 方法。- Project 的ext属性 ( extra ) 。每个 Project 都会维护一个额外属性的映射,它可以包含任意的
名称 -> 值
对。定义后,此作用域的属性是可读写的。比如:project.ext.prop1 = 'it235'
。 - 通过插件被添加到 Project 中的扩展属性 (
extensions
) 。每个扩展名都可以作为只读属性使用,其名称与扩展名相同。比如:project.android.compileSdkVersion
。 - 通过插件添加到 Project 中的约定属性 ( convention ) 。插件可以通过 Project 的 Convention 对象向 Project 中添加属性和方法。此范围的属性的可读可写性取决于约束对象。
- Project 中 Tasks 。可以使用 Task 的名称作为属性名称来访问task。此范围的属性是只读的。
- ext的属性和约定属性从项目的父级继承,递归到根项目。此范围的属性是只读的。
常用的project属性
allprojects
:包含此项目及其子项目的集合。buildDir
:当前项目的编译目录(自动生成)默认值 porjectDir/build
defaultTasks
:当前项目的默认任务的名字集,当前构建没有提供任务名时会执行这些默认任务group
:当前项目的组名logger
:当前项目的日志器,可以用来在 build 文件中写日志name
:此项目的名称parent
:此项目的父项目path
:这个项目的绝对路径project
:当前project对象实例rootDir
:本项目的根目录。根目录是根项目的项目目录rootProject
:当前项目层次结构中的根projectsubprojects
:当前项目的子项目的集合tasks
:本项目的task集合。version
:此项目的版本
方法
方法作用域
- Project 对象自身
- build.gradle 脚本文件
- 通过插件添加到 Project 中的扩展 ( extensions ) 。每个扩展都可以当做参数是闭包或 Action 的方法。
- 插件添加到项目中的约定方法 ( convention ) 。插件可以通过项目的 Convention 对象向项目中添加属性和方法。
- 项目中的 Tasks 。每个 Task 都会添加一个方法,方法名是任务名,参数是单个闭包或者 Action 。该方法使用提供的闭包为相关任务调用
Task.configure( groovy.lang.Closure )
方法。
常用的Project方法
方法 描述 afterEvaluate
可以添加一个闭包,它会在项目完成评估后立即执行。当执行属于该项目的构建文件时,会通知此类监听器。 allprojects
配置当前项目以及它的每个子项目 apply
应用零个或多个插件或脚本。 beforeEvaluate
添加一个闭包,它会在项目开始评估前立即执行 configure
通过闭包配置对象集合。 copy
复制指定的文件 defaultTasks
设置此项目的默认任务的名称。当开始构建时没有提供任务名称时使用这些。 delete
删除文件和目录 exec
执行外部命令 file
解析相对于该项目的项目目录的文件路径 findProject
按路径定位项目。如果路径是相对的,则相对于该项目进行解释。 findProperty
找特定属性,如果未找到,则返回给定属性的值或 null getAllTasks
返回此项目中包含的任务的地图 hasProperty
确定此项目是否具有给定的属性 javaexec
执行 Java 主类 javaexec
执行外部 Java 进程。 mkdir
创建一个目录并返回一个指向它的文件。 property
返回给定属性的值。此方法定位属性如下: setProperty
设置此项目的属性。此方法在以下位置搜索具有给定名称的属性,并将该属性设置在它找到该属性的第一个位置。 subprojects
配置本项目的子项目 task
创建 Task
具有给定名称的 a 并将其添加到此项目中uri
将文件路径解析为 URI,相对于该项目的项目目录
常用方法示例
buildscript{}:配置当前gradle脚本自身需要使用的构建信息或依赖
假设要执行一项指令
./gradlew buildImage
,构建docker镜像,而Gradle官方自身没有,则需要依赖到maven库下载或需要调用第三方插件,虽然这里是调用的task,但是task背后所依赖的插件是需要提前定义在buildscript中的,我们需要在buildscript{}中指定docker的依赖即可。apply plugin: 'idea' apply plugin: 'java' apply plugin: "maven" apply plugin: "war" apply plugin: "com.bmuschko.docker-remote-api" apply plugin: "org.springframework.boot" buildscript { repositories { mavenLocal() maven { url "https://maven.aliyun.com/repository/public" } mavenCentral() } dependencies { classpath "com.bmuschko:gradle-docker-plugin:3.3.4" classpath "org.springframework.boot:spring-boot-gradle-plugin:2.6.5" } }
configurations{}:配置使用声明的依赖项用于特定目的(听君一席话,如听一席话)
我们看个案例,下面的
implementation
和testRuntime
就是Gradle帮我们提供的configuration
,configurations{}
记录着项目中各个分组(implementation
,runtime
)的依赖信息。dependencies { implementation "org.springframework.boot:spring-boot-starter-web" testRuntime "junit:junit:4.13" }
简单一句话概括configurations{}的作用:将本项目需要的相关依赖资源进行分组,A组你可以提供在运行时使用,B组你提供在测试运行时使用,C组编译时使用,D组xxxx?
因为
configuration
支持继承进行扩展,子配置会集成所有父配置的依赖,testImplementation extends implementation
,如下图
那implementation
、testImplementation
是谁帮我们提供的呢?当然是java的插件啦,插件声明如下(后面插件环节会单独讲解):apply plugin: 'java' //或者如下定义 plugins{ id 'java-library' }
下图是java插件提供的测试相关的configuration
关于扩展功能,还允许我们开发者自定义configurations { //声明一个具备加载测试依赖的configuration smokeTest.extendsFrom testImplementation //声明一个具备类编译和运行的configuration compileClasspath.extendsFrom(someConfiguration) runtimeClasspath.extendsFrom(someConfiguration) //全局排除log4j依赖 implementation.exclude group:'org.apache.logging.log4j' implementation.exclude module:'spring-boot-starter-log4j2' //声明一个具备类运行的configuration developmentOnly runtimeClasspath { extendsFrom developmentOnly } } dependencies { testImplementation 'junit:junit:4.13' //读取根目录下的lib目录 smokeTest fileTree('lib') smokeTest 'org.apache.httpcomponents:httpclient:4.5.5' //使用someConfiguration将lib模块添加到本项目依赖 someConfiguration project(":lib") developmentOnly("org.springframework.boot:spring-boot-devtools") } //将文件拷贝到另外一个目录 afterEvaluate{ //configurations属性可以在任意一个task中读取 println configurations.smokeTest.files.first() println configurations.smokeTest.asPath def libPath = projectDir.absolutePath + "/src/main/lib2" copy { from configurations.smokeTest.files.first() into libPath } }
repositories{}:仓库配置
通过 repositories{} 可以配置maven,ivy,local仓库。这样子,在dependencies{}声明的依赖就可以通过repositories{}中指定的仓库查询到具体的JAR资源。
repositories { mavenLocal() mavenCentral() maven { // Name is optional. If not set url property is used name = 'Main Maven repository' url = 'https://maven.aliyun.com/repository/central' } //有权限控制的仓库 maven() { credentials { username = 'username' password = 'password' } url = 'https://maven.aliyun.com/repository/central' } //本地仓库 repositories { flatDir(dir: '../lib', name: 'libs directory') flatDir { dirs '../project-files', '/volumes/shared-libs' name = 'All dependency directories' } } }
dependencies{}
在gradle中dependencies{}是一等公民,它描述了configurations{}中分组依赖的第三方资源。我们可以把依赖简单的分成两大类:- gradle依赖:主要是gradle运行的时候,需要加载一些插件,如android等,此时需要配置它。
- 项目编译/运行依赖:编译一个项目,通常需要依赖其他项目或者JAR。
依赖的常规写法(与maven一致)
//配置依赖 dependencies { //compile已过时,推荐使用implementation //按照maven名称加载jar implementation 'com.google.guava:guava:11.0.2' //排除部分依赖 implementation('org.slf4j:slf4j-simple:1.6.4') { exclude 'org.slf4j:slf4j-api' } //测试模块 testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2' //依赖其他子项目 implementation project(':projectA') //加载目录下所有的jar implementation fileTree(dir: 'libs', include: ['*.jar']) //多个文件 implementation ('com.google.guava:guava:11.0.2'){ //在版本冲突的情况下优先使用该版本 isForce = true //禁用依赖传递 transitive = false } } //使用dependencis任务可以查看当前依赖树,*表示被忽略的
allprojects{}:配置此项目及其每个子项目所需要的依赖。一般在多模块项目场景下我们会把公共的部分配置在根项目的
allprojects
中。apply plugin: 'java' apply plugin: "maven" apply plugin: "war" apply plugin: "com.bmuschko.docker-remote-api" apply plugin: "org.springframework.boot" buildscript { repositories { mavenLocal() maven { url "https://maven.aliyun.com/repository/public" } mavenCentral() } dependencies { classpath "com.bmuschko:gradle-docker-plugin:3.3.4" classpath "org.springframework.boot:spring-boot-gradle-plugin:2.6.5" } } allprojects { apply plugin: "idea" repositories { mavenLocal() maven {url "https://maven.aliyun.com/repository/public"} mavenCentral() } // 依赖管理器 dependencyManagement { imports { mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Hoxton.SR12' mavenBom 'com.alibaba.cloud:spring-cloud-alibaba-dependencies:2.2.6.RELEASE' } } // ...各种配置... //依赖 dependencies { implementation("org.springframework.boot:spring-boot-starter") { exclude module: "tomcat-embed-el" } implementation("org.springframework.boot:spring-boot-starter-web") api 'com.squareup.retrofit2:retrofit:2.4.0' //仅编译 compileOnly 'org.projectlombok:lombok:1.18.16' annotationProcessor 'org.projectlombok:lombok:1.18.16' //仅测试编译 testCompileOnly 'org.projectlombok:lombok:1.18.16' testAnnotationProcessor 'org.projectlombok:lombok:1.18.16' // ...依赖超级多的 jar 包,包括公司的包和三方包.... implementation "xxx.xxx.xxx" ... } }
- subprojects{}:子模块配置
sourceSets{}:配置源码信息
maven
和gradle
默认约定的项目目录一样,如下:src/main/ src/main/java/ src/main/resources/ src/test/ src/test/java/ src/test/jresources/
但是Gradle提供了修改的默认目录的功能,官方叫Changing the project layout,对于Java来说基本上不会调整项目结构,但也会存在特殊情况,如:过滤掉某个main目录下的文件夹,将一个外部文件夹纳入源码资源管理中。这个时候
sourceSets
登场了,sourceSets
的功能攘括了源文件及位置定义、相关依赖管理、定义编译输出的位置这3块。(其中main和test默认被官方定义在的sourceSets
,无需手动指定)sourceSets
有一些属性,比如名字、源文件位置、资源位置、classpath等等,这些属性有些是只读的,有些是可写的,并且可以被当作变量来引用。有可写的属性那么就提供了自定义的功能了,比如,你可以改变一个SourceSet
源代码的位置,像这样下面这样,你就改变了main这个SourceSets
的源代码目录和资源目录。sourceSets { main { java { srcDirs = ['src/java'] } resources { srcDirs = ['src/resources'] } } }
这样,gradle就只在src/java下搜源代码,而不是在src/main/java下。如果你只是想添加额外的目录,而不想覆盖原来的目录,则像下面这样:
sourceSets { main { java { srcDir 'thirdParty/src/main/java' } } }
此时,gradle就会同时在
src/main/java
和thirdParty/src/main/java
两个目录下都进行源代码搜索。注意:你可要注意srcDir和srcDirs的区别
同时我们还可以排除classpath下面的某个目录,你可以这样做,执行build之后,你在jar或war包的classes中不会见到排除掉的目录
sourceSets { main{ java { exclude 'com/it235/first/ct/**' } resources { exclude 'cert/**' } } } 我的目录结构 first-gradle src main java com.xxx resources cert test.cert application.properties
指定源码输出目录
sourceSets { main { //指定main的源码输出位置 output.resourcesDir = output.classesDir = 'WebContent/WEB-INF/classes/' //源码目录 java.srcDir('src') //资源文件目录 resources.srcDir('src') } }
什么情况下我们会添加一个新的
SourceSets
原始的src/test
目录我们一般存放单元测试,但是对于集成测试部分不应存放在src/test目录下,因为这些测试有它们自己的dependencies, classpaths, 以及其他的资源,所以这些测试应该是单独分开的,而不应该和单元测试混为一谈。这样会将2个测试工作进行混淆,也是架构师们不希望看到的局面,所以我们需要重新定义一个用于集成测试的代码环境。但是,集成测试和验收测试也是这个项目的一部分,因此我们也不能为集成测试或验收测试单独建立一个项目。它们也不是子项目,所以用subproject也不合适。此时,我们就可以新建一个集成测试或者验收测试的SourceSets,把相关的测试资源管理起来。
在src目录下新建一个
intTest
作为集成测试的环境,与src/main、src/test保持一致,格式如下first-gradle src intTest -- 存放集成测试的源码 java resources main -- 存放应用源码 java resources test -- 存放单元测试源码 java resources
- 为他们创建一个新的
sourceSets
- 将所需的依赖项添加到该
sourcesSets
配置中 为该源集配置编译和运行时类路径
configurations { //intTestImplementation 扩展至implementation,说明它具备生产源码的各项依赖 intTestImplementation.extendsFrom implementation intTestRuntimeOnly.extendsFrom runtimeOnly //如果集成测试需要单元测试的依赖可以采用如下做法 //intTestImplementation.extendsFrom testImplementation } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' intTestImplementation 'org.springframework.boot:spring-boot-starter-test' } sourceSets { //这里因为我们的intTest目录与闭包命名一致,所以不使用srcDir添加也能自动识别 intTest { //将正式项目的源码编译和运行时路径给到intTest,记住是+=,而不是覆盖 compileClasspath += sourceSets.main.output runtimeClasspath += sourceSets.main.output } }
创建一个任务来运行集成测试
//任务类型是Test task integrationTest(type: Test) { description = 'Runs integration tests.' //这里指定后,该任务将会在IDE右侧的verification分组里面 group = 'verification' //配置已编译的测试类的位置 testClassesDirs = sourceSets.intTest.output.classesDirs //运行我们的集成测试时使用的类路径。 classpath = sourceSets.intTest.runtimeClasspath //声明在test单元测试完成后自动执行集成测试(非必须) shouldRunAfter test //mustRunAfter表示必须在test任务执行完成之后执行 } //定义一个检查任务,让集成测试在检查任务之前运行,如果集成任务构建失败,检查任务也会失败(非必须) check.dependsOn integrationTest
最后注意定义test单元测试引擎
tasks.withType(Test) { useJUnitPlatform() } //以下写法不生效,暂未找到原因 test { useJUnitPlatform() }
完整的配置如下
buildscript { repositories { mavenLocal() maven { url "https://maven.aliyun.com/repository/public" } mavenCentral() } dependencies { classpath "org.springframework.boot:spring-boot-gradle-plugin:2.6.5" } } plugins { id 'org.springframework.boot' version '2.6.4' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } group = 'com.it235' version = '1.0.0' repositories { maven { url 'https://maven.aliyun.com/repository/central' } } configurations { intTestImplementation.extendsFrom implementation intTestRuntimeOnly.extendsFrom runtimeOnly } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' intTestImplementation 'org.springframework.boot:spring-boot-starter-test' } sourceSets { intTest { compileClasspath += sourceSets.test.runtimeClasspath runtimeClasspath += sourceSets.test.runtimeClasspath } } tasks.withType(Test) { useJUnitPlatform() } task integrationTest(type: Test) { //tasks.register('integrationTest', Test) { description = 'Runs integration tests.' group = 'verification' testClassesDirs = sourceSets.intTest.output.classesDirs classpath = sourceSets.intTest.runtimeClasspath shouldRunAfter test } check.dependsOn integrationTest
artifacts{}:配置需要交付的产品组件信息
artifacts的意思是产品、产物,可以理解为一个可以交付的jar或者war。artifacts主要的用途:定义好产物构建规则,然后结合archives或publishing发布到私服或中央仓库
传统的构建artifacts一般是结合IDE工具进行的,这里我新建一个普通的Gradle项目:
second-gradle
,新建也给Java类package com.it235.second; public class SecondApplication { public static void main(String[] args) { System.out.println("start SecondApplication"); } }
然后采用IDEA的传统方式进行打包,流程如下:
File->Project Structrue->Project Settings->Artifacts
,选中From modules with dependencies
15年前我们可能经常这么干,但是以上的操作不够灵活,比如多模块的情况下我们有特殊处理,版本也没有办法管理,随着Maven和Gradle的强大,我们采用了项目管理工具来构建,也就是接下来我要说的。生成artifact产品主要有以下3种途径
使用已经定义好的Task
task myJar(type: Jar) artifacts { archives myJar } 或 configurations { xxx } task myJar2(type: Jar) artifacts { xxx myJar2 } //仔细一看,好像干了什么,又好像什么都没干,使用示例如下 //实例1,定义jar包构成 task myJar2(type: Jar){ from compileJava.outputs , "src/main/resources" manifest { attributes( "Manifest-Version": 1.0, 'Main-Class': 'com.it235.second.SecondApplication', ) } } //实例2,生成sql.zip task dbChangelogZip(type: Zip) { from 'src/main/resource4db' baseName 'order-db-changelog' //输出的目标目录 destinationDir(file('build/libs')) } //实例3,生成source源码 task mySourceJar(type: Jar){ archiveClassifier = 'sources' from sourceSets.main.allSource } artifacts { archives myJar2 archives mySourceJar archives dbChangelogZip }
使用一个file文件(暂时没有想到使用场景)
def someFile = file('build/somefile.txt') artifacts { archives someFile } artifacts { archives file: file('build/libs/second-gradle-1.0-SNAPSHOT.jar'), builtBy: war }
publishing{}:deploy当前项目到仓库
plugins { ... id 'maven-publish' ... } publishing { publications { //打springboot-jar包 uesJar(MavenPublication) { artifact bootJar from components.java } //可单独执行publishDbchangelogPublicationToUesRepository 上传dbchangelog压缩包 dbchangelog(MavenPublication) { artifactId 'ues-db-changelog' artifact dbChangelogZip } } repositories { maven { //忽略https allowInsecureProtocol true name = 'ues' def releasesRepoUrl = "http://192.168.35.xx:8081/nexus/content/repositories/releases/" def snapshotsRepoUrl = "http://192.168.35.xx:8081/nexus/content/repositories/snapshots/" url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl credentials { username 'admin' password 'admin123' } } } }
总结
本篇到这里就结束了,通过对Project的学习和了解,我们知道了build.gradle的构成,已经每个内置api的基本使用,后续文章会跟着实战来详细介绍这些api的使用,我们下一篇见。