☰
Java多协议解析器实战:JT/T 808、32960、DL/T 645、698.45编解码与避坑
2026/9/27 23:07:12 网站建设 项目流程

简介:本资源为基于Java实现的多种通信协议解析器源码合集,面向智能电网、自动化设备及物联网领域的开发者与学习者,重点覆盖32960、808、DLT-645与DLT698.45四类协议。包内共324个文件,以296个Java源码为核心,辅以html页面、ttf与woff2字体、yml与xml配置、css与js脚本等,压缩包约1.2MB,结构清晰便于按协议模块检索。其中DLT-645解析器涉及地址字段、命令代码、数据与校验码处理,DLT698.45则扩展了安全性与加密认证机制,适用于AMI高级计量场景;32960与808解析器则展示数据帧结构分析与编解码思路。已有1991人学习下载,读者可借此理解协议解析原理、掌握Java面向对象设计在通信系统中的落地方式,并获取可直接参考的帧解析与调试思路,对开发与调试智能电网、自动化设备通信具有实用价值。

1. 多协议解析器在Java里到底怎么落地:从32960到698.45的工程视角

如果你手上有一批车载终端、电表或充电桩的数据要接,大概率绕不开这几个协议名:JT/T 808、JT/T 32960、DL/T 645、DL/T 698.45。它们分别对应车载终端通信、新能源车远程监控、多功能电能表、以及更复杂的电能信息采集与交互。单独实现一个协议不难,难的是把它们放进同一个Java工程里,让解析器可复用、可扩展、可测试。我见过太多项目把协议解析写成一大坨if-else,新增一个协议就要动核心链路,最后没人敢改。这篇笔记就围绕“基于Java实现多协议解析器”这个方向,把选型、分层、编解码、测试和踩坑讲清楚。适合正在做物联网平台、充电桩后台、车联网数据接入的Java工程师,也适合想拿这类项目练手但不知道从哪下手的开发者。读完你能自己搭出一个能跑通808和645最小闭环、再逐步扩展到32960和698.45的解析框架。

2. 协议解析器的分层设计:为什么不能一个协议一个类

2.1 先分清四类协议的报文结构差异

在动手写代码之前,必须把四个协议的报文结构差异摸清楚,否则抽象层会设计歪。JT/T 808 是车载终端与平台之间的通信协议,报文头包含消息ID、消息体属性、终端手机号、流水号,消息体属性里还藏着分包标志和加密标志,解析时要先拆头再按消息ID分发。JT/T 32960 是新能源车远程服务与管理平台协议,报文头是起始符、命令标识、应答标志、VIN、加密方式、数据单元长度,结构比808更扁平,但命令标识决定了后续数据单元的解析方式。DL/T 645 是电能表通信协议,有1997和2007两个版本,2007版用控制码区分读、写、读后续等操作,数据域用数据标识编码,解析时要先判断控制码再决定数据域长度。DL/T 698.45 是面向对象的电能信息交互协议,报文有链路层帧头和APDU应用层数据,APDU里又分请求、响应、通知,对象标识OAD、属性标识OI、方法标识OMD层层嵌套,是四个协议里最复杂的。

这四个协议如果各写各的解析类,代码量会迅速膨胀,而且日志、异常处理、连接管理这些公共逻辑会重复。常见做法是抽三层:传输层负责字节流的收发和粘包处理,编解码层负责把字节数组转成协议对象,业务层负责把协议对象转成业务事件。传输层可以用Netty统一处理TCP连接,编解码层每个协议一个Codec,业务层用统一的事件模型。这样新增协议时只需要加一个Codec和对应的消息映射,不动传输和业务。

2.2 用接口隔离编解码,用工厂管理协议类型

编解码层的核心接口我一般定义成两个方法:decode把ByteBuf或byte[]转成ProtocolMessage,encode把ProtocolMessage转成byte[]。ProtocolMessage是一个标记接口,每个协议有自己的实现类,比如Jt808Message、Jt32960Message、Dlt645Message、Dlt698Message。这样做的好处是传输层和业务层都只依赖接口,不依赖具体协议类。

public interface ProtocolCodec { ProtocolMessage decode(ByteBuf in) throws ProtocolException; ByteBuf encode(ProtocolMessage message) throws ProtocolException; ProtocolType getProtocolType(); } public interface ProtocolMessage { ProtocolType getType(); String getDeviceId(); long getTimestamp(); }

