【認識 Gradle】(3)Gradle 起手式

- gradle-series

前二篇介紹在 Gradle 前較廣泛使用的編譯工具 Ant 與 Maven。接下來的課程就會以 Gradle 為主。在正式進入 Gradle 教學的這篇,我們選擇以起手式的角度切入,先不去理解繁複的原理與暫時用不到的設計概念、中心思想,而是要使用編譯工具最重要的就是把專案編譯出來,並且它確實要能動。此篇教學的要點在帶領讀者:安裝、建立第一個 Java 專案、基本指令介紹與實作歷程回顧。

此教學主要用到的資訊儘可能採用 Gradle 官方網站 的資料,它的官網已提供許多有用的資訊,向未接觸 Gradle 的讀者『導讀』這些材料是撰寫這系列教學的重要目標。

安裝 Gradle

Gradle 的安裝很容易,在使用者有安裝 JDK 1.5+ 以上版本,並設定好相關環境變數的前提下,只需由 Gradle 官網下載程式並將執行 script 或 batch file 設到 PATH 路徑內即可使用。進入網官首頁,就能看到很醒目的 Download 按鈕:

撰文時下載的最新穩定版本為 1.8 版 gradle-1.8-all.zip 將它解壓縮後,放在你所希望的安裝路徑,例如放使用者目錄下的 app 資料夾:

/Users/qrtt1/app/gradle-1.8

在 Gradle 的路徑下有這些內容,其中 bin 路徑是必需被加入 PATH 環境變數 的路徑。Gradle 提供的 script 或 batch file 就在這個路徑之下:

qty:gradle-1.8 qrtt1$ ls -alh
total 1832
drwxr-xr-x  13 qrtt1  staff   442B  9 24 09:40 .
drwxr-xr-x  72 qrtt1  staff   2.4K 10 19 12:41 ..
-rw-r--r--@  1 qrtt1  staff    51K  9 17 02:33 LICENSE
-rw-r--r--@  1 qrtt1  staff   838B  9 17 02:33 NOTICE
drwxr-xr-x   4 qrtt1  staff   136B  9 24 09:40 bin
-rw-r--r--@  1 qrtt1  staff   105B  9 17 02:33 changelog.txt
drwxr-xr-x   7 qrtt1  staff   238B  9 24 09:40 docs
-rw-r--r--@  1 qrtt1  staff   855K  9 24 09:34 getting-started.html
drwxr-xr-x   3 qrtt1  staff   102B  9 17 02:33 init.d
drwxr-xr-x  51 qrtt1  staff   1.7K  9 24 09:40 lib
drwxr-xr-x  11 qrtt1  staff   374B  9 17 02:33 media
drwxr-xr-x  33 qrtt1  staff   1.1K  9 24 09:40 samples
drwxr-xr-x   6 qrtt1  staff   204B  9 24 09:40 src

同時它也有附完整的原始碼在 src 路徑與說明文件,當使用者有興趣深入研究它的實作時,可以在其中尋找到更豐富的訊息。init.d 是放置全域初始化設定檔的目錄,每回 Gradle 啟動時,會先確認這個資料夾是否有需要執行的 Gradle Script。

當你將 Gradle 的 bin 路徑加入 PATH 環境變數後,使用 -v 參數執行能獲得 Gradle 的版本資訊。若無法執行成功,請檢查環境變數的設定以及新的環境變數是否已經生效。

qty:gradle_tutorial qrtt1$ gradle -v

------------------------------------------------------------
Gradle 1.8
------------------------------------------------------------

Build time:   2013-09-24 07:32:33 UTC
Build number: none
Revision:     7970ec3503b4f5767ee1c1c69f8b4186c4763e3d

Groovy:       1.8.6
Ant:          Apache Ant(TM) version 1.9.2 compiled on July 8 2013
Ivy:          2.2.0
JVM:          1.6.0_65 (Apple Inc. 20.65-b04-462)
OS:           Mac OS X 10.6.8 x86_64

Gradle 提供的版本資訊內容,有它釋出的時間與對應至版本控制系統的 Revision 代號。另外,還有 Groovy、Ant、Ivy 與 JVM 的版本。Gradle 並不是重無到有獨立建構出一套完整的專案編譯工具,它利用 Ant 與 Ivy 這些既有的編譯工具與相依性管理機制來執行主要的編譯工作,並運用 Groovy 客製化一套 Domain Specific Language (DSL),讓使用 Gradle 的開發者能用撰寫 Script 的方式寫出期望的編譯方式。

Groovy 是執行在 JVM 上的 Scripting Language,對於編寫 Gradle Script 用到的部分並不會太高深,有興趣更加深入 Groovy 的寫法的開發者可以參考它的官方文件,目前我們還沒有要深入 Groovy 的種種特點,只要先記得它是 Gradle 的 DSL 即可。

