在智能制造和工业互联网的场景中,经常需要把 PLC、传感器、数控机床等现场设备的数据采集到上层的 MES、SCADA 或者大数据平台中,同时还要支持上层系统向设备下发控制指令。OPC UA(OPC Unified Architecture)正是为此而生的一套跨平台、面向服务的工业通信协议。它不依赖 Windows 的 COM/DCOM 技术,用纯 TCP 加二进制或 WebSocket 的方式通信,天然适合和 Java 技术栈结合。本文将以 Spring Boot 为基础,结合 Eclipse Milo 这个开源 OPC UA 实现,完整讲解如何搭建一套可用的工业数据集成通道。

一、OPC UA 协议核心概念速览
在动手写代码之前,有必要先理解 OPC UA 的几个核心概念,否则在面对 Milo 的 API 时会感到莫名其妙。第一个概念是地址空间(Address Space)。OPC UA 把所有的数据组织成一棵节点树,每个节点由一个 NodeId 唯一标识,节点之间通过 References 关联起来。类型为 Variable 的节点承载实际的数据值,类型为 Object 的节点用来做分组,这种设计让不同厂商的设备可以用统一的方式暴露数据。
第二个概念是服务(Services)。客户端与服务端之间的交互全部通过一组标准化的服务请求完成,比如 ReadService、WriteService、Subscribe 等。你可以把它类比成一组远程过程调用接口。第三个概念是订阅(Subscription)与监视项(MonitoredItem)。轮询读取在高频场景下效率很低,OPC UA 提供了服务端主动推送的机制:客户端创建订阅,在订阅里添加监视项,当被监视的节点值变化或者达到设定的采样间隔时,服务端会主动发送通知,这比轮询节省了大量带宽。
另外还有安全策略的概念。OPC UA 支持None、Basic256Sha256 等多种安全模式,可以配合 X.509 证书完成双向认证和消息加密。生产环境强烈建议开启证书校验,避免任何一台能连上网络的设备都能随意读写数据。
二、在 Spring Boot 项目中引入 Milo 并搭建 OPC UA 服务端
Eclipse Milo 是目前 Java 生态中最成熟的 OPC UA 实现,由 Eclipse 基金会维护,同时提供服务端和客户端 SDK。首先在 pom.xml 中加入依赖:
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId;milo-server-sdk</artifactId>
<version>0.6.12</version>
</dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>milo-client-sdk</artifactId>
<version>0.6.12</version>
</dependency>接着把服务端的启动和关闭纳入 Spring 的生命周期管理,写一个配置类实现 ApplicationRunner 和 DisposableBean。服务端通常绑定一个命名空间,比如 urn:myfactory:line1,然后在命名空间下创建变量节点。下面的示例创建了一个模拟温度值节点和一个设备启停控制节点:
@Component
public class OpcUaServerRunner implements ApplicationRunner, DisposableBean {
private OpcUaServer server;
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor();
@Override
public void run(ApplicationArguments args) throws Exception {
server = new OpcUaServer(
new OpcUaServerConfigBuilder()
.setBindAddress("0.0.0.0")
.setBindPort(12686)
.setServerName("factory-server")
.build()
);
// 初始化命名空间并添加节点
ServerNodeManager nodeManager =
new ServerNodeManager(server, NamespaceIndex);
NodeId tempNode = nodeManager.createVariable(
"devices/oven1/temperature", Variant.of(25.0));
NodeId switchNode = nodeManager.createVariable(
"devices/oven1/switch", Variant.of(false));
server.startup();
// 模拟温度周期变化,方便客户端观察订阅效果
scheduler.scheduleAtFixedRate(() -> {
double next = 25.0 + ThreadLocalRandom.current().nextDouble(5);
nodeManager.setVariableValue(tempNode, Variant.of(next));
}, 0, 2, TimeUnit.SECONDS);
}
@Override
public void destroy() {
scheduler.shutdownNow();
if (server != null) {
server.shutdown();
}
}
}这段代码有几个值得注意的地方。第一,服务端的生命周期一定要和 Spring 容器绑定,否则应用重启时会留下占用端口的僵尸进程。第二,模拟数据的定时任务建议单独用一个线程池,不要和业务线程混用,工业采集场景对实时性有要求,线程隔离能避免相互干扰。第三,真实项目里节点一般来自 PLC 点表,可以通过读取点表配置文件批量建节点,而不是硬编码。
三、客户端连接、读写与订阅的实现
客户端部分的逻辑稍微复杂一些,核心是建立会话、读取节点、创建订阅三步。推荐把客户端封装成一个 Spring Bean,通过 OpcUaClientManager 统一管理连接状态。下面的代码演示了完整的连接与数据交互流程:
@Component
public class OpcUaClientManager {
private OpcUaClient client;
@PostConstruct
public void init() throws Exception {
client = new OpcUaClient(
"opc.tcp://127.0.0.1:12686/factory",
endpoints -> endpoints.stream()
.filter(e -> e.getSecurityPolicyUri()
.equals(SecurityPolicy.None.getUri()))
.findFirst()
.orElseThrow(() -> new Exception("无可用端点"))
);
client.connect().get();
}
/** 读取节点值 */
public Double readTemperature() throws Exception {
NodeId nodeId = new NodeId(2, "devices/oven1/temperature");
DataValue value = client.readValue(0, TimestampsToReturn.Both, nodeId)
.get();
return (Double) value.getValue().getValue();
}
/** 写入控制指令 */
public void writeSwitch(boolean on) throws Exception {
NodeId nodeId = new NodeId(2, "devices/oven1/switch");
client.writeValue(nodeId, DataValue.valueOnly(
new Variant(Boolean.valueOf(on)))).get();
}
/** 创建订阅,监听温度变化 */
public void subscribeTemperature(Consumer<Double> callback)
throws Exception {
NodeId nodeId = new NodeId(2, "devices/oven1/temperature");
UaSubscription subscription = client.getSubscriptionManager()
.createSubscription(1000.0).get();
UaMonitoredItem item = subscription.createMonitoredItem(
new ReadValueId(nodeId, AttributeId.Value.uid(), null, null),
new MonitoringParameters(
uint(1), 500.0, null, uint(10), true))
.get();
item.setValueConsumer(v -> {
Double temp = (Double) v.getValue().getValue();
callback.accept(temp);
});
}
}读操作和写操作都是异步的,返回的是 CompletableFuture,调用 get() 会阻塞等待结果。在 Web 接口里直接阻塞等待通常没问题,因为单次读写耗时可忽略;但如果要做批量点位的并发采集,建议用 thenCompose 把多个异步操作串联起来,配合自定义线程池,吞吐量能提升数倍。
订阅部分的几个参数要理解清楚。createSubscription 的 1000.0 表示发布间隔,单位毫秒,即服务端多久向客户端推送一批通知;MonitoringParameters 中的 500.0 是采样间隔,服务端按这个频率检查值是否变化;uint(10) 是队列大小,网络抖动时通知会先进入队列,队列满了会丢弃最旧的并标记溢出。把订阅回调拿到的数据推送到 Kafka 或者内存时序缓存,就完成了采集链路的最后一环。
四、断线重连、安全配置与生产环境注意事项
工业现场网络环境远不如机房稳定,断线重连是绕不开的话题。Milo 自带了故障转移机制,当检测到连接异常时,客户端会自动尝试恢复会话并重新传输订阅,但业务代码里最好再加一层保障:用一个后台线程定时调用 readValue 做心跳探测,一旦连续失败超过阈值,就主动关闭旧连接并走重建流程。同时要处理好回调中抛出的异常,否则一个坏消息可能让整个订阅静默失效。
安全方面,None 策略意味着明文传输,测试阶段可以图省事,上生产环境必须换成 Basic256Sha256 并启用证书校验。Milo 提供了 KeyStoreCertificateManager 来生成和管理自签名证书,客户端和服务端首次握手时会交换证书,需要把对方证书放入信任列表。这些证书文件建议挂载到容器外的持久化目录,否则每次重新部署都要重新做信任配置。
部署层面还有几点经验。端口规划上要提前和现场网络管理员确认 12686 等端口是否放通;点表配置和节点地址尽量外置到数据库或配置中心,方便设备变更时热更新;采集到的数据在入库前做一次范围校验,工业现场经常出现传感器故障输出极端值的情况,直接入库会污染统计数据。把这些细节处理好,基于 Spring Boot 加 Milo 的 OPC UA 集成方案完全可以稳定支撑日均百万级点位的采集任务。
Spring BootOPC UAMilo修改时间:2026-09-08 04:12:35