使用Gradle但不使用Java插件構建Java項目


本文目標是探索在沒有使用任何額外插件的情況下,如何使用 Gradle 構建一個 Java 項目,以此對比使用 Java 插件時得到的好處。

初始化項目

使用 Gradle Init 插件提供的 init task 來創建一個 Gradle 項目:

gradle init --type basic --dsl groovy --project-name gradle-demo

運行完成后,我們將得到這些文件:

tree
.
├── build.gradle ├── gradle │ └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle

接下來,我們將關注點放到 build.gradle 上面,這是接下來編寫構建腳本的地方。

Hello World

首先,我們編寫一個 Java 的 HelloWorld,做為業務代碼的代表:

public class HelloWorld { public static void main(String[] args) { System.out.println("Hello Wrold"); } }

然后,將這個內容保存到 src/HelloWorld.java 文件中,不按照 maven 的約定來組織項目結構。

編譯 Java

接着,我們需要給我們的構建腳本添加任務來編譯剛才寫的 Java 文件。這里就需要使用到 Task 。關於 Task , Gradle 上有比較詳細的文檔描述如何使用它: https://docs.gradle.org/curre... & https://docs.gradle.org/curre... 。

現在,我們可以創建一個 JavaCompile 類型的 Task 對象,命名為 compileJava :

task compileJava(type: JavaCompile) {
  source fileTree("$projectDir/src") include "**/*.java" destinationDir = file("${buildDir}/classes") sourceCompatibility = '1.8' targetCompatibility = '1.8' classpath = files("${buildDir}/classes") }

在上面的代碼中,我們:

  1. 通過 source & include 方法指定了要被編譯的文件所在的目錄和文件的擴展名
  2. 通過 destinationDir 指定了編譯后的 class 文件的存放目錄
  3. 通過 sourceCompatibility & targetCompatibility 指定了源碼的 Java 版本和 class 文件的版本
  4. 通過 classpath 指定了編譯時使用的 classpath

那么,接下來我們就可以執行 compileJava 這個任務了:

❯ gradle compileJava
❯ tree build
build
├── classes
│   └── HelloWorld.class
└── tmp
    └── compileJava
❯ cd build/classes
❯ java HelloWorld
Hello World

我們可以看到,HelloWorld 已經編譯成功,並且可以被正確執行。

添加第三方依賴

在實際的項目中,難免會使用到其他人開發的庫。要使用別人開發的庫,就需要添加依賴。在Gradle 中添加依賴,需要做這樣四個事情:

  1. 申明 repository
  2. 定義 configuration
  3. 申明 dependency
  4. dependency 添加到 classpath

申明 repository

Gradle 中可以定義項目在哪些 repository 中尋找依賴,通過 dependencies 語法塊申明:

repositories {
  mavenCentral()
  maven {
    url 'https://maven.springframework.org/release'
  }
}

因為 mavenCentral 和 jcenter 是比較常見的兩個倉庫,所以 Gradle 提供了函數可以直接使用。而其他的倉庫則需要自己指定倉庫的地址。

申明了 repository 之后, Gradle 才會知道在哪里尋找申明的依賴。

定義 configuration

如果你使用過 maven 的話,也許 repository 和 dependency 都能理解,但對 configuration卻可能感到陌生。

Configuration 是一組為了完成一個具體目標的依賴的集合。那些需要使用依賴的地方,比如Task ,應該使用 configuration ,而不是直接使用依賴。這個概念僅在依賴管理范圍內適用。

Configuration 還可以擴展其他 configuration ,被擴展的 configuration 中的依賴,都將被傳遞到擴展的 configuration 中。

我們可以來創建給 HelloWorld 程序使用的 configuration :

configurations {
  forHelloWorld }

定義 configuration 僅僅需要定義名字,不需要進行其他配置。如果需要擴展,可以使用 extendsFrom 方法:

configurations {
  testHelloWorld.extendsFrom forHelloWorld }

申明 dependency

申明 dependency 需要使用到上一步的 configuration ,將依賴關聯到一個 configuration中:

dependencies {
  forHelloWorld 'com.google.guava:guava:28.2-jre' }

通過這樣的申明,在 forHelloWorld 這個 configuration 中就存在了 guava 這個依賴。

dependency 添加到 classpath

接下來,我們就需要將 guava 這個依賴添加到 compileJava 這個 task 的 classpath 中,這樣我們在代碼中使用的 guava 提供的代碼就能在編譯期被 JVM 識別到。

但就像在中描述的那樣,我們需要消費 configuration 以達到使用依賴的目的,而不能直接使用依賴。所以我們需要將 compileJava.classpath 修改成下面這樣:

classpath = files("${buildDir}/classes", configurations.forHelloWorld)

修改 HelloWorld

