English | 中文
FastProto是一款Java编写的二进制数据处理工具,开发者可以通过注解来标记二进制数据中的字段信息(数据类型、字节偏移、大小开端等),然后调用简单的API即可实现解析和封包二进制数据。 它简化了二进制数据处理的流程,开发者并不需要编写复杂的代码。
- 通过注解来标记字段信息,快速地解析和封包二进制数据
- 支持Java基本数据类型、无符号类型、字符串类型、时间类型、数组类型和集合类型等
- 支持反向寻址,适用于非固定长度二进制数据,例如-1表示二进制数据的末尾
- 支持自定义字节顺序(大小开端)
- 自定义编码公式 & 解码公式,支持Lambda表达式
- 细化API文档
- 代码结构 & 性能优化
<dependency>
<groupId>org.indunet</groupId>
<artifactId>fastproto</artifactId>
<version>3.9.3</version>
</dependency>
有这样一个应用场景,一台气象监测设备实时采集气象数据,并以二进制格式发送数据到气象站,数据报文固定长度20字节,具体如下:
65 00 7F 69 3D 84 7A 01 00 00 55 00 F1 FF 0D 00 00 00 07 00
数据报文包含8种不同类型的信号,具体协议如下:
字节偏移 | 位偏移 | 数据类型(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 | 预留 |
气象站接收到数据后,需要将其解析成Java数据对象,以便后续的业务功能开发。
首先,按照协议定义Java数据对象Weather
,然后使用FastProto注解修饰各个字段,注解的offset属性信号的字节偏移量(地址)。
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()
方法将Java数据对象Weather
封包成二进制数据,其中方法的第二个参数是字节数组长度,如果用户不指定,那么FastProto会自动推测。
byte[] datagram = FastProto.encode(weather, 20);
也许你已经注意到压力信号对应一个换算公式,通常需要用户自行将序列化后的结果乘以0.1,这是物联网数据交换时极其常见的操作。
为了帮助用户减少中间步骤,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;
}
FastProto支持Java基础数据类型,考虑到跨语言跨平台的数据交换,还引入了无符号类型。
注解 | Java | C/C++ | 大小 |
---|---|---|---|
@BoolType | Boolean/boolean | bool | 1 位 |
@AsciiTye | Character/char | char | 1 字节 |
@CharTye | 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 字节 |
@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 字节 |
注解 | 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 | 编码公式 |
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表达式
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; // default Int32Type
@AutoType(offset = 14)
long pressure; // default Int64Type
}
在特殊场景下,如果在解析时忽略某些字段,或者封包时忽略某些字段,那么可通过注解@DecodingIgnore
和@EncodingIgnore
实现。
import org.indunet.fastproto.annotation.*;
public class Weather {
@DecodingFormula
@Int16Type(offset = 10)
int humidity; // ignore when parsing
@EncodingIgnore
@Int32Type(offset = 14)
long pressure; // ignore when packaging
}
FastProto支持case class,但是Scala并不完全兼容Java注解,所以请使用如下方式引用FastProto。
import org.indunet.fastproto.annotation.scala._
在一些特殊的情况下,开发者不希望或者无法使用注解修饰数据对象,例如数据对象来自第三方库,开发者不能修改源代码,又如开发者仅希望通过简单的方法创建二进制数据块。 FastProto提供了精简的API解决了上述问题,具体如下:
- 直接解析,不需要数据对象
boolean f1 = FastProto.decode(bytes)
.boolType(0, 0)
.getAsBoolean();
int f2 = FastProto.decode(bytes)
.int8Type(1) // 在字节偏移量1位置解析有符号8位整型数据
.getAsInt();
int f3 = FastProto.decode(bytes)
.int16Type(2) // 在字节偏移量2位置解析有符号16位整型数据
.getAsInt();
- 解析后映射成数据对象
byte[] bytes = ... // 待解析的二进制数据
public class DataObject {
Boolean f1;
Integer f2;
Integer f3;
}
JavaObject obj = FastProto.decode(bytes)
.boolType(0, 0, "f1")
.int8Type(1, "f2") // 在字节偏移量1位置解析有符号8位整型数据,字段名称f2
.int16Type(2, "f3")
.mapTo(JavaObject.class); // 将解析结果按照字段名称映射成指定的数据对象
byte[] bytes = FastProto.create()
.length(16) // 二进制数据块的长度
.uint8Type(0, 1) // 在字节偏移量0位置写入无符号8位整型数据1
.uint16Type(2, 3, 4) // 在字节偏移量2位置连续写入2个无符号16位整型数据3和4
.uint32Type(6, ByteOrder.BIG, 32)
.get();
- windows 11, i7 11th, 32gb
- openjdk 1.8.0_292
- 二进制数据固定大小60字节,数据对象共包含13个不同类型的字段
- 注解式API
Benchmark | 模式 | 样本数量 | 评分 | 误差 | 单位 |
---|---|---|---|---|---|
FastProto::decode |
吞吐量 | 10 | 240 | ± 4.6 | 次/毫秒 |
FastProto::encode |
吞吐量 | 10 | 317 | ± 11.9 | 次/毫秒 |
- 方法链式API
Benchmark | 模式 | 样本数量 | 评分 | 误差 | 单位 |
---|---|---|---|---|---|
FastProto::decode |
吞吐量 | 10 | 1273 | ± 17 | 次/毫秒 |
FastProto::create |
吞吐量 | 10 | 6911 | ± 162 | 次/毫秒 |
- Java 1.8+
- Maven 3.5+
FastProto取得了JetBrain开源计划的支持,可提供核心开发人员免费的全家桶许可证。 如果你对该项目感兴趣,并希望加入承担部分工作(开发/测试/文档),请通过邮件[email protected]联系我。
FastProto is released under the Apache 2.0 license.
Copyright 2019-2021 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.