vlambda博客
学习文章列表

读书笔记《gradle-effective-implementations-guide-second-edition》测试、生成和发布构件

Chapter 6.  Testing, Building, and Publishing Artifacts

开发软件的一个重要部分是为我们的代码编写测试。在本章中,我们将讨论如何在构建过程中运行我们的测试代码。 Gradle 支持 JUnit 和 TestNG 测试框架。我们甚至可以同时运行测试以缩短构建时间,从而实现快速构建。

我们还将讨论如何在 Gradle 构建中运行 Java 应用程序。作为构建的一部分,我们可以使用应用程序插件自动执行 Java 应用程序。

在我们编写并测试了我们的代码之后,是时候发布代码以便其他人可以使用它了。我们将构建一个包并将我们的代码部署到公司存储库或任何其他存储库。

Testing our projects


Gradle 为我们的 Java 项目运行测试提供了内置支持。当我们将 Java 插件添加到我们的项目中时,我们将获得新的任务来编译和运行测试。我们还将获得 testCompiletestRuntime 依赖配置。我们使用这些依赖项来设置类路径以在我们的代码库中运行测试:

  1. 让我们为示例 Java 类编写一个简单的 JUnit 测试。  gradle.sample.Sample 的实现有 getWelcomeMessage() 方法,我们从 < code class="literal">file 属性,然后返回值。以下示例包含 Sample 类的代码:

            // File: src/main/java/gradle/sample/Sample.java 
            package gradle.sample; 
     
            import java.util.ResourceBundle; 
     
            /** 
            * Read welcome message from external properties file 
            * <code>messages.properties</code>. 
            */ 
    
  2. 接下来,我们必须添加 Sample 类使用的资源属性文件。我们将在 src/main/resources/gradle/sample目录下创建 messages.properties文件,内容如下:

            # File: src/main/resources/gradle/
              sample/messages.properties 
            welcome = Welcome to Gradle. 
    

    我们的测试非常简单。我们将创建一个 Sample 对象并调用 getWelcomeMessage() 方法。我们将返回值与我们期望返回的值进行比较。

  3. 以下示例包含使用预期 String 值检查 getWelcomeMessage() 方法的值的测试,  欢迎使用 Gradle。我们需要在 src/test/java/gradle/sample目录下创建 SampleTest.java文件:

            // File: src/test/java/gradle/sample/SampleTest.java 
            package gradle.sample; 
     
            import org.junit.Assert; 
            import org.junit.Test; 
     
            public class SampleTest { 
     
              @Test 
              public void readWelcomeMessage() { 
                final Sample sample = new Sample(); 
     
                final String realMessage = sample.getWelcomeMessage(); 
     
                final String expectedMessage = "Welcome to Gradle."; 
                Assert.assertEquals( 
                  "Get text from properties file", 
                  expectedMessage, realMessage); 
              } 
     
          }  
    

    这些文件的 Gradle 构建脚本非常简单。我们首先应用 Java 插件,并且由于我们遵循 Gradle 的配置约定,我们不必配置或定义任何其他内容。我们的测试是作为 JUnit 测试编写的。 JUnit 是 Java 项目最常用的测试框架之一。为了确保所需的 JUnit 类可用于编译和运行测试类,我们必须将 JUnit 作为依赖项添加到我们的项目中。 Java 插件添加了我们可以使用的 testCompiletestRuntime 配置。我们将 JUnit 依赖项添加到 testCompile 配置中。所有 JUnit 类现在都可用于编译测试类。

  4. 以下示例构建文件包含执行测试所需的所有代码:

            apply plugin: 'java' 
     
            repositories { 
                jcenter() 
            } 
     
            dependencies { 
                testCompile('junit:junit:4.12') 
            } 
    
  5. 要运行我们的测试,我们只需要从命令行调用 Java 插件添加的 test 任务:

    $ gradle test
    :compileJava
    :processResources
    :classes
    :compileTestJava
    :processTestResources UP-TO-DATE
    :testClasses
    :test
    gradle.sample.SampleTest > readWelcomeMessage FAILED
      java.util.MissingResourceException at SampleTest.java:13
    1 test completed, 1 failed
    :test FAILED
    FAILURE: Build failed with an exception.
    * What went wrong:
    Execution failed for task ':test'.
    > There were failing tests. See the report  at:       file:///Users/mrhaki/Projects/
          gradle-effective-implementation-guide-2/
          gradle- impl-guide-         2/src/docs/asciidoc/Chapter6/Code_Files/testing/
          sample/build/reports/tests/index.html
    * Try:
    Run with --stacktrace option to get the stack
          trace. Run with --info
          or --debug option to get more log output.
    BUILD FAILED
    Total time: 2.656 secs
    

    如果我们查看输出,我们将看到测试失败,但我们不明白为什么。找出它的一种方法是使用额外的日志记录重新运行 test 任务。

  6. 因此,现在我们可以使用 --info(或 -i) 参数,如以下命令所示:

    $ gradle test --info
    ...
    Gradle Test Executor 1 started executing tests.
    Gradle Test Executor 1 finished executing tests.
    gradle.sample.SampleTest > readWelcomeMessage FAILED
    org.junit.ComparisonFailure: Get text from properties 
          file expected:<Welcome to Gradle[.]> but was:
         <Welcome to Gradle[!]>
          at org.junit.Assert.assertEquals(Assert.java:115)
          at  gradle.sample.SampleTest.readWelcomeMessage
         (SampleTest.java:16)
    ...
    

    现在我们可以看到为什么我们的测试失败了。在我们的测试中,我们希望在 String 的末尾有一个点 (.),而不是感叹号 (!) 我们从属性文件中获得。要修复我们的测试,我们必须更改属性文件的内容并将感叹号替换为点。在我们这样做之前,我们将使用不同的方式来查看测试结果。到目前为止,我们在运行 test 任务后查看了命令行的输出。在 build/reports/tests 目录中,有一个 HTML 文件报告,其中包含我们的测试运行结果。

  7. 如果我们在 Web 浏览器中打开 build/reports/tests/index.html 文件,我们会清楚地了解已运行和失败的测试:

    读书笔记《gradle-effective-implementations-guide-second-edition》测试、生成和发布构件

    生成的带有测试概述的 HTML 页面

  8. 我们可以单击失败测试的方法名称来查看详细信息。在这里,我们再次看到消息指出预期的 String 值在行尾有一个点而不是感叹号:

    读书笔记《gradle-effective-implementations-guide-second-edition》测试、生成和发布构件

    有关已运行测试的更多详细信息

  9. 让我们更改 messages.properties 文件的内容,并在行尾使用点而不是感叹号:

              # File: src/main/resources/gradle/sample/
                messages.properties 
              welcome = Welcome to Gradle. 
    
  10. 现在我们再次从命令行运行 test 任务:

    $ gradle test
    :compileJava UP-TO-DATE
    :processResources
    :classes
    :compileTestJava
    :processTestResources UP-TO-DATE
    :testClasses
    :test
    BUILD SUCCESSFUL
    Total time: 1.174 secs
    

    这次 Gradle 构建没有失败,并且是成功的。我们的测试已经运行,我们从 getWelcomeMessage() 方法得到了预期的结果。

  11. 以下屏幕截图显示测试 100% 成功,并且还记录在生成的测试 HTML 报告中:

    读书笔记《gradle-effective-implementations-guide-second-edition》测试、生成和发布构件

    生成的 HTML 页面,其中包含已运行的测试概览

