跳到内容

Wire

“一个人必须有一套准则!” - 奥马尔·利特尔

随着我们的团队和程序的发展,数据的种类和数量也在增加。成功会将你简单的数据模型变得复杂!无论你的应用程序是将数据存储到磁盘还是通过网络传输,该数据的结构和解释都应该清晰明了。消费者在使用他们理解的数据时效率最高!

Schema(模式)描述和文档化数据模型。如果你有数据,就应该有一个 schema。

Protocol Buffers

Google 的 Protocol Buffers 是围绕一个出色的 schema 语言构建的

  • 它是跨平台且与语言无关的。无论你使用哪种编程语言,都可以在你的应用程序中使用 proto schema。

  • Proto schema 具有向后兼容性,且面向未来。你可以随着应用程序废弃旧功能和添加新功能来演进你的 schema。

  • 它很专注。Proto schema 描述你的数据模型。仅此而已。

Protocol Buffer 示例这是一个 [示例消息][dinosaur_proto] 定义
syntax = "proto3";

package squareup.dinosaurs;

option java_package = "com.squareup.dinosaurs";

import "squareup/geology/period.proto";

message Dinosaur {
  // Common name of this dinosaur, like "Stegosaurus".
  string name = 1;

  // URLs with images of this dinosaur.
  repeated string picture_urls = 2;

  squareup.geology.Period period = 5;
}
这是 [枚举][period_proto] 定义
syntax = "proto3";

package squareup.geology;


option java_package = "com.squareup.geology";

enum Period {
  // 145.5 million years ago — 66.0 million years ago.
  CRETACEOUS = 0;

  // 201.3 million years ago — 145.0 million years ago.
  JURASSIC = 1;

  // 252.17 million years ago — 201.3 million years ago.
  TRIASSIC = 2;
}
这种 schema 语言是 Protocol Buffers 最好的特性。你甚至可以纯粹将其用于文档目的,例如描述一个 JSON API。Protocol Buffers 还定义了一种符合 schema 的消息的紧凑二进制编码。这种编码编码速度快、解码速度快、传输小、存储小。二进制编码使用 schema 中的数字标签,例如上面 `period` 的 `5`。例如,让我们编码这个 dinosaur(恐龙)
{
  name: "Stegosaurus",
  period: JURASSIC
}
编码后的值只有 15 字节
Hex  Description
 0a  tag: name(1), field encoding: LENGTH_DELIMITED(2). 1 << 3 | 2
 0b  "Stegosaurus".length()
 53  'S'
 74  't'
 65  'e'
 67  'g'
 6f  'o'
 73  's'
 61  'a'
 75  'u'
 72  'r'
 75  'u'
 73  's'
 28  tag: period(5), field encoding: VARINT(0). 5 << 3 | 0
 02  JURASSIC(2)

为什么选择 Wire?

Protocol Buffers schema 语言和二进制编码均由 Google 定义。Wire 是 Square 的一个独立实现,专门为 Android 和 Java 设计。

对于 schema 中定义的每种消息类型,Wire 生成一个不可变模型类及其 builder。生成的代码看起来就像你手写的代码一样:有文档、格式良好且简单。Wire 的 API 应该让喜欢 Effective Java 的程序员感到亲切。

话虽如此,Wire 中有一些有趣的设计决策

  • Wire 消息声明 public final 字段,而不是通常的 getter 方法。这减少了生成的代码和执行的代码。更少的代码对于 Android 程序尤其有利。

  • Wire 避免大小写映射。schema 中声明为 picture_urls 的字段在 Java 中也会是字段 picture_urls,而不是传统的 pictureUrls 驼峰命名法。虽然这个名字起初感觉很别扭,但当你使用 grep 或更复杂的搜索工具时,它非常棒。在 schema、Java 源代码和数据之间导航时,不再需要映射。它也温和地提醒调用代码 proto 消息有点特殊。

  • 原始类型总是会被装箱。如果字段缺失,其值是 null。这用于自然可选的字段,例如时代未知的恐龙。由于 schema 演进,字段也可能是 null:如果我们明天在消息定义中添加一个 carnivore 布尔值字段,今天的数据将不会有该字段的值。

