gradle構建多module的專案
配置子專案
在多模組的專案中,Gradle遵循慣例優於配置 (Convention Over Configuration)原則。
在父專案的根目錄下尋找settings.gradle檔案,在該檔案中設定想要包括到專案構建中的子專案。在構建的初始化階段(Initialization),Gradle會根據settings.gradle 檔案來判斷有哪些子專案被include到了構建中,併為每一個子專案初始化一個Project物件,在構建指令碼中通過project(‘:sub-project-name’)來引用子專案對應的Project物件。
通常,多模組專案的目錄結構要求將子模組放在父專案的根目錄下,但是如果有特殊的目錄結構,可以在settings.gradle檔案中配置。
我所在的專案包括:
- 一個描述核心業務的core模組
- 一個遺留的Enterprise Java Bean(enterprise-beans)模組
- 兩個提供不同服務的Web專案(cis-war和admin-war)
- 一個通過schema生成jaxb物件的jaxb專案以及一個用來用來打ear包的ear專案
- 一個用於存放專案配置檔案相關的config子目錄。它不是子模組,所以 config不應該被加到專案的構建中去。
它們都放置在根專案目錄下。我們通過如下的settings.gradle來設定專案中的子專案:
include 'core', 'enterprise-beans', 'cis-war', 'admin-war', 'jaxb', 'ear'
我們將需要加入到專案構建中的子專案配置在settings.gradle檔案中,而沒有加入不需要的config子目錄
2.
共享配置
在大型Java專案中,子專案之間必然具有相同的配置項。我們在編寫程式碼時,要追求程式碼重用和程式碼整潔;而在編寫Gradle指令碼時,同樣需要保持程式碼重用和程式碼整潔。Gradle 提供了不同的方式使不同的專案能夠共享配置。
-
allprojects:allprojects是父Project的一個屬性,該屬性會返回該Project物件以及其所有子專案。在父專案的build.gradle腳本里,可以通過給allprojects傳一個包含配置資訊的閉包,來配置所有專案(包括父專案)的共同設定。通常可以在這裡配置IDE的外掛,group和version等資訊,比如:
allprojects { apply plugin: 'idea' }
這樣就會給所有的專案(包括當前專案以及其子專案)應用上idea外掛。
-
subprojects:subprojects和allprojects一樣,也是父Project的一個屬性,該屬性會返回所有子專案。在父專案的build.gradle腳本里,給 subprojects傳一個包含配置資訊的閉包,可以配置所有子專案共有的設定,比如共同的外掛、repositories、依賴版本以及依賴配置:
subprojects { apply plugin: 'java' repositories { mavenCentral() } ext { guavaVersion = ’14.0.1’ junitVersion = ‘4.10’ } dependencies { compile( “com.google.guava:guava:${guavaVersion}” ) testCompile( “junit:junit:${junitVersion}” ) } }
這就會給所有子專案設定上java的外掛、使用mavenCentral作為 所有子專案的repository以及對Guava[4]和JUnit的專案依賴。此外,這裡還在ext裡配置依賴包的版本,方便以後升級依賴的版本。
-
configure:在專案中,並不是所有的子專案都會具有相同的配置,但是會有部分子專案具有相同的配置,比如在我所在的專案裡除了cis-war和admin-war是web專案之外,其他子專案都不是。所以需要給這兩個子專案新增war外掛。Gradle的configure可以傳入子專案陣列,併為這些子專案設定相關配置。在我的專案中使用如下的配置:
configure(subprojects.findAll {it.name.contains('war')}) { apply plugin: 'war' }
configure需要傳入一個Project物件的陣列,通過查詢所有專案名包含war的子專案,併為其設定war外掛。
獨享配置
在專案中,除了設定共同配置之外, 每個子專案還會有其獨有的配置。比如每個子專案具有不同的依賴以及每個子專案特殊的task等。Gradle提供了兩種方式來分別為每個子專案設定獨有的配置。
-
在父專案的build.gradle檔案中通過project(‘:sub-project-name’)來設定對應的子專案的配置。比如在子專案core需要Hibernate的依賴,可以在父專案的build.gradle檔案中新增如下的配置:
project(‘:core’) { ext{ hibernateVersion = ‘4.2.1.Final’ } dependencies { compile “org.hibernate:hibernate-core:${hibernateVersion}” } }
注意這裡子專案名字前面有一個冒號(:)。 通過這種方式,指定對應的子專案,並對其進行配置。
-
我們還可以在每個子專案的目錄裡建立自己的構建指令碼。在上例中,可以在子專案core目錄下為其建立一個build.gradle檔案,並在該構建指令碼中配置core子專案所需的所有配置。例如,在該build.gradle檔案中新增如下配置:
ext{ hibernateVersion = ‘4.2.1.Final’ } dependencies { compile “org.hibernate:hibernate-core:${hibernateVersion}” }
根據我對Gradle的使用經驗,對於子專案少,配置簡單的小型專案,推薦使用第一種方式配置,這樣就可以把所有的配置資訊放在同一個build.gradle檔案裡。例如我同事鄭曄的開源專案moco。它只有兩個子專案,因而就使用了第一種方式配置,在專案根目錄下的build.gradle檔案中設定專案相關的配置資訊。但是,若是對於子專案多,並且配置複雜的大型專案,使用第二種方式對專案進行配置會更好。因為,第二種配置方式將各個專案的配置分別放到單獨的build.gradle檔案中去,可以方便設定和管理每個子專案的配置資訊。
1.4 其他共享
在Gradle中,除了上面提到的配置資訊共享,還可以共享方法以及Task。可以在根目錄的build.gradle檔案中新增所有子專案都需要的方法,在子專案的build.gradle檔案中呼叫在父專案build.gradle腳本里定義的方法。例如我定義了這樣一個方法,它可以從命令列中獲取屬性,若沒有提供該屬性,則使用預設值:
def defaultProperty(propertyName, defaultValue) { return hasProperty(propertyName) ? project[propertyName] : defaultValue }
注意,這段指令碼完全就是一段Groovy程式碼,具有非常好的可讀性。
由於在父專案中定義了defaultProperty方法,因而在子專案的build.gradle檔案中,也可以呼叫該方法。
2.1 Properties配置
要為不同的環境提供不一樣的配置資訊,Maven選擇使用profile,而Gradle則提供了兩種方法為構建指令碼提供Properties配置:
-
第一種方式是使用傳統的properties檔案, 然後在使用Gradle時,通過傳入不同的引數載入不同的properties檔案。例如,我們可以在專案中提供development.properties、test.properties和production.properties。在專案執行時,使用-Pprofile=development來指定載入開發環境的配置。構建指令碼中載入properties檔案的程式碼如下:
ext { profile = project['profile'] } def loadProperties(){ def props = new Properties() new File("${rootProject.projectDir}/config/${profile}.properties") .withInputStream { stream -> props.load(stream) } props }
在執行指令碼的時候,傳入的-Pprofile=development可以指定使用哪個執行環境的配置檔案。程式碼中使用了project['profile']從命令列裡讀取-P傳入的引數,Gradle會去父專案根目錄下的config資料夾中需找對應的properties檔案。
-
另外一種方式就是使用Groovy的語法,定義可讀性更高的配置檔案。比如可以在專案中定義config.groovy的配置檔案,內容如下:
environments { development { jdbc { url = 'development' user = 'xxxx' password = 'xxxx' } } test { jdbc { url = 'test' user = 'xxxx' password = 'xxxx' } } production { jdbc { url = 'production' user = 'xxxx' password = 'xxxx' } } }
這裡定義了三個環境下的不同資料庫配置,在構建指令碼中使用如下的程式碼來載入:
ext { profile = project['profile'] } def loadGroovy(){ def configFile = file('config.groovy') new ConfigSlurper(profile).parse(configFile.toURL()).toProperties() }
這裡在ConfigSlurper的建構函式裡傳入從命令列裡取到的-P的引數。呼叫loadGroovy方法就可以載入專案根目錄下的config.groovy檔案,並作為一個Map返回,這樣就可以通過jdbc.url來獲取url的值。
從可讀性以及程式碼整潔(配置檔案也需要程式碼整潔)而言,我推薦使用第二種方式來配置,因為這種方法具有清晰的結構。如上面的例子,就可以把資料庫相關的資訊都放在jdbc這個大的節點下,而不用像properties檔案這樣的扁平結構。但是對於一些已經使用properties檔案來為不同環境提供配置資訊的遺留專案裡,使用properties檔案也沒有問題。
2.2 替換
通過不同的方式載入不同環境的配置後,就需要把它們替換到有佔位符的配置檔案中去。在配置檔案中使用@[email protected]來標註要被替換的位置,比如在config資料夾中有jdbc.properties檔案,其內容如下:
[email protected]@ [email protected]@ [email protected][email protected]
在Gradle構建過程中,有一個processResources的Task,可以修改該Task的配置,讓其在構建過程中替換資原始檔中的佔位符:
processResources { from(sourceSets.main.resources.srcDirs) { filter(org.apache.tools.ant.filters.ReplaceTokens, tokens: loadGroovyConfig() ) } }
上面這種做法用來處理子專案src/main/resources資料夾下的資原始檔,所以需要將這段程式碼放在子專案的獨立配置檔案裡。
在一些複雜的專案中,經常會把配置檔案放置到一個目錄進行統一管理。比如在我所在的專案,就專門提供了一個config子目錄,裡面存放了所有的配置資訊。在處理這些資原始檔時, Gradle預設提供的processResources就不夠用了,我們需要在Gradle指令碼中定義一個Task去替換這些包含佔位符的配置檔案,然後讓package或者deploy的Task依賴這個Task。該Task的程式碼如下:
task replace(type: Sync) { def configHome = "${project.rootDir}/config" from(configHome) { include '**/*.properties' include '**/*.xml' filter org.apache.tools.ant.filters.ReplaceTokens, tokens: loadGroovyConfig() } into "${buildDir}/resources/main" }
這裡定義了一個Sync型別的Task,會將父專案的根目錄下的config資料夾的所有properties和xml檔案使用從loadGroovyConfig()方法中加載出來的配置替換,並將替換之後的檔案放到build資料夾下的resource/main目錄中。再讓打包的Task依賴這個Task,就會把替換之後的配置檔案打到包中。
2.3 更復雜的情況
上面介紹了在專案中如何使用Gradle處理 properties和xml檔案中具有相同配置,但其中的一些值並不相同的情況 。然而,在有些專案中不同的環境配置之間變化的不僅是值,很有可能整個配置檔案都不相同;那麼,使用上面替換的處理方式就無法滿足要求了。
在我所在的專案中,我們需要依賴一個外部的Web Service。在開發環境上,我們使用了Stub來模擬和Web Service之間的互動,為開發環境提供測試資料,這些資料都放置在一個Spring的配置檔案中;而在測試和產品環境上,又要使用對應的測試和產品環境的Web Service。這時,開發、測試與產品環境的配置完全不同。對於這種複雜的情況,Gradle可以在構建過程中為不同的環境指定不同的資原始檔夾,在不同的資原始檔夾中包含不同的配置檔案。
例如,在我們專案的config目錄下包含了application資料夾,定義了不同環境所需的不同配置檔案,其目錄結構如下圖所示:
在構建指令碼中,根據從命令列讀入的-P引數,使用不同的資原始檔夾,其程式碼如下:
sourceSets { main { resources { srcDir "config/application/spring/${profile}", "config/application/properties/${profile}" } } }
這樣在打包的過程中,就可以使用-P傳入的引數的資原始檔夾下面的properties和xml檔案作為專案的配置檔案。
2.4 初始化資料庫
在專案開發過程中,為了方便為不同環境構建相同的資料庫及資料,我們通常需建立資料庫的表以及插入一些初始化資料。Gradle目前沒有提供相關的Task或者Plugin,但是我們可以自己建立Task去執行SQL來初始化各個環境上的資料庫。
前面也提到Gradle是Groovy定義的DSL,所以我們可以在Gradle中使用Groovy的程式碼來執行SQL指令碼檔案。在Gradle指令碼中,使用Groovy載入資料庫的Driver之後,就可以使用Groovy提供的Sql類去執行SQL來初始化資料庫了。程式碼如下:
groovy.sql.Sql oracleSql = Sql.newInstance(props.getProperty('database.connection.url'), props.getProperty('database.userid'), props.getProperty('database.password'), props.getProperty('database.connection.driver')) try { new File(script).text.split(";").each { logger.info it oracleSql.execute(it) } } catch (Exception e) { }
這段程式碼會初始化執行SQL的groovy.sql.Sql物件,然後按照分號(;)分割SQL指令碼檔案裡的每一條SQL並執行。對於一些必須執行成功的SQL檔案,可以在catch塊裡通過丟擲異常來中止資料庫的初始化。需要注意的是需要將資料庫的Driver載入到ClassPath裡才可以正確地執行。
因為在Gradle中包含了Ant,所以我們除了使用Groovy提供的API來執行SQL之外,還可以使用Ant的sql任務來執行SQL指令碼檔案。但若非特殊情況,我並不推薦使用Ant任務,這部分內容與本文無關,這裡不再細述 。
3. 程式碼質量
程式碼質量是軟體開發質量的一部分,除了人工程式碼評審之外,在把程式碼提交到程式碼庫之前,還應該使用自動檢查工具來自動檢查程式碼,來保證專案的程式碼質量。下面介紹一下Gradle提供的支援程式碼檢查的外掛 。
3.1 CheckStyle
CheckStyle是SourceForge下的一個專案,提供了一個幫助JAVA開發人員遵守某些編碼規範的工具。它能夠自動化程式碼規範檢查過程,從而使得開發人員從這項重要卻枯燥的任務中解脫出來。Gradle官方提供了CheckStyle的外掛,在Gradle的構建指令碼中只需要應用該外掛:
apply plugin: 'checkstyle'
預設情況下,該外掛會找/config/checkstyle/checkstyle.xml作為CheckStyle的配置檔案,可以在checkstyle外掛的配置階段(Configuration) 設定CheckStyle的配置檔案:
checkstyle{ configFile = file('config/checkstyle/checkstyle-main.xml') }
還可以通過checkstyle設定CheckStyle外掛的其他配置。
3.2 FindBugs
FindBugs 是一個靜態分析工具,它檢查類或者 JAR 檔案,將位元組碼與一組缺陷模式進行對比以發現可能的問題。Gradle使用如下的程式碼為專案的構建指令碼新增FindBugs的外掛:
apply plugin: 'findbugs'
同樣也可以在FindBugs的配置階段(Configuration)設定其相關的屬性,比如Report的輸出目錄、檢查哪些sourceSet等。
3.3 JDepend
在開發Java專案時經常會遇到關於包混亂的問題, JDepend工具可以幫助你在開發過程中隨時跟蹤每個包的依賴性(引用/被引用),從而設計高維護性的架構,不論是在打包釋出還是版本升級都會更加輕鬆。在構建指令碼中加入如下程式碼即可:
apply plugin: 'jdepend'
3.4 PMD
PMD是一種開源分析Java程式碼錯誤的工具。與其他分析工具不同的是,PMD通過靜態分析獲知程式碼錯誤,即在不執行Java程式的情況下報告錯誤。PMD附帶了許多可以直接使用的規則,利用這些規則可以找出Java源程式的許多問題。此外,使用者還可以自己定義規則,檢查Java程式碼是否符合某些特定的編碼規範。在構建指令碼中加入如下程式碼:
apply plugin: 'pmd'
3.5 小結
上面提到的幾種程式碼檢查外掛apply到構建指令碼之後,可以執行:
gradle check
來執行程式碼質量檢查。更詳細的資訊請查閱Gradle的官方文件。執行結束後會在對應的專案目錄下的build資料夾下生成report。
對於Gradle沒有提供的程式碼檢查工具,我們可以有兩種選擇:第一就是自己實現一個Gradle外掛,第二就是呼叫Ant任務,讓Ant作為一個媒介去呼叫在Ant中已經有的程式碼檢查工具,比如測試覆蓋率的Cobertura。我們的專案使用了Ant來呼叫Cobertura,但是為了使用方便,我們將它封裝為一個Gradle外掛,這樣就可以在不同的專案裡重用。
4. 依賴
幾乎每個Java專案都會用到開源框架。同時,對於具有多個子模組的專案來說,專案之間也會有所依賴。所以,管理專案中對開源框架和其他模組的依賴是每個專案必須面對的問題。同時,Gradle也使用Repository來管理依賴。
4.1 Jar包依賴管理
Maven提出了使用Repository來管理Jar包,Ant也提供了使用Ivy來管理jar包。Gradle提供了對所有這些Respository的支援,可以從Gradle的官方文件上了解更詳細的資訊。
Gradle沿用Maven的依賴管理方法,通過groupId、name和version到配置的Repository裡尋找指定的Jar包。同樣,它也提供了和Maven一樣的構建生命週期,compile、runtime、testCompile和testRuntime分別對應專案不同階段的依賴。通過如下方式為構建指令碼指定依賴:
dependencies { compile group: 'org.hibernate', name: 'hibernate-core', version: '3.6.7.Final' testCompile group:'junit', name: 'junit', version '4.11' }
這裡分別指定group、name以及version,但是Gradle提供了一種更簡單的方式來指定依賴:
dependencies { compile 'org.hibernate:hibernate-core:3.6.7.Final' testCompile 'junit:junit:4.11' }
這樣比Maven使用XML來管理依賴簡單多了,但是還可以更簡單一點。實際上這裡的compile和testCompile是Groovy為Gradle提供的方法,可以為其傳入多個引數,所以當compile有多個Jar包依賴的時候,可以同時指定到compile裡去,程式碼如下:
compile( 'org.hibernate:hibernate-core:3.6.7.Final', 'org.springframework:spring-context:3.1.4.RELEASE' )
另外,當在Respository無法找到Jar包時(如資料庫的driver),就可以將這些Jar包放在專案的一個子目錄中,然後讓專案管理依賴。例如,我們可以在專案的根目錄下建立一個lib資料夾,用以存放這些Jar包。使用如下程式碼可以將其新增到專案依賴中:
dependencies { compile( 'org.hibernate:hibernate-core:3.6.7.Final', 'org.springframework:spring-context:3.1.4.RELEASE', fileTree(dir: "${rootProject.projectDir}/lib", include: '*.jar') ) }
4.2 子專案之間的依賴
對於多模組的專案,專案中的某些模組需要依賴於其他模組,前面提到在初始化階段,Gradle為每個模組都建立了一個Project物件,並且可以通過模組的名字引用到該物件。在配置模組之間的依賴時,使用這種方式可以告訴Gradle當前模組依賴了哪些子模組。例如,在我們的專案中,cis-war會依賴core子專案,就可以在cis-war的構建指令碼中加上如下程式碼:
dependencies { compile( 'org.hibernate:hibernate-core:3.6.7.Final', project(':core') ) }
通過project(':core')來引用core子專案,在構建cis-war時,Gradle會把core加到ClassPath中。
4.3 構建指令碼的依賴
除了專案需要依賴之外,構建指令碼本身也可以有自己的依賴。當使用一個非Gradle官方提供的外掛時,就需要在構建腳本里指定其依賴,當然還需要指定該外掛的Repository。在Gradle中,使用buildscript塊為構建指令碼配置依賴。
比如在專案中使用cucumber-JVM作為專案BDD工具,而Gradle官方沒有提供它的外掛,好在開源社群有人提供cucumber的外掛。在構建指令碼中新增如下程式碼:
buildscript { repositories { mavenCentral() } dependencies { classpath "gradle-cucumber-plugin:gradle-cucumber-plugin:0.2" } } apply plugin: com.excella.gradle.cucumber.CucumberPlugin
5. 其他
5.1 apply其他Gradle檔案
當一個專案很複雜的時候,Gradle指令碼也會很複雜,除了將子專案的配置移到對應專案的構建指令碼之外,還可以可以按照不同的功能將複雜的構建指令碼拆分成小的構建指令碼,然後在build.gradle裡使用apply from,將這些小的構建指令碼引入到整體的構建指令碼中去。比如在一個專案中既使用了Jetty,又使用了Cargo外掛啟動JBoss,就可以把他們分別提到jetty.gradle和jboss.gradle,然後在build.gradle裡使用如下的程式碼將他們引入進來:
apply from: "jetty.gradle" apply from: "jboss.gradle"
5.2 project的目錄
在指令碼檔案中,需要訪問專案中的各級目錄結構。Gradle為Project物件定義了一些屬性指向專案的根目錄,方便在指令碼中引用。
- rootDir:在子專案的指令碼檔案中可以通過該屬性訪問到根專案路徑。
- rootProject:在子專案中,可以通過該屬性獲取父專案的Project物件。
5.3 使用Wrapper指定Gradle的版本
為了統一專案中Gradle的版本,可以在構建指令碼中通過定義一個wrapper的Task,並在該Task中指定Gradle的版本以及存放Gradle的位置。
task wrapper(type: Wrapper) { gradleVersion = '1.0' archiveBase = 'PROJECT' archivePath = 'gradle/dists' }
執行gradle wrapper, 就會在根專案目錄下建立一個wrapper的資料夾,會包含wrapper的Jar包和properties檔案。之後就可以使用gradlew來執行task。第一次使用gradlew執行task的時候,會在專案根目錄下的gradle/dists下下載你指定的Gradle版本 。這樣在專案構建的時候,就會使用該目錄下的Gradle,保證整個團隊使用了相同的Gradle版本。
5.4 使用gradle.properties檔案
Gradle構建指令碼會自動找同級目錄下的gradle.properties檔案,在這個檔案中可以定義一些property,以供構建指令碼使用。例如,我們要使用的Repository需要提供使用者名稱和密碼,就可以將其配置在gradle.properties中。這樣,每個團隊成員都可以修改該配置檔案,卻不用上傳到程式碼庫中對團隊其他成員造成影響。可以使用如下的程式碼定義:
username=user password=password
在構建指令碼中使用"${username} "就可以訪問該檔案中定義的相關值。