逻辑说明:decode方法接收Netty的ByteBuf,返回协议消息对象。encode反向操作。getProtocolType用于工厂路由。ProtocolMessage接口里我放了设备ID和时间戳,因为不管哪个协议,业务层最关心的就是“哪个设备在什么时候发了什么”。参数说明:ByteBuf是Netty的缓冲区,支持读写指针分离,适合处理粘包;ProtocolException是自定义异常,携带错误码和原始字节,方便排查。

工厂类根据协议类型返回对应的Codec,协议类型可以从配置、端口号或报文特征推断。比如808和32960都是TCP,但端口不同;645和698.45可能走串口或TCP,用端口区分最省事。如果端口不能区分,就在decode前先peek几个字节做特征匹配,但这样会牺牲一点性能,我一般优先用端口或通道属性绑定协议类型。

2.3 粘包和半包:Netty的LengthFieldBasedFrameDecoder怎么配

四个协议里,808和32960有明确的消息长度字段,645和698.45的帧长度需要根据控制码或链路层长度计算。Netty自带的LengthFieldBasedFrameDecoder能解决大部分粘包问题,但参数要按协议调。以808为例,消息头是12字节,其中第2到第3字节是消息体属性,低10位是消息体长度。所以LengthFieldBasedFrameDecoder的配置是:maxFrameLength设一个上限比如8192,lengthFieldOffset为2,lengthFieldLength为2,lengthAdjustment为-2,initialBytesToStrip为0。这样Netty会先读长度字段,再截取完整帧,但不会剥离头部,因为后续Codec还需要头部信息。

pipeline.addLast(new LengthFieldBasedFrameDecoder(8192, 2, 2, -2, 0)); pipeline.addLast(new Jt808Codec());

逻辑说明:lengthFieldOffset=2表示长度字段从第3个字节开始(索引2),lengthFieldLength=2表示长度字段占2字节,lengthAdjustment=-2表示长度字段的值不包含自身,所以实际帧长要减去2,initialBytesToStrip=0表示不剥离任何字节,把完整帧交给Codec。参数说明:maxFrameLength根据业务最大报文设置,太小会截断,太大会占内存。645和698.45如果走TCP,也需要类似的帧解码器,但长度字段位置不同,要单独配。

提示:如果协议既有TCP又有串口,串口那边通常用SerialPortHandler,粘包处理逻辑可以复用同一个FrameDecoder,但要注意串口读到的可能是半包,需要缓冲。

2.4 协议注册与热插拔:用SPI还是配置表

新增协议时,我不建议改工厂类的switch,而是用配置表或SPI。配置表最简单:在application.yml里配协议类型和Codec类的映射,启动时反射实例化。SPI更灵活,但Java原生的SPI会加载所有实现,如果某个Codec依赖的类缺失会报错。我一般用配置表加一个ProtocolCodecRegistry,注册时校验协议类型是否重复。

@Component public class ProtocolCodecRegistry { private final Map<ProtocolType, ProtocolCodec> codecMap = new ConcurrentHashMap<>(); public void register(ProtocolCodec codec) { codecMap.put(codec.getProtocolType(), codec); } public ProtocolCodec get(ProtocolType type) { ProtocolCodec codec = codecMap.get(type); if (codec == null) { throw new ProtocolException("未注册的协议类型: " + type); } return codec; } }

逻辑说明:register方法在Spring启动时由各Codec的@PostConstruct调用,把自身注册进Map。get方法按类型取Codec,取不到就抛异常。参数说明:ProtocolType是枚举,包含JT808、JT32960、DLT645、DLT698;ConcurrentHashMap保证线程安全,因为Netty的IO线程可能并发调用。这样新增协议只需要加一个Codec实现类,在类上加@Component,启动时自动注册,不用改工厂。

3. 808和32960的编解码实现:从字节到对象的完整链路

3.1 808消息头解析:消息ID、手机号、流水号怎么拆

808的消息头固定12字节,结构是:消息ID(2字节)、消息体属性(2字节)、终端手机号(6字节BCD)、流水号(2字节)。消息体属性里,bit0到bit9是消息体长度,bit10是加密标志,bit11到bit12是分包标志,bit13是保留位。解析时先把消息体属性转成二进制,再按位取值。