Using TestNG for testing

我们使用 JUnit 测试框架编写了一个测试。 Gradle 还支持使用 TestNG 测试框架编写的测试。 Gradle 扫描所有类文件的测试类路径并检查它们是否具有特定的 JUnit 或 TestNG 注释。如果测试类或超类扩展了 TestCase 或 GroovyTestCase 或用  @RunWith 注解,测试类也被确定为JUnit测试。

为了让 Gradle 在我们运行 test 任务时使用 JUnit 或 TestNG 测试,我们调用 useJUnit() 或 < code class="literal">useTestNG() 方法,分别强制 Gradle 使用正确的测试框架。 Gradle 默认使用 JUnit 作为测试框架,这样我们在使用 JUnit 或兼容 JUnit 的测试框架测试代码时就不必使用 useJUnit() 方法.

让我们编写一个新的测试,但这一次,我们将使用 TestNG 注释和类。以下示例类与我们之前看到的测试相同,但它是使用 TestNG 框架编写的:

// File: src/test/java/gradle/sample/SampleTestNG.java 
package gradle.sample; 
 
 
import org.testng.AssertJUnit; 
import org.testng.annotations.Test; 
 
public class SampleTestNG { 
 
    @Test 
    public void readWelcomeMessage() { 
        final Sample sample = new Sample(); 
 
        final String realMessage = sample.getWelcomeMessage(); 
 
        final String expectedMessage = "Welcome to Gradle."; 
        AssertJUnit.assertEquals( 
                "Get text from properties file", 
                expectedMessage, realMessage); 
    } 
 
} 

我们需要将 TestNG 依赖添加到 testCompile 依赖配置中。此外,我们在 test 任务上调用 useTestNG() 方法,以便 Gradle 获取我们的新测试。我们将创建一个新的构建文件并添加以下代码:

apply plugin: 'java' 
 
repositories { 
    jcenter() 
} 
 
dependencies { 
    testCompile('org.testng:testng:6.5.1') 
} 
 
test.useTestNG() 

现在我们可以再次运行 test 任务;但这一次,Gradle 将使用我们的 TestNG 测试:

$ gradle test
:compileJava
:processResources
:classes
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test
BUILD SUCCESSFUL
Total time: 1.262 secs

生成的 HTML 测试报告位于 build/reports/tests 目录中。我们可以在 Web 浏览器中打开 index.html 文件并查看 TestNG 框架生成的输出。以下屏幕截图显示了我们可以查看的输出示例:

读书笔记《gradle-effective-implementations-guide-second-edition》测试、生成和发布构件

生成的带有运行测试概述的 HTML 页面

Gradle 不能使用 test 任务同时运行 JUnit 和 TestNG 测试。如果我们的项目中有两种类型的测试并且想要运行它们,我们必须添加一个新的 Test类型的任务。这个新任务可以运行其中一个框架的特定测试。

我们添加一个 Test 类型的新任务来在我们的构建文件中运行 TestNG 测试:

apply plugin: 'java' 
 
repositories { 
    jcenter() 
} 
 
dependencies { 
    testCompile('junit:junit:4.12') 
    testCompile('org.testng:testng:6.5.1') 
} 
 
// New task of type Test 
// for running TestNG classes. 
task testNG(type: Test) { 
    useTestNG() 
} 
 
// Make sure testNG is executed, 
// when the test task is executed. 
test.dependsOn(testNG) 

要为 TestNG 添加配置选项,我们可以将闭包传递给 useTestNG() 方法。闭包有一个 org.gradle.api.tasks.testing.testng.TestNGOptions 类型的参数。下表显示了我们可以设置的选项:

选项名称

类型

说明

excludeGroups

这是要排除的组集

includeGroups

这是要包括的一组组

javadocAnnotations

布尔

true 时,Javadoc 注解用于这些测试

监听器

设置

这是一组合格的 TestNG 监听器

并行

字符串

这是用于运行测试的并行模式; method 或 tests 是有效选项

套件名称

字符串

如果未在 suite.xml 文件或源代码中指定,这将设置测试套件的默认名称

suiteXmlBuilder

MarkupBuilder

MarkupBuilder 创建套件 XML

suiteXmlWriter

StringWriter

StringWriter 写出 XML

testName

字符串

如果未在 suite.xml 文件或源代码中指定,这将设置测试的默认名称

testResources

列表

这是包含测试源的所有目录的列表

threadCount

int

这些是用于此运行的线程数

