应用构建

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>.jarJAR
  • 一个javadoc任务,为main类生成Javadoc

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

Source sets

GradleJava支持是第一个为构建基于源的项目引入新概念的: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只会直接在srctest中搜索相应的源代码。如果你不想覆盖这个惯例,而只是想增加一个额外的源码目录,也许是包含一些你想保持独立的第三方源码的目录呢?语法是类似的。

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

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

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

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

Processing non-source files for a source set

像以前一样,阴影框代表Source sets的属性,在这种情况下,它包括资源文件的位置和它们被复制到哪里。除了主Source setsJava库插件还定义了一个测试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任务自动构建的。此外,如果需要的话,该插件可以被配置为提供javadocJarsourcesJar任务来打包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、WarEar任务的每个实例都有一个清单属性,允许您自定义进入相应存档的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()
                }
            }
        }
    }
}
上一页