public Jt808Message decode(ByteBuf in) throws ProtocolException { if (in.readableBytes() < 12) { throw new ProtocolException("808消息头长度不足"); } int msgId = in.readUnsignedShort(); int bodyProps = in.readUnsignedShort(); int bodyLength = bodyProps & 0x03FF; boolean encrypted = ((bodyProps >> 10) & 0x01) == 1; int subpackage = (bodyProps >> 11) & 0x03; String phone = BcdUtil.decode(in, 6); int serial = in.readUnsignedShort(); if (in.readableBytes() < bodyLength) { throw new ProtocolException("808消息体长度不足,期望" + bodyLength + "实际" + in.readableBytes()); } byte[] body = new byte[bodyLength]; in.readBytes(body); Jt808Message message = new Jt808Message(); message.setMsgId(msgId); message.setPhone(phone); message.setSerial(serial); message.setEncrypted(encrypted); message.setSubpackage(subpackage); message.setBody(body); return message; }

逻辑说明:先读2字节消息ID,再读2字节消息体属性,用位运算取出长度、加密标志、分包标志。手机号是6字节BCD,需要专门工具类解码。流水号2字节。然后按bodyLength读消息体。参数说明:bodyProps & 0x03FF取低10位作为长度,最大1023字节;>>10 & 0x01取加密标志;>>11 & 0x03取分包标志,0表示不分包,1表示分包且是第一条,2表示分包且是最后一条,3表示分包且是中间条。如果readableBytes不够bodyLength,说明半包,抛异常让上层处理。

3.2 32960命令标识与数据单元解析

32960的报文头是:起始符(2字节,固定0x2323)、命令标识(1字节)、应答标志(1字节)、VIN(17字节ASCII)、加密方式(1字节)、数据单元长度(2字节)。命令标识决定数据单元的结构,比如0x01是车辆登入,0x02是实时信息上报,0x03是补发信息上报,0x04是车辆登出,0x05是平台登入,0x06是平台登出。实时信息上报的数据单元里又包含信息类型标志,每个标志对应不同的数据项。

public Jt32960Message decode(ByteBuf in) throws ProtocolException { if (in.readableBytes() < 24) { throw new ProtocolException("32960报文头长度不足"); } int start = in.readUnsignedShort(); if (start != 0x2323) { throw new ProtocolException("32960起始符错误: " + Integer.toHexString(start)); } int command = in.readUnsignedByte(); int response = in.readUnsignedByte(); String vin = in.readCharSequence(17, StandardCharsets.US_ASCII).toString(); int encrypt = in.readUnsignedByte(); int dataLength = in.readUnsignedShort(); if (in.readableBytes() < dataLength) { throw new ProtocolException("32960数据单元长度不足"); } byte[] data = new byte[dataLength]; in.readBytes(data); Jt32960Message message = new Jt32960Message(); message.setCommand(command); message.setResponse(response); message.setVin(vin); message.setEncrypt(encrypt); message.setData(data); return message; }

逻辑说明:先读2字节起始符并校验,再读命令标识、应答标志、VIN、加密方式、数据单元长度,最后读数据单元。参数说明:VIN是17字节ASCII,用readCharSequence直接转字符串;dataLength是2字节无符号短整型,最大65535;data保留原始字节,后续按命令标识再解析。如果起始符不对,说明帧同步丢了,直接抛异常让上层断开重连。

3.3 分包处理:808和32960的分包重组策略

808和32960都支持分包,但机制不同。808的分包标志在消息体属性里,分包消息有总包数和包序号,需要按手机号加流水号做key缓存,收齐后合并。32960的分包在数据单元里,命令标识0x02的实时信息上报可能分多条,但32960没有显式的分包标志,而是靠数据单元里的信息类型标志和时间戳判断,实际项目中我一般按VIN加时间戳做去重和合并。

public class SubpackageCache { private final Map<String, List<Jt808Message>> cache = new ConcurrentHashMap<>(); public Jt808Message merge(Jt808Message message) { if (message.getSubpackage() == 0) { return message; } String key = message.getPhone() + ":" + message.getSerial(); cache.computeIfAbsent(key, k -> new ArrayList<>()).add(message); List<Jt808Message> list = cache.get(key); int total = list.get(0).getTotalPackets(); if (list.size() == total) { list.sort(Comparator.comparingInt(Jt808Message::getPacketIndex)); ByteArrayOutputStream out = new ByteArrayOutputStream(); for (Jt808Message m : list) { out.write(m.getBody(), 0, m.getBody().length); } Jt808Message merged = list.get(0); merged.setBody(out.toByteArray()); merged.setSubpackage(0); cache.remove(key); return merged; } return null; } }