DSL 直譯成中文為『領域特定語言』,它是用在特定領域的,以我們現在的例子它用在 Building Tool 的領域。如同 SQL 也是一種領域特定語言,用在資料庫查詢領域。簡而言之,它們將語言的功能與目的有著特定的意圖,相較於 Java 就沒特定的意圖是屬於泛用型的語言。你也能用 Java 來實作編譯工作,但它並沒有事先定義實作『編譯工作』的常用元件,寫起來就比較原始且麻煩。換言之,用 Gradle 的 DSL 就事先定義了許多常用的元件,開發者可以方便地重複使用它,並加上一些針對專案需要客製的部分即可。

第一個 Gradle 專案

繼續沿用前二個系列的 Hello World,目錄結構如下:

qty:gradle_tutorial qrtt1$ tree HelloWorld
HelloWorld
├── build.gradle
└── src
    └── main
        ├── java
        │   └── tw
        │       └── com
        │           └── codedata
        │               └── HelloWorld.java
        └── resources
            └── log4j.properties

7 directories, 3 files

HelloWorld.java 原始碼與 log4j.properties 設定檔內容如下:

package tw.com.codedata;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class HelloWorld {

    static Log logger = LogFactory.getLog(HelloWorld.class);

    public static void main(String[] args) {
        logger.info("Hello World");
    }

}
log4j.rootLogger=info, stdout, R

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n

log4j.appender.R=org.apache.log4j.RollingFileAppender
log4j.appender.R.File=codedata.log
log4j.appender.R.Append=true
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=%p %t [%d{yy/MM/dd HH:mm:ss:SSS}] %c - %m%n

build.gradle 內容為:

/* 引用 java plugin 獲得編譯 java 專案相關的 task $ */
apply plugin: 'java' 

/* 引用 application plugin 獲得執行 java 專案相關的 task $ */
apply plugin:'application'

/* 執行 application plugin 用到的參數 $ */
mainClassName = "tw.com.codedata.HelloWorld"

/* 設定 maven repository server $ */
repositories {
    mavenCentral()
}

/* 宣告專案的相依函式庫 $ */
dependencies {
    compile group: 'commons-logging', name: 'commons-logging', version: '1.1.1'
    compile group: 'log4j', name: 'log4j', version: '1.2.16'
}

使用 gradle 指令執行 run task

qty:HelloWorld qrtt1$ gradle run
:compileJava
:processResources
:classes
:run
 INFO [main] (HelloWorld.java:11) - Hello World

BUILD SUCCESSFUL

Total time: 6.027 secs

可以觀察到它依序執行:

  1. compileJava
  2. processResources
  3. classes
  4. run

這些 task 命名與先前介紹的 Maven Build Phase 相似,不過實際上它採用的是 Ant Target Depdenency 概念,執行某個 task 前,要先執行過一些指定的 task。無論它採用何種實作方式,使用者只要知道目標是執行 run task 即可,該執行的相關 task 都會被執行。

回頭看看 build.gradle,使用 DSL 來撰寫 Build Script 相較於 Maven 用 XML 來描述專案狀態或覆寫既有設定,它顯得更為簡易。它的『擴充』方式不是依賴在 Build Phase 綁定新的 Plugin Goal,而是採用像 Ant Build File 相依的 Import 機制,使用 apply 指定要新增的 Plug In,理解起來更為直覺。

認識 Gradle 指令

在起手式的最後,讀者可先認識一些基本的指令。先由什麼參數都不加開始,在 Gradle 專案路徑(含有 build.gradle 的路徑)下,執行 gradle:

qty:HelloWorld qrtt1$ gradle
:help

Welcome to Gradle 1.8.

To run a build, run gradle <task> ...

To see a list of available tasks, run gradle tasks

To see a list of command-line options, run gradle --help

BUILD SUCCESSFUL

Total time: 3.948 secs

它提示使用者,gradle 需要指定 task,若你不知道該執行哪個 task 請使用 tasks,執行後可以看到許多的 task,它們以特定的群組為單位集合在一起:

qty:HelloWorld qrtt1$ gradle tasks
:tasks

------------------------------------------------------------
All tasks runnable from root project
------------------------------------------------------------

Application tasks
-----------------
distTar - Bundles the project as a JVM application with libs and OS specific scripts.
distZip - Bundles the project as a JVM application with libs and OS specific scripts.
installApp - Installs the project as a JVM application along with libs and OS specific scripts.
run - Runs this project as a JVM application

Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles classes 'main'.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the main classes.
testClasses - Assembles classes 'test'.

