vlambda博客
学习文章列表

读书笔记《gradle-essentials》多国语言项目

Chapter 9. Polyglot Projects

我们生活在一个语言不够的时代。开发人员应该是多语种程序员,并为工作选择合适的工具。虽然这始终是一个主观决定,但我们会尝试根据执行速度、开发人员生产力、可用库和资源、团队对语言的舒适度等各种参数来选择语言和生态系统。

当我们已经承担了使用不同语言的认知负担时,Gradle 成为我们的好朋友,因为即使我们正在使用其他语言构建项目,我们也不必更改构建工具。我们甚至可以在同一个项目中使用多种语言,并让 Gradle 为整个项目编排构建。除了一系列基于 JVM 的语言之外,Gradle 还支持 C、C++、Objective C 等来生成原生应用程序。 Gradle 也是 Android 平台的官方构建工具。支持的语言列表正在增加。除了官方插件,还有很多社区支持的语言插件。

尽管在整本书中我们主要关注 Java 作为语言,但我们可以很好地使用 Groovy 或 Scala 来编写示例。 java 插件(连同 java-base 插件,由 java 应用 项目插件)为基于 JVM 的项目提供基本功能。 scalagroovy 等语言特定插件扩展了 java 插件以支持以一致的方式常见的成语。所以,一旦我们使用了 java 插件,我们就已经熟悉了 sourceSet 是什么,配置 的工作原理,如何添加库依赖等等,这些知识在我们使用这些语言插件时非常有用。在本章中,我们将了解如何通过添加 Groovy 或 Scala 轻松地为 Java 项目添加更多趣味。

The polyglot application


对于 代码示例,在本章中,让我们构建一个简单的每日报价< /span> 服务 根据一年中的某一天返回报价。由于我们商店中的报价可能较少,因此服务应该以循环方式重复报价。同样,像往常一样,我们将尝试使其尽可能简单,更多地关注构建方面而不是应用程序逻辑。我们将创建两个独立的 Gradle 项目来实现完全相同的功能,一次在 Groovy 中,然后在 Scala 中。

在进入特定语言的细节之前,让我们从定义 QotdService 接口开始,它只声明了一个方法,getQuote。合同是,只要我们通过相同的日期,我们应该得到相同的报价:

package com.packtpub.ge.qotd;

import java.util.Date;

interface QotdService {
  String getQuote(Date day);
}

实现 getQuote 的逻辑可以使用 Date以任何方式反对,例如使用包括时间在内的整个日期来确定报价。但是,为了简单起见,我们将在实现中仅使用 Date 对象的日期组件。此外,因为我们希望我们的接口对未来的实现开放,我们让 getQuoteDate 对象作为参数。

这个接口是我们将在两个项目中都有的 Java 文件。这只是为了演示 Java 和 Groovy/Scala 源代码在一个项目中的集成。

Building Groovy projects


让我们首先在Groovy 中实现QotdService 接口。此外,我们将编写一些单元测试以确保功能按预期工作。要启动项目,让我们创建如下目录结构:

qotd-groovy
├── build.gradle
└── src
    ├── main
    │   ├── groovy
    │   │   └── com
    │   │       └── packtpub
    │   │           └── ge
    │   │               └── qotd
    │   │                   └── GroovyQotdService.groovy
    │   └── java
    │       └── com
    │           └── packtpub
    │               └── ge
    │                   └── qotd
    │                       └── QotdService.java
    └── test
        └── groovy
            └── com
                └── packtpub
                    └── ge
                        └── qotd
                            └── GroovyQotdServiceTest.groovy

src/main/java 目录是 Java 源代码的默认目录。同样,默认使用 src/main/groovy 来编译 Groovy 源文件。同样,这只是一个约定,源目录的路径和名称可以通过 sourceSets 轻松配置。

让我们首先为我们的 Groovy 项目编写 构建脚本。在项目根目录中创建一个 build.gradle 文件,内容如下:

apply plugin: 'groovy'

repositories {
  mavenCentral()
}

