使用 Gradle 但不使用 Java 插件构建 Java 项目

原文地址:https://blog.gaoyuexiang.cn/2020/02/22/use-gradle-to-build-java-project-without-plugin/,内容无差别。

本文目标是探索在没有使用任何额外插件的情况下,如何使用 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。关于 TaskGradle 上有比较详细的文档描述如何使用它:https://docs.gradle.org/current/dsl/org.gradle.api.Task.html#org.gradle.api.Task & https://docs.gradle.org/current/userguide/more_about_tasks.html

现在,我们可以创建一个 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'
  }
}

因为 mavenCentraljcenter 是比较常见的两个仓库,所以 Gradle 提供了函数可以直接使用。而其他的仓库则需要自己指定仓库的地址。

申明了 repository 之后,Gradle 才会知道在哪里寻找申明的依赖。

定义 configuration

如果你使用过 maven 的话,也许 repositorydependency 都能理解,但对 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 这个 taskclasspath 中,这样我们在代码中使用的 guava 提供的代码就能在编译期被 JVM 识别到。

但就像在定义 configuration 中描述的那样,我们需要消费 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 的输出中拷贝文件,这样就隐式的添加了 jarcompileJava 的依赖
  3. 使用 include 要求仅复制 class 文件
  4. 可以使用 manifestMETA-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 以区别 fatJarjar 产生的不同 jar 文件
  2. 使用 fromforHelloWorld 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 提供的一小点功能,伤害太明显。

完整的例子可以在这个 repocommit 中找到 https://github.com/kbyyd24/gradle-practice

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,172评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,346评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,788评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,299评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,409评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,467评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,476评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,262评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,699评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,994评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,167评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,499评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,149评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,387评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,028评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,055评论 2 352

推荐阅读更多精彩内容

  • 学习android的同学都知道android工程从使用android studio开发以后就使用了[gradle作...
    qndroid阅读 1,157评论 0 1
  • 前言 为什么需要学Gradle? Gradle 是 Android 现在主流的编译工具,虽然在Gradle 出现之...
    真笨笨鱼阅读 1,489评论 0 0
  • 在 Android Studio 构建的项目中,基于 Gradle 进行项目的构建,同时使用 Android DS...
    Ant_way阅读 7,338评论 0 16
  • 本文已授权微信公众号 Android技术经验分享 独家发布转载请注明出处:Gradle从入门到了解 通过这篇文章,...
    hongjay阅读 5,343评论 7 82
  • 包书皮 周末的晚上,我站在书架前,想选一本书来读,无意中发现我的很多书都是包着书皮的。 书皮各式各样,有的是报纸,...
    玉雕小蜜蜂阅读 790评论 0 1