useDefaultListeners

布尔

这说明是否应使用默认侦听器和报告器

以下示例构建文件使用以下一些选项来配置 TestNG:

apply plugin: 'java' 
 
repositories { 
    jcenter() 
} 
 
dependencies { 
    testCompile('org.testng:testng:6.5.1') 
} 
 
test { 
    useTestNG { options -> 
        options.excludeGroups = ['functional'] as Set 
        options.parallel = 'method' 
        options.threadCount = 4 
    } 
} 

Configuring the test process

test 任务执行的测试在单独的隔离 JVM 进程中运行。我们可以使用几个属性来控制这个过程。我们可以设置系统属性和 JVM 参数,我们可以配置需要执行的 Java 类来运行测试。

要调试测试,我们可以设置 test 任务的 debug 属性。 Gradle 将在调试模式下启动测试进程并监听端口 5005 以获取要附加的调试进程。这样我们就可以运行我们的测试并使用 IDE 调试器来单步调试代码。

默认情况下,如果任何测试失败,Gradle 都会使构建失败。如果我们想更改此设置,我们必须将 ignoreFailures 属性设置为 true。即使我们的测试中有错误,我们的构建也不会失败。生成的测试报告仍然会有错误。忽略失败是不好的做法,但如果我们需要,知道有一个选项是很好的。

以下构建文件使用刚刚讨论的属性配置 test 任务:

apply plugin: 'java' 
 
repositories { 
    jcenter() 
} 
 
dependencies { 
    testCompile('junit:junit:4.12') 
} 
 
test { 
    // Add System property to running tests. 
    systemProperty 'sysProp', 'value' 
 
    // Use the following JVM arguments for each test process. 
    jvmArgs '-Xms256m', '-Xmx512m' 
 
    // Enable debugging mode. 
    debug = true 
 
    // Ignore any test failues and don't fail the build. 
    ignoreFailures = true 
 
    // Enable assertions for test with the assert keyword. 
    enableAssertions = true 
} 

Gradle 可以同时执行测试。这意味着 Gradle 将同时启动多个测试进程。一个测试进程一次只执行一个测试。通过启用并行测试执行,如果我们有很多测试,test 任务的总执行时间可以大大减少。我们必须使用 maxParallelForks 属性来设置我们想要并行运行的测试进程的数量。默认值为 1,这意味着测试不会同时运行。

每个测试进程都会设置一个具有唯一值的 org.gradle.test.worker 系统属性。我们可以使用这个值来为测试过程生成唯一的文件。

如果我们有很多由单个测试进程执行的测试,我们可能会遇到堆大小或 PermGen 问题。通过 forkEvery 属性,我们可以设置在一个新的测试进程开始执行更多测试之前需要在单个测试进程中运行的测试数量。因此,如果 Gradle 发现测试的数量超过了分配给  forkEvery 属性的给定数量,则测试过程将重新启动并执行以下一组测试。

让我们创建一个新的构建文件并对其进行配置,以便我们可以同时运行四个测试进程并在 10 测试后重新启动测试进程:

apply plugin: 'java' 
 
repositories { 
    jcenter() 
} 
 
dependencies { 
    testCompile('junit:junit:4.12') 
} 
 
test { 
    // Run four tests in parallel. 
    maxParallelForks = 4 
 


    // Restart proces after 
    // 10 executions. 
    forkEvery = 10 
} 

Determining tests

为了确定测试文件,Gradle 将检查编译的类文件。如果一个类或其方法具有 @Test 注解,Gradle 会将其视为 JUnit 或 TestNG 测试。如果类扩展了 TestCase 或 GroovyTestCase 或用 @RunWith ,Gradle 会将其作为 JUnit 测试处理。不检查抽象类。

我们可以使用 test 任务的 scanForTestClasses 属性禁用此自动检查。如果我们将该属性设置为 false,Gradle 将使用隐式的 /Tests.class 和 /*Test.class 包含规则和 */Abstract*.class排除规则。

我们还可以设置自己的包含和排除规则来查找测试。我们可以使用 test 任务的 include() 方法来定义我们自己的测试类规则。如果我们想排除某些类文件,可以使用 exclude()方法来定义排除规则。或者,我们可以使用 includes 和 excludes 属性。

在以下构建文件中,我们将禁用测试类的自动类检查,并明确设置测试类的包含和排除规则:

apply plugin: 'java' 
 
repositories { 
    jcenter() 
} 
 
dependencies { 
    testCompile('junit:junit:4.12') 
} 
 
test { 
    // Disable automatic scanning 
    // for test classes. 
    scanForTestClasses = false 
 
    // Include test classes. 
    include('**/*Test.class', '**/*Spec.class') 
 
    // Exclude test classes. 
    exclude('**/Abstract*.class', '**/Run*.class') 
} 

Logging test output

我们已经注意到,如果我们简单地运行 test 任务在  测试我们的项目部分。我们必须将日志级别设置为 info 或 debug 以获取有关测试生成的输出的更多信息。我们可以通过testLogging 属性配置 test 任务以显示更多输出。此属性属于 org.gradle.api.tasks.testing.logging.TestLoggingContainer 类型。我们可以为每个日志级别设置不同的选项。如果我们不指定日志级别,则 lifecyle 日志级别是隐含的。

TestLoggingContainer 有 showStandardStreams 选项,我们可以设置为 true 或 false。如果我们将属性的值设置为 true,我们将得到 System.out和 的输出class="literal">System.err 当我们运行 test 任务时。默认值为 false,然后我们看不到执行的测试的输出。

我们还可以使用 events() 方法来设置记录在命令行输出中的事件。例如,我们可以配置我们也想查看通过的测试,将 String 值作为参数传递。我们可以使用  standardOut 和  standardError 参数来获得与  showStandardStreams 属性。其他有效参数是 failed、 started和 skippedrr .