Build Setup tasks
-----------------
setupBuild - Initializes a new Gradle build. [incubating]
wrapper - Generates Gradle wrapper files. [incubating]

Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the main source code.

Help tasks
----------
dependencies - Displays all dependencies declared in root project 'HelloWorld'.
dependencyInsight - Displays the insight into a specific dependency in root project 'HelloWorld'.
help - Displays a help message
projects - Displays the sub-projects of root project 'HelloWorld'.
properties - Displays the properties of root project 'HelloWorld'.
tasks - Displays the tasks runnable from root project 'HelloWorld'.

Verification tasks
------------------
check - Runs all checks.
test - Runs the unit tests.

Rules
-----
Pattern: build<ConfigurationName>: Assembles the artifacts of a configuration.
Pattern: upload<ConfigurationName>: Assembles and uploads the artifacts belonging to a configuration.
Pattern: clean<TaskName>: Cleans the output files of a task.

To see all tasks and more detail, run with --all.

BUILD SUCCESSFUL

Total time: 4.574 secs

除了先前執行過的 run task,我們能試試由 tasks 查詢到的能執行的 task,例如 distZip

qty:HelloWorld qrtt1$ gradle distZip
:compileJava
:processResources
:classes
:jar
:startScripts
:distZip

BUILD SUCCESSFUL

Total time: 6.736 secs

Gradle 產出的檔案通常在 build 目錄下,我在 build/distributions 下找到 zip 檔,它的內容如下:

qty:HelloWorld qrtt1$ unzip -l build/distributions/HelloWorld.zip 
Archive:  build/distributions/HelloWorld.zip
  Length     Date   Time    Name
 --------    ----   ----    ----
        0  10-20-13 12:01   HelloWorld/
        0  10-20-13 12:01   HelloWorld/lib/
     1425  10-20-13 12:01   HelloWorld/lib/HelloWorld.jar
    60686  10-20-13 10:47   HelloWorld/lib/commons-logging-1.1.1.jar
   481535  10-19-13 12:43   HelloWorld/lib/log4j-1.2.16.jar
        0  10-20-13 12:01   HelloWorld/bin/
     5088  10-20-13 12:01   HelloWorld/bin/HelloWorld
     2444  10-20-13 12:01   HelloWorld/bin/HelloWorld.bat
 --------                   -------
   551178                   8 files

還能試試顯示相依性的 dependencies:

qty:HelloWorld qrtt1$ gradle dependencies
:dependencies

------------------------------------------------------------
Root project
------------------------------------------------------------

archives - Configuration for archive artifacts.
No dependencies

compile - Compile classpath for source set 'main'.
+--- commons-logging:commons-logging:1.1.1
\--- log4j:log4j:1.2.16

default - Configuration for default artifacts.
+--- commons-logging:commons-logging:1.1.1
\--- log4j:log4j:1.2.16

runtime - Runtime classpath for source set 'main'.
+--- commons-logging:commons-logging:1.1.1
\--- log4j:log4j:1.2.16

testCompile - Compile classpath for source set 'test'.
+--- commons-logging:commons-logging:1.1.1
\--- log4j:log4j:1.2.16

testRuntime - Runtime classpath for source set 'test'.
+--- commons-logging:commons-logging:1.1.1
\--- log4j:log4j:1.2.16

BUILD SUCCESSFUL

Total time: 4.915 secs

dependencies 是個相當長的 task 名稱,Gradle 允許使用者使用縮寫的方式呼叫 task,例如:

gradle d
gradle de
gradle dep

上述的縮寫都會呼叫 dependencies,若是遇到無法判斷時,Gradle 會提供候選名單,例如只輸入 t 時,Gradle 無法確定你要執行 tasks 還是 test:

qty:HelloWorld qrtt1$ gradle t

FAILURE: Could not determine which tasks to execute.

* What went wrong:
Task 't' is ambiguous in root project 'HelloWorld'. Candidates are: 'tasks', 'test'.

* Try:
Run gradle tasks to get a list of available tasks.

BUILD FAILED

Total time: 3.922 secs

你需明確指定要 ta 或是 te,給予足夠的縮寫長度讓它能判斷。

內容回顧

  1. 在這篇文章中,我們學習使用 Gradle 建立第一個 Java 專案,認識 build.gradle 簡單的寫法。能使用 Gradle 來編譯簡單的 Java 專案。
  2. java 與 application PlugIn 是你最初認識的 Gradle PlugIn,後續我們會講解其他常用的 PlugIn,並在 build.gradle 內撰寫自己的 task。
  3. 知道 gradle 指令的使用,會查詢現有的 task,能使用縮寫的功能呼叫名稱很長的 task