跳到内容

Wire 编译器与 Gradle 插件

Wire 有两个关键组件:一个在构建时生成源代码的编译器,以及一个在程序执行时支持生成代码的运行时库。编译器是高度可配置的;本指南将解释其特性和用法。

入门

配置和执行 Wire 编译器的最佳方法是通过我们的 Gradle 插件。它需要 Gradle 5.5 或更高版本。

一个典型的项目在其标准目录 src/main/proto 中包含 .proto 文件。

src/
  main/
    proto/
      com/
        example/
          pizza/
            pizza_delivery.proto
            pizza.proto
          sales/
            price.proto
            store.proto
          geo/
            address.proto
            country.proto
    kotlin/
      com/
        example/
          pizza/
            PizzaApp.kt
  test/
    kotlin/
      com/
        example/
          pizza/
            PizzaAppTest.kt
build.gradle

由于此项目使用默认配置,插件设置很简单。只需在 build.gradle 中添加一个简单的块即可,

使用插件应用

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath 'com.squareup.wire:wire-gradle-plugin:<version>'
  }
}

apply plugin: 'com.squareup.wire'

wire {
  kotlin {}
}

使用 plugins DSL

plugins {
  id 'application'
  id 'org.jetbrains.kotlin.jvm'
  id 'com.squareup.wire'
}

wire {
  kotlin {}
}

该插件将读取 .proto 模式定义,验证它们,链接它们,并生成 Kotlin 代码。它内部使用 KotlinPoet 生成紧凑易读的源文件。

Basic

如果未设置 sourcePath,Wire 默认会在 src/main/proto 中查找 .proto 文件。

Wire 会在 build/generated/source/wire 中生成文件。它还会将此目录注册为项目的源目录,以便生成的源文件由 Kotlin 编译器编译。

kotlin 替换为 java 以生成 Java 源文件。

输入和输出

Wire 可以从本地目录、本地 .jar 文件或使用 Maven 坐标指定外部构件中拉取源 .proto 模式定义。

wire {
  sourcePath {
    srcDir 'src/main/protos'
  }

  sourcePath {
    srcJar 'lib/pizza-protos.jar'
  }

  sourcePath {
    srcJar 'com.example.pizza:pizza-protos:1.0.0'
  }

  ...
}

如果您的资源或目录包含比您需要编译的文件更多的 .proto 文件,您可以将输入限制为特定文件

wire {
  sourcePath {
    srcDir 'src/main/protos'
    include 'com/example/pizza/pizza_delivery.proto'
    include 'com/example/pizza/pizza.proto'
  }
}

也支持通配符(带有两个星号)

wire {
  sourcePath {
    srcDir 'src/main/protos'
    include 'com/example/pizza/**'
  }
}

您也可以反过来排除一些文件

wire {
  sourcePath {
    srcDir 'src/main/protos'
    exclude 'com/example/juice/**'
    exclude 'com/example/pizza/toppings.proto'
  }
}

Wire 可以在同一构建中生成多种语言的代码。使用 includes 指定针对目标语言生成哪些类型;随后的语言将生成剩余的部分。

wire {
  ...

  kotlin {
    // Kotlin emits the matched types only.
    includes = ['com.example.pizza.*']
  }
  java {
    // Java gets everything else!
  }
}

请注意,源指定为文件系统路径(使用斜杠分隔),目标指定为 Proto 类型名称(使用点分隔)。

Inputs and Outputs

您也可以使用 excludes 指定目标。如果同时存在 includes 和 excludes,则 excludes 优先。

wire {
  ...

  kotlin {
    // Kotlin emits everything but sales and geo packages.
    excludes = ['com.example.sales.*', 'com.example.geo.*']
  }
  java {
    // Java gets those because they're left over.
  }
}

如果 includes 或 excludes 中的任何名称未使用,Wire 将打印警告。

配置 Wire 时,必须使用 .proto 文件中 package 声明的包名。option java_package 名称不用于 Wire 配置。

库的 Proto 路径

