一 背景
前一段时间对接了南网电动充电桩通信规约,该充电桩协议是基于IEC104规约扩展的,里面扩展的三个消息类型标识。以下是南网电动的类型标识:
类型标识 | 值 | 说明 |
---|---|---|
M_SP_NA_1 | 1 | 不带时标的单点信息 |
M_ME_NB_1 | 11 | 测量值标度化值(长度等于2字节) |
M_IT_NA_1 | 15 | 累积量(不带时标) |
M_RE_NA_1 | 130 | 充电设备业务数据 |
M_MD_NA_1 | 132 | 测量值标度化值(长度大于2字节) |
M_JC_NA_1 | 134 | 充电设备实时监测数据项 |
C_IC_NA_1 | 100 | 总召唤命令 |
C_CI_NA_1 | 101 | 计数量总召命令 |
C_CS_NA_1 | 103 | 时钟同步命令 |
C_SD_NA_1 | 133 | 下发数据项 |
其中,130、133、134类型标识,都是充电桩通信规约中扩展的类型标识,这三个类型的信息对象部分,都是标准的一个信息对象,信息对象包含:信息对象地址、记录类型、业务数据。
而业务数据,针对每种记录类型,都是标准的数据格式,即自上到下的每个字段和字段长度都是标准的,基于这种情况,如果是你用Java去解析,你会如何解析?
我们要做的是一个插件,既要解析南网电动协议,还要把我们桩的自定义协议协议,或者反向转对象等,互转和反向转的交互涉及到几十处。
二 通常的解析方式
通常情况下,针对这种数据,我们可能会对每个记录类型中的每个字段一个一个字段读取,然后处理这些记录类型数据的地方,到处都是写了很多的读取逻辑,如下:
1 | /** |
以上这些代码,是对我们桩协议中信息体部分的解析代码,看了以上这段代码,不知道你是一种什么感觉,我就问你易读性如何?如果要修改容易改吗?
如果协议都是标准的,只需要解析这么一次,这样做其实问题也不大,关键是,要对接的记录类型至少十多个,每个都这么写,容不容易出错?出错后容不容易检查定位?
三 优雅的解析或转字节数组做法
通过反射机制和自定义注解方式,做到优雅的读取和对象转字节数组。具体方式如下:
自定义注解
1 | import java.lang.annotation.Documented; |
1 | import java.lang.annotation.Documented; |
写转换工具类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315import com.mafgwo.annotation.ByteType;
import com.mafgwo.annotation.ListType;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 记录类型工具类
*
* @author mafgwo
* @since 2022/10/20
*/
4j
public class RecordDataUtils {
private RecordDataUtils() {
}
/**
* 云端字节转对象
* @param bytes
* @param cls
* @param <T>
* @return
*/
public static <T> T cloudConvertToObj(byte[] bytes, Class<T> cls) {
return convertToObj(new ByteAndIdx().setBytes(bytes).setIdx(0), cls, true);
}
/**
* 云端对象转字节
* @param obj
* @return
*/
public static byte[] cloudConvertToBytes(Object obj) {
return convertToBytes(obj, true, null);
}
/**
* 本地16进制字符串转对象
* @param hexData
* @param cls
* @param <T>
* @return
*/
public static <T> T localConvertToObj(String hexData, Class<T> cls) {
return convertToObj(new ByteAndIdx().setBytes(ByteUtil.toBytesFromHexString(hexData)).setIdx(0), cls, false);
}
/**
* 本地对象转16进制字符串
* @param obj
* @return
*/
public static String localConvertToHexString(Object obj) {
byte[] bytes = convertToBytes(obj, false, null);
return ByteUtil.byteArrayToHexString(bytes);
}
/**
* 字节转换为对象
*
* @param byteAndIdx
* @param cls
* @param <T>
* @return
*/
public static <T> T convertToObj(ByteAndIdx byteAndIdx, Class<T> cls, boolean bcdReverse) {
byte[] bytes = byteAndIdx.getBytes();
List<Field> declaredFields = ClassUtil.getAllFields(cls);
try {
// 数字类型的字段名与值映射
Map<String, Number> fieldNameValMap = new HashMap<>();
T obj = cls.newInstance();
for (Field declaredField : declaredFields) {
declaredField.setAccessible(true);
int len = 0;
int offset = 0;
ByteType annotation = declaredField.getDeclaredAnnotation(ByteType.class);
if (annotation != null) {
len = annotation.len();
offset = annotation.offset();
if (!"".equals(annotation.lenField())) {
Number number = fieldNameValMap.get(annotation.lenField());
Assert.notNull(number, "ByteType注解中的" + annotation.lenField() + "属性未找到值");
len = number.intValue();
}
if (len <= 0) {
continue;
}
}
Object val = null;
// 如果是字节则直接取第一个字节
if (declaredField.getType() == Byte.class) {
val = (byte) (bytes[byteAndIdx.idx++] + offset);
fieldNameValMap.put(declaredField.getName(), (Number) val);
}
// Short 2个字节
else if (declaredField.getType() == Short.class) {
val = (short) (ByteUtil.byteArrayToShort(ByteUtil.getByte(bytes, byteAndIdx.idx, 2)) + offset);
byteAndIdx.idx += 2;
fieldNameValMap.put(declaredField.getName(), (Number) val);
}
// Integer 4个字节
else if (declaredField.getType() == Integer.class) {
val = ByteUtil.byteArrayToInt(ByteUtil.getByte(bytes, byteAndIdx.idx, 4)) + offset;
byteAndIdx.idx += 4;
fieldNameValMap.put(declaredField.getName(), (Number) val);
}
// Long 8个字节
else if (declaredField.getType() == Long.class) {
val = ByteUtil.byteArrayToLong(ByteUtil.getByte(bytes, byteAndIdx.idx, 8)) + offset;
byteAndIdx.idx += 8;
fieldNameValMap.put(declaredField.getName(), (Number) val);
}
// Float或Double类型
else if (declaredField.getType() == Float.class || declaredField.getType() == Double.class) {
Assert.notNull(annotation, "Float/Double类型属性必须包含ByteType注解");
double v = ByteUtil.byteArrayToLong(ByteUtil.getByte(bytes, byteAndIdx.idx, len)) / Math.pow(10, annotation.decimalNum());
if (declaredField.getType() == Float.class) {
val = (float) (v + offset);
} else {
val = v + offset;
}
byteAndIdx.idx += len;
}
// 时间类型
else if (declaredField.getType() == Date.class) {
val = ByteUtil.byte2Hdate(ByteUtil.getByte(bytes, byteAndIdx.idx, 7));
byteAndIdx.idx += 7;
}
// 字符串类型
else if (declaredField.getType() == String.class) {
Assert.notNull(annotation, "String类型属性必须包含ByteType注解");
// 如果是ascii编码
if (annotation.asciiFlag() == 1) {
val = ByteUtil.toAsciiFromBytes(ByteUtil.getByte(bytes, byteAndIdx.idx, len), false);
}
else if (annotation.asciiFlag() == 2) {
val = ByteUtil.toAsciiFromBytes(ByteUtil.getByte(bytes, byteAndIdx.idx, len), true);
} else {
val = Util.byteArray2HexString(ByteUtil.getByte(bytes, byteAndIdx.idx, len), Integer.MAX_VALUE, false, bcdReverse);
}
byteAndIdx.idx += len;
}
// List类型
else if (declaredField.getType() == List.class) {
ListType listType = declaredField.getDeclaredAnnotation(ListType.class);
Assert.notNull(listType, "List类型属性必须包含ListType注解");
Number itemNum = fieldNameValMap.get(listType.itemNumField());
Assert.notNull(itemNum, "List类型注解中的" + listType.itemNumField() + "属性未找到值");
// 如果是List类型,得到其Generic的类型
Type genericType = declaredField.getGenericType();
Assert.notNull(genericType, "List类型的泛型类型不允许为空");
Assert.isTrue(genericType instanceof ParameterizedType, "List类型的泛型类型必须是自定义对象");
Class<?> genericClazz = (Class<?>) ((ParameterizedType) genericType).getActualTypeArguments()[0];
List list = new ArrayList(itemNum.intValue());
for (int i = 0; i < itemNum.intValue(); i++) {
list.add(convertToObj(byteAndIdx, genericClazz, bcdReverse));
}
val = list;
}
declaredField.set(obj, val);
}
return obj;
} catch (Exception e) {
log.error("字节转换为对象异常,bytes:{}", ByteUtil.byteArrayToHexString(bytes), e);
return null;
}
}
/**
* 对象转字节 如果字段为null直接用FF填充
*
* @param obj
* @param bcdReverse
* @param fieldName 字段名称 没有则是所有字段
* @return
*/
public static byte[] convertToBytes(Object obj, boolean bcdReverse, String fieldName) {
try {
// 数字类型的字段名与值映射
Map<String, Number> fieldNameValMap = new HashMap<>();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
List<Field> declaredFields = ClassUtil.getAllFields(obj.getClass());
// int curr = 0;
// 数字类型的字段名与值映射
for (Field declaredField : declaredFields) {
// 如果有传入字段名 字段名不匹配则跳过
if (fieldName != null && !Objects.equals(declaredField.getName(), fieldName)) {
continue;
}
declaredField.setAccessible(true);
Object val = declaredField.get(obj);
ByteType annotation = declaredField.getDeclaredAnnotation(ByteType.class);
int len = 0;
int offset = 0;
if (annotation != null) {
len = annotation.len();
offset = annotation.offset();
if (!"".equals(annotation.lenField())) {
Number number = fieldNameValMap.get(annotation.lenField());
Assert.notNull(number, "ByteType注解中的" + annotation.lenField() + "属性未找到值");
len = number.intValue();
}
if (len <= 0) {
continue;
}
}
// 如果是字节则直接取第一个字节
if (declaredField.getType() == Byte.class) {
outputStream.write(val != null ? ((byte) val) - offset : 0xFF);
fieldNameValMap.put(declaredField.getName(), (Number) val);
}
// Short 2个字节
else if (declaredField.getType() == Short.class) {
outputStream.write(val != null ? ByteUtil.shortToByteArray((short) ((short) val - offset)) : ByteUtil.get0xFFBytes(2));
fieldNameValMap.put(declaredField.getName(), (Number) val);
}
// Integer 4个字节
else if (declaredField.getType() == Integer.class) {
outputStream.write(val != null ? ByteUtil.intToByteArray((int) val - offset) : ByteUtil.get0xFFBytes(4));
fieldNameValMap.put(declaredField.getName(), (Number) val);
}
// Long 8个字节
else if (declaredField.getType() == Long.class) {
outputStream.write(val != null ? ByteUtil.longToByteArray((long) val - offset) : ByteUtil.get0xFFBytes(8));
fieldNameValMap.put(declaredField.getName(), (Number) val);
}
// Float或Double类型
else if (declaredField.getType() == Float.class || declaredField.getType() == Double.class) {
Assert.notNull(annotation, "Float/Double类型属性必须包含ByteType注解");
if (val == null) {
outputStream.write(ByteUtil.get0xFFBytes(len));
} else {
long v = Math.round((((Number) val).doubleValue() - offset) * Math.pow(10, annotation.decimalNum()));
outputStream.write(ByteUtil.longToByteArray(v, len));
}
}
// 时间类型
else if (declaredField.getType() == Date.class) {
if (val == null) {
outputStream.write(ByteUtil.get0xFFBytes(7));
} else {
byte[] v = ByteUtil.date2Hbyte((Date) val);
outputStream.write(v);
}
}
// 字符串类型
else if (declaredField.getType() == String.class) {
Assert.notNull(annotation, "String类型属性必须包含ByteType注解");
if (val == null) {
outputStream.write(ByteUtil.get0xFFBytes(len));
} else {
int length = ((String) val).length() / 2;
Assert.isTrue(len >= length, "字符串长度超过ByteType注解中定义长度");
// bcdReverse 反向为前补0
if ((annotation.asciiFlag() == 2 || bcdReverse) && len > length ) {
outputStream.write(ByteUtil.getZeroBytes(len - length));
}
// 如果是ascii编码
if (annotation.asciiFlag() > 0) {
outputStream.write(ByteUtil.toBytesFromAscii((String)val));
} else {
outputStream.write(ByteUtil.toBytesFromHexString((String)val, bcdReverse));
}
// bcdReverse 正向为后补0
if (!(annotation.asciiFlag() == 2 || bcdReverse) && len > length ) {
outputStream.write(ByteUtil.getZeroBytes(len - length));
}
}
}
// List类型
else if (declaredField.getType() == List.class) {
List list = (List) val;
for (Object item : list) {
byte[] itemBytes = convertToBytes(item, bcdReverse, fieldName);
Assert.notNull(itemBytes, "记录对象子项转字节为null,item:" + item);
outputStream.write(itemBytes);
}
}
if (fieldName != null && Objects.equals(declaredField.getName(), fieldName)) {
break;
}
// log.info("----------------{}:{}", declaredField.getName(), outputStream.size() - curr);
// curr = outputStream.size();
}
return outputStream.toByteArray();
} catch (Exception e) {
log.error("对象转换为字节数组异常,obj:{}", obj, e);
return null;
}
}
true) (chain =
public static class ByteAndIdx {
private byte[] bytes;
private int idx;
}
}
依赖的其他工具类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* 类工具类
*
* @author mafgwo
* @since 2022/10/21
*/
public class ClassUtil {
private ClassUtil() {
}
/**
* 获取所有的属性(含父类属性)
*
* @param cls
* @return
*/
public static List<Field> getAllFields(Class<?> cls) {
if (cls == Object.class) {
return Collections.emptyList();
}
Field[] declaredFields = cls.getDeclaredFields();
Class<?> genericSuperclass = (Class<?>) cls.getGenericSuperclass();
if (genericSuperclass == Object.class) {
return Arrays.asList(declaredFields);
} else {
List<Field> genericFields = getAllFields(genericSuperclass);
List<Field> allFields = new ArrayList<>(genericFields.size() + declaredFields.length);
allFields.addAll(genericFields);
allFields.addAll(Arrays.asList(declaredFields));
return allFields;
}
}
}
1 | import org.springframework.util.StringUtils; |
编写信息体中含记录类型和业务数据的信息对象1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/**
* 云端计费模型
* [1] 下发记录类型
* [2-9] 充电设备编号 压缩 BCD 码 8Byte 充电设备编号
* [10] 充电接口标识 BIN 码 1Byte 充电设备为一桩多充时用来标记接口号,一桩一充时此项为0。多个接口时顺序对每个接口进行编号
* [11-18] 计费模型 ID BIN 码 8Byte 运营管理系统产生
* [19-25] 生效时间 BIN 码 7Byte CP56Time2a 格式
* [26-32] 失效时间 BIN 码 7Byte CP56Time2a 格式
* [33-34] 执行状态 压缩 BCD 码 2Byte 0001-有效 0002-无效
* [35-36] 计量类型 压缩 BCD 码 2Byte 0001-充电量
* [37] 时段数 N BIN 码 1Byte 取值范围:0—12
* [38-39] 第 1 个时段起始时间点 BIN 码 2Byte 高字节:小时(0-24)低字节:分钟(0-60)
* [40] 第 1 个时段标志 BIN 码 1Byte 1:尖时段;2:峰时段 3:平时段,4:谷时段
* [41-44] 第 1 个尖时段尖电价 BIN 码 4Byte 精确到小数点后五位
* [45-48] 第 1 个峰时段电价 BIN 码 4Byte 精确到小数点后五位
* [49-52] 第 1 个平时段电价 BIN 码 4Byte 精确到小数点后五位
* [53-56] 第 1 个谷时段电价 BIN 码 4Byte 精确到小数点后五位
* [57-60] 第 1 个服务费单价 BIN 码 4Byte 精确到小数点后五位
* [61-64] 第 1 个占位费单价 BIN 码 4Byte 精确到小数点后五位
* [65-68] 第 1 个预约费单价 BIN 码 4Byte 精确到小数点后五位
* @author mafgwo
* @since 2022/10/20
*/
true) (callSuper =
true) (callSuper =
public class CloudFeeModeVO extends RecordBaseVO {
/**
* 下发数据类型
*/
private Byte recordType;
/**
* 充电设备编号
*/
8) (len =
private String cloudPileSn;
/**
* 充电接口标识
*/
private Byte gunNo;
/**
* 计费模型ID
*/
private Long feeModeId;
/**
* 生效时间
*/
private Date activeTime;
/**
* 失效时间
*/
private Date inActiveTime;
/**
* 执行状态 0001-有效 0002-无效
*/
2) (len =
private String execStatus;
/**
* 计量类型
*/
2) (len =
private String measureType;
/**
* 时段数
*/
private Byte periodNum;
/**
* 时段
*/
"periodNum") (itemNumField =
private List<PeriodVO> periods;
/**
* [38-39] 第 1 个时段起始时间点 BIN 码 2Byte 高字节:小时(0-24)低字节:分钟(0-60)
* [40] 第 1 个时段标志 BIN 码 1Byte 1:尖时段;2:峰时段 3:平时段,4:谷时段
* [41-44] 第 1 个尖时段尖电价 BIN 码 4Byte 精确到小数点后五位
* [45-48] 第 1 个峰时段电价 BIN 码 4Byte 精确到小数点后五位
* [49-52] 第 1 个平时段电价 BIN 码 4Byte 精确到小数点后五位
* [53-56] 第 1 个谷时段电价 BIN 码 4Byte 精确到小数点后五位
* [57-60] 第 1 个服务费单价 BIN 码 4Byte 精确到小数点后五位
* [61-64] 第 1 个占位费单价 BIN 码 4Byte 精确到小数点后五位
* [65-68] 第 1 个预约费单价 BIN 码 4Byte 精确到小数点后五位
*/
public static class PeriodVO {
/**
* 时段起始时间点 分钟
*/
private Byte minute;
/**
* 时段起始时间点 小时
*/
private Byte hour;
/**
* 时段标志 BIN 码 1Byte 1:尖时段;2:峰时段 3:平时段,4:谷时段
*/
private Byte periodType;
/**
* 尖时段尖电价
*/
4, decimalNum = 5) (len =
private Float sharpPrice;
/**
* 峰时段电价
*/
4, decimalNum = 5) (len =
private Float peakPrice;
/**
* 平时段电价
*/
4, decimalNum = 5) (len =
private Float flatPrice;
/**
* 谷时段电价
*/
4, decimalNum = 5) (len =
private Float valleyPrice;
/**
* 服务费
*/
4, decimalNum = 5) (len =
private Float serviceFee;
/**
* 占位费
*/
4, decimalNum = 5) (len =
private Float spaceFee;
/**
* 预约费
*/
4, decimalNum = 5) (len =
private Float retainingFee;
}
}
字节数组转对象1
2
3
4// 由于上层已经封装好了IEC104规约的应用规约控制信息(APCI)和数据单元标识符 通俗理解即头部信息已经封装好了。只需要对消息体的差异做单独解析即可。
// msgBytes 是从记录类型开始直到结束的所有字节数组
// 以下一行代码即可完成解析
CloudFeeModeVO cloudFeeModeVO = RecordDataUtils.cloudConvertToObj(msgBytes, CloudFeeModeVO.class);
四 总结
作为一名优秀的码农,我们写代码前,一定要思考一下如何实现才是最好的。
也许有些实现方式会让你前期花不少功夫去磨针,但是等针磨好了,后续开发效率和代码质量都会非常高。
如果,你也熟悉充电桩规约,或者正在对接充电桩协议,都非常欢迎关注一下我,然后加我微信沟通交流。