靈活強大的構建系統Gradle
來自: http://www.uml.org.cn/jchgj/201603012.asp
前言
構建,軟件生命周期中重要的一環,在現代軟件開發過程中,起著越來越重要的作用。過去在Java或類Java的世界里,Ant、Maven再熟悉不過了,Maven憑借其強大的依賴配置戰勝Ant,基本上成為了Java構建的標準。而在現代,系統日益復雜,構建的靈活性要求越來越高,比如:構建過程中需要打包上傳到服務器,Maven無法很好地支持這種復雜的系統構建,所以,我選擇了Gradle,一個基于Groovy,更靈活更強大的構建系統,能幫助我們構建更復雜的項目。
為什么選擇Gradle
從框架方向來看:
Gradle是很成熟的技術,可以處理大規模構建
Gradle對多語言、多平臺有更natural的支持
Gradle關注在構建效率上
Gradle發布很頻繁,重要feature開發計劃透明化
Gradle社區很活躍,并且增加迅速
從語言特性來看:
1.代碼很精簡
2.Gradle基于Groovy,能完成復雜系統的構建任務
3.DSL比XML更簡潔高效
Gradle在開源項目中的使用
現在使用Gradle構建的開源項目很多,我有過接觸的比如:Grails, Griffon, Groovy, Hibernate, Spring
還有很多其它開源項目也都在用Gradle,比如Tapestry,Qi4J,Netflix下所有開源項目(python、c++、html等除外)等等。
Gradle在企業中的使用
現在使用Gradle來做構建體系的公司也越來越多,linkedin就很早開始切換到Gradle。
Gradle體驗
Gradle的安裝非常方便,下載ZIP包,解壓到本地目錄,設置 GRADLE_HOME 環境變量并將 GRADLE_HOME/bin 加到 PATH 環境變量中,安裝就完成了。用戶可以運行gradle -v命令驗證安裝,這些初始的步驟和Maven沒什么兩樣。我這里安裝的Gradle版本是1.10,詳細信息見下:
bob [10:42] ? gradle -v
------------------------------------------------------------
Gradle 1.10
------------------------------------------------------------
Build time: 2013-12-17 09:28:15 UTC
Build number: none
Revision: 36ced393628875ff15575fa03d16c1349ffe8bb6
Groovy: 1.8.6
Ant: Apache Ant(TM) version 1.9.2 compiled on July 8 2013
Ivy: 2.2.0
JVM: 1.7.0_45 (Oracle Corporation 24.45-b08)
OS: Mac OS X 10.9.2 x86_64
Gradle的Features很多,官網doc介紹很詳細,我這里就不多說。下面簡單介紹一下Gradle構建相關的東西。
Gradle基礎
1,Gradle有兩個最基本的概念:project和task。Gradle里面的所有東西都基于這兩個概念。project通常指一個項目,而task指構建過程中的任務。一次構建可以有1到n個project,每個project有1到n個task。
2,Gradle有一個類似Maven中pom.xml的配置文件:build.gradle。功能也基本一樣,負責當前project的構建定義。看一個build.gradle的簡單例子:
bob [10:46] ? pwd /Users/bob/framework/gradle-1.10/samples/userguide/tutorial/hello // 在你安裝的gradle根目錄下有對應的samples目錄,里面有很多例子
bob [10:46] ? cat build.gradle
task hello {
doLast {
println 'Hello world!'
}
}
文件中定義了一個task:hello,task的內容是 "println 'Hello world!'",我們來執行一下:
bob [10:49] ? gradle -q hello Hello world!
可以看到,輸出了"Hello world!",這里-q的意思是quiet模式,只輸出構建中的必要信息。
gradle里可以定義多個task,task之間也可以有依賴關系,還可以定義默認task,看一個例子:
帶有task依賴關系:
bob [10:53] ? cat userguide/tutorial/lazyDependsOn/build.gradle task taskX(dependsOn: 'taskY') << { println 'taskX' } task taskY << { println 'taskY' }
帶有默認task例子:
bob [10:59] ? cat userguide/tutorial/defaultTasks/build.gradle defaultTasks 'clean', 'run'
task clean << {
println 'Default Cleaning!'
}
task run << {
println 'Default Running!'
}
task other << {
println "I'm not a default task!"
}
看看執行情況:
bob [10:59] ? gradle -q Default Cleaning! Default Running!
bob [11:00] ? gradle -q otherI'm not a default task!
默認task,當沒有task指定時,則會執行默認的task。
Gradle依賴
Gradle和Maven在依賴管理上幾乎差不多,核心的概念是一樣的,只不過Gradle語法更精簡,并且多了一些更靈活的自定義配置。我們先看一個例子,Maven的pom.xml:
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> </dependency> </dependencies>
更換成Gradle腳本,結果是這樣:
dependencies { compile('org.springframework:spring-core:3.2.4.RELEASE') compile('org.springframework:spring-beans:3.2.4.RELEASE') compile('org.springframework:spring-context:3.2.4.RELEASE') testCompile('junit:junit:4.7') }
代碼塊少了很多。試想,生產環境下的中、大型應用如果用都用Gradle替換Maven,那勢必會大大減少配置文件代碼塊,并有更強的可讀性,也就意味著系統更加穩健。
1,Gradle在依賴配置上面,和Maven一樣,支持傳遞性依賴,然后和Maven不同的是,它還支持排除傳遞性依賴以及關閉傳遞性依賴。
2,Gradle的依賴scope,也基本和Maven一樣,不過它是通過配置來定義,plugin來支撐和加強的,所以除了基本的compile、runtime等scope外,Gradle還可以自定義出很多配置,針對不同的配置寫不同的task來完成更復雜更靈活的構建任務。
依賴相關的倉庫配置很靈活,支持多種repository,看下面repository定義例子:
bob [11:07] ? cat userguide/artifacts/defineRepository/build.gradle
repositories {
mavenCentral() // 定義倉庫為maven中心倉庫
}
repositories {
jcenter() // 定義倉庫為jcenter倉庫
}
repositories {
maven {
url "http://repo.mycompany.com/maven2" // 定義依賴包協議是maven,地址是公司的倉庫地址
}
}
repositories { // 定義本地倉庫目錄
flatDir {
dirs 'lib'
}
}
repositories { // 定義ivy協議類型的倉庫
ivy {
url "http://repo.mycompany.com/repo"
}
}
可以看到,對于常用的maven、ivy、local以及jcenter的repository都有支持,語法很簡單。而且還可以通過編寫task來支持更復雜的repository,更多詳情可以查看安裝包里的對應目錄下文件查看。
Gradle構建
和Maven一樣,Gradle也是通過artifact來打包構建的。得益于上述的Gradle本身的特性,artifact在Gradle里實現得更靈活一些。看一個例子:
bob [13:00] ? cat userguide/artifacts/uploading/build.gradle
## jar類型的artifact
task myJar(type: Jar)
artifacts {
archives myJar
}
## file類型的artifact
def someFile = file('build/somefile.txt')
artifacts {
archives someFile
}
## 根據自定義task來完成artifact
task myTask(type: MyTaskType) {
destFile = file('build/somefile.txt')
}
artifacts {
archives(myTask.destFile) {
name 'my-artifact'
type 'text'
builtBy myTask
}
}
## 根據自定義task來完成artifact
task generate(type: MyTaskType) {
destFile = file('build/somefile.txt')
}
artifacts {
archives file: generate.destFile, name: 'my-artifact', type: 'text', builtBy: generate
}
這樣就簡單地定義了好幾種artifact生成的定義,根據不同的場景需求,生成文本文件、jar包或者zip,還可以再上傳到服務器上。一般情況下,常用的插件,比如說"Java plugin"都默認定義了"jar"這樣的artifact task,所以一般不需要額外開發。但是,針對于一些復雜情況,或者在plugin基礎上增強的話,自定義artifact task還是非常有用的。
Gradle構建的項目,發布到倉庫中,也非常容易:
apply plugin: 'maven'
uploadArchives {
repositories {
ivy {
credentials {
username "username"
password "pw"
}
url "http://repo.mycompany.com"
}
}
}
Gradle 插件
上面簡介介紹了一下Gradle的一些概念和配置,要用到項目中run起來,現在還還要一步,就是本節介紹的Gradle插件。Gradle現在已經支持很多插件,這給開發者帶來極大的便利,先說說Java插件吧。
1,使用Java plugin,只需要在build.gradle中加入這句話:
apply plugin: 'java'
2,了解或設置Java project布局。Gradle和Maven一樣,采用了“約定優于配置”的方式對Java project布局,并且布局方式是和Maven一樣的,此外,Gradle還可以方便的自定義布局。在Gradle中,一般把這些目錄叫做source set。看下官方的答案:
這里要注意,每個plugin的source set可能都不一樣。
同樣的,Java plugin還定義好了一堆task,讓我們可以直接使用,比如:clean、test、build等等。這些task都是圍繞著Java plugin的構建生命周期的:
圖中每一塊都是一個task,箭頭表示task執行順序/依賴,比如執行task jar,那么必須先執行task compileJava和task processResources。另外可以看到,Gradle的Java plugin構建生命周期比較復雜,但是也表明了更加靈活,而且,在項目中,一般只使用其中常用的幾個:clean test check build 等等。
gradle構建過程中,所有的依賴都表現為配置,比如說系統運行時的依賴是runtime,gradle里有一個依賴配置叫runtime,那么系統運行時會加載這個依賴配置以及它的相關依賴。這里說的有點繞,可以簡單理解依賴和maven類似,只不過gradle用configuration實現,所以更靈活,有更多選擇。下圖是依賴配置關系圖以及和task調用的關系圖:
可以看到,基本和Maven是一樣的。其實Gradle里面這些依賴(scope)都是通過configuration來實現的,這里就不細說,有興趣的可以研究一下官方資料。
關于“約定優于配置”,還有很多東西,這里不細說,官方doc已經說的很詳細了。
Gradle 其它不錯的特性
1,所有聲明都是一等公民
2,多project構建
3,引用外部/通用構建腳本
4,Gradle wrapper
小結
1,Gradle非常簡潔,項目本身的配置代碼非常少。
2,Gradle在外部project構建也支持很好,整體構建簡單,并且通過公用外部構建腳本,讓配置內容盡量沒有冗余。
3,Gradle很靈活,可以方面的增加和修改構建過程。而Maven卻需要開發插件來支持。
4,Gradle是基于Groovy的,也就是說配置中可以編寫自定義代碼,能適應更復雜的場景,能完成更強大的功能,比如說:自動上傳、分發、部署等等。
項目實戰
Gradle介紹了那么多,可以看出,gradle是非常靈活的,可以適應各種復雜環境。建議各位從架構角度考慮gradle構建,而不僅僅把它當作一個構建工具。下面來說說我們實際項目中的Gradle改造工作。
背景:
我們的項目經過一個半Q的迅速發展,整個項目已經由1個簡易后臺變成4個系統+若干腳本任務了,項目中存在很多冗余代碼和重復配置。我們使用上面介紹的方法對項目進行了改造,以解決這兩個問題。
步驟:
要解決冗余代碼和通用配置的問題,最簡單的做法就是抽取出共同部分,作為其它所有項目的parent/common項目。方法:
1,使用git submodule
將所有系統中公共的類庫和通用的配置,放到獨立的倉庫Common中。因為我們用git來管理代碼,而git本身提倡多branch,多倉庫,所以采用git submodule方式,其它項目需要添加Common這個submodule:
git submodule add yourGitRepo deps/Common
最后的"deps/Common"是自定義的,意思就是在當前的deps目錄下用Common名字來當作submodule的clone。
如果你clone別的帶有submodule的項目時,默認情況下,當前的project并不會把submodule的代碼都clone下來,可以執行:
git submodule foreach git pull
以下這段一般大家經常會遇到:
當你clone項目時,submodule會以最新的master分支上的commit id作為本次的tag下載,類似一個副本,因為一般大家都是用submodule,而不是修改它。所以當你的submodule需要更新的時候,需要先執行這段代碼:
git submodule foreach git checkout master
讓submodule切換到master分支了,然后就可以用上面的submodule pull來更新了。
2,gradle構建:
鑒于上文對gradle優點的描述,我們采用gradle來構建。我們的項目最初都是基于maven來構建的,從maven切換到gradle很簡單,在項目根目錄下,先執行(假設你的機器已經安裝了gradle環境,一般負責構建的人首次需要安裝,開發人員可以不安裝):
gradle init wrapper
這樣,就會自動生成相關的gradlew,build.gradle,settings.gradle等文件和相關目錄,并會自動下載對應版本的gradle binary包(所以以后不需要安裝)。Gradle會自動識別Maven里的配置,并相應的導入進來,有少量部分配置可能需要修改。
注:在已有的gradle項目里,盡量使用生成的gradlew這個wrapper,因為它會自動下載對應版本的Gradle,也就是說團隊合作的其他人開發機上是不需要手動安裝Gradle的,并且wrapper也讓大家的Gradle版本一致,避免問題。
3,gradle腳本修改
上面執行完之后,環境已經準備好了,現在要做的就是修改構建腳本:
因為已經通過git submodule把公共項目放到獨立目錄(deps/Common)了,并且它本身也是獨立可構建的項目,那么也就是說當前有兩個項目了,一個是當前project,一個是Common項目,要做的就是告訴gradle,要多項目構建,編輯settings.gradle,增加項目配置:
include "deps:Common"
以上就是把Common引入到當前項目了。
根據項目的不同,然后對應修改build.gradle,就大功告成了。看一個例子:
// 這一段主要是把公共庫Common的構建腳本引入,因為一般會有通用的配置在里面 def userGradleScript = file("deps/Common/build.gradle") if (userGradleScript.exists()) { apply from: userGradleScript } // 使用war插件,這樣就默認引入了java插件 apply plugin: 'war' // for jetty apply plugin: 'jetty' stopKey = 'yourStopKey' // 自定義的stopkey stopPort = xxxx // 停止端口 httpPort = xxxx // 啟動http端口
// 項目屬性
group = 'yourApp'
version = '1.0.0'
description = """這里描述你的項目"""
// checkstyle config文件地址
checkstyle {
configFile = file("deps/Common/config/checkstyle/checkstyle.xml")
}
// lib依賴
dependencies {
// 依賴公共庫Common,compile是和maven里的compile scope一樣
compile project(':deps:Common')
compile 'commons-validator:commons-validator:1.4.0'
compile('javax.servlet.jsp.jstl:jstl-api:1.2') {
exclude(module: 'servlet-api') // 防止版本沖突
}
compile 'javax.persistence:persistence-api:1.0.2'
runtime 'mysql:mysql-connector-java:5.1.26'
providedCompile 'org.apache.tomcat:tomcat-servlet-api:7.0.30'
// providedCompile 這個conf在java插件里是報錯的,war里是正確的
providedCompile 'javax.servlet.jsp:jsp-api:2.1'
...
}
我們再來簡單看下公共項目Common的構建腳本:
// 定義一堆基礎插件 apply plugin: 'java' apply plugin: 'maven' apply plugin: "jacoco" apply plugin: 'checkstyle' apply plugin: 'pmd' apply plugin: 'findbugs' apply plugin: 'eclipse' apply plugin: 'idea' // 定義項目屬性 group = 'Common' version = '1.0.0' description = """Giant common library"""
// 定義依賴倉庫
repositories {
mavenCentral()
}
// project的額外屬性,這里用于定義profile屬性,模擬maven的profile
ext {
if (project.hasProperty('profile')) {
profile = project['profile']
} else {
profile = "dev"
}
println "profile:" + profile
}
// 額外增加source path
sourceSets {
main {
resources {
srcDir "src/main/profiles/${profile}"
}
}
}
// project依賴
dependencies {
compile 'ch.qos.logback:logback-core:1.0.13'
compile 'ch.qos.logback:logback-classic:1.0.13'
compile 'ch.qos.logback:logback-access:1.0.13'
compile 'commons-io:commons-io:2.0.1'
compile 'commons-lang:commons-lang:2.6'
compile 'joda-time:joda-time:1.6.2'
compile 'org.testng:testng:6.8.7'
compile 'com.googlecode.jmockit:jmockit:1.5'
...
}
// task配置
checkstyle {
ignoreFailures = true
sourceSets = [sourceSets.main]
}
findbugs {
ignoreFailures = true
sourceSets = [sourceSets.main]
}
pmd {
ruleSets = ["basic", "braces", "design"]
ignoreFailures = true
sourceSets = [sourceSets.main]
}
jacocoTestReport {
reports {
xml.enabled true
html.enabled true
csv.enabled false
}
sourceSets sourceSets.main
}
tasks.withType(Compile) {
options.encoding = "UTF-8"
}
test {
useTestNG()
jacoco {
excludes = ["org.*"]
}
}
這樣,就可以在公共項目里配置好一堆基礎的task,dependencies等等,而使用這個公共項目的其它項目則可以直接使用,無需再額外配置。
4,run
腳本修改完了,就可以開始構建了(不需要安裝gradle,直接使用生成的gradlew就行):
./gradlew build
// 基于profile構建./gradlew -Pprofile=dev build
常用構建命令:
clean:清除之前的構建
test:執行測試
compileJava:編譯java
check:在test之后做一個check,一般代碼檢查插件,都是在這個階段做的
build:構建打包
總結
隨著公司業務的發展,軟件系統變得日益復雜和龐大,這就要求有更靈活、更高效的構建系統來支撐。現代構建系統Gradle提供了強大的功能、簡潔的語法、靈活的配置,能適應各種復雜的構建環境。利用多project構建,讓整個系統模塊化,管理更高效。