生成的 Java 代码这是上面定义的 `Dinosaur` 消息的紧凑 [生成代码][dinosaur_java]
// Code generated by Wire protocol buffer compiler, do not edit.
// Source: squareup.dinosaurs.Dinosaur in squareup/dinosaurs/dinosaur.proto
package com.squareup.dinosaurs;

import com.squareup.geology.Period;
import com.squareup.wire.Message;
import com.squareup.wire.ProtoAdapter;
import com.squareup.wire.Syntax;
import com.squareup.wire.WireField;
import com.squareup.wire.internal.Internal;
import java.lang.Object;
import java.lang.Override;
import java.lang.String;
import java.util.List;
import okio.ByteString;

public final class Dinosaur extends Message<Dinosaur, Dinosaur.Builder> {
  public static final ProtoAdapter<Dinosaur> ADAPTER = ProtoAdapter.newMessageAdapter(Dinosaur.class, "type.googleapis.com/squareup.dinosaurs.Dinosaur", Syntax.PROTO_3);

  private static final long serialVersionUID = 0L;

  /**
   * Common name of this dinosaur, like "Stegosaurus".
   */
  @WireField(
      tag = 1,
      adapter = "com.squareup.wire.ProtoAdapter#STRING",
      label = WireField.Label.OMIT_IDENTITY
  )
  public final String name;

  /**
   * URLs with images of this dinosaur.
   */
  @WireField(
      tag = 2,
      adapter = "com.squareup.wire.ProtoAdapter#STRING",
      label = WireField.Label.REPEATED,
      jsonName = "pictureUrls"
  )
  public final List<String> picture_urls;

  @WireField(
      tag = 5,
      adapter = "com.squareup.geology.Period#ADAPTER",
      label = WireField.Label.OMIT_IDENTITY
  )
  public final Period period;

  public Dinosaur(String name, List<String> picture_urls, Period period) {
    this(name, picture_urls, period, ByteString.EMPTY);
  }

  public Dinosaur(String name, List<String> picture_urls, Period period, ByteString unknownFields) {
    super(ADAPTER, unknownFields);
    if (name == null) {
      throw new IllegalArgumentException("name == null");
    }
    this.name = name;
    this.picture_urls = Internal.immutableCopyOf("picture_urls", picture_urls);
    if (period == null) {
      throw new IllegalArgumentException("period == null");
    }
    this.period = period;
  }

  @Override
  public Builder newBuilder() {
    Builder builder = new Builder();
    builder.name = name;
    builder.picture_urls = Internal.copyOf(picture_urls);
    builder.period = period;
    builder.addUnknownFields(unknownFields());
    return builder;
  }

  @Override
  public boolean equals(Object other) {
    if (other == this) return true;
    if (!(other instanceof Dinosaur)) return false;
    Dinosaur o = (Dinosaur) other;
    return unknownFields().equals(o.unknownFields())
        && Internal.equals(name, o.name)
        && picture_urls.equals(o.picture_urls)
        && Internal.equals(period, o.period);
  }

  @Override
  public int hashCode() {
    int result = super.hashCode;
    if (result == 0) {
      result = unknownFields().hashCode();
      result = result * 37 + (name != null ? name.hashCode() : 0);
      result = result * 37 + picture_urls.hashCode();
      result = result * 37 + (period != null ? period.hashCode() : 0);
      super.hashCode = result;
    }
    return result;
  }

  public static final class Builder extends Message.Builder<Dinosaur, Builder> {
    public String name;

    public List<String> picture_urls;

    public Period period;

    public Builder() {
      name = "";
      picture_urls = Internal.newMutableList();
      period = Period.CRETACEOUS;
    }

    /**
     * Common name of this dinosaur, like "Stegosaurus".
     */
    public Builder name(String name) {
      this.name = name;
      return this;
    }

    /**
     * URLs with images of this dinosaur.
     */
    public Builder picture_urls(List<String> picture_urls) {
      Internal.checkElementsNotNull(picture_urls);
      this.picture_urls = picture_urls;
      return this;
    }

    public Builder period(Period period) {
      this.period = period;
      return this;
    }

    @Override
    public Dinosaur build() {
      return new Dinosaur(name, picture_urls, period, super.buildUnknownFields());
    }
  }
}
创建和访问 proto 模型的 Java 代码紧凑且可读
Dinosaur stegosaurus = new Dinosaur.Builder()
    .name("Stegosaurus")
    .period(Period.JURASSIC)
    .build();

