Skip to content

数据实体定义的最佳实践

ray.zh edited this page Dec 3, 2023 · 21 revisions

框架对于数据结构实体定义是没有限制的。 可以使用以下数据类型:

  • byte/boolean/char/short/int/long/float/double
  • Byte/Boolean/Char/Short/Integer/Long/Float/Double/String/UByte/UShort/UInt/ULong/UNumber
  • 其他有正确定义数据格式的实体类

对于框架来说,在实际项目使用中,两种使用方式:

  • 一是使用框架的消息注册,此时需要消息体中至少存在一个命令字段,此时在进行消息pack操作时,可以不传入目标类,框架将会根据注册消息自动寻找并使用。
  • 二是自定义消息,然后分段解析后再进行业务操作

下面示例中将分别展示这两种方式使用上的区别。

1. 使用消息注册解析

1.1. 代码示例

完整代码位于项目com.github.misterchangray.core.messager目录下

在本例中假设存在两种消息报文TeacherStudent,其中Teacher是使用组合的方式将报文头进行整合。

1.1.1. 数据实体类定义

// 这是公共头
@MagicClass
public class Head {
    @MagicField(order = 1, defaultVal = 0xA8)
    private byte head;
    @MagicField(order = 2)
    private UInt len;
    // cmdField = true,标记此字段为消息id字段
    @MagicField(order = 3, cmdField = true)
    private byte cmd = 34;
    // ignore setter/getter
}


// 注意需要实现 MagicMessage  类
// 此消息中cmd属性在定义时申明为33, 即注册后cmd=33代表为`Student`消息
@MagicClass
public class Student implements MagicMessage {
    @MagicField(order = 1)
    private byte head;
    @MagicField(order = 2, calcLength = true)
    private UInt len;
    // 因为每个消息都有独立的cmd字段,所以这里将消息类型在申明时固定写死为33
    // 没有使用公共头,所以类型字段建议static,final定义,避免被修改
    // cmdField = true,标记此字段为消息id字段
    @MagicField(order = 3, cmdField = true)
    private static final byte cmd = 33;
    @MagicField(order = 4)
    private int age;
    @MagicField(order = 5, size = 10)
    private String name;

    @Override
    public int cmd() {
        return this.cmd;
    }

    // ignore setter/getter
}

// 注意需要实现 MagicMessage  类
// 此消息使用给公共头,所以注册时需要指定消息类型
@MagicClass
public class Teacher implements MagicMessage {
    @MagicField(order = 1)
    private Head head;
    @MagicField(order = 4)
    private int age;
    @MagicField(order = 5, size = 10)
    private String name;
    @Override
    public int cmd() {
        return this.head.getCmd();
    }
    // ignore setter/getter
}

1.1.2. 消息解析示例

 public void main() {
        // 消息注册,Student消息类型在定义时已申明,所以可以直接注册
        MagicByte.registerCMD(Student.class);

        // 准备测试数据
        Student student = new Student();
        student.setAge(13);
        student.setName("AA");

        // 序列号转换
        byte[] bytes = MagicByte.unpackToByte(student);
        MagicMessage s1 = MagicByte.pack(bytes);

        // 可以通过 cmd() 函数获取当前消息类型, 前提是必须要实现接口  MagicMessage.cmd  方法
        if(s1.cmd() == 33) {
            // 将消息转为具体的消息值即可实现业务逻辑
            Student s = (Student) s1;

        }


        // Teacher消息使用公共头,所以注册时需要指定消息id
        // 注册后cmd=34代表为 Teacher 消息
        MagicByte.registerCMD(34, Teacher.class);

        // 准备测试数据
        Head head = new Head();
        head.setCmd((byte) 34);
        head.setLen(UInt.valueOf(17));
        Teacher teacher = new Teacher();
        teacher.setHead(head);
        teacher.setAge(38);
        teacher.setName("EABC");

        // 序列号转换
        byte[] bytes2 = MagicByte.unpackToByte(teacher);
        MagicMessage s2 = MagicByte.pack(bytes2);

        // 可以通过 cmd() 函数获取当前消息类型, 前提是必须要实现接口  MagicMessage.cmd  方法
        if(s2.cmd() == 33) {
            // 将消息转为具体的消息值即可实现业务逻辑
            Teacher s3  = (Teacher)s2;

        }

    }

