读书笔记《gradle-essentials》构建Java项目
在上一章中,我们看到了一个非常基本的构建脚本,它只是在控制台上打印了惯用的 Hello World
。现在我们已经熟悉了 Gradle 命令行界面,现在是我们通过一个简单的 Java 项目开始我们的旅程的最佳时机。
在本章中,我们将了解如何使用 Gradle 构建和测试简单的 Java 项目,如何将外部依赖项添加到类路径中,以及如何构建可分发的二进制文件。
我们将尽量减少 Java 代码,以便我们可以更专注于项目的构建。在此过程中,我们将学习一些基于 Gradle 的项目应遵循的最佳实践。如果我们无法理解本章中的所有构建脚本语法也没关系,因为我们将在 Chapter 4, 揭秘构建脚本。
为了演示使用 Gradle 构建 Java 项目,让我们创建一个非常简单的 Java 应用程序来迎接用户。就应用程序逻辑而言,仅比 hello world
多一点。
首先,创建一个名为 hello-java
的目录。这是我们的项目目录。对于以下步骤,请随意选择您选择的 IDE/文本编辑器来编辑文件。
默认情况下,和 Maven 一样,Java 源文件是从 src/main/java
目录中读取的该项目。当然,我们可以配置它,但让我们稍后保存。让我们在我们的项目中创建这个目录结构。
现在,我们需要创建一个 Java 类来生成问候消息。此外,我们将创建一个带有 main
方法的 Main
类,以便可以从命令行运行应用程序。 Java 文件应保存在适当的包结构下的源根目录中。我们将在此示例中使用 com.packtpub.ge.hello
包:
正如我们在前面的结构中看到的,我们在 src/main/java
源码根目录下创建了包结构。
让我们创建 GreetingService.java
文件:
这个类只公开了一个名为 greet
的方法,我们可以使用它来生成问候消息。
下面是我们的 Main.java
的样子:
这个类有一个 main
方法,在程序运行时会被调用。它实例化 GreetingService
并打印 greet
的输出控制台上的代码>方法。
添加 Java 文件后,我们现在要编译项目并生成类文件。可以通过从命令行调用以下任务来简单地完成:
编译的类进入 build/classes/main
相对于项目根目录。您可以通过再次检查项目树来确认。我们现在将忽略其他文件和目录:
此时,我们可以直接运行该类,但让我们要求更多并为我们的应用程序生成 .jar
文件。让我们运行以下任务:
它在 build/libs
目录中为我们的项目生成一个 Jar:
让我们测试一下 Jar 是否按预期工作。要运行 Jar,请发出以下命令:
我们将 Reader
作为参数传递给我们的 java Main
类的 main
方法。这将产生以下输出:
.jar
文件的名称与项目的名称相同。这可以通过在 build.gradle
文件中设置 archivesBaseName
属性来配置。例如,要生成名为 my-app.jar
的 Jar,请将以下代码行添加到构建文件中:
现在,让我们开火:
此外,再次检查目录树。毫不奇怪,它已被清理,保持源文件完好无损。
从我们对 Ant 的 经验中我们知道,即使对于这种规模的项目,我们也必须至少定义几个目标,这将是相当几行 XML。虽然 Maven 会按照惯例工作,但 Maven 的 pom.xml
仍然需要一些仪式才能成为有效的 pom.xml
文件.因此,一个最小的 pom.xml
文件看起来仍然像五到六行 XML。
将其与 Gradle 的简单性和精心挑选的合理默认值进行比较。
这是一个很好的点,我们应该看到 java
插件将所有任务带入我们的构建中:
有趣的是,仅仅通过应用 java
插件,我们的构建中就有许多有用的任务可用。显然,Gradle 采用了一种非常强大的插件机制,可以利用它来应用 不要重复自己 (< strong>DRY) 原则关于构建逻辑。
Gradle 本身就是 一个任务运行器。它不知道如何编译 Java 文件或从何处读取源文件。这意味着这些任务默认情况下不存在。正如我们在上一章中看到的,没有应用任何插件的 Gradle 构建文件只包含很少的任务。
插件将相关任务和约定添加到 Gradle 构建中。在我们当前的示例中,所有任务,例如 compileJava
、build
、clean
,以及更多内容本质上是由我们应用于构建的 java
插件引入的。
这意味着,Gradle 不会强迫我们使用特定的方式来编译 Java 项目。为我们的构建选择 java
插件完全取决于我们。我们可以对其进行配置以满足我们的需求。如果我们仍然不喜欢它的工作方式,我们可以自由地将我们自己的任务直接添加到构建中,或者通过自定义插件以我们想要的方式工作。
Gradle 有许多开箱即用的 插件。 java
插件就是这样一个插件。在本书的整个过程中,我们将看到许多这样的插件,它们将为我们的构建带来很多有趣的功能。
单元测试 是软件开发不可或缺的方面。测试让我们相信我们的代码可以正常工作,并在重构时为我们提供安全网。幸运的是,Gradle 的 Java 插件使您的代码单元测试变得简单易行。
我们将为上面创建的同一个示例应用程序编写一个简单的测试。我们现在将使用 JUnit (v4.12) 库创建我们的第一个单元测试。
Note
有关 JUnit 的更多信息,请访问 http ://junit.org。
同样,与 Maven 一样,Java 测试源保存在 src/test/java
目录中,相对于项目根。我们将创建这个目录,作为一个好的实践,测试包结构将反映与源包相同的层次结构。
我们将为 GreetingService
添加测试。按照惯例,测试的名称将是 GreetingServiceTest.java
。以下是该文件的代码:
测试设置了一个 实例strong>System Under Test (SUT),即GreetingService
,testGreet
方法检查 SUT 的 greet
方法输出的相等性以获得预期的消息。
现在,花点时间尝试使用 compileTestJava
任务来编译测试,它与 compileJava
完全相同,但可以编译测试源文件。它编译得好吗?如果没有,我们可以猜测可能出了什么问题吗?
该任务应该因一堆编译错误而失败,因为作为外部库的 JUnit 不在编译文件的类路径上。
要编译和运行这个 测试用例,我们需要类路径上的 JUnit 库。 重要的是要记住只有在编译和运行测试时才需要这种依赖关系。我们的应用程序的编译或运行时不依赖于 JUnit。我们还需要告诉在哪里搜索这个工件,以便 Gradle 可以在需要时下载它。为此,我们需要更新 build.gradle
文件,如下所示:
根据我们已经知道的,这个构建文件有两个添加。
在 dependencies
部分中,我们列出了项目的所有依赖项及其范围。我们声明 JUnit 在 testCompile
范围内可用。
在 repositories
部分,我们配置存储库的类型和位置,外部依赖项将在其中找到。在此示例中,我们告诉 Gradle 从 Maven 中央存储库获取依赖项。由于 Maven central 是一个非常常用的 repo,Gradle 提供了一个 快捷方式来通过 mavenCentral()
方法调用。
我们有兴趣运行测试 以检查一切是否按预期工作。让我们运行 test
任务,它也会依次运行 test
任务所依赖的所有任务。我们还可以通过查看列出所有已作为此构建的一部分运行的任务的输出来验证这一点:
看起来测试通过了。为了看看 Gradle 是如何告诉我们测试失败的,让我们有意将断言中的期望值更改为 Test Hello
以便断言失败:
然后再次运行命令查看测试失败时的结果:
无论测试是否通过,都会创建一个 漂亮的 HTML 报告,其中包含所有正在运行的测试的详细信息。默认情况下,此报告位于相对于项目根目录的 build/reports/tests/index.html
中。您可以在浏览器中打开此文件。
对于上述失败,报告如下所示:
我们可以查看org.junit.ComparisonFailure: expected:<[Test Hello]>但在堆栈跟踪的第一行中是:<[Hello Test]>
。
现在我们已经进行了测试,只构建我们的项目二进制文件 (.jar
) 才有意义如果 测试通过。为此,我们需要在任务之间定义某种流,这样,如果任务失败,管道就会在那里中断,后续任务不会执行。因此,在我们的示例中,构建的执行应该取决于测试的成功。
你猜怎么着,java
插件已经为我们处理好了。我们只需要调用流程中的最后一个任务,被调用的任务所依赖的所有任务都会被顺序调用,如果其中任何一个任务失败,构建将不会成功。
此外,我们不需要显式调用构建所依赖的所有任务,因为它们无论如何都会被调用。
现在让我们修复测试并再次创建 Jar:
耶!所以测试已经通过 ,我们可以再次构建应用程序的二进制文件。
请注意,Gradle 多么聪明地发现,如果只更改测试,它只会编译测试。在前面的输出中,compileJava
显示 UP-TO-DATE
,这意味着没有任何改变,因此,Gradle 没有不必要的再次编译源文件。
如果我们再次查看测试报告,它们将如下所示:
在第一个示例中,我们直接从命令行使用 java
命令运行我们的应用程序。通常,此类命令行应用程序附带运行应用程序的脚本,因此最终用户不必总是手动编写整个命令。此外,在开发过程中,我们反复需要运行应用程序。如果我们可以在构建文件中编写一个任务,这样应用程序就可以在一次 Gradle 调用中运行,那就更好了。
好消息是,已经有一个名为 application
的插件随 Gradle 一起提供,它可以为我们做这两件事。对于这个例子,我们将 hello-test
项目复制为 hello-app
。让我们对我们的 build.gradle
进行简单的修改,如下所示:
第二行将 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 tasks
和 Distribution tasks
下看到一些新增内容团体:
我们先来看看 run
任务。我们将使用 –q
标志调用此任务到 通过 Gradle 抑制其他消息:
正如预期的那样,我们在控制台上看到了输出。当我们进行更改时,这项任务真的很出色,并且可以在一个命令中运行我们的应用程序,如下所示:
我们暂时更改了 GreetingService
以返回 "Hola
" 而不是 "Hello
" 并查看 run 是否反映了更改:
是的,它确实。
Tip
有人可能想知道 如何传递命令行参数以从命令行本身而不是构建文件运行任务,如下所示:
但是,它不能以这种方式工作。由于 Gradle 可以从命令行接受多个任务名称,因此 Gradle 无法知道 Reader
是需要传递以运行任务的参数,还是任务名称本身。例如,以下命令调用两个任务:
当然,如果您确实需要在每次调用运行任务时将命令行传递给程序,则有一些 解决方法。一种这样的方法是使用 –Pproperty=value
命令行选项,然后在 run
任务中提取属性的值以将其作为 args
发送给程序。 –P
将属性添加到 Gradle Project
。
为此,请更新 build.gradle
中的 run.args
,如下所示:
此外,然后从命令行通过调用提供属性值:
在前面的示例中,我们在调用 gradle
命令时提供了属性的值。
或者,我们可以在项目的根目录中创建一个与 build.gradle
文件平行的 gradle.properties
。在这种情况下,对于本示例,它将仅包含 runArgs=world
。但是它可以声明更多的属性,这些属性可以在构建中作为项目对象的属性使用。
还有其他声明属性的方法,可以在 https ://docs.gradle.org/current/userguide/build_environment.html。
另一个有趣的任务是distZip
,它将应用程序与特定于操作系统的启动脚本一起打包:
它会在相对于项目根目录的 build/distributions
中生成 ZIP 格式的应用程序分发。 ZIP 的名称默认为项目名称。在这种情况下,它将是 hello-app.zip
。如果需要,可以使用 build.gradle
中的以下属性进行更改:
让我们解压缩存档以查看其内容:
我们在 ZIP 中看到了一个漂亮的 标准目录结构。它包含一个 shell 脚本和 windows BAT 脚本来运行我们的应用程序。此外,它还包含我们应用程序的 JAR 文件。 lib
目录还包含应用程序的运行时依赖项。我们可以配置 distribution
插件以在我们的发行版中添加更多文件,例如 Javadoc、README 等。
我们可以运行脚本来验证它是否有效。使用命令提示符,我们可以在 Windows 中执行此命令。为此,请使用 cd
命令,并将目录更改为解压缩 ZIP 文件的 bin
目录。
在 Mac OS X/Linux 上,执行以下命令:
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 执行以下命令:
我们从构建一个非常简单的 Java 项目开始本章。我们看到了 java
插件的智能约定如何帮助我们保持构建文件的简洁。然后,我们向这个项目添加了单元测试,并包含了来自 Maven 中央存储库的 JUnit 库。我们使测试失败并检查报告以查看解释。然后,我们看到了如何使用 application
插件创建应用程序的分发。最后,我们看到了 idea
和 eclipse
插件,它们帮助我们为项目生成特定于 IDE 的文件。
总的来说,我们意识到 Gradle 中的插件系统有多么强大。开箱即用的 Gradle 附带了许多有趣的插件,但我们并非被迫使用其中任何一个。我们将在下一章构建一个 Web 应用程序,并学习配置和依赖管理的工作原理。