大型项目可能跨越多个模块。为了支持这一点,Wire 引入了‘proto 路径’。此路径上的 .proto 模式文件用于链接和验证,但不会生成输出文件。

proto 路径支持与源路径相同的输入:目录、.jar 文件和 Maven 坐标。类似地,proto 路径可以使用 include 进行过滤。

wire {
  protoPath {
    srcDir 'src/main/address-protos'
  }

  protoPath {
    srcJar 'lib/price.jar'
  }

  protoPath {
    srcJar 'com.example:countries:1.0.0'
    include 'com/example/geo/country.proto'
  }

  protoPath {
    srcDir 'src/main/extra-protos'
    exclude 'com/example/**'
  }

  ...
}

源路径和 proto 路径是相互关联的,但只生成源路径上的类型。

Library

Gradle 模块间的依赖关系

Wire 支持定义同一项目内模块之间的依赖关系。

模块可以将其 .proto 文件包含到输出资源中。当您的 .jar 文件可用作其他 proto 或 Wire 项目的库时,请使用此选项。请注意,只会包含库中使用的 .proto 文件。

wire {
  protoLibrary = true
}

Wire 还创建了两个配置 protoPathprotoSource,您可以使用它们来定义对其他 proto 或 Wire 项目的依赖。

dependencies {
  // The task `:common-protos:jar` will be added into the dependency
  // graph of this module for the Wire generating tasks.
  protoPath(project(':common-protos'))
  implementation(project(':common-protos'))
}

wire {
  kotlin {
  }
}

请注意,protoPathprotoSource 依赖默认不是传递的。如果需要,您可以手动更改。

configurations.protoPath {
  transitive = true
}

修剪

对于移动项目,很容易生成大量不必要的代码。例如,一个 .proto 模式可能支持已过时功能的类型。

R8ProGuard 这样的通用代码压缩工具难以压缩 Wire 生成的源代码,因为 encode()decode() 函数仍在使用它们。相反,Wire 提供了自己的代码压缩器,以便及早消除不需要的声明。

使用 prune 可以精确地消除不需要的类型或成员。它们将连同所有对它们的引用一起被消除。

wire {
  ...

  prune 'com.example.store.Store'
  prune 'com.example.geo.Country'

  ...
}

Pruning

或者,您可以选择指定保留哪些内容,修剪所有不可达的内容。

wire {
  ...

  root 'com.example.pizza.PizzaDelivery'

  ...
}

默认情况下,对于未知的 rootprune 参数,此功能是严格的。您可能希望其更为宽松

wire {
  rejectUnusedRootsOrPrunes = false
  ...
}

版本匹配

修剪过时字段的另一种方法是为其分配版本,然后根据版本范围或唯一版本生成代码。超出版本范围的字段将被修剪。

成员可以使用 wire.sincewire.until 选项声明;枚举常量可以使用 wire.constant_sincewire.constant_until。例如,这些选项声明了一个字段 age,它在版本 “5.0” 中被 birth_date 替换。

import "wire/extensions.proto";

message Singer {
  optional string name = 1;
  optional int32 age = 2 [(wire.until) = "5.0"];
  optional Date birth_date = 3 [(wire.since) = "5.0"];
}

客户端代码通常应针对单个版本。在此示例中,客户端将只包含 namebirth_date 字段。

wire {
  onlyVersion "5.0"
}

支持多个客户端的服务代码应支持所有受支持客户端的版本联合。此类代码将包含 name,以及 agebirth_date 字段。

wire {
  sinceVersion "3.0"
  untilVersion "6.0"
}

Proto 库

默认情况下,生成的 .jar 构件不包含 .proto 输入文件。使用 protoLibrary 选项可以包含它们

wire {
  protoLibrary = true
}

这在为其他 wire 任务构建 .jar 文件作为依赖项时最有用。请注意,只包含真正的源文件——被修剪或未使用的 proto 消息不包含在输出构件中。

空运行

使用 dry run 选项,编译器只会将本应生成到标准输出的源文件名输出。

wire {
  dryRun = true
}

自定义输出

Java

这是一个详尽的 Java 配置。布尔值显示其默认行为。