2: 自行解析数据和代码

2.1. 代码示例:

完整代码位于项目com.github.misterchangray.core.example目录下

在本例中假设存在两种数据报文HeatbeatCmdEchoCmd

2.1.1. 数据实体类定义

/**
 * 报文头
 * 一般来说为公共结构, 即每个数据都应该有此字段
 */
@MagicClass
public class Head {
    @MagicField(order = 1)
    private byte header;
    // calcLength = true 时,此字段将会在序列化时自动填充数据实际长度
    @MagicField(order = 3, calcLength = true)
    private byte length;
    // 假设 1=HeartbeatCmd,2=EchoCmd
    @MagicField(order = 5)
    private byte cmd;
    // ignore setter/getter
}

/**
 * 心跳报文
 *
 * 一般来说会周期性上报
 */
@MagicClass()
public class HeartbeatCmd {
    // 这里组合使用 Head 结构
    @MagicField(order = 1)
    private Head head;
    // 8 bytes
    @MagicField(order = 3)
    private long timestamp;
    // 1 byte
    @MagicField(order = 5)
    private byte checkCode;
    // ignore setter/getter
}

/**
 * 指定的业务报文
 *
 * 一般来说,这里应该是业务报文,
 * 这里假设设备端存在一个回响报文, 设备端原样返回云端下发数据
 */
@MagicClass()
public class EchoCmd {
    // 这里组合使用 Head 结构
    @MagicField(order = 1)
    private Head head;
    // string 总字节数为10
    @MagicField(order = 3, size = 10)
    private String body;
    // 1 byte
    @MagicField(order = 5)
    private byte checkCode;
    // ignore setter/getter
}

2.1.2. 设备上行报文解析示例

定义完成报文结构体后,当设备上行数据到达时, 先解析消息头,然后根据消息头标识进行消息体解析,代码如下:

/**
 * 这里是项目的启动类
 */
public class ApplicationStartup {
    public static void main(String[] args) {
        // 假设此处数据为设备的上行数据
        byte[] deviceUploadMessage = new byte[]{};

        // 以下为业务端逻辑解析建议方式
        // 1. 首先应该解析报文头
        Head pack = MagicByte.pack(deviceUploadMessage, Head.class);
        // 2. 这里省略检查报文头逻辑
        // 3. 根据报文类型解析为特定的报文格式
        switch (pack.getCmd()) {
            case 1:
                HeartbeatCmd pack1 = MagicByte.pack(deviceUploadMessage, HeartbeatCmd.class);
                doHeartbeatCmd(pack1);
                break;
            case 2:
                EchoCmd pack2 = MagicByte.pack(deviceUploadMessage, EchoCmd.class);
                doEchoCmd(pack2);
                break;
            // 更多的报文类型
        }
    }

    private static void doEchoCmd(EchoCmd pack2) {
        // 这里处理EchoCmd 的业务
    }

    private static void doHeartbeatCmd(HeartbeatCmd pack1) {
        // 这里处理 HeartbeatCmd 的业务
    }
}

以上是框架的建议的使用方式。其他的情况框架将会做如下处理:

  • 未被 @MagicField 进行标注的字段将会被字节忽略
  • 继承的属性直接被忽略。因为不支持继承。
  • 未知的数据类型, 即没有自定义转换且也没有受到支持的数据类型;比如使用 Map, Object 什么的。
  • 如果使用包装类型且未赋值, 则将使用原始类型默认值。
  • 数组或对象为Null时, 将使用 0x00 进行填充。
  • 如果需要计算校验和的场景,建议返回buffer然后直接附加校验和结果
  • strict=true模式将会自动继承, 并且要求数据完整(即数据长度不匹配将不会解析); 默认false