完成上面四步之后,我們就可以在我們的代碼中使用 guava 的代碼了:

import com.google.common.collect.ImmutableMap;
public class HelloWrold { public static void main(String[] args) { ImmutableMap.of("Hello", "World") .forEach((key, value) -> System.out.println(key + " " + value)); } }

打包

前面已經了解過如何進行編譯,接着我們來看看如何打包。

Java 打包好之后,往往有兩種類型的 Jar :

  1. 一種是普通的 Jar ,里面不包含自己的依賴,而是在 Jar 文件外的一個 metadata 文件申明依賴,比如 maven 中的 pom.xml
  2. 另一種被稱作 fatJar (or uberJar ) ,里面已經包含了所有的運行時需要的 class 文件和 resource 文件。

創建普通的 Jar 文件

在這個練習中,我們就只關注 Jar 本身,不關心 metadata 文件。

在這里,我們自然是要創建一個 task ,類型就使用 Jar :

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") }

在這個例子中,我們:

  1. 指定了 archiveBaseName , archiveAppendix , archiveVersion 屬性,他們和 archiveClassfier , archiveExtension 將決定最后打包好的 jar 文件名
  2. 使用 from 方法,指定要從 compileJava 的輸出中拷貝文件,這樣就隱式的添加了 jar 對 compileJava 的依賴
  3. 使用 include 要求僅復制 class 文件
  4. 可以使用 manifest 給 META-INF/MANIFEST.MF 文件添加信息
  5. setDestinationDir 方法已經被標記為 deprecated 但沒有替代的方法

接着,我們就可以使用 jar 進行打包:

❯ gradle jar
❯ tree build
build
├── classes
│   └── HelloWorld.class
├── lib
│   └── base-name-appendix-0.0.1.jar └── tmp ├── compileJava └── jar └── MANIFEST.MF ❯ zipinfo build/lib/base-name-appendix-0.0.1.jar ❯ zipinfo build/lib/base-name-appendix-0.0.1.jar Archive: build/lib/base-name-appendix-0.0.1.jar Zip file size: 1165 bytes, number of entries: 3 drwxr-xr-x 2.0 unx 0 b- defN 20-Feb-22 23:14 META-INF/ -rw-r--r-- 2.0 unx 43 b- defN 20-Feb-22 23:14 META-INF/MANIFEST.MF -rw-r--r-- 2.0 unx 1635 b- defN 20-Feb-22 23:14 HelloWorld.class 3 files, 1678 bytes uncompressed, 825 bytes compressed: 50.8%

創建 fatJar

接着,同樣使用 Jar 這個類型,我們創建一個 fatJar 任務:

task('fatJar', type: Jar) { archiveBaseName = 'base-name' archiveAppendix = 'appendix' archiveVersion = '0.0.1' archiveClassifier = 'boot' from compileJava from configurations.forHelloWorld.collect { it.isDirectory() ? it : zipTree(it) } manifest { attributes "Main-Class": "HelloWorld" } setDestinationDir file("$buildDir/lib") }

相比於 jar ,我們的配置變更在於:

  1. 添加 archiveClassfier 以區別 fatJar 和 jar 產生的不同 jar 文件
  2. 使用 from 將 forHelloWorld configuration 的依賴全部解壓后拷貝到 jar 文件
  3. 指定 Main-Class 屬性,以便直接運行 jar 文件

然后我們再執行 fatJar :

❯ gradle fatJar
❯ tree build
build
├── classes
│   └── HelloWorld.class
├── lib
│   ├── base-name-appendix-0.0.1-boot.jar │ └── base-name-appendix-0.0.1.jar └── tmp ├── compileJava ├── fatJar │ └── MANIFEST.MF └── jar └── MANIFEST.MF ❯ java -jar build/lib/base-name-appendix-0.0.1-boot.jar Hello World

總結

通過練習在不使用 Java Plugin 的情況下,使用 Gradle 來構建項目,實現了編譯源碼、依賴管理和打包的功能,並得到了如下完整的 gradle.build 文件:

repositories {
  mavenCentral()
}

configurations { forHelloWorld } dependencies { forHelloWorld group: 'com.google.guava', name: 'guava', version: '28.2-jre' } 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) } compileJava.doLast { println 'compile success!' } 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") } task('fatJar', type: Jar) { archiveBaseName = 'base-name' archiveAppendix = 'appendix' archiveVersion = '0.0.1' archiveClassifier = 'boot' from compileJava from configurations.forHelloWorld.collect { it.isDirectory() ? it : zipTree(it) } manifest { attributes "Main-Class": "HelloWorld" } setDestinationDir file("$buildDir/lib") }

寫了這么多構建腳本,僅僅完成了 Java Plugin 提供的一小點功能,傷害太明顯。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM