vlambda博客
学习文章列表

读书笔记《gradle-essentials》构建Java项目

Chapter 2. Building Java Projects

在上一章中,我们看到了一个非常基本的构建脚本,它只是在控制台上打印了惯用的 Hello World。现在我们已经熟悉了 Gradle 命令行界面,现在是我们通过一个简单的 Java 项目开始我们的旅程的最佳时机。

在本章中,我们将了解如何使用 Gradle 构建和测试简单的 Java 项目,如何将外部依赖项添加到类路径中,以及如何构建可分发的二进制文件。

我们将尽量减少 Java 代码,以便我们可以更专注于项目的构建。在此过程中,我们将学习一些基于 Gradle 的项目应遵循的最佳实践。如果我们无法理解本章中的所有构建脚本语法也没关系,因为我们将在 Chapter 4, 揭秘构建脚本

Building a simple Java project


为了演示使用 Gradle 构建 Java 项目,让我们创建一个非常简单的 Java 应用程序来迎接用户。就应用程序逻辑而言,仅比 hello world 多一点。

首先,创建一个名为 hello-java 的目录。这是我们的项目目录。对于以下步骤,请随意选择您选择的 IDE/文本编辑器来编辑文件。

Creating a build file

在项目根目录目录下,我们创建build.gradle文件,添加如下代码行对它:

apply plugin: 'java'

是的,这就是现在构建文件中的所有内容,一行。我们很快就会明白这意味着什么。

Adding source files

默认情况下,和 Maven 一样,Java 源文件是从 src/main/java 目录中读取的该项目。当然,我们可以配置它,但让我们稍后保存。让我们在我们的项目中创建这个目录结构。

现在,我们需要创建一个 Java 类来生成问候消息。此外,我们将创建一个带有 main 方法的 Main 类,以便可以从命令行运行应用程序。 Java 文件应保存在适当的包结构下的源根目录中。我们将在此示例中使用 com.packtpub.ge.hello 包:

hello-java
├── build.gradle               // build file
└── src
    └── main
        └── java               // source root
            └── com
                └── packtpub
                    └── ge
                        └── hello
                            ├── GreetingService.java
                            └── Main.java    

正如我们在前面的结构中看到的,我们在 src/main/java 源码根目录下创建了包结构。

让我们创建 GreetingService.java 文件:

package com.packtpub.ge.hello;

public class GreetingService {
    public String greet(String user) {
        return "Hello " + user;
    }
}

这个类只公开了一个名为 greet 的方法,我们可以使用它来生成问候消息。

下面是我们的 Main.java 的样子:

package com.packtpub.ge.hello;

public class Main {
    public static void main(String[] args) {
        GreetingService service = new GreetingService();
        System.out.println(service.greet(args[0]));
    }
}

这个类有一个 main 方法,在程序运行时会被调用。它实例化 GreetingService 并打印 greet 的输出控制台上的代码>方法。

Building the project

添加 Java 文件后,我们现在要编译项目并生成类文件。可以通过从命令行调用以下任务来简单地完成:

$ gradle compileJava

编译的类进入 build/classes/main 相对于项目根目录。您可以通过再次检查项目树来确认。我们现在将忽略其他文件和目录:

hello-java
...
├── build
│   ├── classes
│   │   └── main
│   │       └── com
│   │           └── packtpub
│   │               └── ge
│   │                   └── hello
│   │                       ├── GreetingService.class
│   │                       └── Main.class
...

此时,我们可以直接运行该类,但让我们要求更多并为我们的应用程序生成 .jar 文件。让我们运行以下任务:

$ gradle build

它在 build/libs 目录中为我们的项目生成一个 Jar:

hello-java
...
├── build
│   ...
│   ├── libs
│   │   └── hello-java.jar
...

让我们测试一下 Jar 是否按预期工作。要运行 Jar,请发出以下命令:

$ java -cp build/libs/hello-java.jar \ com.packtpub.ge.hello.Main Reader

我们将 Reader 作为参数传递给我们的 java Main 类的 main 方法。这将产生以下输出:

Hello Reader

Note

当我们运行 build 任务时,Gradle 在实际执行构建任务之前也会调用 compileJava 和其他依赖任务。所以,我们不需要在这里显式调用 compileJava 来编译类。

.jar 文件的名称与项目的名称相同。这可以通过在 build.gradle 文件中设置 archivesBaseName 属性来配置。例如,要生成名为 my-app.jar 的 Jar,请将以下代码行添加到构建文件中:

archivesBaseName = "my-app"

现在,让我们开火:

$ gradle clean

此外,再次检查目录树。毫不奇怪,它已被清理,保持源文件完好无损。

从我们对 Ant 的 经验中我们知道,即使对于这种规模的项目,我们也必须至少定义几个目标,这将是相当几行 XML。虽然 Maven 会按照惯例工作,但 Maven 的 pom.xml 仍然需要一些仪式才能成为有效的 pom.xml 文件.因此,一个最小的 pom.xml 文件看起来仍然像五到六行 XML。

将其与 Gradle 的简单性和精心挑选的合理默认值进行比较。

这是一个很好的点,我们应该看到 java 插件将所有任务带入我们的构建中:

$ gradle –q tasks
------------------------------------------------------------
All tasks runnable from root project
------------------------------------------------------------

Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles main classes.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the main classes.
testClasses - Assembles test classes.

Build Setup tasks
-----------------
init - Initializes a new Gradle build. [incubating]
wrapper - Generates Gradle wrapper files. [incubating]

Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the main source code.
Help tasks
----------
components - Displays the components produced by root project 'hello-java'. [incubating]
dependencies - Displays all dependencies declared in root project 'hello-java'.
dependencyInsight - Displays the insight into a specific dependency in root project 'hello-java'.
help - Displays a help message.
model - Displays the configuration model of root project 'hello-java'. [incubating]
projects - Displays the sub-projects of root project 'hello-java'.
properties - Displays the properties of root project 'hello-java'.
tasks - Displays the tasks runnable from root project 'hello-java'.

Verification tasks
------------------
check - Runs all checks.
test - Runs the unit tests.
...

有趣的是,仅仅通过应用 java 插件,我们的构建中就有许多有用的任务可用。显然,Gradle 采用了一种非常强大的插件机制,可以利用它来应用 不要重复自己 (< strong>DRY) 原则关于构建逻辑。

A brief introduction to plugins

Gradle 本身就是 一个任务运行器。它不知道如何编译 Java 文件或从何处读取源文件。这意味着这些任务默认情况下不存在。正如我们在上一章中看到的,没有应用任何插件的 Gradle 构建文件只包含很少的任务。

插件将相关任务和约定添加到 Gradle 构建中。在我们当前的示例中,所有任务,例如 compileJavabuildclean,以及更多内容本质上是由我们应用于构建的 java 插件引入的。

这意味着,Gradle 不会强迫我们使用特定的方式来编译 Java 项目。为我们的构建选择 java 插件完全取决于我们。我们可以对其进行配置以满足我们的需求。如果我们仍然不喜欢它的工作方式,我们可以自由地将我们自己的任务直接添加到构建中,或者通过自定义插件以我们想要的方式工作。

Gradle 有许多开箱即用的 插件。 java 插件就是这样一个插件。在本书的整个过程中,我们将看到许多这样的插件,它们将为我们的构建带来很多有趣的功能。

Unit testing


单元测试 是软件开发不可或缺的方面。测试让我们相信我们的代码可以正常工作,并在重构时为我们提供安全网。幸运的是,Gradle 的 Java 插件使您的代码单元测试变得简单易行。

我们将为上面创建的同一个示例应用程序编写一个简单的测试。我们现在将使用 JUnit (v4.12) 库创建我们的第一个单元测试。

Note

有关 JUnit 的更多信息,请访问 http ://junit.org

Adding a unit test source

同样,与 Maven 一样,Java 测试源保存在 src/test/java 目录中,相对于项目根。我们将创建这个目录,作为一个好的实践,测试包结构将反映与源包相同的层次结构。

...
src
└── test
    └── java        // test source root
        └── com
            └── packtpub
                └── ge
                    └── hello
                        └── GreetingServiceTest.java
...

我们将为 GreetingService 添加测试。按照惯例,测试的名称将是 GreetingServiceTest.java。以下是该文件的代码:

package com.packtpub.ge.hello;

import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class GreetingServiceTest {

    GreetingService service;

    @Before
    public void setup() {
        service = new GreetingService();
    }

    @Test
    public void testGreet() {
        assertEquals("Hello Test", service.greet("Test"));
    }
}

测试设置了一个 实例strong>System Under Test (SUT),即GreetingServicetestGreet 方法检查 SUT 的 greet 方法输出的相等性以获得预期的消息。

现在,花点时间尝试使用 compileTestJava 任务来编译测试,它与 compileJava 完全相同,但可以编译测试源文件。它编译得好吗?如果没有,我们可以猜测可能出了什么问题吗?

该任务应该因一堆编译错误而失败,因为作为外部库的 JUnit 不在编译文件的类路径上。

Adding the JUnit to the classpath

要编译和运行这个 测试用例,我们需要类路径上的 JUnit 库。 重要的是要记住只有在编译和运行测试时才需要这种依赖关系。我们的应用程序的编译或运行时不依赖于 JUnit。我们还需要告诉在哪里搜索这个工件,以便 Gradle 可以在需要时下载它。为此,我们需要更新 build.gradle 文件,如下所示:

apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    testCompile 'junit:junit:4.12'
}

根据我们已经知道的,这个构建文件有两个添加。

dependencies 部分中,我们列出了项目的所有依赖项及其范围。我们声明 JUnit 在 testCompile 范围内可用。

repositories 部分,我们配置存储库的类型和位置,外部依赖项将在其中找到。在此示例中,我们告诉 Gradle 从 Maven 中央存储库获取依赖项。由于 Maven central 是一个非常常用的 repo,Gradle 提供了一个 快捷方式来通过 mavenCentral() 方法调用。

我们将在下一章更深入地介绍这两个部分

Running the test

我们有兴趣运行测试 以检查一切是否按预期工作。让我们运行 test 任务,它也会依次运行 test 任务所依赖的所有任务。我们还可以通过查看列出所有已作为此构建的一部分运行的任务的输出来验证这一点:

$ gradle test
:compileJava
:processResources UP-TO-DATE
:classes
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test

BUILD SUCCESSFUL

Total time: 1.662 secs

看起来测试通过了。为了看看 Gradle 是如何告诉我们测试失败的,让我们有意将断言中的期望值更改为 Test Hello 以便断言失败:

@Test
public void testGreet() {
    assertEquals("Test Hello", service.greet("Guest"));
}

然后再次运行命令查看测试失败时的结果:

$ gradle test
:compileJava
:processResources UP-TO-DATE
:classes
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test

com.packtpub.ge.hello.GreetingServiceTest > testGreet FAILEDorg.junit.ComparisonFailure at GreetingServiceTest.java:18
1 test completed, 1 failed
:test FAILED

FAILURE: Build failed with an exception.

......

是的,所以测试失败了,输出 也会告诉你文件和行号。此外,它会将您指向报告文件,其中包含测试失败的更多详细信息。

Viewing test reports

无论测试是否通过,都会创建一个 漂亮的 HTML 报告,其中包含所有正在运行的测试的详细信息。默认情况下,此报告位于相对于项目根目录的 build/reports/tests/index.html 中。您可以在浏览器中打开此文件。

对于上述失败,报告如下所示:

读书笔记《gradle-essentials》构建Java项目

如果我们点击失败的测试,我们会在看到失败的细节:

读书笔记《gradle-essentials》构建Java项目

我们可以查看org.junit.ComparisonFailure: expected:<[Test Hello]>但在堆栈跟踪的第一行中是:<[Hello Test]>

Fitting tests in the workflow

现在我们已经进行了测试,只构建我们的项目二进制文件 (.jar) 才有意义如果 测试通过。为此,我们需要在任务之间定义某种流,这样,如果任务失败,管道就会在那里中断,后续任务不会执行。因此,在我们的示例中,构建的执行应该取决于测试的成功。

你猜怎么着,java 插件已经为我们处理好了。我们只需要调用流程中的最后一个任务,被调用的任务所依赖的所有任务都会被顺序调用,如果其中任何一个任务失败,构建将不会成功。

$ gradle build

此外,我们不需要显式调用构建所依赖的所有任务,因为它们无论如何都会被调用。

现在让我们修复测试并再次创建 Jar:

$gradle build
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:assemble UP-TO-DATE
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test
:check
:build

BUILD SUCCESSFUL

Total time: 1.617 secs

耶!所以测试已经通过 ,我们可以再次构建应用程序的二进制文件。

请注意,Gradle 多么聪明地发现,如果只更改测试,它只会编译测试。在前面的输出中,compileJava 显示 UP-TO-DATE,这意味着没有任何改变,因此,Gradle 没有不必要的再次编译源文件。

Tip

如果我们需要强制运行任务动作,即使两次运行之间没有任何变化,我们可以在命令行上传递 --rerun-tasks 标志,以便所有任务动作都可以跑。

