Wire
“一个人必须有点代码!” - Omar Little
有关文档和 API 的详细信息,请参阅项目网站 project website。
随着我们团队和项目的增长,数据的种类和数量也在增长。成功会将您的简单数据模型转化为复杂模型!无论您的应用程序是否将数据存储在磁盘上还是通过网络传输,该数据的结构和解释应该是清晰的。消费者最好与他们理解的数据一起工作!
模式描述并记录数据模型。如果您有数据,就应该有模式。
Protocol Buffers
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。
协议缓冲区还定义了一个符合模式的消息的二进制压缩编码。这种编码方式编码速度快,解码速度快,传输体积小,存储体积小。二进制编码使用了来自模式的数字标签,比如上面代码中的 5
代表 period
。
例如,让我们来编码这个恐龙
{
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?
协议缓冲区模式语言和二进制编码都由Google定义。Wire是Square的一个独立实现,专门为Android和Java设计。
对于方案中定义的每个消息类型,Wire会生成一个不可变模型类及其构建器。生成的代码看起来像是亲手编写的代码:它已文档化、格式化且简单。Wire的API应该让喜欢Effective Java的程序员感到得很熟悉。
尽管如此,Wire中有一些有趣的设计决策
-
Wire消息声明公共最终字段而不是常用的getter方法。这减少了生成的代码和执行的代码。更少的代码对于Android程序特别有益。
-
Wire避免了大小写映射。在模式下声明的字段
picture_urls
生成Java字段picture_urls
,而不是传统的pictureUrls
驼峰命名。虽然一开始这个名字可能感觉有些不自然,但当你使用grep
或更高级的搜索工具时却非常出色。在模式、Java源代码和数据之间导航不再需要映射。它还向调用代码提供了柔和的提示,说明协议消息有些特别。 -
原始类型总是装箱。如果一个字段不存在,它的值是
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 Compiler & Gradle Plugin了解配置方法。
Kotlin是一种实用和表达性的编程语言,使建模数据变得容易。这是我们如何使用Kotlin建模协议缓冲区消息的例子
-
消息看起来像是
data
类,但实际上并不是。编译器仍然为您生成equals()
、hashCode()
、toString()
和copy()
方法。然而,Wire不会生成componentN()
函数,我们相信解构声明不适合Protocol Buffers:如果模式发生了变化,移除或添加了一个字段,可能会导致您的解构声明仍然可以编译,但现在描述了完全不同的字段子集,从而使您的代码错误。 -
copy()
是Builder
的替代品,不再使用了。如果您的程序依赖于Builder
的存在,您可能需要使用Java互操作性模式,- Wire 编译器 & Gradle 插件解释了它是如何工作的。 -
字段作为属性生成。虽然在 Kotlin 中这是惯例,但 Java 代码现在必须使用 getter 访问字段。如果您的程序依赖于直接访问字段,请使用 Java 互操作性模式 - 编译器将为每个字段生成
@JvmField
注解。 -
每个字段类型的可空性取决于其标签:
required
、repeated
和map
字段具有不可可空类型,而optional
字段为可可空类型。 -
除
required
字段外,每个字段都有一个默认值- 对于
optional
字段为空值 emptyList()
对于repeated
字段emptyMap()
对于map
字段。
- 对于
生成的 Kotlin 代码
以下是上面定义的Dinosaur
消息的紧凑generate code
(生成代码
// 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 支持“beta”级别,可能会进行破坏性更改。尽管如此,Block 已将其用于生产应用程序和 SDK。
Swift 是一种务实且富有表现力的编程语言,具有丰富的值类型支持。以下是我们将 Swift 用于建模 Protocol Buffers 消息的方式
- 消息是符合
Equatable
、Codable
和Sendable
的 struct。所有消息都有值语义。 - 消息有一个成员初始化器来填充字段。
- 字段作为属性生成。
- 每个字段类型的可空性取决于其标签:
required
、repeated
和map
字段具有不可可空类型,而optional
字段为可可空类型。
生成的 Swift 代码
以下是上面定义的Dinosaur
消息的紧凑 generate code
(生成代码)
// 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.*
以从生成的代码中省略选项。