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;
}
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;
}
{
name: "Stegosaurus",
period: JURASSIC
}
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());
}
}
}
Dinosaur stegosaurus = new Dinosaur.Builder()
.name("Stegosaurus")
.period(Period.JURASSIC)
.build();
System.out.println("My favorite dinosaur existed in the " + stegosaurus.period + " period.");
Dinosaur stegosaurus = ...
byte[] stegosaurusBytes = Dinosaur.ADAPTER.encode(stegosaurus);
byte[] tyrannosaurusBytes = ...
Dinosaur tyrannosaurus = Dinosaur.ADAPTER.decode(tyrannosaurusBytes);
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
注解。 - 每个字段类型的可空性取决于其标签:
required
、repeated
和map
字段获得非可空类型,而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
}
}
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 消息进行建模的方法
- 消息是符合
Equatable
、Codable
和Sendable
的结构体。所有消息都具有值语义。 - 字段被生成为属性。
- 每个字段类型的可空性取决于其标签:
required
、repeated
和map
字段获得非可空类型,而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
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.*
以从生成的代码中省略选项。