WireCompiler 5.0.0

WireCompiler 5.0.0

Eric FirestoneBenoit QuenaudonDimitris Koutsogiorgas 维护。



  • Eric Firestone

Wire

“一个程序员的灵魂必须有一段代码!” - Omar Little

查看项目网站以获取文档和 API。square.github.io/wire/

随着我们的团队和程序的增长,数据的种类和数量也在增长。成功将让您的简单数据模型变得更加复杂!无论您的应用程序是存储数据到磁盘还是通过网络传输数据,该数据的结构和解释应该是清晰的。消费者与我们理解的数据工作得最好!

模式描述并记录数据模型。如果您拥有数据,您应该有一个模式。

协议缓冲区

Google 的 Protocol Buffers 围绕一个强大的模式语言构建

  • 它是跨平台的且语言无关。您使用什么编程语言,您都将能够将其与您的应用一起使用 proto 模式。

  • proto 模式向后兼容并面向未来。随着您的应用程序删除旧功能并获得新功能,您可以继续发展您的模式。

  • 它是专业的。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;
}

以下是 枚举定义

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;
}

这种语言是 Protocol Buffers 的最佳特性。您甚至可以仅用于文档目的,例如描述 JSON API。

Protocol Buffers 还定义了符合该模式的消息的紧凑二进制编码。这种编码速度快,解码快,传输储存都小。二进制编码使用来自模式的数字标签,如上面 period5

例如,让我们编码这个恐龙

{
  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的架构语言和二进制编码都由Google定义。Wire是Square开发的一个独立实现,专门为Android和Java设计。

对于架构中定义的每种消息类型,Wire都会生成一个不可变的模型类及其builder。生成的代码看起来就像手动编写的代码:它有文档、格式规范且简单。Wire的API对于喜欢Effective Java的程序员来说应该很熟悉。

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

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

  • Wire避免了映射上下文。一个架构中声明为picture_urls的字段会产生一个Java字段picture_urls而不是传统的pictureUrls驼峰命名。尽管一开始名字显得有些别扭,但在使用grep或更复杂的搜索工具时,这非常好。在架构、Java源代码和数据之间导航时不再需要映射。这也为调用代码提供了一个温和的提醒,即proto消息有点特殊。

  • 原始类型始终被装箱。如果一个字段不存在,它的值是null。这用于自然地为可选字段提供,例如一种时期不明的恐龙。一个字段也可能因为架构的演变而为null:如果明天我们在我们的消息定义中添加一个carnivore布尔值,那么今天的数据将不会有该字段的值。

生成的Java代码

以下是上面定义的Dinosaur消息的简化生成代码

// 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编译器和Gradle插件以了解如何配置构建。

Kotlin是一种实用且富有表现力的编程语言,它使得建模数据变得很容易。以下是它是如何使用Kotlin来建模Protocol Buffers消息的

  • 消息感觉像是data类,但实际上它们不是。编译器仍然为你生成equals()hashCode()toString()copy()。但Wire不会生成componentN()函数,我们相信解构声明不适合Protobuf:当模式发生更改,移除或添加字段时,可能会导致你的解构声明仍然可以编译,但现在描述的却是完全不同的字段子集,从而使你的代码变得不正确。

  • copy()Builder的替代品,现在不再使用。如果你的程序依赖于Builder存在,你可以在Java互操作性模式下生成代码 - Wire编译器与Gradle插件解释了它是如何工作的。

  • 字段被生成为属性。虽然这在Kotlin中很常见,但现在Java代码必须使用getter来访问字段。如果你的程序依赖于直接访问字段,请使用Java互操作性模式 - 编译器将为每个字段生成@JvmField注解。

  • 每个字段的类型是否为null取决于其标记:requiredrepeatedmap字段得到非null类型,而optional字段为null类型。

  • required字段外,每个字段都有一个默认值

    • 对于optional字段为null,
    • 对于repeated字段为emptyList()
    • 对于map字段为emptyMap()
生成的Kotlin代码

这是上面定义的Dinosaur消息的紧凑型生成代码

// 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编译器与Gradle插件了解如何配置你的构建。

Swift支持目前被视为“测试版”,可能还会出现破坏性更改。尽管如此,Block正在将它们用于生产应用程序和SDK中。

Swift是一种实用和表达性强且对值类型支持丰富的编程语言。以下是使用Swift来模拟Protocol Buffers消息的方式

  • 消息是符合EquatableCodableSendable的struct。所有消息都有值语义。
  • 消息有一个成员初始化器来填充字段。
  • 字段被生成为属性。
  • 每个字段的类型是否为null取决于其标记:requiredrepeatedmap字段得到非null类型,而optional字段为null类型。
生成的Swift代码

这是上面定义的Dinosaur消息的紧凑型生成代码

// 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".
     */
    public var name: String?
    /**
     * URLs with images of this dinosaur.
     */
    public var picture_urls: [String]
    public var length_meters: Double?
    public var mass_kilograms: Double?
    public var period: Period?
    public var unknownFields: Data = .init()

    public init(
        name: String? = nil,
        picture_urls: [String] = [],
        length_meters: Double? = nil,
        mass_kilograms: Double? = nil,
        period: Period? = nil
    ) {
        self.name = name
        self.picture_urls = picture_urls
        self.length_meters = length_meters
        self.mass_kilograms = mass_kilograms
        self.period = period
    }

}

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

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

#if swift(>=5.5)
extension Dinosaur : Sendable {
}
#endif

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

extension Dinosaur : Proto2Codable {
    public init(from reader: 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 reader.beginMessage()
        while let tag = try reader.nextTag(token: token) {
            switch tag {
            case 1: name = try reader.decode(String.self)
            case 2: try reader.decode(into: &picture_urls)
            case 3: length_meters = try reader.decode(Double.self)
            case 4: mass_kilograms = try reader.decode(Double.self)
            case 5: period = try reader.decode(Period.self)
            default: try reader.readUnknownField(tag: tag)
            }
        }
        self.unknownFields = try reader.endMessage(token: token)

        self.name = name
        self.picture_urls = picture_urls
        self.length_meters = length_meters
        self.mass_kilograms = mass_kilograms
        self.period = period
    }

    public func encode(to writer: ProtoWriter) throws {
        try writer.encode(tag: 1, value: self.name)
        try writer.encode(tag: 2, value: self.picture_urls)
        try writer.encode(tag: 3, value: self.length_meters)
        try writer.encode(tag: 4, value: self.mass_kilograms)
        try writer.encode(tag: 5, value: self.period)
        try writer.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 = try container.decodeIfPresent(String.self, forKey: "name")
        self.picture_urls = try container.decodeProtoArray(String.self, firstOfKeys: "pictureUrls", "picture_urls")
        self.length_meters = try container.decodeIfPresent(Double.self, firstOfKeys: "lengthMeters", "length_meters")
        self.mass_kilograms = 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(
    name: "Stegosaurus",
    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文件。

编译器可以可选地剪枝您的模式到根类型及其传递依赖项的子集。当在项目之间共享模式时很有用:Java服务和Android应用可能各自使用一个较大的共享模式的子集。

有关如何开始的更多信息,请参阅Wire编译器和Gradle插件

如果您不使用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运行时库和您的生成协议缓冲区(当然,这些简单的规则会错过缩放和优化代码的机会)

-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.7.2</version>
</dependency>

使用Gradle

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

开发版本的快照可以在Sonatype的snapshots仓库中找到。

不支持

Wire 不支持

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

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

进一步文档

查看Google关于proto模式结构和语法的优秀文档 Google's excellent documentation