Gradle詳解
先看下Gradle的定義:
Gradle是一個基於Apache Ant和Apache Maven概念的專案自動化構建開源工具。它使用一種基於Groovy的特定領域語言(DSL)來宣告專案設定,拋棄了基於XML的各種繁瑣配置。
抽住定義中的核心就是DSL版本的編譯工具,解決XML版本編譯工具的缺點。
雖然gradle是基於groovy的,但是由於其提供了一整套DSL,所以在開發gradle指令碼時幾乎脫離了groovy的感知。我們在這裡把gradle當成一個獨立的部署工具,而不去深究原理,從這個角度來看,groovy不是學習gradle的前提條件。
在開始學習gradle指令碼之前,我們先弄清楚gradle中的3個組成部分:
Project:專案
1 每個專案都會有自己的gradle領域,配置指令碼檔名預設是不變的build.gradle(後面會介紹如何指定自定義檔名)
2 Project之間如果出現父子關係,只有根Project才會有setting.gradle配置檔案,該配置檔案的作用是宣告其包含的子專案
Task:任務
每個Project是由N個Task組織成的一個“有向無環圖”(關於有向無環圖的說明可以參考Spark中的解釋),task之間有依賴關係從而決定了它們的執行順序。
依賴方式如下:
task taskA(dependsOn: 'taskB') << {
println 'taskA'
}
Task的來源有三種
第一種是Gradle預設自帶了幾種task:
Dependencies:顯示Project的依賴資訊
Projects:顯示所有Project,包括根Project和子Project
Properties
:
顯示一個Project所包含的所有Property
第二種是直接在project中顯式建立
hello << { println 'Hello Task' }
第三種是成品task以plugin的方式引入
通過apply方式引入:apply plugin: 'groovy'
Property:屬性
Project除開包含Task外還包含Property,Property預設自帶了一些屬性,也可以通過ext.XXX的方法來擴充套件。
Gradle的工作流:
一次Gradle的工作流分為3大部:
第一:解析setting.gradle並遍歷根目錄,檢查子專案是否滿足
第二:解析每個子project的gradle,根據task之間的關係建立有向無環圖
第三:執行,涉及到download依賴、build釋出包等
Gradle指令碼詳解:
有了以上的基礎知識,我們開始真正的指令碼學習之旅:
單project的專案:
buildscript {
repositories {
jcenter()
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath 'com.netflix.nebula:gradle-netflixoss-project-plugin:5.1.1-rc.1'
}
}
apply plugin: 'nebula.netflixoss'
apply plugin: 'groovy'
apply plugin: 'idea'
group = 'com.netflix.spinnaker.gradle'
sourceCompatibility = 1.8
targetCompatibility = 1.8
idea {
project {
jdkName = sourceCompatibility
languageLevel = targetCompatibility
vcs = 'Git'
}
}
bintray {
pkg {
userOrg = 'spinnaker'
repo = 'gradle'
labels = ['Spinnaker', 'Netflix']
}
}
repositories {
jcenter()
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
compile gradleApi()
compile localGroovy()
compile 'com.netflix.nebula:gradle-netflixoss-project-plugin:5.1.1-rc.1'
compile 'com.netflix.nebula:gradle-ospackage-plugin:4.9.3'
compile 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.1'
compile 'org.yaml:snakeyaml:1.17'
}
buildscript:因為gradle本身也是groovy語言,所以指令碼一開始要為gradle準備好執行環境。
repositories:倉庫地址,這裡配置了兩個倉庫jcenter和maven,我們先解釋下兩者的區別和聯絡。jcenter和maven分別是JFrog和Sonatype公司提供的倉庫服務,jcenter的優點是因為採取了CDN技術所以獲取非常迅速;maven的優點是倉庫內容比jcenter要全面,在jcenter中找不到的內容可以在maven中找到。也正是因為這兩個倉庫各自的優缺點才有了你所看到的指令碼中配置的先後順序(gradle尋找依賴包是按照指令碼書寫的先後順序來執行的)。Gradle預設不提前定義任何倉庫,我們必須手動在使用外部依賴之前定義自己的倉庫
一個專案可以有好幾個庫,Gradle會根據依賴定義的順序在各個庫裡尋找它們,在第一個庫裡找到了就不會再在第二個庫裡找。
apply plugin:task的第三類引入-外掛式
dependencies:具體的依賴包,可以看到在buildscript中出現一次,在指令碼結尾出現一次。buildscript中出現是因為它是gradle執行的依賴(classpath指定);第二次出現時專案本身的依賴(compile指定)。
repositories:同dependencies一樣在buildscript內外各出現一次,分別是gradle指令碼的依賴和專案本身的依賴。
bintra:這個task坑較多,目的是將最終的專案釋出到jcenter中,裡面配置了組織名、倉庫名等資訊。
compile:用來編譯專案原始碼的依賴,先看最後4行,他們的格式是group:name:version,很好理解。gradleApi()和localGroovy()這兩行並沒有按照我們所說的group:name:version的格式,這是幹什麼用的麼?我們需要再學習下“自定義plugin”,詳見下一章節。
與build.gradle平級目錄還需要一個settings.gradle,該專案只有一行:
rootProject.name = 'spinnaker-gradle-project'
指明編譯的根專案。
自定義plugin
上面在介紹task的時候曾經強調過task的3種來源,其中最簡單的引入方式就是plugin了。假設我們有個規模較大的工程,設計到很多個project,我們可以將一些公共的task抽象出來開發一套自己的plugin,那麼gradle指令碼就會變得乾淨高度可讀了。
下面看下如何開發自定義plugin,案例程式碼:
package com.netflix.spinnaker.gradle.dependency
import org.gradle.api.Plugin
import org.gradle.api.Project
class SpinnakerDependencyPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.extensions.create('spinnaker', SpinnakerDependency, project)
}
}
實現org.gradle.api.Plugin介面並實現apply(Project project)方法,就是自定義plugin程式碼部分的所有內容。如果要掌握apply中的內容需要詳細的學習gradle的原理,例如你瞭解了project的擴充套件屬性是如何定義的才能明白為什麼要用extensions.create()這個方法。
這裡我們import了org.gradle.api進來,所以前面單專案指令碼時我們compile gradleApi()、compile localGroovy()就是為了自定義plugin而引入的依賴。
我們自定義的plugin有類名、包名,使用起來非常費勁,所以我們在resources的META_INF下的gradle-plugins下定義出外掛名與外掛具體類的對映關係。
這樣其它專案只要dependence了該專案後就可以用apply plugin spinnaker.project的方式來引入自定義的plug了。
多project的專案:
對於多專案的場景,我們要先看settings.gradle檔案搞明白各專案之間的父子關係。
例如我的settings.gradle如下:
rootProject.name = 'rosco'
include 'rosco-core', 'rosco-web', 'rosco-manifests'
def setBuildFile(project) {
project.buildFileName = "${project.name}.gradle"
project.children.each {
setBuildFile(it)
}
}
rootProject.children.each {
setBuildFile(it)
}
跟專案是rosco,它有3個子專案,而且自定義了setBuildFile(project)這個task,我們前面介紹了預設的指令碼檔案是build.gradle,該task的作用是強制指定了每個子專案的gradle的指令碼檔案。
再來看根專案的build.gradle指令碼:
buildscript {
ext {
springBootVersion = "1.5.4.RELEASE"
}
repositories {
jcenter()
maven { url "https://spinnaker.bintray.com/gradle" }
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath 'com.netflix.spinnaker.gradle:spinnaker-gradle-project:4.0.0'
classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
}
}
allprojects {
apply plugin: "spinnaker.project"
apply plugin: "groovy"
group = "com.netflix.spinnaker.rosco"
ext {
spinnakerDependenciesVersion = project.hasProperty('spinnakerDependenciesVersion') ? project.property('spinnakerDependenciesVersion') : '0.149.1'
}
def checkLocalVersions = [spinnakerDependenciesVersion: spinnakerDependenciesVersion]
if (ext.has('versions')) {
def extVers = ext.get('versions')
if (extVers instanceof Map) {
checkLocalVersions.putAll(extVers)
}
}
def localVersions = checkLocalVersions.findAll { it.value.endsWith('-SNAPSHOT') }
if (localVersions) {
logger.info("Enabling mavenLocal repo for $localVersions")
repositories {
mavenLocal()
}
}
spinnaker {
dependenciesVersion = spinnakerDependenciesVersion
}
configurations.all {
resolutionStrategy {
force 'org.apache.httpcomponents:httpclient:4.3.5'
force 'org.apache.httpcomponents:httpcore:4.3.2'
}
exclude group: 'javax.servlet', module: 'servlet-api'
exclude group: "org.slf4j", module: "slf4j-log4j12"
exclude group: "org.mortbay.jetty", module: "servlet-api"
}
dependencies {
spinnaker.group("test")
compile spinnaker.dependency("bootActuator")
compile spinnaker.dependency("groovy")
compile spinnaker.dependency("korkSwagger")
compile spinnaker.dependency("spectatorReg")
compile spinnaker.dependency("korkArtifacts")
}
test {
testLogging {
exceptionFormat = 'full'
}
}
tasks.withType(JavaExec) {
if (System.getProperty('DEBUG', 'false') == 'true') {
jvmArgs '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8187'
}
}
}
defaultTasks ':rosco-web:bootRun'
buildscript:該部分與單專案中的區別不大,不再做詳細解釋。
classpath 'com.netflix.spinnaker.gradle:spinnaker-gradle-project:4.0.0'這個就是我們前面自定義的那個plugin了,com.netflix.spinnaker.gradle是那個專案的group,spinnaker-gradle-project是對映的name,4.0.0是版本號。
allprojects:指所有的project都要進行設定,如果只設置子專案,替換成subprojects
ext. spinnakerDependenciesVersion:project的擴充套件屬性,這裡的意義是要給所有專案新增一個統一的版本號
configurations.all這裡是要修改依賴的版本,gradle預設會用最新版本的依賴,如果想要強制指定版本號就通過這種方式。
defaultTasks:指定專案的主程式入口
再看一個子專案的的指令碼,子專案是沒有setting.gradle的,只有根專案才有setting.gradle:
dependencies {
compileOnly spinnaker.dependency("lombok")
compile group: 'commons-io', name: 'commons-io', version: '2.4'
compile project(":rosco-core")
}
看得出子專案的gradle指令碼相對簡單多了,主要負責兩部分功能。第一指定子專案之間的依賴關係,這將影響到最終的有向無環圖;第二子專案自身的依賴控制。當然你也可以定製子專案私有的task,這裡不再做介紹。
該指令碼就指定依賴了平級的rosco-core子專案
最後回到我們前面講過的gradle的執行過程:
第一:解析setting.gradle並遍歷根目錄,檢查子專案是否滿足
第二:解析每個子project的gradle,根據task之間的關係建立有向無環圖
第三:執行,涉及到download依賴、build釋出包等
最後一步就是下載所有依賴,按有向無環圖的一步步執行了,最終build成釋出包。
對於產出,按照你的gralde裡內容來設定,你可以產出jar包,或者只是編譯成class,甚至利用外掛產出.deb格式用於ubuntu系統apt-get安裝,都可以。
Gradlew和Gradle的區別和聯絡:
我們通過IDE引入一個gradle專案的時候會有類似下面的選擇,例如我的IDE:
前兩個選擇是Gradle Wrapper的,也就是Gradlew;第三個選擇是Gradle的,那麼這兩種有啥區別呢?
Local Gradle,是我們本地安裝的gradle,就跟安裝JDK一樣,下載安裝gradle,配置path環境變數,然後本地就可以gradle build命令來執行gradle了。
Gradlew方式對專案的使用者更友好,因為你可以自己不準備上面的gradle環境,而是交給gradlew來幫你下載安裝搞定環境問題,所以本質上Gradlew和Gradle是同一個東西的自動封裝。