System.out.println("My favorite dinosaur existed in the " + stegosaurus.period + " period.");
每种类型都有相应的 `ProtoAdapter`,可以将消息编码为字节,并将字节解码回消息。
Dinosaur stegosaurus = ...
byte[] stegosaurusBytes = Dinosaur.ADAPTER.encode(stegosaurus);

byte[] tyrannosaurusBytes = ...
Dinosaur tyrannosaurus = Dinosaur.ADAPTER.decode(tyrannosaurusBytes);
访问字段时,使用 `Wire.get()` 将 null 值替换为相应的默认值
Period period = Wire.get(stegosaurus.period, Dinosaur.DEFAULT_PERIOD);
这等同于以下内容
Period period = stegosaurus.period != null ? stegosaurus.period : Dinosaur.DEFAULT_PERIOD;

Wire Kotlin

从 3.0.0 版本开始,Wire 可以生成 Kotlin 代码。请参阅 Wire Compiler & Gradle Plugin 了解如何配置你的构建。

Kotlin 是一种实用且富有表现力的编程语言,可以轻松地对数据进行建模。以下是我们如何使用 Kotlin 对 Protocol Buffers 消息进行建模的方法

  • 消息感觉像 data 类,但实际上并非如此。编译器仍然会为你生成 equals()hashCode()toString()copy()。不过,Wire 不生成 componentN() 函数,我们认为解构声明不适合 Protocol Buffers:schema 的更改(移除或添加字段)可能导致你的解构声明仍然可以编译,但现在描述的是完全不同的字段子集,使你的代码不正确。
  • copy()Builder 的替代,Builder 不再使用。如果你的程序依赖于 Builder 的存在,你可以在 Java 互操作模式下生成代码 - Wire Compiler & Gradle Plugin 解释了其工作原理。
  • 字段被生成为属性。虽然这在 Kotlin 中是惯用的,但 Java 代码现在必须使用 getter 访问字段。如果你的程序依赖于直接访问字段,请使用 Java 互操作模式 - 编译器将为每个字段生成 @JvmField 注解。
  • 每个字段类型的可空性取决于其标签:requiredrepeatedmap 字段获得非可空类型,而 optional 字段是可空类型。
  • 除了 required 字段外,每个字段都有一个默认值

  • optional 字段为 null,

  • repeated 字段为 emptyList()
  • map 字段为 emptyMap()
生成的 Kotlin 代码这是上面定义的 `Dinosaur` 消息的紧凑 [生成代码][dinosaur_kotlin]
// Code generated by Wire protocol buffer compiler, do not edit.
// Source: squareup.dinosaurs.Dinosaur in squareup/dinosaurs/dinosaur.proto
package com.squareup.dinosaurs

import com.squareup.geology.Period
import com.squareup.wire.FieldEncoding
import com.squareup.wire.Message
import com.squareup.wire.ProtoAdapter
import com.squareup.wire.ProtoReader
import com.squareup.wire.ProtoWriter
import com.squareup.wire.Syntax.PROTO_3
import com.squareup.wire.WireField
import com.squareup.wire.internal.immutableCopyOf
import com.squareup.wire.internal.sanitize
import kotlin.Any
import kotlin.AssertionError
import kotlin.Boolean
import kotlin.Deprecated
import kotlin.DeprecationLevel
import kotlin.Int
import kotlin.Long
import kotlin.Nothing
import kotlin.String
import kotlin.collections.List
import kotlin.hashCode
import kotlin.jvm.JvmField
import okio.ByteString

