原文地址:https://blog.gaoyuexiang.cn/2020/02/24/use-gradle-to-build-java-project/,內(nèi)容沒有差別。
在上一篇文章中,我們?cè)跊]有使用任何插件的情況下,練習(xí)了使用 Gradle 構(gòu)建 Java 項(xiàng)目,最后得到一個(gè)脆弱的構(gòu)建腳本和不符合約定的目錄結(jié)構(gòu)。
對(duì)此,Gradle 使用了插件來解決這些問題。
插件
Gradle 中的插件,可以給我們帶來很多好處,包括:
- 添加
Task - 添加領(lǐng)域?qū)ο?/li>
- 約定優(yōu)于配置的實(shí)現(xiàn)
- 擴(kuò)展
Gradle的核心類
Gradle 將插件分為兩類,Script Plugin & Binary Plugin。
那些寫到單獨(dú)的 gradle 文件中,并被 build.gradle 文件使用的腳本文件,就是 Script Plugin。常見的實(shí)踐是將某一插件或某一方面的配置寫到單獨(dú)的文件中,比如 jacoco.gradle,然后通過下面的語法導(dǎo)入到 build.gradle 文件中:
apply from: file("$projectDir/gradle/jacoco.gradle")
而常見的 java、idea 這樣的 core Plugin 和 org.springframework.boot 等可以在 https://plugins.gradle.org/ 找到的插件,就是 Binary Plugin,它們通過 plugins{} 語法塊引入:
plugins {
id 'java'
id 'org.springframework.boot' version '2.2.4.RELEASE'
}
接下來,我們接著上一篇文章的例子,使用 Java Plugin 來改造我們的構(gòu)建腳本。
改造 Hello World
Java 插件的文檔:https://docs.gradle.org/current/userguide/java_plugin.html
Import Java Plugin
如上所述,我們使用 Java Plugin 需要先導(dǎo)入它:
plugins {
id 'java'
}
因?yàn)?Java 插件是 Gradle 提供的核心插件,它是和 Gradle 版本綁定的,所以不需要使用 version 參數(shù)。
SourceSet
引入 Java 插件后,我們先來了解一個(gè)核心概念:SourceSet。這是 Java 插件引入的概念,每一個(gè) SourceSet 都包含了一組相關(guān)的資源。默認(rèn)情況下,一個(gè) SourceSet 對(duì)應(yīng) src 目錄下的一個(gè)目錄,目錄名稱就是 SourceSet 的名稱;目錄下會(huì)有一個(gè) java 目錄和一個(gè) resources 目錄。根據(jù)約定,這兩個(gè)目錄分別是存放 java 文件的目錄和存放配置等資源文件的目錄。
SourceSet 還有更多的信息可以配置,參見:https://docs.gradle.org/current/userguide/building_java_projects.html#sec:java_source_sets
Java 插件還默認(rèn)配置好了兩個(gè) SourceSet,分別是 main & test。所以在使用 Java 插件后,無需任何配置,就可以得到約定的目錄結(jié)構(gòu):
? tree src
src
├── main
│ ├── java
│ └── resources
└── test
├── java
└── resources
所以,我們需要將 HelloWorld.java 從 src 目錄移動(dòng)到符合約定的 src/main/java 目錄下:
? tree src
src
└── main
└── java
└── HelloWorld.java
Task
Java 插件引入的 Task
接著我們來看看 Task 需要做哪些修改。
Java 插件引入了下面的這些 Task,并且添加了依賴關(guān)系:

