@EncodedLength
2026/8/8大约 4 分钟
@EncodedLength
介绍 0.7.0
@EncodedLength 用于标记实体中的独立长度字段。编码时,框架先写入占位值,再根据指定字段范围的 实际编码字节数 回填长度。
它适合下面这类协议结构:
消息头 | 消息体长度 | 消息体 | 校验码消息体可能包含多个字段、条件字段、嵌套对象或继承自子类。手动维护长度容易遗漏, @EncodedLength 可以直接按照最终写入 ByteBuf 的字节数计算。
与前置长度字段的区别
prependLengthFieldType描述的是某个字符串、数组、内嵌对象 或 列表自身携带的前置长度;@EncodedLength描述的是实体中的一个独立字段,可以统计多个字段组成的编码范围。
范围语义
@EncodedLength 使用 [from, until) 左闭右开区间:
from指向第一个被统计的字段,包含该字段until指向统计结束后的第一个字段,不包含该字段- 字段位置按照继承、
order排序后的最终编码顺序判断,而不是只看源码声明顺序(实际上Java从没说过会保证字段声明顺序)
| 写法 | 统计范围 |
|---|---|
@EncodedLength | 从(当前)长度字段之后开始,一直到实体编码结束 |
@EncodedLength(until = "checksum") | 从(当前)长度字段之后开始,到 checksum 之前结束 |
@EncodedLength(from = "payload") | 从 payload 开始,一直到实体编码结束 |
@EncodedLength(from = "payload", until = "checksum") | 从 payload 开始,到 checksum 之前结束 |
继承场景示例
下面的公共父类定义消息头、消息体长度和校验字段。dataLength 位于消息体之前,checksum 位于最终编码顺序的末尾:
/**
* 将公共消息头和校验字段定义在父类中,消息体字段交给具体消息子类声明。
*
* @author hylexus
* @author Codex (AI)
*/
@ReferencedByDocs("guide/core/annotation-driven/encoded-length.md")
@SuppressWarnings("LombokGetterMayBeUsed")
public class BaseMessage {
@Preset.RustStyle.str(order = -600, length = 2)
protected String delimiter;
@Preset.RustStyle.u8(order = -500)
protected int commandFlag;
@Preset.RustStyle.u8(order = -400)
protected int replyFlag;
@Preset.RustStyle.str(order = -300, length = 10)
protected String identifier;
@Preset.RustStyle.u8(order = -200)
protected int encryptFlag;
// 自动统计后续子类消息体的编码字节数,不包含 checksum
@Preset.RustStyle.u16(order = -100)
@EncodedLength(until = "checksum")
protected int dataLength;
// region body
// 由子类定义
// endregion
@Preset.RustStyle.u8(order = 99999)
protected int checksum;from保持默认空字符串,因此范围从dataLength后面立即开始;until = "checksum"表示checksum本身不计入长度。
具体消息只需要继承父类并声明自己的消息体字段:
/**
* 继承 {@link BaseMessage} 的消息体示例。
*
* @author hylexus
* @author Codex (AI)
*/
@ReferencedByDocs("guide/core/annotation-driven/encoded-length.md")
@SuppressWarnings("LombokGetterMayBeUsed")
public class DemoMessage005 extends BaseMessage {
// 数据采集时间 BYTE[6]
@Preset.JtStyle.BcdDateTime
private LocalDateTime time;
// 登入流水号
@Preset.RustStyle.u16
private int serialNumber;
// 集成电路卡识别码(ICCID)
@Preset.RustStyle.str(length = 20)
private String iccid;
// 电池管理系统对应动力蓄电池包个数
@Preset.RustStyle.byte_array(prependLengthFieldType = PrependLengthFieldType.u8)
private byte[] bmsBatteryCount;
@Preset.RustStyle.list(lengthExpression = "getBmsBatteriesEncodedLength()")
private List<BmsBattery> bmsBatteries;
public record BmsBattery(@Preset.RustStyle.str(length = 24) String id) {
}
public int getBmsBatteriesEncodedLength() {
int sum = 0;
for (final byte c : this.bmsBatteryCount) {
sum += c;
}
return sum * 24;
}父类不需要知道子类有哪些字段。
元数据注册时,框架会合并继承字段并按照 order 排序, 因此 time、serialNumber、iccid、bmsBatteryCount 和 bmsBatteries 都会自动纳入 dataLength 的统计范围。
该示例的消息体长度为:
| 字段 | 编码长度 |
|---|---|
time | 6 字节 BCD 时间 |
serialNumber | 2 字节 u16 |
iccid | 20 字节定长字符串 |
bmsBatteryCount | 1 字节前置长度 + 1 字节内容 |
bmsBatteries | 1 个 24 字节记录 |
| 合计 | 54 字节 |
对应测试会同时检查编码缓冲区中的长度字段和解码后的 dataLength:
@ReferencedByDocs("guide/core/annotation-driven/encoded-length.md")
class DemoMessage005Test extends BaseEntityCodecTest {
private static final int DATA_LENGTH_FIELD_OFFSET = 15;
private static final int EXPECTED_BODY_LENGTH = 54;
@Test
void testEncodedLengthAcrossInheritance() {
final DemoMessage005 entity = new DemoMessage005();
// 父类公共字段
entity.setDelimiter("$$")
.setCommandFlag(0x01)
.setReplyFlag(0x01)
.setIdentifier("0123456789")
.setEncryptFlag(0x01)
.setChecksum(111);
// 子类字段
entity.setTime(LocalDateTime.of(2026, 8, 8, 12, 30, 45))
.setSerialNumber(111)
.setIccid("11111111110000000000")
.setBmsBatteryCount(new byte[]{1})
.setBmsBatteries(List.of(new DemoMessage005.BmsBattery("012345678901234567891234")));
final ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
try {
EntityCodec.DEFAULT.encode(entity, buffer);
assertEquals(EXPECTED_BODY_LENGTH, buffer.getUnsignedShort(DATA_LENGTH_FIELD_OFFSET));
assertEquals(0, entity.getDataLength());
final String hexString = FormatUtils.toHexString(buffer);
final ByteBuf buffer2 = XtreamBytes.byteBufFromHexString(ByteBufAllocator.DEFAULT, hexString);
try {
final DemoMessage005 decoded = EntityCodec.DEFAULT.decode(DemoMessage005.class, buffer2);
assertEquals(EXPECTED_BODY_LENGTH, decoded.getDataLength());
assertEquals(entity.getTime(), decoded.getTime());
assertEquals(entity.getSerialNumber(), decoded.getSerialNumber());
assertEquals(entity.getIccid(), decoded.getIccid());
assertArrayEquals(entity.getBmsBatteryCount(), decoded.getBmsBatteryCount());
assertEquals(entity.getBmsBatteries(), decoded.getBmsBatteries());
assertEquals(entity.getChecksum(), decoded.getChecksum());
} finally {
buffer2.release();
}
} finally {
buffer.release();
}
}
}编解码行为
编码
编码长度字段时,框架会:
- 在长度字段位置写入
0作为占位值 - 记录范围开始时的
writerIndex - 正常编码范围内的字段
- 在
until字段之前或实体编码结束时,通过writerIndex差值计算长度 - 使用
ByteBuf#setByte、setShort或setInt原地回填
- 计算过程不会复制消息体,也不会进行第二次编码。
- 条件表达式不成立或值为
null的字段没有写入字节, 因此也不会被计入长度。
长度字段在源对象中的原始值 不会被用于编码 ,框架也不会修改源对象;重新解码后,长度字段会得到实际编码值。
解码
解码时,@EncodedLength 字段按照普通无符号整数字段读取。
@EncodedLength 不会 自动限制后续字段的解码范围。后续字符串、数组、列表、内嵌对象 或 动态字段 仍需通过 自身的固定长度、长度表达式或其他字段编解码配置确定读取方式。
字段要求
当前版本有以下限制:
- 长度字段必须同时使用
@XtreamField或其(内置/自定义)别名注解 - 长度字段仅支持大端无符号
u8、u16、u32 - 一个实体的最终字段列表中只能存在一个
@EncodedLength from、until指向的字段必须存在,并且位于长度字段之后- 同时声明
from和until时,from必须位于until之前 @EncodedLength不能标记在仅作为@DerivedField的字段上
不满足这些条件时,框架会在构建实体元数据时抛出 IllegalArgumentException,而不是等到编码中途失败。