如果测试失败,我们只会看到失败的测试的行号。要为失败的测试获得更多输出,我们可以将 exceptionFormat 选项设置为 full。然后我们得到异常消息,例如断言失败消息。默认值为 short,仅显示行号。使用 stackTraceFilters 属性,我们可以确定记录了多少堆栈跟踪。

我们还可以通过minGranularity来设置日志消息的最大和 最小粒度 和 maxGranularity 属性。我们使用 Gradle 生成的测试套件的值,1 用于每个测试 JVM 生成的测试套件,2 用于测试类和 3 用于 test 方法。

以下示例构建文件设置了一些可用的选项:

apply plugin: 'java' 
 
repositories { 
    jcenter() 
} 
 
dependencies { 
    testCompile('junit:junit:4.12') 
} 
 
test { 
    // Set exception format to full 
    // instead of default value 'short'. 
    testLogging.exceptionFormat 'full' 
 
    // We can also use a script block to configure 
    // the testLogging property. 
    testLogging { 
 
        // No log level specified so the 
        // property is set on LIFECYCLE log level. 
        // We can pass arguments to determine 
        // which test events we want to see in the 
        // command-line output. 
        events 'passed' 
 
        // Show logging events for test methods. 
        minGranularity = 3 
 
        // All valid values for the stackTrace output. 
        stackTraceFilters 'groovy', 'entry_point', 'truncate' 
 
        // Show System.out and System.err output 
        // from the tests. 
        showStandardStreams = true 
 
        // Configure options for DEBUG log level. 
        debug { 
            events 'started' 
        } 
    } 
} 

Changing the test report directory

我们已经看到了在 build/reports/tests 目录中运行测试时生成的 HTML 报告。要更改目录名称,我们可以将 testReportDir 属性设置为 Project 对象的一部分。

除了生成的 HTML 报告之外,我们还有由 test 任务生成的带有测试结果的 XML 文件。这些 XML 文件实际上是生成的 HTML 报告的输入。有很多工具可以使用 JUnit 或 TestNG 生成的 XML 文件并对其进行分析。我们可以在 build/test-results 目录中找到这些文件。如果我们想在不同的目录中生成 HTML 报告,我们必须创建一个新的 TestReport 任务。  TestReports 任务具有用于更改 HTML 报告的输出目录的属性。要更改 HTML 输出目录,我们设置  destinationDir 属性。我们使用 testResultsDir 属性来引用带有 XML 输出的一个或多个目录。

为了禁用测试报告的生成,我们将 reports.enabled 属性设置为 false

以下构建文件显示了如何更改报告目录:

apply plugin: 'java' 
 
repositories { 
    jcenter() 
} 
 
dependencies { 
    testCompile('junit:junit:4.12') 
} 
 
task testReport(type: TestReport) { 
    destinationDir = file("$buildDir/test-reports") 
    testResultDirs = files("$buildDir/test-results") 
    reportOn(test) 
} 
 
// If the test task is finished, 
// we want the testReport to be executed. 
test.finalizedBy(testReport) 

当我们从命令行执行 test 任务时,我们可以看到执行任务的顺序:

:clean
:compileJava
:processResources
:classes
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test
:testReport
BUILD SUCCESSFUL
Total time: 1.285 secs

Running Java applications


如果我们想从 Gradle 构建中执行 Java 可执行文件,我们有几个选择。在我们探索这些选项之前,我们将首先在我们的项目中创建一个带有 main() 方法的新 Java 类。我们将从我们的构建文件中执行这个 Java 类。

src/main/java/gradle/sample 目录下,我们需要新建一个 SampleApp.java 文件。以下代码清单显示了文件的内容。我们将使用我们的 Sample 类将 getWelcomeMessage() 方法的值打印到 System.out:

// File: src/main/java/gradle/sample/SampleApp.java 
package gradle.sample; 
 
public class SampleApp { 
 
    public SampleApp() { 
    } 
 
    public static void main(String[] args) { 
        final SampleApp app = new SampleApp(); 
        app.welcomeMessage(); 
    } 
 
    public void welcomeMessage() { 
        final String welcomeMessage = readMessage(); 
        showMessage(welcomeMessage); 
    } 
 
    private String readMessage() { 
        final Sample sample = new Sample(); 
        final String message = sample.getWelcomeMessage(); 
        return message; 
    } 
 
    private void showMessage(final String message) { 
        System.out.println(message); 
    } 
} 

要运行我们的 SampleApp 类,我们可以使用  javaexec() 方法,它是 Gradle 的 项目类。我们还可以在构建文件中使用 JavaExec 任务。最后,我们可以使用应用程序插件来运行我们的 SampleApp 类。在下一章中,我们将讨论如何使用 javaexec方法。

Running an application from a project

在我们的构建文件中始终可用的 Project 类具有 javaexec() 方法。使用这个方法,我们可以执行一个 Java 类。该方法接受一个用于配置 org.gradle.process.JavaExecSpec对象的闭包。 JavaExecSpec有几个我们可以用来配置需要执行的主类、可选参数和系统属性的方法和属性。许多选项与运行测试的选项相同。

我们将创建一个新的构建文件并使用 javaexec() 方法运行我们的 SampleApp 类和一些额外的选项:

apply plugin: 'java' 
 
task runJava(dependsOn: classes, 
    description: 'Run gradle.sample.SampleApp') << { 
 
    javaexec { 
        // Java main class to execute. 
        main = 'gradle.sample.SampleApp' 
 
        // We need to set the classpath. 
        classpath sourceSets.main.runtimeClasspath 
 
        // Extra options can be set. 
        maxHeapSize = '128m' 
        systemProperty 'sysProp', 'notUsed' 
        jvmArgs '-client' 
    } 
 
} 
repositories { 
    jcenter() 
} 

要运行我们的 Java 类,我们将从命令行执行 runJava 任务并获得以下输出:

$ gradle runJava
:compileJava
:processResources
:classes
:runJava
Welcome to Gradle.
BUILD SUCCESSFUL
Total time: 0.761 secs

Running an application as a task