dependencies {
  compile 'org.codehaus.groovy:groovy-all:2.4.5'
  testCompile 'junit:junit:4.11'
}

构建 Groovy 项目就像构建 Java 项目一样简单。我们不是应用 java 插件,而是应用 groovy 插件,它会自动应用 java 插件给我们。除了应用插件之外,我们还需要将 Groovy 添加为库依赖项,以便它可用于编译,也可在运行时使用。我们还在 testCompile 配置中添加 junit 以便它可用于单元测试。我们将 Maven central 声明为要使用的存储库,但这可以更改为任何可以服务于我们项目依赖项的有效存储库配置。

Note

Gradle 构建脚本是一个 Groovy DSL,部分 Gradle 是用 Groovy 编写的。但是,与 Gradle 本身在运行时依赖的任何其他库一样,Groovy 对我们正在构建的项目并不隐式可用。因此,我们必须明确将 Groovy 声明为项目依赖项,具体取决于我们是在生产源还是测试源中使用 Groovy。

Groovy 插件还负责编译项目中的 Java 源文件。让我们在 Groovy 中实现 QotdService 接口:

package com.packtpub.ge.qotd

class GroovyQotdService implements QotdService {
  List quotes

  GroovyQotdService(List quotes) {
    this.quotes = quotes
  }

  @Override
  String getQuote(Date day) {
    quotes[day[Calendar.DAY_OF_YEAR] % quotes.size()]
  }
}

服务的实现 接受构造函数中的引号列表。 getQuote 方法通过列表中的索引获取报价。为了确保计算的索引始终保持在报价大小的范围内,我们得到了一年中某一天的模数和列表的大小。

为了测试服务,让我们在 Groovy 中编写非常基本的 JUnit 测试用例:

package com.packtpub.ge.qotd

import org.junit.Before
import org.junit.Test

import static org.junit.Assert.assertEquals
import static org.junit.Assert.assertNotSame


public class GroovyQotdServiceTest {

  QotdService service
  Date today, tomorrow, dayAfterTomorrow

  def quotes = [
    "Be the change you wish to see in the world" +
      " - Mahatma Gandhi",
    "A person who never made a mistake never tried anything new" +
      " - Albert Einstein"
  ]

  @Before
  public void setup() {
    service = new GroovyQotdService(quotes)
    today = new Date()
    tomorrow = today + 1
    dayAfterTomorrow = tomorrow + 1
  }

  @Test
  void "return same quote for same date"() {
    assertEquals(service.getQuote(today), service.getQuote(today))
  }

  @Test
  void "return different quote for different dates"() {
    assertNotSame(service.getQuote(today),
      service.getQuote(tomorrow))
  }

  @Test
  void "repeat quotes"() {
    assertEquals(service.getQuote(today),
      service.getQuote(dayAfterTomorrow))
  }
}

我们在设置中准备 测试数据,每个测试用例确保报价服务的合同得到维护。由于报价单仅包含两个报价,因此应每隔一天重复一次。

我们可以使用以下代码从命令行运行测试:

$ gradle test

Building Scala projects


最后一节之后,从应用程序构建的角度来看,这节的大部分内容都是非常可预测的。因此,让我们快速了解一下它的要点。目录结构如下:

qotd-scala
├── build.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── com/packtpub/ge/qotd
    │   │                       └── QotdService.java
    │   └── scala
    │       └── com/packtpub/ge/qotd
    │                           └── ScalaQotdService.scala
    └── test
        └── scala
            └── com/packtpub/ge/qotd
                                └── ScalaQotdServiceTest.scala

所有 Scala 源文件都是从 src/main/scala 和 src/test/scala 读取的,除非使用 sourceSets。这一次,我们唯一需要应用的插件是 scala 插件,它就像 groovy 插件一样,隐式应用java 插件到我们的项目。让我们为这个项目编写 build.gradle 文件:

apply plugin: 'scala'

repositories {
  mavenCentral()
}

