如何用Apache Milo客户端动态发现并填充OPC UA自定义结构体写入服务器
Apache Milo动态处理OPC UA自定义结构体写入方案优化
问题背景
需要通过Apache Milo客户端向OPC UA服务器写入自定义结构体,要求运行时动态发现所有数据类型,无预先定义任何自定义类型信息,需基于结构体的字段名称和数据类型解析JSON、填充结构体并完成写入。已通过DataTypeDictionarySessionInitializer实现连接时自动获取数据类型定义,但当前填充结构体的方式存在诸多问题。
现有方案的痛点
- 依赖反射访问私有字段:通过反射获取
AbstractCodec的私有fields字段,依赖Milo内部实现细节,后续版本升级可能失效 - 类型判断兼容性差:通过类型名字符串判断字段类型,不同服务器实现可能返回不同格式的类型名,导致匹配失败
- 功能覆盖不全:缺少对更多基础类型、嵌套结构体、数组等复杂类型的支持
优化方案
1. 用官方API替代反射获取字段信息
Milo的GenericStructCodec提供了getDefinition()方法,可直接获取StructuredDataTypeDefinition对象,包含所有字段的名称、数据类型NodeId等合法公开信息,完全无需反射:
// 获取结构体Codec final GenericStructCodec dataTypeCodec = (GenericStructCodec) client.getDynamicSerializationContext().getDataTypeManager().getCodec(dataTypeNodeId); // 获取合法公开的结构体定义 StructuredDataTypeDefinition definition = dataTypeCodec.getDefinition(); // 遍历所有字段 for (StructuredDataTypeDefinition.Field field : definition.getFields()) { String fieldName = field.getName(); NodeId fieldDataTypeId = field.getDataTypeId(); // 后续处理字段逻辑 }
2. 基于NodeId判断数据类型,替代字符串匹配
OPC UA的所有基础类型都有固定的标准NodeId(可通过Identifiers类直接引用),对比NodeId的方式完全兼容所有服务器实现,不存在字符串匹配的歧义:
// 示例:判断基础类型 if (fieldDataTypeId.equals(Identifiers.UInt32)) { builder.addMember(fieldName, UInteger.valueOf(jsonNode.toString())); } else if (fieldDataTypeId.equals(Identifiers.String)) { builder.addMember(fieldName, jsonNode.toString()); } else if (fieldDataTypeId.equals(Identifiers.Boolean)) { builder.addMember(fieldName, Boolean.parseBoolean(jsonNode.toString())); }
3. 递归处理嵌套结构体
编写工具方法递归处理嵌套的自定义结构体,自动识别字段类型并递归填充:
private static ExtensionObject buildExtensionObjectFromJson(OpcUaClient client, Object jsonNode, NodeId dataTypeNodeId) throws Exception { DataTypeCodec codec = client.getDynamicSerializationContext().getDataTypeManager().getCodec(dataTypeNodeId); if (codec instanceof GenericStructCodec) { GenericStructCodec structCodec = (GenericStructCodec) codec; StructuredDataTypeDefinition definition = structCodec.getDefinition(); Struct.Builder builder = Struct.builder(definition.getName()); for (StructuredDataTypeDefinition.Field field : definition.getFields()) { String fieldName = field.getName(); NodeId fieldTypeId = field.getDataTypeId(); Object fieldJson = ((Map<?, ?>) jsonNode).get(fieldName); if (fieldJson == null) continue; // 基础类型处理 if (fieldTypeId.equals(Identifiers.UInt32)) { builder.addMember(fieldName, UInteger.valueOf(fieldJson.toString())); } else if (fieldTypeId.equals(Identifiers.String)) { builder.addMember(fieldName, fieldJson.toString()); } else if (fieldTypeId.equals(Identifiers.Boolean)) { builder.addMember(fieldName, Boolean.parseBoolean(fieldJson.toString())); } // 枚举类型处理 else if (client.getDynamicSerializationContext().getDataTypeManager().getCodec(fieldTypeId) instanceof GenericEnumCodec) { builder.addMember(fieldName, Integer.valueOf(fieldJson.toString())); } // 嵌套结构体递归处理 else if (client.getDynamicSerializationContext().getDataTypeManager().getCodec(fieldTypeId) instanceof GenericStructCodec) { ExtensionObject nested = buildExtensionObjectFromJson(client, fieldJson, fieldTypeId); builder.addMember(fieldName, nested); } // 可继续扩展数组、Int32、Double等其他类型 } return ExtensionObject.encode( client.getDynamicSerializationContext(), builder.build(), dataTypeNodeId, OpcUaDefaultBinaryEncoding.getInstance() ); } return null; }
4. 扩展支持数组类型
通过field.getValueRank()判断字段是否为数组,再批量转换JSON数组元素为对应OPC UA类型:
// 判断是否为数组类型(valueRank >= 0表示数组) if (field.getValueRank() >= 0) { List<?> jsonArray = (List<?>) fieldJson; if (fieldTypeId.equals(Identifiers.UInt32)) { UInteger[] uintArray = jsonArray.stream() .map(item -> UInteger.valueOf(item.toString())) .toArray(UInteger[]::new); builder.addMember(fieldName, uintArray); } // 其他数组类型(如String[]、Boolean[])同理扩展 }
总结
- 抛弃反射依赖,使用Milo官方公开API获取字段定义,保证版本兼容性
- 基于标准NodeId判断数据类型,彻底解决字符串匹配的兼容性问题
- 实现递归逻辑支持嵌套结构体,覆盖复杂类型场景
- 可灵活扩展支持更多基础类型、数组等OPC UA数据类型
内容的提问来源于stack exchange,提问作者Daniel Krüger
相关产品推荐
相关产品推荐