除了 javaexec() 方法,我们还可以定义一个新的 org.gradle.api.tasks.JavaExec 任务。要配置任务,我们可以使用与 javaexec() 方法相同的方法和属性。

在下面的示例构建文件中,我们将创建 JavaExec 类型的 runJava 任务。我们将配置任务以设置类路径和主类。此外,我们将看到如何添加其他属性并调用其他方法来进一步配置 Java 类的执行,如下所示:

apply plugin: 'java' 
 
task runJava(type: JavaExec) { 
    dependsOn classes 
    description = 'Run gradle.sample.SampleApp' 
 
    // Java main class to execute. 
    main = 'gradle.sample.SampleApp' 
 
    // We need to set the classpath. 
    classpath sourceSets.main.runtimeClasspath 
 
    // Extra options can be set. 
    systemProperty 'sysProp', 'notUsed' 
    jvmArgs '-client' 
 
    // We can pass arguments to the main() method 
    // of gradle.sample.SampleApp. 
    args 'mainMethodArgument', 'notUsed' 
} 

如果我们运行任务,我们会得到以下输出:

$ gradle runJava
 :compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:runJava
Welcome to Gradle.
BUILD SUCCESSFUL
 Total time: 0.686 secs

Running an application with the application plugin

运行 Java 应用程序的另一种方法是使用应用程序插件。应用程序插件将功能添加到我们的构建文件中,以便运行 Java 应用程序并捆绑 Java 应用程序以进行分发。

要使用应用程序插件,我们必须使用 apply() 方法将插件添加到我们的构建文件中。添加插件后,我们可以使用  mainClassName 属性设置要执行的主类。这一次,我们不必自己创建新任务。该插件添加了 run 任务,我们可以调用它来运行 Java 应用程序。

示例构建文件使用应用程序插件来运行我们的 SampleApp 类,如下所示:

apply plugin: 'application' 
 
mainClassName = 'gradle.sample.SampleApp' 
 
// Extra configuration for run task if needed. 
run { 
    // Extra options can be set. 
    systemProperty 'sysProp', 'notUsed' 
    jvmArgs '-client' 
 
    // We can pass arguments to the main() method 
    // of gradle.sample.SampleApp. 
    args 'mainMethodArgument', 'notUsed' 
} 

我们可以调用 run 任务并检查 SampleApp 类的输出:

$ gradle run
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:run
Welcome to Gradle.
BUILD SUCCESSFUL
Total time: 0.816 secs

请注意,我们不再需要设置 classpath 属性。插件自动包含项目的 runtimeClasspath对象来执行Java类。

Creating a distributable application archive

使用应用程序插件,我们还可以使用我们的 Java 应用程序构建一个发行版。这意味着我们可以分发应用程序,人们可以在没有 Gradle 的情况下运行 Java 应用程序。该插件将创建必要的特定于操作系统的启动脚本并打包所有必要的类和依赖项。

下表显示了我们可以与应用程序插件一起使用来构建分发的额外任务:

任务

取决于

类型

说明

startScripts

jar

CreateStartScripts

这将创建特定于操作系统的脚本来运行 Java 应用程序

installDist

jarstartScripts

同步

这会将应用程序安装到目录中

distZip

jarstartScripts

邮编

这将创建一个完整的分发 ZIP 存档,包括运行 Java 应用程序所需的所有文件

distTar

jarstartScripts

Tar

这将创建一个完整的分发 TAR 存档,包括运行 Java 应用程序所需的所有文件

所有任务都依赖于 Jar 任务。为了获得有意义的 JAR 文件名,我们在构建文件中设置了 archivesBaseName 和 version 属性,如下所示:

apply plugin: 'application' 
 
mainClassName = 'gradle.sample.SampleApp' 
 
archivesBaseName = 'gradle-sample' 
version = '1.0' 

为了创建启动脚本,我们调用 createScript 任务。在我们执行完任务后,我们有两个文件, sample 和 sample.bat,在 构建/脚本 目录。  sample.bat 文件适用于 Windows 操作系统, sample 适用于其他操作系统,例如 Linux 或 OS X。

要将运行应用程序所需的所有文件放在单独的目录中,我们必须运行 installDist 任务。当我们执行任务时,我们在 build/install目录下得到一个 sample目录。示例目录有一个 bin 目录,其中包含启动脚本和一个 lib 目录,其中包含 JAR 文件,其中包含 < code class="literal">SampleApp 应用程序。我们可以切换到 build/install/sample目录,然后调用 bin/sample或者 bin/sample.bat 来运行我们的应用程序:

$ gradle installDist
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:startScripts UP-TO-DATE
:installDist
BUILD SUCCESSFUL
Total time: 0.629 secs
$ cd build/install/sample
$ bin/sample
Welcome to Gradle.

要创建一个包含所有必要文件的 ZIP 存档,以使其他人能够运行该应用程序,我们运行 distZip 任务。生成的 ZIP 存档可以在 build/distributions 目录中找到。我们可以分发这个 ZIP 文件,人们可以在他们的计算机上解压缩存档以运行 Java 应用程序,如下所示:

$ gradle distZip
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:startScripts UP-TO-DATE
:distZip
BUILD SUCCESSFUL
Total time: 0.602 secs
$ jar tvf build/distributions/sample-1.0.zip
0 Wed Dec 16 10:23:42 CET 2015 app-1.0/
0 Wed Dec 16 10:23:42 CET 2015 app-1.0/lib/
2064 Wed Dec 16 10:18:54 CET 2015 app-1.0/lib/gradle-sample-1.0.jar
0 Wed Dec 16 10:23:42 CET 2015 app-1.0/bin/
4874 Wed Dec 16 10:18:54 CET 2015 app-1.0/bin/sample
2329 Wed Dec 16 10:18:54 CET 2015 app-1.0/bin/sample.bat

要创建 Tar 存档,我们使用 distTar 任务。

