
【認識 Gradle】(8)邏輯重用
【認識 Gradle】(7)Java 專案相依管理 << 前情 此回教學訂名為「邏輯重用」顧名思義需要將「曾」寫過的片段能方便在其他專案繼續使用。簡單的方法即為「複製 & 貼上」,但這會有點麻煩,有些需要被重用的邏輯,不是那麼容易看得清楚,它整份邏輯的邊界在哪裡。 它可能被部分或者不完全地「複製 & 貼上」到其他地方。已有重構概念的開發者們也會抗議「複製 & 貼上」實在太糟了,這樣我想改變其中的內容,還得去追其他有「複製 & 貼上」的專案,一個一個比對改掉。「複製 & 貼上」無論在寫 code 或是在組織 Build Tool 都是惱人的方法,是個非一勞永逸且後患無窮的做法。 重用機制「複製 & 貼上」是需要避免的,但這只是無意義的陳述。得指出建議的方向,供人參考、實作之。Gradle 些簡單的方法能讓開發者重用 (reuse) 專案建置的邏輯:
先不管是否要自製 gradle plugin 或製作跨專案通用的 gradle task,大多是依賴 apply 方法引用可重用的邏輯。學習自行製作 gradle plugin 或 task 的情況,可能需要額外處理 build script 本身使用的 classpath 告知 gradle 這些額外的 plugin 或 task 源自於哪些函式庫。 當使用 Gradle 一陣子後,可觀察出團隊專案常用的 gradle plugin 與專案額外的配置有哪些,它大致有個脈落可循,通常會有些慣性存在,也可能都是由上一個專案承襲(複製)而來。舉例來說,開發一個 Java 應用程式最終的成果是需要被部署在某台伺服器上,除了程式本身還需要將相依的函式庫與正式環境的設定檔準備好,以便於實際部署之用。 這回同樣有個簡單的說例 HelloApp: qty:GradleReuseProject qrtt1$ tree . ├── build.gradle ├── scripts │ └── run.sh └── src └── main ├── java │ └── tw │ └── codedata │ └── gradle │ └── HelloApp.java └── resources └── log4j.properties 它是個 Java Application,除了有待編譯的原始碼,它多了個 scripts 目錄放置部署至正式環境要用的相關檔案。於是寫了個簡單的 dist task: // qty:GradleReuseProject qrtt1$ cat build.gradle apply plugin: 'java' apply plugin: 'maven' apply plugin: 'eclipse' [compileJava, compileTestJava, javadoc]*.options*.encoding = 'UTF-8' sourceCompatibility = JavaVersion.VERSION_1_6 repositories { mavenCentral() } dependencies { compile group: 'commons-logging', name: 'commons-logging', version: '1.+' compile group: 'log4j', name: 'log4j', version: '1.+' testCompile group: 'junit', name: 'junit', version: '4.+' } task dist(dependsOn:clean, type:Copy) { destinationDir = file("$buildDir/dist") // put it at root directory from 'scripts' // put into libs subdirectory into('libs'){ from jar.outputs.files } into('libs'){ from configurations.compile } } 它會輸出成下列的結構: dist/ ├── libs │ ├── GradleReuseProject.jar │ ├── commons-logging-1.1.3.jar │ └── log4j-1.2.17.jar └── run.sh 這是一個簡易的專案「打包」的案例,幾乎所有能獨立執行的專案都需要替部署活動準備打包任務。這是一般 Java Application 的例子,實務上它也可能是 Java Web Application 或其它型式的專案。 抽離可重用邏輯「打包」任務是許多專案都需要的,我們希望其它專案也都能有這樣的 gradle task,不倚賴 「複製 & 貼上」的情況下,我們能簡單將它抽離出來至不同的 gradle script。 將 dist task 「截取」出來,放在另一個位置,例如:$HOME/.gradle/task.dist.gradle: //qty:GradleReuseProject qrtt1$ cat ~/.gradle/task.dist.gradle task dist(dependsOn:clean, type:Copy) { destinationDir = file("$buildDir/dist") // put it at root directory from 'scripts' // put into libs subdirectory into('libs'){ from jar.outputs.files } into('libs'){ from configurations.compile } } 原先的 gradle script 可簡化成: apply plugin: 'java' apply plugin: 'maven' apply plugin: 'eclipse' [compileJava, compileTestJava, javadoc]*.options*.encoding = 'UTF-8' sourceCompatibility = JavaVersion.VERSION_1_6 repositories { mavenCentral() } dependencies { compile group: 'commons-logging', name: 'commons-logging', version: '1.+' compile group: 'log4j', name: 'log4j', version: '1.+' testCompile group: 'junit', name: 'junit', version: '4.+' } apply from: System.properties['user.home'] + '/.gradle/task.dist.gradle' 其它專案要有「打包」的功能,就能簡單透過 apply from: System.properties['user.home'] + '/.gradle/task.dist.gradle' 這樣的做法是不是很像重構的提煉函式 (extract method) 的概念呢?這正大此篇教學的核心所在!自製完成且獨立的 gradle plugin 是最後的手段,在那之前我們還其它選擇,透過 gradle 提供的其他機制與重構的手法達到邏輯重用的目的。 參數化重用邏輯使用 apply from: System.properties['user.home'] + '/.gradle/task.dist.gradle' 新的 request 會接踵而來,像是能不能自訂 scripts 目錄的位置呢?若要讓開發者能自訂 scripts 目錄的位置,需要有一個變數,讓 build script 與 dist task 是都能存取的。 Gradle 提供 ExtraPropertiesExtension 機制,在 Project 上提供延伸變數的功能。新的 dist task 可改寫成: // qty:.gradle qrtt1$ cat task.dist.ext.gradle task dist(dependsOn:clean, type:Copy) { destinationDir = file("$buildDir/dist") // put it at root directory if (project.hasProperty('scripts')) { println 'use custom script' from project.ext.scripts } else { println 'use default script' from 'scripts' } // put into libs subdirectory into('libs'){ from jar.outputs.files } into('libs'){ from configurations.compile } } 原先的 apply 就需要在它的前面多加一行設定變數: project.ext.set('scripts', 'new_scripts') apply from: System.properties['user.home'] + '/.gradle/task.dist.ext.gradle' 需注意的是設定 scripts 位置需要在 apply 之前,因為 透過 ExtraPropertiesExtension 機制,我們能在 gradle task 或其他 block 間使用變數,像是 dependencies 或 repositories 等等,像是在 【認識 Gradle】(7)Java 專案相依管理 的範例,將專案做成可發佈至 Maven Repository Server,我們使用相同的方式製作出 maven.gradle 讓專案引用: // maven.gradle apply plugin: 'maven' ext { maven_repo_id = 'admin' maven_repo_pwd = 'admin123' } repositories { maven { credentials { username maven_repo_id password maven_repo_pwd } url "http://127.0.0.1:8081/nexus/content/groups/public/" } } uploadArchives { repositories { mavenDeployer { pom.groupId = maven_group_id pom.artifactId = maven_artifact_id pom.version = maven_version def suffix = pom.version.contains("SNAPSHOT") ? "snapshots" : "releases" repository(url: "http://127.0.0.1:8081/nexus/content/repositories/${suffix}/") { authentication(userName: maven_repo_id, password: maven_repo_pwd) } } } } DSL 擴充機制將固定 task 內 hardcode 的內容參數化是相當直覺的,它確實達到能重用且有限的客製化目標。若希望依不同的用途包入不同的 scripts 資料夾怎麼處理呢?回想一下,在這系列的課程似乎就做過 【認識 Gradle】(6)Java 專案與 Build Script 客製化 內,動態建立 在這麼做之前,先讓我們重新審視一下寫法: project.ext.set('scripts', 'new_scripts') apply from: System.properties['user.home'] + '/.gradle/task.dist.ext.gradle' 由這二行來看,它似乎很難表達出一種「語意」,讓人一看就明白專案要增加 dist task 並且指定額外設定檔 scripts 目錄為 new_scripts 目錄。依一般程式實作的直覺,這樣反而看起來更能接受: apply from: System.properties['user.home'] + '/.gradle/task.dist.ext.gradle' project.ext.set('scripts', 'new_scripts') 有沒有很像 import 了一個 class 再設定全域變數呢?再者 gradle 其實是一個 Domain-specific language (DSL),我們能提功更富語意的實作,像是: apply from: 'commons.gradle' repositories { mavenCentral() } dependencies { compile group: 'commons-logging', name: 'commons-logging', version: '1.+' compile group: 'log4j', name: 'log4j', version: '1.+' testCompile group: 'junit', name: 'junit', version: '4.+' } features { dist name: 'dev', config:['scripts/dev'] dist name: 'production', config:['scripts/production'] } 新的設計提供 // qty:GradleReuseProject qrtt1$ cat commons.gradle apply plugin: 'java' apply plugin: 'maven' apply plugin: 'eclipse' [compileJava, compileTestJava, javadoc]*.options*.encoding = 'UTF-8' sourceCompatibility = JavaVersion.VERSION_1_6 class Features { final Project project; Features(Project project) { this.project = project } def dist(map=[:]) { def taskName = 'dist_' + map['name'] def action = { destinationDir = new File("$buildDir/dist") // put it at root directory map.config.each { dir -> from dir } // put into libs subdirectory into('libs'){ from jar.outputs.files } into('libs'){ from configurations.compile } } def distTask = project.tasks.create name: taskName, type: Copy distTask.dependsOn 'build' distTask.configure action } def propertyMissing(String name) { return project[name] } } project.extensions.create('features', Features, project) 利用 Gradle 內建的擴充機制,project 物件下有個 extensions 物件,呼叫 create 方法,並指定 block 名稱,傳入物件與建子參數: project.extensions.create('features', Features, project) 這就是為何 features block 能使用的原因: features { dist name: 'dev', config:['scripts/dev'] dist name: 'production', config:['scripts/production'] } 其中 dist 是 Features 物件的方法,它接受一個 Map 參數。利用這些設定建出相對應的 dist task。這樣的實作就更具有描述力,意圖更讓人易懂。實作上,額外使用到一個小技巧:由於在 Features 物件內實作 task configuration 它的外圍 owner 不再是 project 物件,所以動態指定給 project 的 property 會找不到,利用 groovy 的 propertyMissing 機制對應至 project 物件。 課程回顧此回課程由重構 build script 的角度切入,怎麼由手工的「複製 & 貼上」進化到利用
我們講述了單純利用 Gradle 與 Groovy 提供的機制滿足邏輯重用的需求,這些機制是後續學習製作 gradle plugin 的基礎。 |