wire {
  java {
    // Proto types to include generated sources for. Types listed here will be
    // generated for this target and not for subsequent targets in the task.
    //
    // This list should contain package names (suffixed with `.*`) and type
    // names only. It should not contain member names.
    includes = ['com.example.pizza.*']

    // Proto types to excluded generated sources for. Types listed here will
    // not be generated for this target.
    //
    // This list should contain package names (suffixed with `.*`) and type
    // names only. It should not contain member names.
    excludes = ['com.example.sales.*']

    // True if types emitted for this target should not also be emitted for
    // other targets. Use this to cause multiple outputs to be emitted for the
    // same input type.
    exclusive = true

    // Directory to emit to.
    out "${buildDir}/custom"

    // True for emitted types to implement android.os.Parcelable.
    android = false

    // True to enable the androidx.annotation. Nullable annotation
    // where applicable.
    androidAnnotations = false

    // True to emit code that uses reflection for reading, writing, and toString
    // methods which are normally implemented with generated code.
    compact = false

    // True to turn visibility of all generated types' constructors
    // to non-public.
    buildersOnly = false

    // True to emit types for options declared on messages, fields, etc.
    emitDeclaredOptions = true

    // True to emit annotations for options applied on messages, fields, etc.
    emitAppliedOptions = true
  }
}

Kotlin

这是一个详尽的 Kotlin 配置。布尔值和枚举显示其默认行为。

wire {
  kotlin {
    // Proto types to include generated sources for. Types listed here will be
    // generated for this target and not for subsequent targets in the task.
    //
    // This list should contain package names (suffixed with `.*`) and type
    // names only. It should not contain member names.
    includes = ['com.example.pizza.*']

    // Proto types to excluded generated sources for. Types listed here will not
    // be generated for this target.
    //
    // This list should contain package names (suffixed with `.*`) and type
    // names only. It should not contain member names.
    excludes = ['com.example.sales.*']

    // True if types emitted for this target should not also be emitted for
    // other targets. Use this to cause multiple outputs to be emitted for the
    // same input type.
    exclusive = true

    // Directory to emit to.
    out "${buildDir}/custom"

    // True for emitted types to implement android.os.Parcelable.
    android = false

    // True for emitted types to implement APIs for easier migration from the
    // Java target.
    javaInterop = false

    // True to turn visibility of all generated types' constructors
    // to non-public.
    buildersOnly = false

    // True to emit types for options declared on messages, fields, etc.
    emitDeclaredOptions = true

    // True to emit annotations for options applied on messages, fields, etc.
    emitAppliedOptions = true

    // `suspending` to generate coroutines APIs that require a Kotlin
    // coroutines context.
    // `blocking` to generate blocking APIs callable by Java and Kotlin.
    rpcCallStyle = 'blocking'

    // `client` to generate interfaces best suited to sending outbound calls.
    // `server` to generate interfaces best suited to receiving inbound calls.
    // `none` to not generate services.
    rpcRole = 'server'

    // If set, the value will be appended to generated service type names. If
    // null, their rpcRole will be used as a suffix instead.
    nameSuffix = "Suffix"

    // True for emitted services to implement one interface per RPC.
    singleMethodServices = false

    // Set how many oneof choices are necessary for generated message classes to use the
    // `OneOf<Key<T>, T>` form rather than the default, where options are flattened into the
    // enclosing type.
    boxOneOfsMinSize = 5000

    // True to escape Kotlin keywords like `value` and `data` with backticks. Otherwise an
    // underscore underscore is added as a suffix, like `value_` and `data_`.
    escapeKotlinKeywords = false

    // Defines how an protobuf enum type is to be generated. See `com.squareup.wire.kotlin.EnumMode`
    enumMode = "enum_class"

    // True to emit a adapters that include a decode() function that accepts a `ProtoReader32`.
    // Use this optimization when targeting Kotlin/JS, where `Long` cursors are inefficient.
    emitProtoReader32 = false
  }
}

Proto