如果我们想将其他文件添加到发行版中,我们可以创建 src/dist 目录并将文件放置在其中。  src/dist 目录中的所有文件都包含在分发 ZIP 存档中。要包含来自另一个目录的文件,我们可以使用 applicationDistribution 复制规范。

以下示例构建文件使用 applicationDistribution 复制规范来包含 docs 任务的输出。 Gradle 会在调用 distZip 任务之前自动执行 docs 任务:

apply plugin: 'application' 
 
archivesBaseName = 'gradle-sample' 
version = '1.0' 
 
mainClassName = 'gradle.sample.SampleApp' 
 
task docs { 
    ext { 
        docsDir = 'docs' 
        docsResultDir = file("$buildDir/$docsDir") 
    } 
 
    // Assign directory to task outputs. 
    outputs.dir docsResultDir 
 
    doLast { 
        docsResultDir.mkdirs() 
        new File(docsResultDir, 'README').write('Please read me.') 
    } 
 
} 
 
applicationDistribution.from(docs) { 
    // Directory in distribution ZIP archive. 
    into 'docs' 
} 

Publishing artifacts


一个软件项目可以包含我们想要发布的工件。工件可以是 ZIP 或 JAR 归档文件或任何其他文件。在 Gradle 中,我们可以为一个项目定义多个工件。我们可以在中央存储库中发布这些工件,以便其他开发人员可以在他们的项目中使用我们的工件。这些中央存储库可以在公司内部网、网络驱动器上或通过 Internet 获得。

在 Gradle 中,我们通过配置对工件进行分组,就像依赖项一样。配置可以包含依赖项和工件。如果我们将 Java 插件添加到我们的项目中,那么每个配置还有两个额外的任务来构建和上传属于该配置的工件。构建工件的任务称为build ,上传工件的任务称为 upload<configurationName> .

Java 插件还添加了可用于分配工件的 archives 配置。 Java 项目的默认 JAR 工件已分配给此配置。我们可以为我们的项目分配更多的工件到这个配置。我们还可以添加新配置来分配项目中的工件。

对于我们的 Java 项目,我们将定义以下示例构建文件:

apply plugin: 'java' 
 
archivesBaseName = 'gradle-sample' 
version = '1.0' 

当我们使用 Java 插件时,我们有 archives 配置可用。当我们执行 buildArchives任务时,我们的Java代码被编译并在 build/libs目录下创建一个JAR文件,称为 gradle-sample-1.0.jar

要发布我们的 JAR 文件,我们可以执行 uploadArchives 任务,但我们必须首先配置发布工件的位置。我们为依赖项定义的存储库不用于上传工件。我们必须在 uploadArchives 任务中定义上传存储库。我们可以引用我们项目中已经定义的存储库,也可以在任务中定义存储库。

以下示例构建文件在项目级别和任务级别定义了上传存储库:

apply plugin: 'java' 
 
archivesBaseName = 'gradle-sample' 
version = '1.0' 
 
repositories { 
    flatDir { 
        name 'uploadRepository' 
        dirs 'upload' 
    } 
} 
 
uploadArchives { 
    repositories { 
        // Use repository defined in project 
        // for uploading the JAR file. 
        add project.repositories.uploadRepository 
 
        // Extra upload repository defined in 
        // the upload task. 
        flatDir { 
            dirs 'libs' 
        } 
    } 
} 

如果我们调用 uploadArchives 任务,则会创建 JAR 文件并将其复制到 libs 和 上传 目录在我们的项目根目录中。  ivy.xml 配置文件也被创建并复制到目录中,如下:

$ gradle uploadArchives
:compileJava
:processResources
:classes
:jar
:uploadArchives
BUILD SUCCESSFUL
Total time: 0.753 secs
$ ls libs/
gradle-sample-1.0.jar
gradle-sample-1.0.jar.sha1
ivy-1.0.xml
ivy-1.0.xml.sha1
$ ls upload/
gradle-sample-1.0.jar
gradle-sample-1.0.jar.sha1
ivy-1.0.xml
ivy-1.0.xml.sha1

我们可以使用所有 Ivy 解析器来定义上传存储库。

Uploading our artifacts to a Maven repository

如果我们要上传到 Maven 仓库,我们必须创建一个 Maven 项目对象模型 ( POM) 文件。 Maven POM 文件包含有关我们的工件的所有必要信息。 Gradle 可以为我们生成 POM 文件。我们必须将 Maven 插件添加到我们的项目中才能使其工作。

我们必须通过 mavenDeployer() 方法的闭包参数为我们的 uploadArchives 任务配置存储库。在以下示例构建文件中,我们将使用 file 协议定义一个 Maven 存储库:

apply plugin: 'java' 
apply plugin: 'maven' 
 
archivesBaseName = 'gradle-sample' 
group = 'gradle.sample' 
version = '1.0' 
 
uploadArchives { 
    repositories { 
        mavenDeployer { 
            repository(url: 'file:./maven') 
        } 
    } 
} 

请注意,我们设置了项目的 group 属性,以便它可以用作 Maven POM 的 groupId。  version 属性用作 version 和 archivesBaseName 属性用作工件 ID。我们可以调用 uploadArchives 任务来部署我们的工件,如下所示:

$ gradle uploadArchives
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:uploadArchives
BUILD SUCCESSFUL
Total time: 1.121 secs
$ ls maven/gradle/sample/gradle-sample/1.0/
gradle-sample-1.0.jar
gradle-sample-1.0.jar.md5
gradle-sample-1.0.jar.sha1
gradle-sample-1.0.pom
gradle-sample-1.0.pom.md5
gradle-sample-1.0.pom.sha1

生成的gradle-sample-1.0.pom POM文件内容如下:

<?xml version="1.0" encoding="UTF-8"?> 
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 
  <modelVersion>4.0.0</modelVersion> 
  <groupId>gradle.sample</groupId> 
  <artifactId>gradle-sample</artifactId> 
  <version>1.0</version> 
</project> 

