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

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

本文目标是探索在没有使用任何额外插件的状况下,如何使用 Gradle 构建一个 Java 项目,以此对比使用 Java 插件时获得的好处。java

初始化项目

使用 Gradle Init 插件提供的 init task 来建立一个 Gradle 项目:git

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

运行完成后,咱们将获得这些文件:github

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

接下来,咱们将关注点放到 build.gradle 上面,这是接下来编写构建脚本的地方。spring

Hello World

首先,咱们编写一个 Java 的 HelloWorld,作为业务代码的表明:shell

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

而后,将这个内容保存到 src/HelloWorld.java 文件中,不按照 maven 的约定来组织项目结构。api

编译 Java

接着,咱们须要给咱们的构建脚本添加任务来编译刚才写的 Java 文件。这里就须要使用到 Task。关于 Task Gradle 上有比较详细的文档描述如何使用它:https://docs.gradle.org/curre... & https://docs.gradle.org/curre...bash

如今,咱们能够建立一个 JavaCompile 类型的 Task 对象,命名为 compileJavaapp

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

在上面的代码中,咱们:maven

  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/gr...

相关文章
相关标签/搜索