这是一个详尽的 Proto 配置。您可以使用此目标生成修剪后的 proto 模式,或对其进行漂亮打印。请注意,即使设置了多个输出,proto 目标也将始终输出模式的所有类型、所有服务和所有扩展。

wire {
  proto {
    // Directory to emit to.
    out "${buildDir}/custom"
  }
}

自定义处理程序

通过自定义模式处理程序,您可以以您想要的任何方式处理 proto 模式,包括代码生成或其他副作用,如验证、日志记录等。

您需要首先扩展 SchemaHandler 类,然后扩展 SchemaHandler.Factory 接口,该接口用于返回前者的实例。请参阅我们的 recipes 以了解不同用例的实现。

将其构建为一个 jar 构件,并将其作为构建脚本依赖项添加到您的 Gradle 项目中。

buildscript {
  dependencies {
    classpath "com.example.my-custom-handler:my-custom-handler:1.0.0"
  }
}

接下来配置 Wire 插件以调用您的自定义处理程序。这是一个详尽的自定义配置。布尔值和枚举显示其默认行为。

wire {
  custom {
    // The name of a Java class to generate code with. This class must:
    //  * be in the buildscript dependencies for this Gradle project
    //  * be a public class
    //  * have a public no-arguments constructor
    //  * implement the com.squareup.wire.schema.SchemaHandler.Factory interface
    schemaHandlerFactoryClass = "com.example.MyCustomHandlerFactory"

    // These options work the same as the java and kotlin targets above.
    includes = ['com.example.pizza.*']
    excludes = ['com.example.sales.*']
    exclusive = true
    out "${buildDir}/custom"

    // Custom payload which can be passed to the `SchemaHandler.Factory`.
    options = [a: "one", b: "two", c: "three"]
  }
}

Swift 支持

最简单的入门方法是使用 CocoaPods。

# Add the Wire compiler so that it is downloaded and available.
# CocoaPods will download the source and build the compiler directly,
# so you'll need Java installed.
pod 'WireCompiler'

# Add the Wire runtime to do the serializing/deserializing
pod 'Wire'

然后运行 pod install 以获取依赖项并构建 Wire 编译器。

也支持 Swift Package Manager 用于链接 Wire 运行时。

构建您的 Protos

Wire 编译器使用 SwiftPoet 生成 Swift 代码。生成的对象自动遵循 EquatableCodableSendable 协议。

假设您已使用 CocoaPods 下载了 Wire 编译器,要将您的 protos 编译成 Swift 文件

java -jar ./Pods/WireCompiler/compiler.jar \
  "--proto_path=<directory containing .proto files>" \
  "--swift_out=<directory where the generated .swift files go>" \
  "--experimental-module-manifest=<path to manifest yaml file>"

Swift Manifest

Swift 引入了一个 Kotlin 和 Java 中不存在的新挑战:模块。Kotlin 和 Java 都使用完全限定的包名,但 Swift 模块由其编译单元定义,因此命名空间不在类型或文件级别声明。这意味着我们需要为 Swift 构建一个新的打包系统,以处理 Swift 模块命名空间和导入。

我们认为,调用方定义模块的最简单方法是让 Wire 直接处理这些定义。一个 manifest 文件定义了模块、它们的名称、它们的依赖关系以及上面提到的内容根和修剪规则。

在此示例 manifest 中,DarkSide 和 LightSide 模块将依赖并导入 CommonProtos 模块

CommonProtos:
  roots:
    - jedi.Lightsaber
    - jedi.MindTrick
    - jedi.TheForce
    - jedi.Anakin

DarkSideProtos:
  dependencies:
    - CommonProtos
  roots:
    - darkside.*
    - jedi.Lightning
  prunes:
    - jedi.Mercy

LightSideProtos:
  dependencies:
    - CommonProtos
  roots:
    - lightside.*
    # Import the rest of the Jedi powers not already in CommonProtos
    - jedi.*
  prunes:
    # Remove unused lightsaber colors
    - jedi.LightsaberColor#red
    # Remove deprecated field. Use green_lightsaber instead.
    - lightside.Luke#blue_lightsaber
    # Remove dark-side-only types
    - jedi.Lightning