应用构建
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 本身的属性。在此基础上,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 是如何与这个任务配合的。
像以前一样,阴影框代表 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()
}
}
}
}
}