English | 中文
FastProto is a powerful Java library designed to simplify binary data processing. It offers an intuitive, annotation-driven approach to encoding and decoding complex binary data structures, eliminating the need for cumbersome manual coding.
- Annotation-Driven: Use annotations to define binary data structures, making parsing and packaging fast and intuitive.
- Extensive Type Support: Handle Java primitives, unsigned types, strings, time types, arrays, and collections.
- Flexible Addressing: Support for reverse addressing, ideal for non-fixed-length binary data.
- Customizable Byte Order: Specify big-endian or little-endian byte order to match various data specifications.
- Encoding and Decoding Formulas: Support for custom formulas, including lambda expressions, for complex data operations.
- Diverse APIs: Variety of APIs to suit different application scenarios, ensuring efficiency and reliability.
- Code structure & performance optimization
- Add crc checksum support
<dependency>
<groupId>org.indunet</groupId>
<artifactId>fastproto</artifactId>
<version>3.10.3</version>
</dependency>
Imagine such an application, there is a monitoring device collecting weather data in realtime and sends to the weather station in binary format,the binary data has fixed length of 20 bytes:
65 00 7F 69 3D 84 7A 01 00 00 55 00 F1 FF 0D 00 00 00 07 00
The binary data contains 8 different types of signals, the specific protocol is as follows:
Byte Offset | Bit Offset | Data Type(C/C++) | Signal Name | Unit | Formula |
---|---|---|---|---|---|
0 | unsigned char | device id | |||
1 | reserved | ||||
2-9 | long | time | ms | ||
10-11 | unsigned short | humidity | %RH | ||
12-13 | short | temperature | ℃ | ||
14-17 | unsigned int | pressure | Pa | p * 0.1 | |
18 | 0 | bool | device valid | ||
18 | 3-7 | reserved | |||
19 | reserved |
After the weather station receives the data, it needs to be converted into Java data objects for subsequent business function development.
First, define the Java data object Weather
according to the protocol, and then use the FastProto data type annotation to annotate each attribute.
It should be noted that the offset
attribute of annotation corresponds to the byte offset of the signal.
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;
}
Invoke the FastProto::decode()
method to parse the binary data into the Java data object Weather
// datagram sent by monitoring device.
byte[] datagram = ...
Weather weather = FastProto.decode(datagram, Weather.class);
Invoke the FastProto::encode()
method to package the Java data object Weather
into binary data.
The second parameter of this method is the length of the binary data.
If the user does not specify it, FastProto will automatically guess the length.
byte[] datagram = FastProto.encode(weather, 20);
Perhaps you have noticed that the pressure signal corresponds to a conversion formula, usually requiring the user to multiply
the serialized result by 0.1, which is an extremely common operation in IoT data exchange.
To help users reduce intermediate steps, FastProto introduces decoding formula annotation @DecodingFormula
and encoding formula annotation @EncodingFormula
,
the above simple formula transformation can be implemented by Lambda expression.
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;
}
FastProto supports Java primitive data types, taking into account cross-language and cross-platform data exchange, unsigned types are also introduced.
Annotation | Java | C/C++ | Size |
---|---|---|---|
@BoolType | Boolean/boolean | bool | 1 bit |
@AsciiType | Character/char | char | 1 bytes |
@CharType | Character/char | -- | 2 bytes |
@Int8Type | Byte/byte/Integer/int | char | 1 byte |
@Int16Type | Short/short / Integer/int | short | 2 bytes |
@Int32Type | Integer/int | int | 4 bytes |
@Int64Type | Long/long | long | 8 bytes |
@UInt8Type | Integer/int | unsigned char | 1 byte |
@UInt16Type | Integer/int | unsigned short | 2 bytes |
@UInt32Type | Long/long | unsigned int | 4 bytes |
@UInt64Type | BigInteger | unsigned long | 8 bytes |
@FloatType | Float/float | float | 4 bytes |
@DoubleType | Double/double | double | 8 bytes |
Annotation | Java | C/C++ | Size |
---|---|---|---|
@StringType | String/StringBuilder/StringBuffer | -- | N bytes |
@TimeType | Timestamp/Date/Calendar/Instant | long | 8 bytes |
@EnumType | enum | enum | 1 bytes |
Annotation | 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 also provides some auxiliary annotations to help users further customize the binary format, decoding and encoding process.
Annotation | Scope | Description |
---|---|---|
@DefaultByteOrder | Class | Default byte order, use little endian if not specified. |
@DefaultBitOrder | Class | Default bit order, use LSB_0 if not specified. |
@DecodingIgnore | Field | Ignore the field when decoding. |
@EncodingIgnore | Field | Ignore the field when encoding. |
@DecodingFormula | Field | Decoding formula. |
@EncodingFormula | Field | Encoding formula. |
@AutoType | Field | Use default type. |
FastProto uses little endian by default. You can modify the global byte order through @DefaultByteOrder
annotation, or you can
modify the byte order of specific field through byteOrder
attribute which has a higher priority.
Similarly, FastProto uses LSB_0 by default. You can modify the global bit order through @DefaultBitOrder
annotation, or you can
modify the bit order of specific field through bitOrder
attribute which has a higher priority.
import org.indunet.fastproto.BitOrder;
import org.indunet.fastproto.ByteOrder;
import org.indunet.fastproto.annotation.DefaultBitOrder;
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;
}
Users can customize formula in two ways. For simple formulas, it is recommended to use Lambda expression, while for more
complex formula, it is recommended to customize formula classes by implementing the java.lang.function.Function
interface.
- Lambda Expression
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 after parsing equals uint32 * 0.1
@EncodingFormula(lambda = "x -> (long) (x * 10)") // Data written into binary equals (pressure * 0.1) cast to long
double pressure;
}
- Custom Formula Class
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;
}
Users can specify only the encoding formula or only the decoding formula as needed. If both lambda expression and custom formula class are specified, the latter has a higher priority.
FastProto can automatically infer type if field is annotated by @AutoType
.
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
}
In special cases, if you want to ignore certain fields during parsing, or ignore certain fields during packaging,
you can use @DecodingIgnore
and @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 supports case class,but Scala is not fully compatible with Java annotations, so please refer to FastProto as follows.
import org.indunet.fastproto.annotation.scala._
In some special cases, developers do not want or cannot use annotations to decorate data objects, for example, data objects come from third-party libraries, developers cannot modify the source code, and developers only want to create binary data blocks in a simple way. FastProto provides simple API to solve the above problems, as follows:
- Decode with data object
byte[] bytes = ... // Binary data to be decoded
public class DataObject {
Boolean f1;
Integer f2;
Integer f3;
}
DataObject obj = FastProto.decode(bytes)
.readBool("f1", 0, 0) // Decode boolean data at byte offset 0 and bit offset 0
.readInt8("f2", 1) // Decode signed 8-bit integer data at byte offset 1
.readInt16("f3", 2) // Decode signed 8-bit integer data at byte offset 2
.mapTo(DataObject.class); // Map decoded result into Java data object according to the field name
- Decode without data object
import org.indunet.fastproto.util.DecodeUtils;
byte[] bytes = ... // Binary data to be decoded
boolean f1 = DecodeUtils.readBool(bytes, 0, 0); // Decode boolean data at byte offset 0 and bit offset 0
int f2 = DecodeUtils.readInt8(bytes, 1); // Decode signed 8-bit integer data at byte offset 1
int f3 = DecodeUtils.readInt16(bytes, 2); // Decode signed 8-bit integer data at byte offset 2
byte[] bytes = FastProto.create(16) // Create binary block with 16 bytes
.writeInt8(0, 1) // Write unsigned 8-bit integer 1 at byte offset 0
.writeUInt16(2, 3, 4) // Write 2 unsigned 16-bit integer 3 and 4 consecutively at byte offset 2
.writeUInt32(6, ByteOrder.BIG, 256) // Write unsigned 32-bit integer 256 at byte offset 6 with big endian
.get();
import org.indunet.fastproto.util.EncodeUtils;
byte[] bytes = new byte[16];
EncodeUtils.writeInt8(bytes, 0, 1); // Write unsigned 8-bit integer 1 at byte offset 0
EncodeUtils.writeUInt16(bytes, 2, 3, 4); // Write 2 unsigned 16-bit integer 3 and 4 consecutively at byte offset 2
EncodeUtils.writeUInt32(bytes, 6, ByteOrder.BIG, 256); // Write unsigned 32-bit integer 256 at byte offset 6 with big endian
- windows 11, i7 11th, 32gb
- openjdk 1.8.0_292
- binary data of 60 bytes and protocol class of 13 fields
- api with annotations
Benchmark | Mode | Samples | Score | Error | Units |
---|---|---|---|---|---|
FastProto::decode |
throughput | 10 | 240 | ± 4.6 | ops/ms |
FastProto::encode |
throughput | 10 | 317 | ± 11.9 | ops/ms |
- api without annotations
Benchmark | Mode | Samples | Score | Error | Units |
---|---|---|---|---|---|
FastProto::decode |
throughput | 10 | 1273 | ± 17 | ops/ms |
FastProto::create |
throughput | 10 | 6911 | ± 162 | ops/ms |
- Java 1.8+
- Maven 3.5+
FastProto has obtained the support of JetBrain Open Source Project, which can provide free license of all product pack for all core contributors. If you are interested in this project and want to join and undertake part of the work (development/testing/documentation), please feel free to contact me via email [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.