【認識 Gradle】(8)邏輯重用 by qrtt1 | CodeData
top

【認識 Gradle】(8)邏輯重用

分享:

【認識 Gradle】(7)Java 專案相依管理 << 前情

此回教學訂名為「邏輯重用」顧名思義需要將「曾」寫過的片段能方便在其他專案繼續使用。簡單的方法即為「複製 & 貼上」,但這會有點麻煩,有些需要被重用的邏輯,不是那麼容易看得清楚,它整份邏輯的邊界在哪裡。

它可能被部分或者不完全地「複製 & 貼上」到其他地方。已有重構概念的開發者們也會抗議「複製 & 貼上」實在太糟了,這樣我想改變其中的內容,還得去追其他有「複製 & 貼上」的專案,一個一個比對改掉。「複製 & 貼上」無論在寫 code 或是在組織 Build Tool 都是惱人的方法,是個非一勞永逸且後患無窮的做法。

重用機制

「複製 & 貼上」是需要避免的,但這只是無意義的陳述。得指出建議的方向,供人參考、實作之。Gradle 些簡單的方法能讓開發者重用 (reuse) 專案建置的邏輯:

  1. 使用 apply plugin: 引用其他 gradle plugin,像過去的示範中我們引用的 java 與 maven 等 gradle plugin
  2. 使用 apply from: 引用其他 gradle script,這可以是一個本地端的檔案或填寫一個網址,它是正規的「複製 & 貼上」機制。所以,使用的時候需要注意邏輯上的相依關係。像是在引用的 script 是依賴某個 gradle plugin 的變數時,需確定它放在該 gradle plugin 之後才行。

先不管是否要自製 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: 替專案增加 dist task:

apply from: System.properties['user.home'] + '/.gradle/task.dist.gradle'

這樣的做法是不是很像重構的提煉函式 (extract method) 的概念呢?這正大此篇教學的核心所在!自製完成且獨立的 gradle plugin 是最後的手段,在那之前我們還其它選擇,透過 gradle 提供的其他機制與重構的手法達到邏輯重用的目的。

參數化重用邏輯

使用 apply from: 實際上是原封不動的「複製 & 貼上」。在整理好共用的 task 後,向同事介紹了可以透過這一行指令獲得 dist task:

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 之前,因為 apply from: 是單純的把檔案讀入,合併成一個新的 build script。設定變數如果在 dist task 建立之後,那就會在產生 task 物件後才設值了。

透過 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 客製化 內,動態建立 pack_devpack_production 分別代表打包時要帶入開發環境用的設定檔或開發環境的設定檔。這例子其實與此篇教學相當雷同,差別在那此的例子是自動尋找 profile_ 字首的目錄,建立出對應的 task 並實作將專案打包成 ZIP 檔的功能。我們一樣能將會一直被用到的邏輯以前面教過的方法抽出來,放到獨立的 build script 內透過 apply from: 引用,並利將它參數化達成容許客製化的型式。

在這麼做之前,先讓我們重新審視一下寫法:

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']
}

新的設計提供 features block,可以指定要產生的 dist task,它會出多 dist_dev task 與 dist_production task,各自打包指定的目錄。作法很簡單,寫一個 extension 綁在 project 物件上:

// 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 的角度切入,怎麼由手工的「複製 & 貼上」進化到利用 apply from:「複製 & 貼上」,並利用 Gradle 的機制逐部改善它:

  1. 學習 apply from: 提煉共通的 build script
  2. 利用 ExtraPropertiesExtension 機制,將 hardcode 的值參數化。
  3. 利用 Gradle Extension 機制增加 block 提供更良好的語意設計

我們講述了單純利用 Gradle 與 Groovy 提供的機制滿足邏輯重用的需求,這些機制是後續學習製作 gradle plugin 的基礎。

分享:
按讚!加入 CodeData Facebook 粉絲群

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言

關於作者

目前在一家網路應用軟體公司擔任開發工作,對多媒體處理與雲端應用充滿興趣,工作之餘亦常整理開發經驗分享於網路或於社群活動時進行分享。

熱門論壇文章

熱門技術文章