逻辑说明:用手机号加流水号做key,把分包消息缓存到List。当List大小等于总包数时,按包序号排序,合并消息体,返回合并后的消息。参数说明:total从第一条分包消息的消息体里读,808的分包消息体前4字节是总包数和包序号;packetIndex是当前包序号。如果超时没收齐,需要定时清理缓存,防止内存泄漏。32960的分包重组类似,但key用VIN加时间戳,且要处理重复上报。

3.4 编码:平台下发指令怎么拼字节

编码是解析的逆过程,但更容易出错,因为要处理转义。808的消息体里如果出现0x7E、0x7D、0x7E,需要转义成0x7D 0x02、0x7D 0x01,帧头帧尾用0x7E包裹。32960没有转义,但起始符0x2323要保证不出现在数据单元里,如果出现需要处理,实际项目中32960一般不做转义,靠长度字段区分。

public ByteBuf encode(Jt808Message message) { ByteBuf bodyBuf = Unpooled.buffer(); bodyBuf.writeBytes(message.getBody()); int bodyLength = bodyBuf.readableBytes(); int bodyProps = bodyLength & 0x03FF; if (message.isEncrypted()) { bodyProps |= 0x0400; } if (message.getSubpackage() > 0) { bodyProps |= (message.getSubpackage() << 11); } ByteBuf frame = Unpooled.buffer(); frame.writeByte(0x7E); frame.writeShort(message.getMsgId()); frame.writeShort(bodyProps); frame.writeBytes(BcdUtil.encode(message.getPhone(), 6)); frame.writeShort(message.getSerial()); frame.writeBytes(bodyBuf); frame.writeByte(0x7E); return escape(frame); }

逻辑说明:先拼消息体,计算长度和属性,再拼帧头、消息ID、属性、手机号、流水号、消息体、帧尾。最后做转义。参数说明:bodyProps的低10位是长度,bit10是加密,bit11到12是分包。escape方法把0x7E转成0x7D 0x02,0x7D转成0x7D 0x01。注意转义要在帧尾加上之后做,否则帧头帧尾也会被转义。

4. 645和698.45的解析难点:数据标识与面向对象

4.1 645控制码与数据标识的映射关系

DL/T 645 2007版的控制码有:0x01读数据、0x02读后续数据、0x03读通信地址、0x04写数据、0x05写通信地址、0x06冻结命令、0x07更改通信速率、0x08修改密码、0x09最大需量清零、0x0A电表清零、0x0B事件清零。读数据时,数据域是4字节数据标识,比如0x00 0x00 0x00 0x00表示当前正向有功总电能。数据标识是压缩BCD,解析时要按位拆。

public Dlt645Message decode(ByteBuf in) throws ProtocolException { if (in.readableBytes() < 12) { throw new ProtocolException("645帧长度不足"); } int start = in.readUnsignedByte(); if (start != 0x68) { throw new ProtocolException("645起始符错误"); } String address = BcdUtil.decode(in, 6); int start2 = in.readUnsignedByte(); if (start2 != 0x68) { throw new ProtocolException("645第二个起始符错误"); } int control = in.readUnsignedByte(); int dataLength = in.readUnsignedByte(); byte[] data = new byte[dataLength]; in.readBytes(data); int cs = in.readUnsignedByte(); int end = in.readUnsignedByte(); if (end != 0x16) { throw new ProtocolException("645结束符错误"); } Dlt645Message message = new Dlt645Message(); message.setAddress(address); message.setControl(control); message.setData(data); return message; }

逻辑说明:645的帧结构是起始符0x68、地址6字节BCD、起始符0x68、控制码、数据长度、数据域、校验和、结束符0x16。参数说明:控制码决定数据域含义,读数据时数据域是4字节数据标识,写数据时数据域是数据标识加数据。校验和是前面所有字节的模256和,解析时要校验,但示例里省略了。数据标识的解析需要查表,比如0x00 0x00 0x00 0x00对应正向有功总电能,单位是kWh,小数点位数由数据标识的高字节决定。

4.2 698.45的OAD、OI、OMD对象模型