class Dinosaur(
  /**
   * Common name of this dinosaur, like "Stegosaurus".
   */
  @field:WireField(
    tag = 1,
    adapter = "com.squareup.wire.ProtoAdapter#STRING",
    label = WireField.Label.OMIT_IDENTITY
  )
  val name: String = "",
  picture_urls: List<String> = emptyList(),
  @field:WireField(
    tag = 5,
    adapter = "com.squareup.geology.Period#ADAPTER",
    label = WireField.Label.OMIT_IDENTITY
  )
  val period: Period = Period.CRETACEOUS,
  unknownFields: ByteString = ByteString.EMPTY
) : Message<Dinosaur, Nothing>(ADAPTER, unknownFields) {
  /**
   * URLs with images of this dinosaur.
   */
  @field:WireField(
    tag = 2,
    adapter = "com.squareup.wire.ProtoAdapter#STRING",
    label = WireField.Label.REPEATED,
    jsonName = "pictureUrls"
  )
  val picture_urls: List<String> = immutableCopyOf("picture_urls", picture_urls)

  @Deprecated(
    message = "Shouldn't be used in Kotlin",
    level = DeprecationLevel.HIDDEN
  )
  override fun newBuilder(): Nothing = throw AssertionError()

  override fun equals(other: Any?): Boolean {
    if (other === this) return true
    if (other !is Dinosaur) return false
    if (unknownFields != other.unknownFields) return false
    if (name != other.name) return false
    if (picture_urls != other.picture_urls) return false
    if (period != other.period) return false
    return true
  }

  override fun hashCode(): Int {
    var result = super.hashCode
    if (result == 0) {
      result = unknownFields.hashCode()
      result = result * 37 + name.hashCode()
      result = result * 37 + picture_urls.hashCode()
      result = result * 37 + period.hashCode()
      super.hashCode = result
    }
    return result
  }

  override fun toString(): String {
    val result = mutableListOf<String>()
    result += """name=${sanitize(name)}"""
    if (picture_urls.isNotEmpty()) result += """picture_urls=${sanitize(picture_urls)}"""
    result += """period=$period"""
    return result.joinToString(prefix = "Dinosaur{", separator = ", ", postfix = "}")
  }

  fun copy(
    name: String = this.name,
    picture_urls: List<String> = this.picture_urls,
    period: Period = this.period,
    unknownFields: ByteString = this.unknownFields
  ): Dinosaur = Dinosaur(name, picture_urls, period, unknownFields)

  companion object {
    @JvmField
    val ADAPTER: ProtoAdapter<Dinosaur> = object : ProtoAdapter<Dinosaur>(
      FieldEncoding.LENGTH_DELIMITED,
      Dinosaur::class,
      "type.googleapis.com/squareup.dinosaurs.Dinosaur",
      PROTO_3,
      null
    ) {
      override fun encodedSize(value: Dinosaur): Int {
        var size = value.unknownFields.size
        if (value.name != "") size += ProtoAdapter.STRING.encodedSizeWithTag(1, value.name)
        size += ProtoAdapter.STRING.asRepeated().encodedSizeWithTag(2, value.picture_urls)
        if (value.period != Period.CRETACEOUS) size += Period.ADAPTER.encodedSizeWithTag(5,
            value.period)
        return size
      }

      override fun encode(writer: ProtoWriter, value: Dinosaur) {
        if (value.name != "") ProtoAdapter.STRING.encodeWithTag(writer, 1, value.name)
        ProtoAdapter.STRING.asRepeated().encodeWithTag(writer, 2, value.picture_urls)
        if (value.period != Period.CRETACEOUS) Period.ADAPTER.encodeWithTag(writer, 5, value.period)
        writer.writeBytes(value.unknownFields)
      }

      override fun decode(reader: ProtoReader): Dinosaur {
        var name: String = ""
        val picture_urls = mutableListOf<String>()
        var period: Period = Period.CRETACEOUS
        val unknownFields = reader.forEachTag { tag ->
          when (tag) {
            1 -> name = ProtoAdapter.STRING.decode(reader)
            2 -> picture_urls.add(ProtoAdapter.STRING.decode(reader))
            5 -> try {
              period = Period.ADAPTER.decode(reader)
            } catch (e: ProtoAdapter.EnumConstantNotFoundException) {
              reader.addUnknownField(tag, FieldEncoding.VARINT, e.value.toLong())
            }
            else -> reader.readUnknownField(tag)
          }
        }
        return Dinosaur(
          name = name,
          picture_urls = picture_urls,
          period = period,
          unknownFields = unknownFields
        )
      }

      override fun redact(value: Dinosaur): Dinosaur = value.copy(
        unknownFields = ByteString.EMPTY
      )
    }

    private const val serialVersionUID: Long = 0L
  }
}
创建和访问 proto 模型很容易
val stegosaurus = Dinosaur(
    name = "Stegosaurus",
    period = Period.JURASSIC
)

