应用构建

Java & JVM 应用构建

一个 Java 项目的最简单的构建脚本应用了 Java 库插件,并可选择设置项目版本和选择要使用的 Java 工具链。

plugins {
    id 'java-library'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
    }
}

version = '1.2.1'

通过应用 Java 库插件,你可以得到一大堆的功能。

  • 一个 compileJava 任务,编译src/main/java下的所有 Java 源文件。
  • src/test/java 下的源文件制定的 compileTestJava 任务
  • 一个test任务,运行src/test/java下的测试。
  • 一个 jar 任务,将src/main/resources下的 main 编译的类和资源打包成一个名为<project>-<version>.jar的 JAR。
  • 一个javadoc任务,为main类生成 Javadoc。

这并不足以构建任何非复杂的 Java 项目,至少,你可能会有一些文件的依赖性。但这意味着你的构建脚本只需要你的项目所特有的信息。

Source sets

Gradle 的 Java 支持是第一个为构建基于源的项目引入新概念的:Source sets。其主要思想是,源文件和资源通常按类型进行逻辑分组,如应用程序代码、单元测试和集成测试。每个逻辑组通常都有自己的文件依赖性、classpaths 等的集合。值得注意的是,构成源文件集的文件不一定要位于同一个目录中,这一点很重要。

Source sets 是一个强大的概念,它把编译的几个方面联系在一起。

  • 源文件和它们的位置
  • 编译的 classpath,包括任何必要的依赖关系
  • 编译后的类文件的位置

Source sets and Java compilation

阴影框代表 Source sets 本身的属性。在此基础上,Java 库插件为你或插件定义的每个 Source sets 自动创建一个编译任务,名为 compileSourceSetJava 和几个依赖配置。

// Replaces conventional source code directory with list of different directories
sourceSets {
    main {
        java {
            srcDirs = ['src']
        }
    }
    // Replaces conventional test source code directory with list of different directories
    test {
        java {
            srcDirs = ['test']
        }
    }
}

// Changes project output property to directory out
buildDir = 'out'

现在 Gradle 只会直接在 src 和 test 中搜索相应的源代码。如果你不想覆盖这个惯例,而只是想增加一个额外的源码目录,也许是包含一些你想保持独立的第三方源码的目录呢?语法是类似的。

sourceSets {
    main {
        java {
            srcDir 'thirdParty/src/main/java'
        }
    }
}

我们也可以指定直接在项目根目录下存放源代码文件:

sourceSets {
    main {
        java {
            srcDirs += ["$projectDir"]
            srcDirs += ["$projectDir/cadex"]
        }
    }
}

Java 项目通常包括源文件以外的资源,如属性文件,这些资源可能需要处理,例如替换文件中的令牌并打包到最终的 JAR 中。Java 库插件通过为每个定义的 Source sets 自动创建一个专门的任务来处理这个问题,这个任务叫做 processSourceSetResources(或者主 Source sets 的 processResources)。下图显示了 Source sets 是如何与这个任务配合的。

Processing non-source files for a source set

像以前一样,阴影框代表 Source sets 的属性,在这种情况下,它包括资源文件的位置和它们被复制到哪里。除了主 Source sets,Java 库插件还定义了一个测试 Source sets,代表项目的测试。这个 Source sets 被测试任务所使用,它运行测试。你可以在 Java 测试章节中了解更多关于这个任务和相关主题。

项目通常将这个 Source sets 用于单元测试,但如果你愿意,你也可以将它用于集成、验收和其他类型的测试。另一种方法是为你的每个其他测试类型定义一个新的 Source sets,这通常是出于以下一个或两个原因。为了美观和可管理性,你想保持测试彼此分离,不同的测试类型需要不同的编译或运行时 classpaths 或其他一些设置上的差异

依赖管理

绝大多数的 Java 项目都依赖于库,所以管理项目的依赖关系是建立 Java 项目的一个重要部分。依赖关系管理是一个很大的话题,所以我们在这里将重点介绍 Java 项目的基础知识。为你的 Java 项目指定依赖关系只需要三条信息。

  • 你需要哪个依赖,例如名称和版本
  • 需要它做什么,比如说编译或运行
  • 在哪里可以找到它

前两个在依赖关系 {} 块中指定,第三个在资源库 {} 块中指定。例如,要告诉 Gradle 你的项目需要 3.6.7 版的 Hibernate Core 来编译和运行生产代码,并且要从 Maven Central 仓库下载该库,你可以使用下面的片段。

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.hibernate:hibernate-core:3.6.7.Final'
}

