1. 概述

在这个教程中,我们将学习如何根据Apache Avro规范生成Java类。首先,我们将介绍两种方法:使用现有的Gradle插件和在构建脚本中实现自定义任务。然后,我们将分析每种方法的优缺点,并了解它们适用的最佳场景。

2. 开始使用Apache Avro

我们的重点是将Apache Avro规范转换为Java类。在深入代码生成的细节之前,我们先简要回顾一下关键概念。

2.1. Apache Avro规范定义

首先,我们需要准备处理Avro格式所需的依赖。为了序列化和反序列化数据,我们需要添加org.apache.avro:avro模块到libs.version.tomlbuild.gradle文件中:

# libs.versions.toml

[versions]
// project dependencies versions
avro = "1.11.0"

[libraries]
// project libratirs
avro = {module = "org.apache.avro:avro", version.ref = "avro"}
# build.gradle

dependencies {
    implementation libs.avro
    // project dependencies
}

接下来,我们需要定义Avro规范。为了演示,我们将准备两个规范,分别对应本文档中使用的两种方法:

  • /src/main/avro/user.avsc —— 使用Gradle插件的方法
  • /src/main/custom/pet.avsc—— 使用自定义构建脚本任务的方法

我们将规范放在不同的文件夹中,以保持正确的结构。这也有助于避免ClassAlreadyExists异常,并确保Gradle构建系统正确识别并处理我们的Avro规范定义。

文件夹结构也会影响规范定义。User规范属于avro命名空间:

{
    "type": "record",
    "name": "User",
    "namespace": "avro",
    "fields": [
      {
        "name": "firstName",
        "type": "string"
      },
      {
        "name": "lastName",
        "type": "string"
      },
      {
        "name": "phoneNumber",
        "type": "string"
      }
    ]
}

同样地,我们在custom命名空间下定义一个Pet规范:

{
    "type": "record",
    "name": "Pet",
    "namespace": "custom",
    "fields": [
      {
        "name": "petId",
        "type": "string"
      },
      {
        "name": "name",
        "type": "string"
      },
      {
        "name": "species",
        "type": "string"
      },
      {
        "name": "age",
        "type": "int"
      }
    ]
}

选择合适的命名空间对于防止Java类生成时的名称冲突至关重要。这就是为什么我们要遵循广泛接受的做法,利用文件夹层次结构来确定命名空间标识符。

3. Java类生成

现在我们已经定义了规范,是时候编译它们了!

3.1. 使用Avro-Tools命令行

Apache Avro框架本身提供了诸如avro-tools.jar这样的工具,用于生成代码:

java -jar /path/to/avro-tools-1.11.1.jar compile schema <schema file> <destination>

然而,虽然理解avro-tools的功能为我们提供了解决问题的基础,但在实际应用中,这种方法并不方便,因为大多数情况下,我们希望在构建脚本执行期间生成代码

3.2. 使用开源Avro Gradle插件

将代码生成集成到构建的一种可能解决方案是使用davidmc24的开源avro-gradle-plugin

我们只需要导入依赖,并在build.gradle文件中包含插件ID。让我们从官方发布页面获取最新版本:

# libs.versions.toml

[plugins]
avro = { id = "com.github.davidmc24.gradle.plugin.avro", version = "1.9.1" }

3.3. 实现自定义Gradle任务

自定义Gradle任务的目的是利用Apache Avro框架提供的强大机制,通过avro-tools.jar进行代码生成。为此,我们需要相应地更新libs.versions.toml

# libs.versions.toml

[versions]
avro = "1.11.0"

[libraries]
avro = {module = "org.apache.avro:avro", version.ref = "avro"}
avro-tools = {module = "org.apache.avro:avro-tools", version.ref = "avro"}

Avro和Avro-tools库的版本需要保持一致,以防止冲突。

此外,我们需要在构建脚本中添加avro-tools.jar到类路径中。构建过程的时间线至关重要。通常,构建脚本按顺序执行,按照脚本中指定的顺序解析依赖项并执行任务。

在Avro规范代码生成上下文中,负责这项任务的自定义Gradle任务需要在一般依赖加载之前尽早访问avro-tools库:

# build.gradle

buildscript {
    dependencies {
        classpath libs.avro.tools
    }
}

def avroSchemasDir = "src/main/custom"
def avroCodeGenerationDir = "build/generated-main-avro-custom-java"

// Add the generated Avro Java code to the Gradle source files.
sourceSets.main.java.srcDirs += [avroCodeGenerationDir]

在这个步骤中,我们还可以定义源和输出目录,并将它们添加到sourceSets中,确保Gradle脚本可以访问它们。

驱动我们自定义Gradle任务的主要引擎是SpecificCompilerTool。这个类是Avro代码生成流程的核心,提供了类似于我们之前看到的命令的功能:

java -jar /path/to/avro-tools-1.11.1.jar compile schema <schema file> <destination> [..args]

我们可以自定义参数,如编码和字段可见性。有关SpecificCompilerTool的更多信息,请参阅官方文档:SpecificCompilerTool

tasks.register('customAvroCodeGeneration') {
    // Define the task inputs and outputs for the Gradle up-to-date checks.
    inputs.dir(avroSchemasDir)
    outputs.dir(avroCodeGenerationDir)
    // The Avro code generation logs to the standard streams. Redirect the standard streams to the Gradle log.
    logging.captureStandardOutput(LogLevel.INFO);
    logging.captureStandardError(LogLevel.ERROR)
    doLast {
        new SpecificCompilerTool().run(System.in, System.out, System.err, List.of(
                "-encoding", "UTF-8",
                "-string",
                "-fieldVisibility", "private",
                "-noSetters",
                "schema", "$projectDir/$avroSchemasDir".toString(), "$projectDir/$avroCodeGenerationDir".toString()
        ))
    }
}

最后,为了将代码生成纳入构建流程,我们需要添加对customAvroCodeGeneration的依赖:

tasks.withType(JavaCompile).configureEach {
    // Make Java compilation tasks depend on the Avro code generation task.
    dependsOn('customAvroCodeGeneration')
}

这样,每次运行构建命令时,Avro代码生成任务就会触发。

4. 结论

总之,我们熟悉了从Avro规范生成Java代码的两种方法。

第一种方法利用了开源的avro-gradle-plugin,提供了灵活性,并与Gradle项目无缝集成。然而,由于它已被归档,其商业用途可能受到限制。

第二种方法涉及扩展avro-tools库的自定义Gradle任务。这种方法的优势在于它引入的依赖仅限于Apache Avro框架固有的那些,从而降低了因使用不兼容库版本而产生的潜在冲突风险。此外,Gradle任务提供了对生成流程的控制,在需要在编译为Java类之前进行额外检查(例如,向构建管道添加自定义验证)的场景中可能很有帮助。这种方法提供了可靠性,适合对依赖管理有严格要求的生产环境。完整的示例可在GitHub上找到。