println("My favorite dinosaur existed in the ${stegosaurus.period} period.")
以下是如何修改对象以添加额外字段的方法
val stegosaurus = stegosaurus.copy(
    picture_urls = listOf("https://www.flickr.com/photos/tags/Stegosaurus/")
)

println("Here are some photos of ${stegosaurus.name}: ${stegosaurus.picture_urls}")

Wire Swift

从 3.3.0 版本开始,Wire 可以生成 Swift 代码。请参阅 Wire Compiler & Gradle Plugin 了解如何配置你的构建。

Swift 支持被认为是“测试版”,可能仍会包含破坏性更改。尽管如此,Block 已经在生产应用和 SDK 中使用了它。

Swift 是一种实用且富有表现力的编程语言,对值类型有丰富的支持。以下是我们如何使用 Swift 对 Protocol Buffers 消息进行建模的方法

  • 消息是符合 EquatableCodableSendable 的结构体。所有消息都具有值语义。
  • 字段被生成为属性。
  • 每个字段类型的可空性取决于其标签:requiredrepeatedmap 字段获得非可空类型,而 optional 字段是可空类型。
生成的 Swift 代码这是上面定义的 `Dinosaur` 消息的紧凑 [生成代码][dinosaur_swift]
// Code generated by Wire protocol buffer compiler, do not edit.
// Source: squareup.dinosaurs.Dinosaur in squareup/dinosaurs/dinosaur.proto
import Foundation
import Wire

public struct Dinosaur {

    /**
     * Common name of this dinosaur, like "Stegosaurus".
     */
    @ProtoDefaulted
    public var name: String?
    /**
     * URLs with images of this dinosaur.
     */
    public var picture_urls: [String] = []
    @ProtoDefaulted
    public var length_meters: Double?
    @ProtoDefaulted
    public var mass_kilograms: Double?
    public var period: Period?
    public var unknownFields: Foundation.Data = .init()

    public init(configure: (inout Self) -> Swift.Void = { _ in }) {
        configure(&self)
    }

}

#if !WIRE_REMOVE_EQUATABLE
extension Dinosaur : Equatable {
}
#endif

#if !WIRE_REMOVE_HASHABLE
extension Dinosaur : Hashable {
}
#endif

extension Dinosaur : Sendable {
}

extension Dinosaur : ProtoDefaultedValue {

    public static var defaultedValue: Dinosaur {
        Dinosaur()
    }
}

extension Dinosaur : ProtoMessage {

    public static func protoMessageTypeURL() -> String {
        return "type.googleapis.com/squareup.dinosaurs.Dinosaur"
    }

}

extension Dinosaur : Proto2Codable {

    public init(from protoReader: ProtoReader) throws {
        var name: String? = nil
        var picture_urls: [String] = []
        var length_meters: Double? = nil
        var mass_kilograms: Double? = nil
        var period: Period? = nil

        let token = try protoReader.beginMessage()
        while let tag = try protoReader.nextTag(token: token) {
            switch tag {
            case 1: name = try protoReader.decode(String.self)
            case 2: try protoReader.decode(into: &picture_urls)
            case 3: length_meters = try protoReader.decode(Double.self)
            case 4: mass_kilograms = try protoReader.decode(Double.self)
            case 5: period = try protoReader.decode(Period.self)
            default: try protoReader.readUnknownField(tag: tag)
            }
        }
        self.unknownFields = try protoReader.endMessage(token: token)

        self._name.wrappedValue = name
        self.picture_urls = picture_urls
        self._length_meters.wrappedValue = length_meters
        self._mass_kilograms.wrappedValue = mass_kilograms
        self.period = period
    }

    public func encode(to protoWriter: ProtoWriter) throws {
        try protoWriter.encode(tag: 1, value: self.name)
        try protoWriter.encode(tag: 2, value: self.picture_urls)
        try protoWriter.encode(tag: 3, value: self.length_meters)
        try protoWriter.encode(tag: 4, value: self.mass_kilograms)
        try protoWriter.encode(tag: 5, value: self.period)
        try protoWriter.writeUnknownFields(unknownFields)
    }

}