如果我们再次查看测试报告,它们将如下所示:

读书笔记《gradle-essentials》构建Java项目

Test Summary看起来像这样:

读书笔记《gradle-essentials》构建Java项目

Bundling an application distributable


在第一个示例中,我们直接从命令行使用 java 命令运行我们的应用程序。通常,此类命令行应用程序附带运行应用程序的脚本,因此最终用户不必总是手动编写整个命令。此外,在开发过程中,我们反复需要运行应用程序。如果我们可以在构建文件中编写一个任务,这样应用程序就可以在一次 Gradle 调用中运行,那就更好了。

好消息是,已经有一个名为 application 的插件随 Gradle 一起提供,它可以为我们做这两件事。对于这个例子,我们将 hello-test 项目复制为 hello-app。让我们对我们的 build.gradle 进行简单的修改,如下所示:

apply plugin: 'java'
apply plugin: 'application'

mainClassName = "com.packtpub.ge.hello.Main"
run.args = ["Reader"]

repositories {
    mavenCentral()
}

dependencies {
    testCompile 'junit:junit:4.11'
}

第二行将 application 插件应用到我们的构建中。为了使这个插件工作,我们需要配置 Gradle 以使用我们的 Main 入口点类,它具有静态 main 方法需要在我们的应用程序运行时运行。我们通过设置 mainClassName 属性在 #4 行指定,该属性由 应用程序插件。最后,当我们想使用 Gradle 运行应用程序时(即在开发时),我们需要为我们的应用程序提供一些命令行参数。 application 插件将 run 任务添加到我们的构建中。正如我们之前所说,任务是对象,它们像任何常规对象一样具有属性和方法。在 #5 行,我们设置 run 任务的 args 属性到具有一个元素 Reader 的列表,因此每当我们执行运行任务时,Reader 将作为命令行参数传递到我们的主要方法。使用过 IDE 设置运行配置的人可以很容易地联想到这一点。该文件的其余部分与上一个示例相同。

Note

在前面的示例中,由于我们正在应用 application 插件,因此没有必要将 java 插件显式应用为 < code class="literal">application 插件隐式地将 java 插件应用到我们的构建中。

它还隐式应用 distribution 插件,以便我们获得将应用程序打包为 ZIP 或 TAR 存档的任务,并获得在本地安装应用程序分发的任务。

关于 application 插件 的更多信息可以在 https://docs.gradle.org/current/userguide/distribution_plugin.html

现在,如果我们检查构建中可用的任务,我们会在 Application tasksDistribution tasks 下看到一些新增内容团体:

$ gradle tasks
...
Application tasks
-----------------
installApp - Installs the project as a JVM application along with libs and OS specific scripts.
run - Runs this project as a JVM application
...

Distribution tasks
------------------
assembleDist - Assembles the main distributions
distTar - Bundles the project as a distribution.
distZip - Bundles the project as a distribution.
installDist - Installs the project as a distribution as-is.
...

Running the application with Gradle

我们先来看看 run 任务。我们将使用 –q 标志调用此任务到 通过 Gradle 抑制其他消息:

$ gradle -q run
Hello Reader

正如预期的那样,我们在控制台上看到了输出。当我们进行更改时,这项任务真的很出色,并且可以在一个命令中运行我们的应用程序,如下所示:

    public String greet(String user) {
        return "Hola " + user;
    }

我们暂时更改了 GreetingService 以返回 "Hola" 而不是 "Hello " 并查看 run 是否反映了更改:

$ gradle -q run
Hola Reader

是的,它确实。

Tip

有人可能想知道 如何传递命令行参数以从命令行本身而不是构建文件运行任务,如下所示:

$ gradle –q run Reader

但是,它不能以这种方式工作。由于 Gradle 可以从命令行接受多个任务名称,因此 Gradle 无法知道 Reader 是需要传递以运行任务的参数,还是任务名称本身。例如,以下命令调用两个任务:

$ gradle –q clean build

当然,如果您确实需要在每次调用运行任务时将命令行传递给程序,则有一些 解决方法。一种这样的方法是使用 –Pproperty=value 命令行选项,然后在 run 任务中提取属性的值以将其作为 args 发送给程序。 –P 将属性添加到 Gradle Project

为此,请更新 build.gradle 中的 run.args,如下所示:

run.args = [project.runArgs]

此外,然后从命令行通过调用提供属性值:

$ gradle -q run -PrunArgs=world