698.45是面向对象的协议,核心概念是OAD(对象标识)、OI(对象标识)、OMD(方法标识)。OAD是4字节,前2字节是OI,第3字节是属性标识,第4字节是属性索引。比如OAD 0x2000 0x02 0x00表示正向有功总电能的当前值。APDU的结构是:请求类型、请求参数、响应类型、响应参数。解析时要先读链路层帧头,再读APDU,APDU里再按OAD取数据。

public Dlt698Message decode(ByteBuf in) throws ProtocolException { if (in.readableBytes() < 8) { throw new ProtocolException("698.45链路层长度不足"); } int start = in.readUnsignedByte(); if (start != 0x68) { throw new ProtocolException("698.45起始符错误"); } int length = in.readUnsignedShort(); int control = in.readUnsignedByte(); String address = BcdUtil.decode(in, 6); int apduStart = in.readUnsignedByte(); if (apduStart != 0x68) { throw new ProtocolException("698.45 APDU起始符错误"); } byte[] apdu = new byte[length - 8]; in.readBytes(apdu); int cs = in.readUnsignedByte(); int end = in.readUnsignedByte(); if (end != 0x16) { throw new ProtocolException("698.45结束符错误"); } Dlt698Message message = new Dlt698Message(); message.setAddress(address); message.setControl(control); message.setApdu(apdu); return message; }

逻辑说明:698.45的链路层帧是起始符0x68、长度2字节、控制码1字节、地址6字节、APDU、校验和、结束符0x16。APDU里再按请求类型解析。参数说明:length是链路层长度,包含APDU但不包含起始符和结束符;control是链路层控制码,区分请求、响应、通知;apdu是应用层数据,需要进一步解析。OAD的解析要按4字节读,OI是前2字节,属性标识是第3字节,属性索引是第4字节。

4.3 数据标识查表:用枚举还是配置文件

645和698.45的数据标识有几百个,硬编码在代码里不现实。我一般用枚举加配置文件。枚举定义常用的数据标识,比如正向有功总电能、反向有功总电能、A相电压、B相电压、C相电压、A相电流等。配置文件定义数据标识对应的名称、单位、小数点位数、数据类型。解析时先查枚举,查不到再查配置文件,都没有就返回原始字节。

public enum Dlt645DataId { FORWARD_ACTIVE_TOTAL(0x00000000, "正向有功总电能", "kWh", 2), REVERSE_ACTIVE_TOTAL(0x00010000, "反向有功总电能", "kWh", 2), VOLTAGE_A(0x02010100, "A相电压", "V", 1), VOLTAGE_B(0x02010200, "B相电压", "V", 1), VOLTAGE_C(0x02010300, "C相电压", "V", 1), CURRENT_A(0x02020100, "A相电流", "A", 3), CURRENT_B(0x02020200, "B相电流", "A", 3), CURRENT_C(0x02020300, "C相电流", "A", 3); private final int id; private final String name; private final String unit; private final int scale; Dlt645DataId(int id, String name, String unit, int scale) { this.id = id; this.name = name; this.unit = unit; this.scale = scale; } public static Dlt645DataId fromId(int id) { for (Dlt645DataId dataId : values()) { if (dataId.id == id) { return dataId; } } return null; } }

逻辑说明:枚举里定义数据标识的ID、名称、单位、小数点位数。fromId方法按ID查找,查不到返回null。参数说明:id是4字节数据标识的整数值;scale是小数点位数,比如2表示除以100;unit是单位。解析时拿到数据标识后,先fromId,如果非空就按scale转换,否则返回原始字节。配置文件可以用YAML或JSON,启动时加载到Map,和枚举合并。

4.4 698.45的APDU解析:请求与响应的对应关系

698.45的APDU里,请求和响应是成对的。请求有请求类型和请求参数,响应有响应类型和响应参数。比如请求类型0x01是读取请求,参数是OAD列表;响应类型0x81是读取响应,参数是OAD对应的数据。解析时要先读请求类型,再按类型读参数。响应解析时要把OAD和数据对应起来,方便业务层按OAD取值。

public Dlt698Apdu parseApdu(byte[] apdu) { ByteBuf buf = Unpooled.wrappedBuffer(apdu); int type = buf.readUnsignedByte(); Dlt698Apdu result = new Dlt698Apdu(); result.setType(type); if (type == 0x01) { int oadCount = buf.readUnsignedByte(); List<Integer> oads = new ArrayList<>(); for (int i = 0; i < oadCount; i++) { oads.add(buf.readInt()); } result.setOads(oads); } else if (type == 0x81) { int oadCount = buf.readUnsignedByte(); Map<Integer, byte[]> dataMap = new LinkedHashMap<>(); for (int i = 0; i < oadCount; i++) { int oad = buf.readInt(); int dataLength = buf.readUnsignedByte(); byte[] data = new byte[dataLength]; buf.readBytes(data); dataMap.put(oad, data); } result.setDataMap(dataMap); } return result; }