其中有四個(gè) task 是由 base plugin 添加的:clean, check, assemble 和 build。
其中,check, assemble 和 build 是 lifecycle task,本身不執(zhí)行任務(wù),只是定義了執(zhí)行它們時(shí)應(yīng)該執(zhí)行什么樣的任務(wù):
-
check:聚合所有進(jìn)行驗(yàn)證操作的task,比如測試 -
assemble:聚合所有會(huì)產(chǎn)生項(xiàng)目產(chǎn)出物的task,比如打包 -
build:聚合前面兩個(gè)task
其他的 task 中,很容易發(fā)現(xiàn),compileJava 與 compileTestJava、processResources 與 processTestResources、classes 與 testClasses 命名類似。實(shí)際上,每一對(duì) task 表達(dá)的是同樣的含義,只是一個(gè)針對(duì) main sourceSet,一個(gè)針對(duì) test sourceSet 而已。如果你創(chuàng)建了一個(gè)自定義的 SourceSet,那 Java 插件會(huì)自動(dòng)的添加 compileSourceSetJava、processSourceSetResources 和 sourceSetClasses,其中的 sourceSet 就是 SourceSet.name。
-
compileJava:編譯該sourceSet下的java文件 -
processResource:將該sourceSet中的資源文件復(fù)制到build目錄中 -
classes:準(zhǔn)備打包和執(zhí)行需要的class文件和資源文件
注意,執(zhí)行測試是
test任務(wù),它沒有因?yàn)樘砑?sourceSet而自動(dòng)添加sourceSetTest方法。因?yàn)樽远x的SourceSet不一定是組件測試之類的不同類別的測試。所以,如果你添加了這樣的SourceSet,需要自己手動(dòng)編寫Test類型的測試task。
改進(jìn) Hello World
由上面的了解可知,Java 插件已經(jīng)為我們添加了 compileJava 和 jar 這兩個(gè) task,所以我們不需要再創(chuàng)建這樣的 task。但是我們還是可以對(duì)這些 task 進(jìn)行配置。
比如,我們?nèi)匀幌M刂?jar 產(chǎn)出的文件名,那我們的腳本就可以改成這樣:
// task compileJava(type: JavaCompile) {
// source fileTree("$projectDir/src")
// include "**/*.java"
// destinationDir = file("${buildDir}/classes")
// sourceCompatibility = '1.8'
// targetCompatibility = '1.8'
// classpath = files("${buildDir}/classes", configurations.forHelloWorld)
// }
// tasks.create('jar', Jar)
jar {
archiveBaseName = 'base-name'
archiveAppendix = 'appendix'
archiveVersion = '0.0.1'
// from compileJava.outputs
// include "**/*.class"
manifest {
attributes("something": "value")
}
// setDestinationDir file("$buildDir/lib")
}
其中注釋的部分可以刪除,這里僅僅作為修改前后的對(duì)比。
根據(jù) assemble 的定義,我們的 fatJar 的輸出應(yīng)當(dāng)看作項(xiàng)目的產(chǎn)出物,所以需要讓 assemble 依賴于 fatJar :
assemble.dependsOn fatJar
Dependency Configuration
Java 插件引入的 Configuration
上一篇文章講到,在 Gradle 中聲明依賴,需要關(guān)聯(lián)到 configuration。Java 插件也提前為我們?cè)O(shè)計(jì)了一些 configuration,他們的主要關(guān)系可以通過兩幅圖來表示。
與 main sourceSet 相關(guān)的:

其中:
- 灰色文字表示已經(jīng)被廢棄的
configuration - 綠色表示用于聲明依賴的
configuration - 藍(lán)灰色表示給
task使用的configuration - 淺藍(lán)色表示
task
由這個(gè)圖,我們就能看出聲明到不同 configuration 中的依賴最終會(huì)在什么地方使用到。
與 test sourceSet 相關(guān)的:

其中的字體和顏色與上一張圖一致。
我們可以看到,除去 compile, implementation, runtime 和 rumtimeOnly,其他的 configuration 與上圖幾乎一致。這里畫出他們,僅僅是為了展示出擴(kuò)展關(guān)系而已。
如果你使用過以前版本的
Gradle,想必會(huì)比較好奇為什么Compile會(huì)被廢棄。這其實(shí)是出于構(gòu)建工具的性能的考慮,關(guān)閉掉不必要的傳遞依賴。
你也許也發(fā)現(xiàn)了,和 task 一樣,有一些名稱相近的 configuration,所以很自然的推測:添加了自定義的 SourceSet 后,Java 插件會(huì)自動(dòng)的添加一些 configuration。這些 sourceSet configuration 都可以在 Java 插件的頁面上找到。
改進(jìn) Hello World
首先,我們可以直接使用 Java 插件提供的 implementation,而不需要自己創(chuàng)建任何 configuration:
// configurations {
// forHelloWorld
// }
dependencies {
// forHelloWorld group: 'com.google.guava', name: 'guava', version: '28.2-jre'
implementation group: 'com.google.guava', name: 'guava', version: '28.2-jre'
}
同樣,注釋只是為了對(duì)比。
接著,我們的 fatJar 也不能再使用 forHelloWorld 這個(gè) configuration,但也不能直接使用 implementation,而應(yīng)該使用 runtimeClasspath 這個(gè)給 task 消費(fèi)的、語義更符合我們使用目標(biāo)的 configuration:
task('fatJar', type: Jar) {
archiveBaseName = 'base-name'
archiveAppendix = 'appendix'
archiveVersion = '0.0.1'
archiveClassifier = 'boot'
from compileJava
// from configurations.forHelloWorld.collect {
from configurations.rumtimeClasspath.collect {
it.isDirectory() ? it : zipTree(it)
}
manifest {
attributes "Main-Class": "HelloWorld"
}
setDestinationDir file("$buildDir/libs")
}
總結(jié)
經(jīng)過使用 Java 插件,并對(duì)構(gòu)建腳本的修改,我們得到了更具有魯棒性、實(shí)現(xiàn)了約定優(yōu)于配置的構(gòu)建腳本。
完整的腳本如下:
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
implementation group: 'com.google.guava', name: 'guava', version: '28.2-jre'
}
compileJava.doLast {
println 'compile success!'
}
jar {
archiveBaseName = 'base-name'
archiveAppendix = 'appendix'
archiveVersion = '0.0.1'
manifest {
attributes("something": "value")
}
}
task('fatJar', type: Jar) {
archiveBaseName = 'base-name'
archiveAppendix = 'appendix'
archiveVersion = '0.0.1'
archiveClassifier = 'boot'
from compileJava
from configurations.runtimeClasspath.collect {
it.isDirectory() ? it : zipTree(it)
}
manifest {
attributes "Main-Class": "HelloWorld"
}
setDestinationDir file("$buildDir/libs")
}
assemble.dependsOn(fatJar)