Gradle 使用本机 Maven ANT 任务将工件部署到 Maven 存储库。支持 file 协议,无需任何额外配置;但是如果我们想使用其他协议,我们必须配置这些协议所依赖的库:

协议

http

org.apache.maven.wagon:wagon-http:1.0-beta-2

ssh

org.apache.maven.wagon:wagon-ssh:1.0-beta-2

ssh-external

org.apache.maven.wagon:wagon-ssh-external:1.0-beta-2

scp

org.apache.maven.wagon:wagon-scp:1.0-beta-2

ftp

org.apache.maven.wagon:wagon-ftp:1.0-beta-2

webdav

org.apache.maven.wagon:wagon-webdav-jackrabbit:1.0-beta-2

文件

-

在以下示例构建文件中,我们将使用 scp 协议来定义一个 Maven 存储库并使用它来上传项目的工件:

apply plugin: 'java' 
apply plugin: 'maven' 
 
archivesBaseName = 'gradle-sample' 
group = 'gradle.sample' 
version = '1.0' 
 
configurations { 
    mavenScp 
} 
 
repositories { 
    jcenter() 
} 
 
dependencies { 
    mavenScp 'org.apache.maven.wagon:wagon-scp:1.0-beta-2' 
} 
 
uploadArchives { 
    repositories { 
      mavenDeployer { 
        configuration = configurations.mavenScp 
          repository(url: 'scp://localhost/mavenRepo') { 
            authentication(userName: 'user', privateKey: 'id_sha') 
            } 
        } 
    } 
} 

Maven 插件还将 install 任务添加到我们的项目中。使用 install 任务,我们可以将工件安装到我们的本地 Maven 存储库。 Gradle 将使用本地 Maven 存储库的默认位置或在 settings.xml Maven 文件中定义的位置。

Working with multiple artifacts

到目前为止,我们已将单个工件上传到存储库。在 Gradle 项目中,我们可以定义多个工件并部署它们。我们需要定义一个归档任务并将其分配给一个配置。我们将使用 artifacts{} 脚本块来定义配置闭包,以便将工件分配给配置。然后,当我们执行 upload 任务时,工件会被部署到存储库。

在以下示例构建中,我们将使用源代码和 Javadoc 文档创建 JAR 文件。我们将把两个 JAR 文件作为工件分配给 archives 配置:

apply plugin: 'java' 
 
archivesBaseName = 'gradle-sample' 
version = '1.0' 
 
// New task to archive the source files. 
task sourcesJar(type: Jar) { 
    classifier = 'sources' 
    from sourceSets.main.allSource 
} 
 
// New task to archive the documentation. 
task docJar(type: Jar, dependsOn: javadoc) { 
    classifier = 'docs' 
    from javadoc.destinationDir 
} 
 
artifacts { 
    // Assign the output of the sourcesJar 
    // task to the archives configuration. 
    archives sourcesJar 
 
    // Assign the output of the docJar 
    // task to the archives configuration. 
    archives docJar 
} 
 
uploadArchives { 
    repositories { 
        flatDir { 
            dirs 'upload' 
        } 
    } 
} 

Signing artifacts

我们可以使用签名插件在 Gradle 中对工件进行数字签名。该插件仅支持生成 Pretty Good Privacy (PGP) 签名,这是发布到 Maven Central Repository 所需的签名格式。要创建 PGP 签名,我们必须在我们的计算机上安装一些 PGP 工具。每个操作系统的工具安装都不同。使用 PGP 软件,我们需要创建一个可用于签署我们的工件的密钥对。

我们需要使用有关我们的密钥对的信息来配置签名插件。我们需要公钥的十六进制表示,使用我们的私钥的密钥环文件的路径,以及用于保护私钥的密码。这些属性的值分配给 signing.keyId、 signing.secretKeyRingFile 和 signing.password Gradle 项目属性。这些属性的值最好保密,因此最好将它们存储在我们的 Gradle 用户目录下的gradle.properties 文件中,并对文件应用安全文件权限。最好将文件设置为对单个用户只读。

以下 gradle.properties 示例文件设置了签名属性。显示的属性值是示例值。对于其他用户,这些将有所不同:

signing.keyId=4E12C354 
signing.secretKeyRingFile=/Users/current/.gnupg/secring.gpg 
signing.password=secret phassphrase 

我们已准备好签署我们的工件。我们需要配置我们想要签名的工件。签名插件有一个 DSL,我们可以使用它来定义我们想要签名的任务或配置。

在我们的示例 Java 项目中,我们有 archives 配置和项目的工件。要对工件进行签名,我们可以使用 signing() 方法和闭包来配置 归档的所有工件配置需要签名。以下示例构建文件显示了我们如何做到这一点:

apply plugin: 'java' 
apply plugin: 'signing' 
 
archivesBaseName = 'gradle-sample' 
version = '1.0' 
 
signing { 
    sign configurations.archives 
} 

签名插件向我们的项目添加了一个新的 signArchives 任务,因为我们已经配置了要对 archives 配置进行签名的配置。签名插件将具有 sign<configurationName> 模式的任务添加到我们的项目中,用于我们配置要签名的每个配置。

我们可以调用 signArchives 任务来签署我们的 JAR 工件或使用 Jar 任务,它自动依赖于 signArchives 任务,如下:

$ gradle signArchives
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:signArchives
BUILD SUCCESSFUL


Total time: 1.649 secs
$ ls build/libs/gradle-sample-1.0.jar*
build/libs/gradle-sample-1.0.jar
build/libs/gradle-sample-1.0.jar.asc

请注意,gradle-sample-1.0.jar.asc 签名文件放置在工件旁边。

如果我们要签名的工件不是配置的一部分,我们可以使用签名 DSL 来配置要签名的任务。该任务必须创建一个存档文件才能用于签名。在我们配置了要签名的任务后,签名插件会添加一个具有  sign<taskName> 命名模式的新任务。我们可以执行这个任务来签署配置任务的输出。

以下构建文件具有 sourcesJar 任务,用于使用我们项目的源文件创建新存档。我们将使用签名 DSL 来配置我们的签名任务:

