Java 实现 OPC UA 通信(Eclipse Milo)
原稿给出学习资料与实现方向(Milo);下文按官方示例与常用实践补全具体实现步骤(连接 / 浏览 / 读 / 写 / 订阅 / 简易服务端)。
版本说明:示例面向 Milo 1.1.x(JDK 17+)。1.2 起部分订阅 API 有迁移,升级请对照 Milo Client Wiki。
1、资料(原稿)
| 资源 | 链接 |
|---|---|
| 博客标签(OPC 系列) | https://www.cnblogs.com/ioufev/tag/OPC/ |
| 视频讲解 | https://www.bilibili.com/video/BV1TR4y1Q71g/ |
| Eclipse Milo 源码 | https://github.com/eclipse/milo(现主仓多为 eclipse-milo/milo) |
另可配合 UaExpert 等客户端先连上目标服务器,确认 namespaceIndex、节点 identifier,再写 Java。
2、如何实现(原稿 + 展开)
可以使用开源库 Milo 实现 OPC UA 的客户端和服务端。
工业现场常见形态:
PLC / SCADA / 专有 OPC UA Server
↑ opc.tcp://...
Java(Milo Client) ← 你自研网关、采集服务、或二开模块
↓ MQTT / HTTP / Kafka …
ThingsBoard(设备遥测 / 属性)若不想自研协议栈,也可用 ThingsBoard IoT Gateway 的 OPC-UA Connector 直接采数上报平台;本文聚焦 Java + Milo 的可编程实现。
2.1、Maven 依赖(推荐 Milo 1.1.6)
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>milo-sdk-client</artifactId>
<version>1.1.6</version>
</dependency>
<!-- 若要自己起 OPC UA Server -->
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>milo-sdk-server</artifactId>
<version>1.1.6</version>
</dependency>旧教程常见 0.6.x(artifact 多为 sdk-client)。包名与订阅 API 与 1.x 有差异;跟博客代码时注意版本对齐。官方示例见仓库 milo-examples。
2.2、联调服务器
本地没有设备时,可先用公开演示或 Docker:
- Milo 相关演示端点(以当时是否在线为准):
opc.tcp://milo.digitalpetri.com:62541/milo - Gateway 文档常用演示镜像:
docker run -it -p 4840:4840 thingsboard/tb-gw-opcua-server:latest
用 UaExpert 匿名连接,记下要读的节点,例如 namespace=2,identifier=Dynamic/RandomDouble。
2.3、创建并连接客户端
核心思路:OpcUaClient.create(endpointUrl, …) 拉取 Endpoint → 选策略/安全 → connect() 建 SecureChannel 与 Session。
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.sdk.client.identity.AnonymousProvider;
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
public class OpcUaClientFactory {
public static OpcUaClient createAndConnect(String endpointUrl)
throws Exception {
// 演示:选 SecurityPolicy.None + 匿名。
// 也可 OpcUaClient.create(endpointUrl) 自动挑「无安全+匿名」端点。
OpcUaClient client = OpcUaClient.create(
endpointUrl,
endpoints -> endpoints.stream()
.filter(e -> e.getSecurityPolicyUri()
.equals(SecurityPolicy.None.getUri()))
.findFirst(),
configBuilder -> configBuilder
.setApplicationName(LocalizedText.english("hikari-opcua-client"))
.setApplicationUri("urn:hikari:opcua:client")
.setIdentityProvider(new AnonymousProvider()) // 默认即匿名,可省略
.build()
);
client.connect().get();
return client;
}
public static void main(String[] args) throws Exception {
String url = "opc.tcp://127.0.0.1:4840";
OpcUaClient client = createAndConnect(url);
try {
// 读 / 写 / 订阅 …
} finally {
client.disconnect().get();
}
}
}要点:
- 一个应用进程对一台 OPC UA Server 通常共用一个
OpcUaClient,断线由 Milo 尝试恢复 Session / 订阅;业务侧在停机时disconnect()。 - Endpoint 过滤要与服务器实际安全策略一致,否则连不上。
2.4、浏览地址空间(找 NodeId)
import org.eclipse.milo.opcua.sdk.client.nodes.UaNode;
import org.eclipse.milo.opcua.stack.core.Identifiers;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import java.util.List;
public static void browseRoot(OpcUaClient client) throws Exception {
UaNode root = client.getAddressSpace()
.getNode(Identifiers.ObjectsFolder);
List<? extends UaNode> children =
client.getAddressSpace().browseNodes(root);
for (UaNode child : children) {
NodeId id = child.getNodeId();
System.out.printf("browse: %s %s%n",
child.getBrowseName().getName(), id);
}
}现场节点命名因厂商而异(通道.设备.点位)。以 UaExpert 看到的 NodeId 为准,不要猜 namespace。
2.5、读节点
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
public static Object readValue(OpcUaClient client,
int namespaceIndex,
String identifier) throws Exception {
NodeId nodeId = new NodeId(namespaceIndex, identifier);
DataValue dataValue = client.readValue(
0.0, TimestampsToReturn.Both, nodeId).get();
if (dataValue.getStatusCode() == null
|| !dataValue.getStatusCode().isGood()) {
throw new IllegalStateException(
"read failed: " + dataValue.getStatusCode());
}
Object raw = dataValue.getValue().getValue();
System.out.println(identifier + " = " + raw
+ " ts=" + dataValue.getSourceTime());
return raw;
}
// 例:readValue(client, 2, "Dynamic/RandomDouble");批量读可用 client.readValues(maxAge, timestamps, List<NodeId>),减少往返。
2.6、写节点
写入类型必须与服务器节点数据类型一致(Float / Double / Int32 等),否则 Status 非 Good。
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
public static void writeValue(OpcUaClient client,
int namespaceIndex,
String identifier,
Object javaValue) throws Exception {
NodeId nodeId = new NodeId(namespaceIndex, identifier);
DataValue dv = new DataValue(new Variant(javaValue), null, null);
StatusCode status = client.writeValue(nodeId, dv).get();
if (!status.isGood()) {
throw new IllegalStateException("write failed: " + status);
}
}
// 例:writeValue(client, 2, "Demo.Setpoint", 26.5f);2.7、订阅(推荐采集方式)
轮询 read 简单但费带宽;产线更常用 Subscription + MonitoredItem:服务器在值变化(或按采样周期)推送。
import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaMonitoredItem;
import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaSubscription;
import org.eclipse.milo.opcua.stack.core.AttributeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
import org.eclipse.milo.opcua.stack.core.types.enumerated.MonitoringMode;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.MonitoredItemCreateRequest;
import org.eclipse.milo.opcua.stack.core.types.structured.MonitoringParameters;
import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public static void subscribe(OpcUaClient client,
int namespaceIndex,
String identifier) throws Exception {
UaSubscription subscription =
client.getSubscriptionManager()
.createSubscription(1000.0) // 发布间隔 ms
.get();
NodeId nodeId = new NodeId(namespaceIndex, identifier);
ReadValueId readValueId = new ReadValueId(
nodeId,
AttributeId.Value.uid(),
null,
null
);
AtomicInteger handle = new AtomicInteger(1);
MonitoringParameters parameters = new MonitoringParameters(
UInteger.valueOf(handle.getAndIncrement()),
1000.0, // 采样间隔 ms
null, // filter
UInteger.valueOf(10), // 队列长度
true // discard oldest
);
MonitoredItemCreateRequest request =
new MonitoredItemCreateRequest(
readValueId, MonitoringMode.Reporting, parameters);
List<UaMonitoredItem> items = subscription.createMonitoredItems(
TimestampsToReturn.Both,
Collections.singletonList(request),
(item, id) -> item.setValueConsumer((monitoredItem, value) -> {
System.out.println(
"notify " + monitoredItem.getReadValueId().getNodeId()
+ " => " + value.getValue());
// 这里可转成 MQTT 上报 ThingsBoard
})
).get();
for (UaMonitoredItem item : items) {
System.out.println("monitored status=" + item.getStatusCode());
}
// 演示:阻塞保持订阅;正式环境用生命周期托管,勿 sleep 死循环
Thread.sleep(60_000);
}多点批量订阅:为每个 identifier 建一条 MonitoredItemCreateRequest,一次 createMonitoredItems。
2.8、安全与身份(生产必看)
| 方式 | 说明 |
|---|---|
| Anonymous | 仅适合演示 / 内网封闭环境 |
| 用户名密码 | UsernameProvider(具体类名随 Milo 版本) |
| 证书 | 应用证书 + 信任对方证书;策略如 Basic256Sha256 |
生产建议:独立应用证书、明确信任列表、最小权限账户;拒绝策略不匹配的 Endpoint。证书被拒时查服务器 rejected 目录并完成信任 reciprocal。
2.9、简易 OPC UA 服务端(Milo Server)
需要自己模拟点位或对外提供 UA 接口时,引入 milo-sdk-server,参考仓库 milo-examples/server-examples:
- 创建
OpcUaServer/ 命名空间 - 在地址空间中挂
UaVariableNode(温度、设定值等) - 绑定
tcpEndpoint(如opc.tcp://0.0.0.0:4840) server.startup()
客户端连本机该端口即可联调读写订阅。完整样例较长,建议直接克隆 Milo 仓库跑官方 Example,再改成业务命名空间。
2.10、接到 ThingsBoard 的两种路径
路径 A — 自研(本文)
Milo 订阅回调
→ 组装 JSON { "temperature": 26.5, ... }
→ MQTT 发布到 TB 设备遥测主题
(或 HTTP Transport / 规则链)适合:要深度定制映射、多协议汇聚、或嵌入现有 Java 服务。
路径 B — ThingsBoard IoT Gateway OPC-UA 连接器
在 Gateway 中启用 type: opcua,配置 server.url、安全策略、mapping(设备节点 / 属性 / 遥测路径)。官方说明:
https://thingsboard.io/docs/iot-gateway/config/opc-ua/
适合:快速把现有 OPC UA Server 采进平台,少写代码。
3、实现检查清单
- 用 UaExpert 能连上,并抄对 NodeId
- Maven 坐标与 JDK 与所选 Milo 大版本一致
- Endpoint 安全策略过滤正确,
connect()成功 - 读:Status Good;写:类型匹配
- 订阅:
createMonitoredItems成功,回调有值 - 上报 TB:设备令牌 / 主题正确;大包注意 MQTT 解码上限
4、常见问题
| 现象 | 排查 |
|---|---|
| 连不上 | URL、端口、防火墙;安全策略/证书不匹配 |
| 读到 null / Bad | NodeId 错;无读权限;节点未激活 |
| 写失败 | Java 类型与 UA 数据类型不一致 |
| 订阅无回调 | 采样/发布间隔;MonitoringMode;服务器不支持该项 |
| 依赖冲突 | Netty 等版本被 Spring Boot 覆盖,对齐 Milo 传递依赖 |
补充(辅)
- 官方示例目录:
milo-examples/client-examples(Read / Write / Subscription / Browse)。 - 中文系列可对照原稿博客标签逐步跟做,再迁到你现场的 NodeId。
- ThingsBoard 本体默认是 MQTT/HTTP 等接入;OPC UA 通常停在边缘(自研或 Gateway),再转成平台认识的遥测。
