1. 概述
在这个教程中,我们将学习如何根据Apache Avro规范生成Java类。首先,我们将介绍两种方法:使用现有的Gradle插件和在构建脚本中实现自定义任务。然后,我们将分析每种方法的优缺点,并了解它们适用的最佳场景。
2. 开始使用Apache Avro
我们的重点是将Apache Avro规范转换为Java类。在深入代码生成的细节之前,我们先简要回顾一下关键概念。
2.1. Apache Avro规范定义
首先,我们需要准备处理Avro格式所需的依赖。为了序列化和反序列化数据,我们需要添加org.apache.avro:avro
模块到libs.version.toml
和build.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上找到。