在前面的示例中,我们在调用 gradle 命令时提供了属性的值。

或者,我们可以在项目的根目录中创建一个与 build.gradle 文件平行的 gradle.properties。在这种情况下,对于本示例,它将仅包含 runArgs=world。但是它可以声明更多的属性,这些属性可以在构建中作为项目对象的属性使用。

还有其他声明属性的方法,可以在 https ://docs.gradle.org/current/userguide/build_environment.html

Building the distribution archive

另一个有趣的任务是distZip,它将应用程序与特定于操作系统的启动脚本一起打包:

$ gradle distZip
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:startScripts
:distZip

BUILD SUCCESSFUL

Total time: 1.29 secs

它会在相对于项目根目录的 build/distributions 中生成 ZIP 格式的应用程序分发。 ZIP 的名称默认为项目名称。在这种情况下,它将是 hello-app.zip。如果需要,可以使用 build.gradle 中的以下属性进行更改:

distributions.main.baseName = 'someName'

让我们解压缩存档以查看其内容:

hello-app
├── bin
│   ├── hello-app
│   └── hello-app.bat
└── lib
    └── hello-app.jar

我们在 ZIP 中看到了一个漂亮的 标准目录结构。它包含一个 shell 脚本和 windows BAT 脚本来运行我们的应用程序。此外,它还包含我们应用程序的 JAR 文件。 lib 目录还包含应用程序的运行时依赖项。我们可以配置 distribution 插件以在我们的发行版中添加更多文件,例如 Javadoc、README 等。

我们可以运行脚本来验证它是否有效。使用命令提示符,我们可以在 Windows 中执行此命令。为此,请使用 cd 命令,并将目录更改为解压缩 ZIP 文件的 bin 目录。

$ hello-app Reader
Hello Reader

在 Mac OS X/Linux 上,执行以下命令:

$ ./hello-app Reader
Hello Reader

Generating IDE project files


IDE 是 Java 开发人员工具链和工作流程中不可或缺的一部分。但是,手动设置 IDE 以正确识别任何中等规模项目的项目结构和依赖关系并非易事。

签入特定于 IDE 的文件或目录,例如 .classpath.project。 ipr, .iws, .nbproject, .idea , .settings, .iml,不是个好主意。我们知道有些人仍然这样做,因为每次有人将项目从版本控制系统中检查出来时,手动生成 IDE 文件是很困难的。但是,签入此类文件会产生问题,因为它们最终会与主构建文件不同步。此外,这会迫使整个团队使用相同的 IDE,并在构建发生更改时手动更新 IDE 文件。

如果我们可以只签入独立于 IDE 构建项目所需的那些文件,并让我们的构建系统生成一个特定于我们最喜欢的 IDE 的文件,那该多好?我们的愿望实现了。此外,这是最好的部分。您需要在 Gradle 构建文件中修改的行数只有 1 行。 Gradle 提供了非常好的插件,可以生成特定于 IDE 的项目文件。 IntelliJ IDEA 和 Eclipse 都包含在各自的插件中。根据您要支持的 IDE,您将包括 apply plugin: 'idea'apply plugin: 'eclipse' .

事实上,两者都包含并没有什么坏处。

现在,从命令行,分别为 Eclipse 和 IntelliJ IDEA 执行以下命令:

$ gradle eclipse
$ gradle idea

它应该为您生成 IDE 特定文件,现在您可以直接在任一 IDE 中打开项目。

Tip

确保在版本控制中忽略特定于 IDE 的文件。例如,如果您使用 Git,请考虑在 .gitignore 文件中添加以下条目,以防止有人意外提交特定于 IDE 的文件:

.idea/
*.iml
*.ipr
*.iws
.classpath
.project
.settings/

Summary


我们从构建一个非常简单的 Java 项目开始本章。我们看到了 java 插件的智能约定如何帮助我们保持构建文件的简洁。然后,我们向这个项目添加了单元测试,并包含了来自 Maven 中央存储库的 JUnit 库。我们使测试失败并检查报告以查看解释。然后,我们看到了如何使用 application 插件创建应用程序的分发。最后,我们看到了 ideaeclipse 插件,它们帮助我们为项目生成特定于 IDE 的文件。

总的来说,我们意识到 Gradle 中的插件系统有多么强大。开箱即用的 Gradle 附带了许多有趣的插件,但我们并非被迫使用其中任何一个。我们将在下一章构建一个 Web 应用程序,并学习配置和依赖管理的工作原理。