这三个要素的 Gradle 术语如下。

  • 仓库"(例如:“mavenCentral()")–在那里可以找到你声明为依赖的模块
  • 配置”(ex: implementation)–命名的依赖关系集合,为特定目标(如编译或运行模块)而分组–是 Maven 作用域的更灵活形式
  • 模块坐标"(例如:org.hibernate:hibernate-core-3.6.7.Final)–依赖关系的 ID,通常采用"<组>:<模块>:<版本>“的形式(或 Maven 术语中的”<组ID>:<工件ID>:<版本>")。

你可以找到一份更全面的依赖性管理术语表这里

就配置而言,主要有以下几种。

  • compileOnly–用于编译生产代码所需的依赖项,但不应该成为运行时 classpath 的一部分。
  • implementation (取代compile) - 用于编译和运行时。
  • runtimeOnly (取代runtime) - 只在运行时使用,不用于编译
  • testCompileOnly - 与compileOnly相同,只是用于测试。
  • testImplementation - 测试相当于implementation
  • testRuntimeOnly–相当于runtimeOnly的测试。

编译

现在你可以构建你的项目了,java 插件添加了一个 build 任务到你项目中,build 任务编译你的代码、运行测试然后打包成 jar 文件,所有都是按序执行的。运行 gradle build 之后你的输出应该是类似这样的:

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

输出的每一行都表示一个可执行的任务,你可能注意到有一些任务标记为 UP_TO-DATE,这意味着这些任务被跳过了,gradle 能够自动检查哪些部分没有发生改变,就把这部分标记下来,省的重复执行。在大型的企业项目中可以节省不少时间。执行完 gradle build 之后项目结构应该是类似这样的:

在项目的根目录你可以找到一个 build 目录,这里包含了所有的输出,包含 class 文件,测试报告,打包的 jar 文件,以及一些用来归档的临时文件。如果你之前使用过 maven,它的标准输出是 target,这两个结构应该很类似。jar 文件目录 build/libs 下可以直接运行,jar 文件的名称直接由项目名称得来的。

自定义属性

Java 插件是一个非常固执的框架,对于项目很多的方面它都假定有默认值,比如项目布局,如果你看待世界的方法是不一样的,Gradle 给你提供了一个自定义约定的选项。想知道哪些东西是可以配置的?可以参考这个手册:http://www.gradle.org/docs/current/dsl/,之前提到过,运行命令行 gradle properties 可以列出可配置的标准和插件属性以及他们的默认值。

// Identifies project’sversion through a number scheme
version = 0.1

// Sets Java version compilation compatibility to 1.6
sourceCompatibility = 1.6

// Adds Main-Class header to JAR file’s manifest
jar {
    manifest {
        attributes 'Main-Class': 'com.manning.gia.todo.ToDoApp'
    }
}

打包成 JAR 之后,你会发现 JAR 文件的名称变成了 todo-app-0.1.jar,这个 jar 包含了 main-class 首部,你就可以通过命令 java -jar build/libs/todo-app-0.1.jar 运行了。

打包与发布

你如何打包并可能发布你的 Java 项目,取决于它是什么类型的项目:库、应用程序、Web 应用程序和企业应用程序都有不同的要求。默认情况下,Java Library Plugin 提供了 jar 任务,将所有编译好的生产类和资源打包成一个 JAR。这个 JAR 也是由 assemble 任务自动构建的。此外,如果需要的话,该插件可以被配置为提供 javadocJar 和 sourcesJar 任务来打包 Javadoc 和源代码。如果使用了一个发布插件,这些任务将在发布过程中自动运行,或者可以直接调用。

java {
    withJavadocJar()
    withSourcesJar()
}

如果你想创建一个 “超级”(又称 Fat)JAR,那么你可以使用这样的任务定义。

plugins {
    id 'java'
}

version = '1.0.0'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'commons-io:commons-io:2.6'
}

tasks.register('uberJar', Jar) {
    archiveClassifier = 'uber'

    from sourceSets.main.output

    dependsOn configurations.runtimeClasspath
    from {
        configurations.runtimeClasspath.findAll { it.name.endsWith('jar') }.collect { zipTree(it) }
    }
}

修改 Jar 包元定义

Jar、War 和 Ear 任务的每个实例都有一个清单属性,允许您自定义进入相应存档的 MANIFEST.MF 文件。下面的例子演示了如何在 JAR 的清单中设置属性。

jar {
    manifest {
        attributes("Implementation-Title": "Gradle",
                   "Implementation-Version": archiveVersion)
    }
}

您还可以创建 Manifest 的独立实例。这样做的一个原因是为了在 JAR 之间共享清单信息。下面的例子演示了如何在 JAR 之间共享共同属性。

ext.sharedManifest = manifest {
    attributes("Implementation-Title": "Gradle",
               "Implementation-Version": version)
}
tasks.register('fooJar', Jar) {
    manifest = project.manifest {
        from sharedManifest
    }
}

另一个可供您使用的选项是将舱单合并到一个舱单对象中。这些源清单的形式可以是文本,也可以是另一个 Manifest 对象。在下面的例子中,源清单都是文本文件,只有 sharedManifest 除外,它是前面例子中的清单对象。

tasks.register('barJar', Jar) {
    manifest {
        attributes key1: 'value1'
        from sharedManifest, 'src/config/basemanifest.txt'
        from(['src/config/javabasemanifest.txt', 'src/config/libbasemanifest.txt']) {
            eachEntry { details ->
                if (details.baseValue != details.mergeValue) {
                    details.value = baseValue
                }
                if (details.key == 'foo') {
                    details.exclude()
                }
            }
        }
    }
}
上一页