English | 中文
FastProto 是一个面向 二进制通信协议 的高性能序列化 / 反序列化 工具库(Library),通过注解即可定义复杂的字节流结构,使 Java 开发者能够以最自然的方式读写 IoT、车载系统、工控设备、能源计量等场景中的自定义二进制报文。
- 声明式优于命令式: 协议字段用注解定义,结构一目了然,代码即文档。
- 性能与可维护并重: 在架构层做反射缓存等优化,在保持高吞吐的同时兼顾可维护性。
- 兼容工程现实: 支持大小端混排、位域、BCD、CRC 等传统协议特征,方便对接存量系统。
- 注解驱动协议映射: 用少量注解描述二进制布局,替代手写偏移和位运算。
- 类型与布局支持丰富: 覆盖基础类型(含无符号)、字符串、时间、数组/集合,并支持可变长度、动态偏移和反向地址。
- 表示精确可控: 可配置字节序/位序,支持 BCD、位域以及自定义编解码公式。
- 内置校验和/CRC: 通过
@Checksum或工具方法直接使用常见 CRC / 校验和算法。 - 生态与 API 友好: 提供 Netty 编解码器、Kafka 序列化组件,以及无需注解的链式 API。
- 代码结构 & 性能优化
- 丰富文档(新增核心功能使用指南)
- Maven
<dependency>
<groupId>org.indunet</groupId>
<artifactId>fastproto</artifactId>
<version>4.1.0</version>
</dependency>在Java环境下高效、安全地处理各种传统二进制协议,是工业界一个常见但长期被忽视的痛点,下面将以解析一条 20 字节的气象报文为例进行说明:
65 00 7F 69 3D 84 7A 01 00 00 55 00 F1 FF 0D 00 00 00 07 00
报文共包含八个信号,协议如下:
| 字节偏移 | 位偏移 | 数据类型(C/C++) | 信号名称 | 单位 | 换算公式 |
|---|---|---|---|---|---|
| 0 | unsigned char | 设备编号 | |||
| 1 | 预留 | ||||
| 2-9 | long | 时间戳 | ms | ||
| 10-11 | unsigned short | 湿度 | %RH | ||
| 12-13 | short | 温度 | ℃ | ||
| 14-17 | unsigned int | 气压 | Pa | p * 0.1 | |
| 18 | 0 | bool | 设备有效标识 | ||
| 18 | 3-7 | 预留 | |||
| 19 | 预留 |
在没有使用 FastProto 之前,传统做法通常是手写各种偏移和位运算,大致如下所示:
byte[] datagram = ...;
int id = datagram[0] & 0xFF;
long timeMillis =
((long) datagram[2] & 0xFF) |
(((long) datagram[3] & 0xFF) << 8) |
(((long) datagram[4] & 0xFF) << 16) |
(((long) datagram[5] & 0xFF) << 24) |
(((long) datagram[6] & 0xFF) << 32) |
(((long) datagram[7] & 0xFF) << 40) |
(((long) datagram[8] & 0xFF) << 48) |
(((long) datagram[9] & 0xFF) << 56);
Timestamp time = new Timestamp(timeMillis);
int humidity = (datagram[10] & 0xFF) | ((datagram[11] & 0xFF) << 8);
int temperature = (short) ((datagram[12] & 0xFF) | ((datagram[13] & 0xFF) << 8));
long rawPressure =
((long) datagram[14] & 0xFF) |
(((long) datagram[15] & 0xFF) << 8) |
(((long) datagram[16] & 0xFF) << 16) |
(((long) datagram[17] & 0xFF) << 24);
double pressure = rawPressure * 0.1;
boolean deviceValid = (datagram[18] & 0x01) != 0;随着字段增多、协议版本演进,这类位运算代码既冗长又难维护,稍不注意就会在偏移、大小端或符号位上出错。下面用 FastProto 重新表述同一份协议,只需要在 Weather 类字段上写注解即可:
气象站接收到报文后,需要把它解码成 Java 对象。按照协议定义 Weather 类,并在字段上标注字节偏移。
import org.indunet.fastproto.annotation.*;
public class Weather {
@UInt8Type(offset = 0)
int id;
@TimeType(offset = 2)
Timestamp time;
@UInt16Type(offset = 10)
int humidity;
@Int16Type(offset = 12)
int temperature;
@UInt32Type(offset = 14)
long pressure;
@BoolType(byteOffset = 18, bitOffset = 0)
boolean deviceValid;
}调用FastProto::decode()方法将二进制数据解码成Java数据对象Weather
byte[] datagram = ... // 检测设备发送的二进制报文
Weather weather = FastProto.decode(datagram, Weather.class);调用 FastProto::encode() 将 Weather 对象写回字节数组。第二个参数是数据长度,留空时 FastProto 会自动推测。
byte[] datagram = FastProto.encode(weather, 20);压力字段需要做简单的换算。FastProto 提供 @EncodingFormula 和 @DecodingFormula,可直接用 Lambda 表达式完成转换。 查看 变换公式文档 了解更多。
import org.indunet.fastproto.annotation.DecodingFormula;
import org.indunet.fastproto.annotation.EncodingFormula;
public class Weather {
...
@UInt32Type(offset = 14)
@DecodingFormula(lambda = "x -> x * 0.1") // 解码后得到的pressure等于uint32 * 0.1
@EncodingFormula(lambda = "x -> (long) (x * 10)") // 写入二进制的数据等于强制转换为长整型的(pressure * 0.1)
double pressure;
}注意: Lambda 公式需要
fastproto-processor模块,请将其添加为provided作用域的依赖。详见 Android 指南。
FastProto支持Java基础数据类型,考虑到跨语言跨平台的数据交换,还引入了无符号类型。
| 注解 | Java | C/C++ | 大小 |
|---|---|---|---|
| @BoolType | Boolean/boolean | bool | 1 位 |
| @AsciiType | Character/char | char | 1 字节 |
| @CharType | Character/char | -- | 2 字节 |
| @Int8Type | Byte/byte/Integer/int | char | 1 字节 |
| @Int16Type | Short/short/Integer/int | short | 2 字节 |
| @Int32Type | Integer/int | int | 4 字节 |
| @Int64Type | Long/long | long | 8 字节 |
| @BitFieldType | Integer/int | -- | 1..31 位 |
| @UInt8Type | Integer/int | unsigned char | 1 字节 |
| @UInt16Type | Integer/int | unsigned short | 2 字节 |
| @UInt32Type | Long/long | unsigned int | 4 字节 |
| @UInt64Type | BigInteger | unsigned long | 8 字节 |
| @FloatType | Float/float | float | 4 字节 |
| @DoubleType | Double/double | double | 8 字节 |
| @BcdType | Integer/int | BCD | N 字节 |
| 注解 | Java | C/C++ | 大小 |
|---|---|---|---|
| @StringType | String/ StringBuilder/StringBuffer | -- | N 字节 |
| @TimeType | Timestamp/Date/Calendar/Instant | long | 8 字节 |
| @EnumType | enum | enum | 1 字节 |
| 注解 | Java | C/C++ |
|---|---|---|
| @BinaryType | Byte[]/byte[]/Collection<Byte> | char[] |
| @BoolArrayType | Boolean[]/boolean[]/Collection<Boolean> | bool[] |
| @AsciiArrayType | Character[]/char[]/Collection<Character> | char[] |
| @CharArrayType | Character[]/char[]/Collection<Character> | -- |
| @Int8ArrayType | Byte[]/byte[]/Integer[]/int[]/Collection<Byte>/Collection<Integer> | char[] |
| @Int16ArrayType | Short[]/short[]/Integer[]/int[]/Collection<Short>/Collection<Integer> | short[] |
| @Int32ArrayType | Integer[]/int[]/Collection<Integer> | int[] |
| @Int64ArrayType | Long[]/long[]/Collection<Long> | long[] |
| @UInt8ArrayType | Integer[]/int[]/Collection<Integer> | unsigned char[] |
| @UInt16ArrayType | Integer[]/int[]/Collection<Integer> | unsigned short[] |
| @UInt32ArrayType | Long[]/long[]/Collection<Long> | unsigned int[] |
| @UInt64ArrayType | BigInteger[]/Collection<BigInteger> | unsigned long[] |
| @FloatArrayType | Float[]/float[]/Collection<Float> | float[] |
| @DoubleArrayType | Double[]/double[]/Collection<Double> | double[] |
FastProto还提供了一些辅助注解,帮助用户进一步自定义二进制格式、解码和编码流程。
| 注解 | 作用域 | 描述 |
|---|---|---|
| @DefaultByteOrder | Class | 默认字节顺序,如无指定,使用小端 |
| @DefaultBitOrder | Class | 默认位顺序,如无指定,使用LSB_0 |
| @DecodingIgnore | Field | 反序列化时忽略该字段 |
| @EncodingIgnore | Field | 序列化时忽略该字段 |
| @DecodingFormula | Field | 解码公式 |
| @EncodingFormula | Field | 编码公式 |
| @Expect | Field | 固定断言:在固定偏移校验/写入常量 |
| @Expects | Field | 可重复 @Expect 的容器注解 |
FastProto默认使用小端,可以通过@DefaultByteOrder注解修改全局字节顺序,也可以通过数据类型注解中的byteOrder属性修改特定字段的字节顺序,后者优先级更高。 查看 字节序与位序文档 了解更多。
同理,FastProto默认使用LSB_0,可以通过@DefaultBitOrder注解修改全局位顺序,也可以通过数据类型注解中的bitOrder属性修改特定字段的位顺序,后者优先级更高。
import org.indunet.fastproto.ByteOrder;
import org.indunet.fastproto.annotation.DefaultByteOrder;
@DefaultByteOrder(ByteOrder.BIG)
@DefaultBitOrder(BitOrder.LSB_0)
public class Weather {
@UInt16Type(offset = 10, byteOrder = ByteOrder.LITTLE)
int humidity;
@BoolType(byteOffset = 18, bitOffset = 0, bitOrder = BitOrder.MSB_0)
boolean deviceValid;
}用户可以通过两种方式自定义公式,形式较为简单的公式建议使用Lambda表达式,形式较为复杂的公式建议自定义公式类并实现java.lang.function.Function接口。
- Lambda表达式
Lambda 公式由 FastProto 注解处理器(fastproto-processor)在编译时处理,这确保了与 Android 和 Java 11+ JRE 环境的兼容性。只需添加处理器依赖:
<dependency>
<groupId>org.indunet</groupId>
<artifactId>fastproto-processor</artifactId>
<version>4.0.0</version>
<scope>provided</scope>
</dependency>import org.indunet.fastproto.annotation.DecodingFormula;
import org.indunet.fastproto.annotation.EncodingFormula;
public class Weather {
...
@UInt32Type(offset = 14)
@DecodingFormula(lambda = "x -> x * 0.1")
@EncodingFormula(lambda = "x -> (long) (x * 10)")
double pressure;
}- 自定义公式类
import java.util.function.Function;
public class PressureDecodeFormula implements Function<Long, Double> {
@Override
public Double apply(Long value) {
return value * 0.1;
}
}import java.util.function.Function;
public class PressureEncodeFormula implements Function<Double, Long> {
@Override
public Long apply(Double value) {
return (long) (value * 10);
}
}import org.indunet.fastproto.annotation.DecodingFormula;
import org.indunet.fastproto.annotation.EncodingFormula;
public class Weather {
...
@UInt32Type(offset = 14)
@DecodingFormula(PressureDecodeFormula.class)
@EncodingFormula(PressureEncodeFormula.class)
double pressure;
}用户可以根据需要仅指定编码公式,或者仅指定解码公式,如果同时指定Lambda表达式和自定义公式类,后者有更高的优先级。
如果字段被@AutoType修饰,那么FastProto会自动推测类型。
import org.indunet.fastproto.annotation.AutoType;
public class Weather {
@AutoType(offset = 10, byteOrder = ByteOrder.LITTLE)
int humidity; // 默认 Int32Type
@AutoType(offset = 14)
long pressure; // 默认 Int64Type
}在一些特殊情况下,如果你希望在解析时忽略某些字段,或在封装时忽略某些字段,可以使用 @DecodingIgnore 与 @EncodingIgnore。
import org.indunet.fastproto.annotation.*;
public class Weather {
@DecodingIgnore
@Int16Type(offset = 10)
int humidity; // 解析时忽略
@EncodingIgnore
@Int32Type(offset = 14)
long pressure; // 封装时忽略
}使用 @Checksum 一次性定义“起始地址 + 长度 + 校验和存放地址”。FastProto 会在编码时自动写入校验和,在解码时自动校验并在不匹配时抛出异常。 查看 校验和文档 了解更多。
- CRC16(小端)示例:计算区间 [0,5),CRC 写入字节 5..6
import org.indunet.fastproto.ByteOrder;
import org.indunet.fastproto.FastProto;
import org.indunet.fastproto.annotation.*;
public class Packet {
@UInt8Type(offset = 0) int b1;
@UInt8Type(offset = 1) int b2;
@UInt8Type(offset = 2) int b3;
@UInt8Type(offset = 3) int b4;
@UInt8Type(offset = 4) int b5;
// 只需一个注解即可:起始=0,长度=5,CRC16 小端写到 5..6
@Checksum(start = 0, length = 5, offset = 5, type = Checksum.Type.CRC16, byteOrder = ByteOrder.LITTLE)
int crc16;
}
// 编码:自动计算并写入 CRC
Packet p = new Packet();
p.b1 = 0x31; p.b2 = 0x32; p.b3 = 0x33; p.b4 = 0x34; p.b5 = 0x35;
byte[] bytes = FastProto.encode(p, 7); // 5 字节数据 + 2 字节 CRC16
// 解码:自动校验 CRC,不匹配将抛出 DecodingException
Packet q = FastProto.decode(bytes, Packet.class);- 也可不使用注解,直接调用工具方法计算校验和:
import org.indunet.fastproto.annotation.Checksum;
import org.indunet.fastproto.checksum.ChecksumUtils;
byte[] bytes = new byte[]{0x31,0x32,0x33,0x34,0x35};
long crc = ChecksumUtils.calculate(bytes, /*start=*/0, /*length=*/5, Checksum.Type.CRC16);
// 或者:
int crc16 = ChecksumUtils.crc16(bytes) & 0xFFFF; // 计算整个数组的 CRC16在某些场景下,开发者不想或不能用注解修饰数据对象,例如对象来源于第三方库无法修改源代码,或只是想以更直接的方式创建二进制数据块。FastProto 提供了简单 API 来满足上述需求。
- 解码到数据对象
byte[] bytes = ... // 待解码的二进制数据
public class DataObject {
Boolean f1;
Integer f2;
Integer f3;
}
DataObject obj = FastProto.decode(bytes)
.readBool("f1", 0, 0) // 读取字节偏移 0、位偏移 0 的布尔值
.readInt8("f2", 1) // 读取字节偏移 1 的有符号 8 位整数
.readInt16("f3", 2) // 读取字节偏移 2 的有符号 16 位整数
.mapTo(DataObject.class); // 根据字段名映射到 Java 对象- 不使用数据对象
import org.indunet.fastproto.util.DecodeUtils;
byte[] bytes = ... // 待解码的二进制数据
boolean f1 = DecodeUtils.readBool(bytes, 0, 0); // 读取字节偏移 0、位偏移 0 的布尔值
int f2 = DecodeUtils.readInt8(bytes, 1); // 读取字节偏移 1 的有符号 8 位整数
int f3 = DecodeUtils.readInt16(bytes, 2); // 读取字节偏移 2 的有符号 16 位整数byte[] bytes = FastProto.create(16) // 创建长度为 16 字节的二进制块
.writeInt8(0, 1) // 在偏移 0 写入无符号 8 位整数 1
.writeUInt16(2, 3, 4) // 在偏移 2 连续写入两个无符号 16 位整数 3、4
.writeUInt32(6, ByteOrder.BIG, 256) // 在偏移 6 按大端写入无符号 32 位整数 256
.get();import org.indunet.fastproto.util.EncodeUtils;
byte[] bytes = new byte[16];
EncodeUtils.writeInt8(bytes, 0, 1); // 在偏移 0 写入无符号 8 位整数 1
EncodeUtils.writeUInt16(bytes, 2, 3, 4); // 在偏移 2 连续写入两个无符号 16 位整数 3、4
EncodeUtils.writeUInt32(bytes, 6, ByteOrder.BIG, 256); // 在偏移 6 按大端写入无符号 32 位整数 256- windows 11, Intel Core Ultra 9 275HX, 64GB
- openjdk 11.0.20
- 以下性能测试均在单线程环境下执行,在多线程场景下,性能可实现接近线性的提升。
- 60 字节二进制数据、13 个字段的协议类。
- 注解 API
| Benchmark | Mode | Samples | Score | Error | Units |
|---|---|---|---|---|---|
FastProto::decode |
throughput | 10 | 239 | ± 4.6 | ops/ms |
FastProto::encode |
throughput | 10 | 271 | ± 11.9 | ops/ms |
- 非注解 API
| Benchmark | Mode | Samples | Score | Error | Units |
|---|---|---|---|---|---|
decode |
throughput | 10 | 1699 | ± 17 | ops/ms |
create |
throughput | 10 | 10882 | ± 162 | ops/ms |
- Java 1.8+
- Maven 3.5+
如果你对本项目感兴趣并希望参与(开发/测试/文档),欢迎通过邮箱联系:deng_ran@aliyun.com
开发 FastProto 并非出于盈利,而是在纷繁日常里,写代码能让我回到内心的宁静。若它也对你有所助益,便是我持续打磨的动力。
FastProto 以 Apache 2.0 许可 发布。
Copyright 2019-2025 indunet.org
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at the following link.
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