#if !WIRE_REMOVE_CODABLE
extension Dinosaur : Codable {

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: StringLiteralCodingKeys.self)
        self._name.wrappedValue = try container.decodeIfPresent(String.self, forKey: "name")
        self.picture_urls = try container.decodeProtoArray(String.self, firstOfKeys: "pictureUrls", "picture_urls")
        self._length_meters.wrappedValue = try container.decodeIfPresent(Double.self, firstOfKeys: "lengthMeters", "length_meters")
        self._mass_kilograms.wrappedValue = try container.decodeIfPresent(Double.self, firstOfKeys: "massKilograms", "mass_kilograms")
        self.period = try container.decodeIfPresent(Period.self, forKey: "period")
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: StringLiteralCodingKeys.self)
        let preferCamelCase = encoder.protoKeyNameEncodingStrategy == .camelCase
        let includeDefaults = encoder.protoDefaultValuesEncodingStrategy == .include

        try container.encodeIfPresent(self.name, forKey: "name")
        if includeDefaults || !self.picture_urls.isEmpty {
            try container.encodeProtoArray(self.picture_urls, forKey: preferCamelCase ? "pictureUrls" : "picture_urls")
        }
        try container.encodeIfPresent(self.length_meters, forKey: preferCamelCase ? "lengthMeters" : "length_meters")
        try container.encodeIfPresent(self.mass_kilograms, forKey: preferCamelCase ? "massKilograms" : "mass_kilograms")
        try container.encodeIfPresent(self.period, forKey: "period")
    }

}
#endif
创建和访问 proto 模型很容易
let stegosaurus = Dinosaur {
    $0.name = "Stegosaurus"
    $0.period = .JURASSIC
}

print("My favorite dinosaur existed in the \(stegosaurus.period) period.")
以下是如何修改对象以添加额外字段的方法
var stegosaurus = stegosaurus
stegosaurus.picture_urls = ["https://www.flickr.com/photos/tags/Stegosaurus/"]

print("Here are some photos of \(stegosaurus.name): \(stegosaurus.picture_urls)")

Wire gRPC

从 3.0.0 版本开始,Wire 支持 gRPC

使用 Wire 生成代码

Wire 可以从本地文件系统以及 .jar 文件中读取 .proto 文件。

编译器可以选择性地将你的 schema 裁剪为根类型及其传递依赖项的子集。这在项目间共享 schema 时非常有用:Java 服务和 Android 应用都可以使用一个更大的共享 schema 的子集。

有关如何开始的更多信息,请参阅 Wire Compiler & Gradle Plugin

如果你不使用 Gradle,编译器也有一个命令行界面。只需将 wire-compiler-VERSION-jar-with-dependencies.jar 替换为你的 jar 路径即可。下载最新的预编译 jar。

% java -jar wire-compiler-VERSION-jar-with-dependencies.jar \
    --proto_path=src/main/proto \
    --java_out=out \
    squareup/dinosaurs/dinosaur.proto \
    squareup/geology/period.proto
Writing com.squareup.dinosaurs.Dinosaur to out
Writing com.squareup.geology.Period to out

为编译器提供 --android 标志会使 Wire 消息实现 Parcelable

如果你使用 Proguard,则需要添加 keep 规则。最简单的选项是告诉 Proguard 不要触碰 Wire 运行时库和你生成的 protocol buffers(当然,这些简单的规则会错过缩小和优化代码的机会)

-keep class com.squareup.wire.** { *; }
-keep class com.yourcompany.yourgeneratedcode.** { *; }

获取 Wire

wire-runtime 包包含必须包含在使用 Wire 生成代码的应用程序中的运行时支持库。

使用 Maven

<dependency>
  <groupId>com.squareup.wire</groupId>
  <artifactId>wire-runtime-jvm</artifactId>
  <version>4.9.2</version>
</dependency>

使用 Gradle

api "com.squareup.wire:wire-runtime:4.9.2"

开发版本的快照可在 Sonatype 的 snapshots 仓库中获取。

不支持的功能

Wire 不支持

  • Groups - 解析二进制输入数据时会跳过它们

Wire 支持消息和字段上的自定义选项。其他自定义选项将被忽略。向编译器传递 --excludes=google.protobuf.* 以从生成的代码中省略选项。

更多文档

请参阅 Google 关于 proto schema 结构和语法的出色文档