dependencies {
  compile 'org.scala-lang:scala-library:2.11.7'
  testCompile 'org.specs2:specs2-junit_2.11:2.4.15',
    'junit:junit:4.11'
}

在这里,我们必须提供 scala-library 作为依赖项。我们还添加了 specs2 作为测试配置的依赖项。我们正在使用 JUnit runner 进行测试。

Note

specs2 一个流行的 Scala 测试库,它支持单元测试和验收测试以及 BDD/TDD写作风格测试。更多信息可在http://etorreborre.github.io/specs2/

继续服务的Scala实现,我们可以如下实现它:

package com.packtpub.ge.qotd

import java.util.{Calendar, Date}

class ScalaQotdService(quotes: Seq[String]) extends QotdService {

  def getQuote(day: Date) = {
    val calendar = Calendar.getInstance()
    calendar.setTime(day)

    quotes(calendar.get(Calendar.DAY_OF_YEAR) % quotes.size)
  }
}

该实现不是非常惯用的 Scala,但这超出了本书的范围。该类在构造函数中采用引号 Seq 并以与 Groovy 对应物类似的方式实现 getQuote 方法。

现在服务已经实现,让我们通过编写单元测试来验证它是否符合 QotdService 的语义。为简洁起见,我们将仅介绍重要的测试用例:

package com.packtpub.ge.qotd

import java.util.{Calendar, Date}

import org.junit.runner.RunWith
import org.specs2.mutable._
import org.specs2.runner.JUnitRunner

@RunWith(classOf[JUnitRunner])
class ScalaQotdServiceTest extends SpecificationWithJUnit {

  def service = new ScalaQotdService(Seq(
    "Be the change you wish to see in the world" +
      " - Mahatma Gandhi",
    "A person who never made a mistake never tried anything new" +
      " - Albert Einstein"
  ))

  val today = new Date()
  val tomorrow = incrementDay(today)
  val dayAfterTomorrow = incrementDay(tomorrow)

  "Quote service" should {
    "return same quote for same day in multiple invocations" in {
      service.getQuote(today) must be(service.getQuote(today))
    }

    "return different quote for different days" in {
      service.getQuote(today) must not be (
        service.getQuote(tomorrow))
    }

    "repeat quote if total quotes are less than days in year" in {
      service.getQuote(today) must be(
        service.getQuote(dayAfterTomorrow))
    }
  }

  def incrementDay(date: Date) = {
    val cal = Calendar.getInstance()
    cal.setTime(date)
    cal.add(Calendar.DATE, 1)
    cal.getTime
  }
}

运行测试用例的任务与 Groovy 对应的任务相同。我们可以使用以下代码运行测试:

$ gradle test

Joint compilation


在本章前面的示例中,我们用Java 声明了一个接口,并分别用Groovy 和Scala 实现了它。这是可能的,因为 java 插件编译的类可用于 Groovy 和 Scala 类。

如果我们希望 Java 类能够访问 Groovy 或 Scala 类进行编译,那么我们必须使用支持的 联合编译 来编译 Java 源文件由各自的插件。 groovyscala插件都支持联合编译,可以编译Java源码。

要在 Java 类中引用 Groovy 类,最简单的方法是将相应的 Java 源文件移动到 src/main/groovy (或任何 Groovy srcDirssourceSets) 配置,Groovy 编译器在编译时使 Java 类可以使用 Groovy 类。 Scala 联合编译也是如此。我们可以将需要 Scala 类进行编译的 Java 文件放在任何 Scala srcDirs (src/main/scala 默认情况下)。

References


本章讨论的详细的语言插件官方文档可以在 以下网址:

Gradle 附带的各种语言和其他插件的官方文档链接可以在以下 URL 中找到:

https://docs.gradle.org/current/userguide/standard_plugins。 html

Summary


我们举了一个简单的示例问题,并在 Groovy 和 Scala 中实现了一个解决方案,以演示 Gradle 如何使多语言项目开发变得容易。我们没有深入探讨语言和插件特定的细节和差异,而是试图关注 Gradle 带来的共性和一致性。