使用 Gradle 的 Java 插件構(gòu)建 Java 項(xiàng)目

原文地址: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 中的插件,可以給我們帶來很多好處,包括:

  1. 添加 Task
  2. 添加領(lǐng)域?qū)ο?/li>
  3. 約定優(yōu)于配置的實(shí)現(xiàn)
  4. 擴(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 Pluginorg.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.javasrc 目錄移動(dòng)到符合約定的 src/main/java 目錄下:

? tree src
src
└── main
    └── java
        └── HelloWorld.java

Task

Java 插件引入的 Task

接著我們來看看 Task 需要做哪些修改。

Java 插件引入了下面的這些 Task,并且添加了依賴關(guān)系:

image

其中有四個(gè) task 是由 base plugin 添加的:clean, check, assemblebuild

其中,check, assemblebuildlifecycle 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),compileJavacompileTestJava、processResourcesprocessTestResourcesclassestestClasses 命名類似。實(shí)際上,每一對(duì) task 表達(dá)的是同樣的含義,只是一個(gè)針對(duì) main sourceSet,一個(gè)針對(duì) test sourceSet 而已。如果你創(chuàng)建了一個(gè)自定義的 SourceSet,那 Java 插件會(huì)自動(dòng)的添加 compileSourceSetJavaprocessSourceSetResourcessourceSetClasses,其中的 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)為我們添加了 compileJavajar 這兩個(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)的:

image

其中:

  1. 灰色文字表示已經(jīng)被廢棄的 configuration
  2. 綠色表示用于聲明依賴的 configuration
  3. 藍(lán)灰色表示給 task 使用的 configuration
  4. 淺藍(lán)色表示 task

由這個(gè)圖,我們就能看出聲明到不同 configuration 中的依賴最終會(huì)在什么地方使用到。

test sourceSet 相關(guān)的:

image

其中的字體和顏色與上一張圖一致。

我們可以看到,除去 compile, implementation, runtimerumtimeOnly,其他的 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)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過簡信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容