逻辑说明:先读APDU类型,0x01是读取请求,后面跟OAD数量、OAD列表;0x81是读取响应,后面跟OAD数量、每个OAD的数据长度和数据。参数说明:OAD是4字节整数,用readInt读;dataLength是1字节,最大255;dataMap用LinkedHashMap保持顺序。解析后的APDU交给业务层,业务层按OAD查数据标识表,转换成带单位的物理量。

5. 避坑与排查:多协议解析器最容易翻车的五个地方

5.1 字节序搞反导致数据全错

现象:解析出来的电压是几万伏,电流是负数,或者时间戳是1970年。原因:Java默认大端序,但有些协议或设备用小端序,比如645的某些数据域、698.45的某些字段。解决:用ByteBuf的order(ByteOrder.LITTLE_ENDIAN)显式设置字节序,或者在读多字节整数时用readIntLE、readShortLE。我一般会在Codec的decode开头统一设置字节序,但要注意协议里可能混用大小端,比如808的消息头是大端,消息体里的某些字段可能是小端,要按协议文档逐个确认。

5.2 BCD码解码时高位低位颠倒

现象:手机号解析出来是乱码,或者电表地址是反的。原因:BCD码有压缩BCD和非压缩BCD,压缩BCD一个字节存两位十进制数,高位在前还是低位在前要看协议。808的手机号是压缩BCD,高位在前;645的地址是压缩BCD,但低位在前,需要反转。解决:写一个BcdUtil,提供decode和encode方法,decode时按协议指定是否反转。我一般会为每个协议单独写BCD处理,不共用,因为反转规则不同。

5.3 粘包处理不当导致消息错位

现象:日志里出现大量“起始符错误”或“长度不足”,但设备明明在正常上报。原因:LengthFieldBasedFrameDecoder的参数配错,或者协议的长度字段不包含自身,导致截取的帧多一个或少一个字节。解决:先用Wireshark或tcpdump抓包,确认帧的实际长度和长度字段的值,再调lengthAdjustment。808的lengthAdjustment是-2,因为长度字段不包含自身;32960的lengthAdjustment是0,因为长度字段包含自身。如果还不对,就在Codec里打印原始字节的十六进制,对比协议文档。

5.4 分包重组时内存泄漏

现象:运行几天后OOM,堆转储里全是SubpackageCache里的List。原因:分包消息没收齐,缓存一直不清理。解决:给SubpackageCache加过期时间,比如30秒,用ScheduledExecutorService定时清理。或者用Caffeine做带过期的缓存,key是手机号加流水号,value是分包列表,设置expireAfterWrite。另外,分包消息的总包数和包序号要校验,如果包序号超出总包数,直接丢弃,防止恶意报文撑爆缓存。

5.5 698.45的APDU长度计算错误

现象:解析698.45时APDU读多了或读少了,导致后续帧全部错位。原因:698.45的链路层长度字段是2字节,但包含的内容和645不同,645的长度字段是1字节且只包含数据域,698.45的长度字段包含控制码、地址、APDU,但不包含起始符和结束符。解决:仔细看协议文档的长度定义,用公式算:链路层总长 = 1(起始符)+ 2(长度)+ 长度字段的值 + 1(校验和)+ 1(结束符)。如果长度字段的值是N,那么从控制码到APDU结束共N字节。解析时先读长度,再按长度读剩余字节,最后校验结束符。

6. 用JUnit和真实报文做回归:让解析器敢改

解析器最怕改一处崩三处,所以必须有回归测试。我一般用JUnit 5加真实报文做参数化测试,每个协议准备一组十六进制字符串,解析后断言关键字段。比如808的车辆登入报文、32960的实时信息上报报文、645的读电压响应报文、698.45的读取响应报文。测试类里用@ParameterizedTest和@CsvSource,把十六进制和期望值放一起。

@ParameterizedTest @CsvSource({ "7E0200001E012345678901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 <p> <a href="https://download.csdn.net/download/tianqiquan/87791411" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询