apply plugin: 'java' 
apply plugin: 'signing' 
 
archivesBaseName = 'gradle-sample' 
version = '1.0' 
 
task sourcesJar(type: Jar) { 
    classifier = 'sources' 
    from sourceSets.main.allSource 
} 
 
signing { 
    sign sourcesJar 
} 

我们可以调用 signSourcesJar 任务来使用我们项目的源对我们的 JAR 文件进行数字签名。生成的签名文件放在 build/libs目录中的JAR文件旁边。我们还可以调用 assemble 任务来创建数字签名的 JAR 文件,因为该任务依赖于我们所有的存档任务,包括签名任务:

$ gradle signSourcesJar
:sourcesJar
:signSourcesJar
BUILD SUCCESSFUL
Total time: 0.87 secs
$ ls build/libs/gradle-sample-1.0-sources.jar*
build/libs/gradle-sample-1.0-sources.jar
build/libs/gradle-sample-1.0-sources.jar.asc

Packaging Java Enterprise Edition applications


在本章和上一章中,我们已经讨论了如何使用 Gradle 创建 ZIP、TAR 和 JAR 档案。在Java项目中,我们也可以将我们的应用打包成Web application Archive (WAR) 或 企业存档 (EAR)文件。对于 Web 应用程序,我们希望将应用程序打包为 WAR 文件,而 Java Enterprise Edition 应用程序可以打包为 EAR 文件. Gradle 还通过插件和任务支持这些类型的存档。

Creating a WAR file

要创建 WAR 文件,我们可以将 War 类型的新任务添加到我们的 Java 项目中。  War 任务的属性和方法与其他归档任务相同,例如 Jar。事实上, War 任务扩展了 Jar 任务。

War 任务有一个额外的 webInf() 方法来定义 WEB-INF WAR 文件中的目录。  webXml 属性可用于引用需要复制到 WAR 文件的 web.xml 文件。这只是包含 web.xml文件的另一种方式,我们也可以将 web.xml文件放在  ;WEB-INF 我们为 WAR 文件定义的根源目录的目录。

使用 classpath() 方法,我们可以定义一个依赖配置或目录,其中包含我们想要复制到 WAR 文件的库或类文件。如果文件是JAR或ZIP文件,则复制到 WEB-INF/lib目录,其他文件复制到  WEB-INF/classes 目录。

在下面的示例构建文件中,我们将定义一个新的 War 任务。我们将 WAR 文件内容的根目录设置为 src/main/webapp 目录。我们使用 webInf()和 classpath()方法来自定义WEB-INF的内容, < code class="literal">WEB-INF/classes 和WEB-INF/lib 文件夹。我们还用任务的 webXml属性设置了一个自定义的 web.xml文件,如下:

apply plugin: 'java' 
 
version = '1.0' 
 
// Custom archive task with 
// specific properties for a WAR archive. 
task war(type: War) { 
    dependsOn classes 
 
    from 'src/main/webapp' 
 
    // Files copied to WEB-INF. 
    webInf { 
        from 'src/main/webInf' 
    } 
 
    // Copied to WEB-INF/classes. 
    classpath sourceSets.main.runtimeClasspath 
 
    // Copied to WEB-INF/lib. 
    classpath fileTree('libs') 
 
    // Custom web.xml. 
    webXml = file('  ') 
    baseName = 'gradle-webapp' 
} 
 
assemble.dependsOn war 

要创建 WAR 文件,我们可以执行 War 或 assemble 任务。 War 任务作为任务依赖项添加到assemble 任务中。这就是为什么如果我们调用 assemble 任务,Gradle 将执行 War 任务。一旦我们执行了任务, gradle-webapp-1.0.war WAR 文件就会在 build/libs 中创建目录:

$ gradle war
:compileJava
:processResources
:classes
:war
BUILD SUCCESSFUL
Total time: 0.727 secs
$ ls build/libs
gradle-webapp-1.0.war

Creating an EAR file

要创建 EAR 文件,我们可以创建 Ear 类型的新任务。此任务与 Jar 任务具有相同的属性和方法。  Ear 任务扩展了 Jar 任务。

通过lib()方法,我们可以在EAR文件中定义需要复制到lib目录的文件.

以下构建文件有一个简单的 Ear 任务:

apply plugin: 'java' 
 
version = '1.0' 
 
// Create custom archive task 
// with specific properties to 
// create an EAR archive file. 
task ear(type: Ear) { 
    from 'src/main/application' 
 
    lib { 
        from fileTree('earLibs') 
    } 
 
    baseName = 'gradle-enterprise-app' 
} 
 
assemble.dependsOn ear 

我们可以执行 Ear 任务并在 build/libs目录下查看结果 gradle-enterprise-app-1.0.ear 文件:

$ gradle ear
:ear
BUILD SUCCESSFUL
Total time: 0.694 secs
$ ls build/libs
gradle-enterprise-app-1.0.ear

Summary


在本章中,我们讨论了如何从 Gradle 构建运行 JUnit 或 TestNG 测试。我们还了解了如何获取通过执行测试生成的测试结果和报告。

通过应用程序插件,我们讨论了如何创建一个可分发的 ZIP 文件,其中包含运行我们构建的 Java 应用程序所需的所有代码和脚本。

我们还讨论了如何将我们的项目工件上传到存储库,以便其他项目可以使用我们的代码。我们已经看到我们可以使用 Gradle 创建一个可以上传到 Maven 存储库的工件。

为了对我们的工件进行数字签名,我们了解了如何将签名插件与本地安装的 PGP 工具一起使用。

此外,我们还了解了如何使用 warear 插件来使用 Gradle 创建 Web 和企业应用程序。我们可以使用任务、方法和配置属性来配置打包输出。

在下一章中,我们将了解如何使用 Gradle 运行和创建一个多模块项目。我们还将讨论如何在项目之间创建依赖关系,并一次将通用配置应用于多个项目。