ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java读取OPC DA数据:JOpc与JeasyOpc从环境配置到代码实现

Java读取OPC DA数据:JOpc与JeasyOpc从环境配置到代码实现

简介:面向需要在Java环境中对接工业OPC Server的开发者,压缩包围绕JOpc与JeasyOpc库,解决Java原生不支持OPC时与自动化设备进行数据交换的难题。压缩包内共860个文件,涵盖Java源码、编译后的class文件、工程配置与日志文件等,包体约1.99MB;由于目录中保留大量svn-work和svn-base等版本管理残留,实际源码与示例可重点关注java与class文件即可。已有1644人浏览学习该资源,适合初步接触OPC协议或想在Java项目中快速上手的开发者参考。内容包含基于JOpc和JEasyOpc的连接示例,覆盖连接OPC Server、浏览组与项、订阅数据变化、读写数值及断开连接等关键环节,同时提供无内存泄漏示例,方便对照平时易忽略的释放问题。

1. 从工控数据到 Java 业务系统:JOpc/JeasyOpc 把 OPC 变成普通读取接口

工业现场最常见的取数场景是:PLC 把数据送到 OPC Server,而业务系统是 Java 写的 MES 或可视化大屏。Java 直接读 OPC DA 走的是 COM/DCOM 协议,等于跨了一堵墙。JOpc 负责在 Java 和 Windows COM 之间垫一层 JNI,JeasyOpc 又把连接、会话、组和项的细节包成了像 JDBC 一样直观的 API。这个组合解决的核心问题是:不用碰 C++,也不用理解 COM 接口的晦涩细节,就能从 OPC Server 里拿到节点值。适合做上位机集成、设备数据网关和产线看板的 Java 工程师,也适合在测试环境里接模拟器快速做出数据原型。

2. 两个库的分工与 DCOM 协议边界:为什么配置 Windows 比重写代码更关键

2.1 先搞清楚 JOpc 和 JeasyOpc 到底谁干活

很多第一次接触的人会把 JOpc 和 JeasyOpc 当成同一个库的两个名字。它们确实经常一起出现,但职责完全不同。

OPC DA 服务器最底层的通话协议是 COM/DCOM。Java 进程没有原生的 COM 通道,所以必须有一个人帮忙翻译。JOpc 干的就是这个事:它通过 JNI 加载 Windows 本地的 COM 组件,让 Java 方法能直接调用 OPC 服务器的 COM 接口。这里的调用有大量指针、HRESULT 返回值、VARIANT 结构体,直接写 Java 会疯掉。JeasyOpc 是在 JOpc 之上的封装层,它把 OpcServer、OpcGroup、OpcItem 这类概念映射成 Java 对象,连接字符串、会话管理、组刷新周期都变成了构造函数和参数。

常见的做法是业务代码只面向 JeasyOpc 的 API,底层是否用 JOpc 由依赖决定。也可以理解成:JOpc 是网卡,JeasyOpc 是 HTTP 客户端。没有网卡,HTTP 客户端跑不动;没有 HTTP 客户端,网卡也能用,但体验很差。在实际项目里,我一般会在 Maven 依赖中同时引入这两个库,代码里完全按 JeasyOpc 的类来写。这样出了问题也好排查:高层 API 报错说明参数或会话问题,底层 JNI 报错说明环境问题。

很多人问为什么不能只用其中一个。只用 JeasyOpc 的话,它内部还是得靠 JOpc 或者同类 JNI 桥来触达 Windows 组件;只用 JOpc 的话,你得自己处理 Join、Leave、Group 管理这一堆 OPC 概念。所以资源包里把两者绑定是有道理的。依赖设计上,JeasyOpc 对 JOpc 的版本比较敏感,升级 JOPC 版本前最好跑一遍现有读取用例。

2.2 DCOM 的端口与权限:两个里任何一个没配好都会卡很久

传统 OPC DA 走 DCOM,DCOM 默认使用 135 端口做 RPC 映射,然后为每个连接分配一段动态端口,默认范围在 49152 到 65535(旧系统可能是 1024 以上)。如果 Windows 防火墙处于开启状态,只放行 135 端口还不够,通讯数据包的端口全被挡掉了,就会出现“连接建立,但是读不出数据”这种错乱表现。

更稳妥的做法是直接把 OPC Server 所在的 Windows 机器上的防火墙规则设成允许远程 DCOM 访问,或者固定 RPC 动态端口范围。固定端口范围需要在注册表里设置:

  • 32 位系统:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Rpc\Internet
  • 64 位系统:HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Rpc\Internet

这里设置Ports值,比如49152-49200,Protocol值为TCP。改完必须重启系统才生效,只重启服务是没用的。然后防火墙只放行 135 和 49152-49200 两个范围。注意 64 位系统上路径差别很大,改错地方就白调了。

项值说明
RPC 动态端口49152-49200固定后防火墙好配置
协议TCPDCOM 通讯数据流
防火墙规则放行 135 与动态端口对所有 OPC 访问来源开放

权限上,OPC Server 的 DCOM 访问和启动权限必须把运行 Java 程序的 Windows 账号放进去。具体在“运行”里输入dcomcnfg,到“组件服务 → 计算机 → 我的电脑 → DCOM 配置”,找到 OPC 服务器实例,设置“启动权限”和“访问权限”。常见错误是给了“访问”没给“启动”,结果程序能发现服务器,但创建不了会话。还有个容易忽略的点:如果 Java 程序以 Windows 服务方式运行,则使用的是服务账户,不是当前登录用户,这时候要在 DCOM 权限里同时放行网络访问和登录身份相关配置。

这里要说明一下跨平台的情况。很多 Java 应用部署在 Linux 上,而 DCOM 是 Windows 到 Windows 的机制。如果 Java 进程跑在 Linux 上,就不能直接通过 JeasyOpc 连 Windows 上的 OPC Server。这种场景要么在 Windows 侧写一个代理网关,要么改用 OPC UA,要么把 Java 数据采集服务部署到 Windows 机器上。很多人第一次卡在这,不是代码问题,而是部署边界没认清。

2.3 为什么还在用 OPC DA:老旧设备与存量系统决定的选型

有人会问:既然 OPC UA 是趋势,为什么还要搞 JOpc 和 JeasyOpc? 原因很现实:现场很多设备、SCADA、旧版 OPC Server 只支持 OPC DA 2.0 / 3.0,没有 UA 接口。厂商不升级,你只能兼容 DA。另一个场景是自动化测试环境里跑了一个本地 OPC 模拟器,需要快速生成数据给 Java 系统,用 UA 升级改造的成本不低,用 JOpc 组合半天就能接完。

所以这个选题不是怀旧,而是大量的存量工业设备带来的现实需求。理解这一点,就知道重点不是学新协议,而是把 DCOM 环境、JNI 位数、API 调用顺序这几个固定套路摸熟。这套东西一旦跑通,以后接别的 DA 服务器就是换参数的事。

另外,从工程排错的角度看,OPC DA 层的问题多数出现在连接阶段而不是数据读取阶段。连接一旦成功,之后的读写就是稳定的内存指针操作。因此花在环境配置上的时间,永远比写业务代码的时间要多。不要觉得这是浪费时间,这是这类集成项目的正常分布。

3. 从环境到数据:一条能跑通的 JOpc/JeasyOpc 连接链路

3.1 环境准备:先装对 OPC 核心组件、仿真器和 JDK

要复现这套流程,环境占七成。先检查三样东西:

第一,OPC Core Components Redistributable。很多 OPC Server 安装包会自带,但如果你只拿到了一个 DLL 版 OPC Server,就必需手动装这个运行时。不然 Java 端调用注册表时会找不到任何 OPC 服务器。

第二,OPC 仿真服务器。现场有真实服务器就不需要,但测试环境强烈建议装一个支持 DA 的模拟器,它能持续产生随机数据,方便验证订阅和读取。

第三,JDK 版本。位数必须和 OPC Server 的组件方案一致。如果 OPC Server 是 32 位的 COM 组件,Java 进程也应该是 32 位;64 位同理。用 64 位 JDK 去调 32 位 Server,JNI 层经常报UnsatisfiedLinkError或ACCESS_VOLATION。

检查项建议值原因
OPC Core Components已安装没有它注册表无数据
仿真器DA 2.0 或 3.0方便验证读写
JDK8/11/17 均可位数必须匹配
DCOM 用户名管理员或域用户有启动权限

同机器测试时用localhost,跨机器测试时先保证两台 Windows 机器能 ping 通。如果是域环境,还要注意域名前缀的写法。顺便说一个经验:在虚拟机里做实验时,Windows 防火墙默认开的是专用网络配置,和桥接模式的网络类型对不上也会导致 135 端口不通。

3.2 把 JeasyOpc 依赖放进 Maven 工程

由于大部分 JeasyOpc 版本不在中央仓库,常见做法是先拿到本地 jar 或公司内部仓库坐标。如果只有 jar 包,就把 jar 安装到本地仓库:

mvn install:install-file -Dfile=./jeasyopc.jar -DgroupId=org.jeasyopc -DartifactId=jeasyopc -Dversion=1.0 -Dpackaging=jar

然后pom.xml里写:

<dependency> <groupId>org.jeasyopc</groupId> <artifactId>jeasyopc</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>org.jopc</groupId> <artifactId>jopc</artifactId> <version>1.0</version> </dependency>

这段的逻辑是先解决依赖来源,再把两个库同时引进去。实际使用中,JeasyOpc 内部会依赖 JOpc 的包,所以版本不能差太多。

参数说明:-DgroupId、-DartifactId、-Dversion三个值会构成 Maven 仓库路径,也可以按公司规范改成别的组名。重点是-Dfile要指向 jar 的真实路径。如果公司有私有 Nexus 仓库,可以直接把 jar 推到仓库里,然后在pom.xml中使用正式坐标,这种做法的好处是团队其他人不用重复执行本地安装命令。

3.3 连接、建组、读点:最常用的三段式

下面这段代码是连接 OPC Server 并读取一个节点的最典型流程,以 JeasyOpc 的 API 为例:

import org.jeasyopc.OpcServer; import org.jeasyopc.OpcGroup; import org.jeasyopc.OpcItem; public class OpcReader { public static void main(String[] args) throws Exception { // 1. 指定 OPC Server 的 ProgID 和主机名 OpcServer server = new OpcServer("MyOpc.Server.1", "192.168.1.20"); // 2. 用域账号建立连接 server.connect("domain\\serviceAcc", "password"); // 3. 建组:第二个参数 true 表示后台线程自动刷新 OpcGroup group = server.addGroup("JavaGroup", true, 500); // 4. 添加需要读取的数据点 OpcItem item = group.addItem("Bucket.Value.Int32"); // 5. 读取一次 Object value = item.read(); System.out.println("Value = " + value); // 6. 断开前先释放组 group.terminate(); server.disconnect(); } }

逻辑说明:前三步解决服务器发现和会话建立,第四步创建 OPC 组。组是 OPC DA 的容器概念,所有的 item 都必须归属某个组;第五步 read 会拿到当前值对应的 Java Object,值类型由 OPC 服务器的 VARIANT 类型自动映射而来。

参数说明:"MyOpc.Server.1"是 OPC Server 的 ProgID,可以从注册表HKEY_CLASSES_ROOT下查;主机名支持 IP 地址;如果 OPC Server 和 Java 程序在同一台机器,可以只填localhost。true, 500表示用组内后台线程每 500 毫秒刷新一次缓存,三个参数分别是组名、是否启用自动刷新、刷新周期(毫秒)。

这段代码没有异常处理,真实环境里不行。如果 group 和 server 不关闭,Windows 侧会残留 COM 引用,导致后续连接越来越多,最后 DCOM 被耗尽。我一般在finally块里判断非空再关闭,或者用 try-with-resources 风格封装一个连接对象。

3.4 订阅值变化:不被轮询拖垮的写法

读一次只能拿快照,产线看板需要持续变化。常见的做法是给 OpcGroup 注册监听器:

group.addItemValueListener(new OpcItemValueListener() { @Override public void valueChanged(OpcItem item, Object value, long timestamp) { System.out.println(item.getName() + "=" + value + " @ " + timestamp); } }); Thread.sleep(10000);

监听器回调的触发依赖组里的后台刷新线程,刷新周期设得越小,实时性越好,但 CPU 和网络开销也越大。生产现场一般 500 到 1000 毫秒就够,低频场景可以放到 2 秒。这里的常见错误是在监听器里做重活,比如写数据库、发 MQ,这样会导致回调阻塞,后续值全部延迟。我一般会:

  1. 在回调里只把数据塞进一个并发队列。
  2. 另开一个线程池消费队列。
  3. 队列使用有界容量,满了就丢弃旧值,保最新值。

这样能避免 DCOM 调用被业务逻辑卡死。很多人觉得订阅回调是高性能方案,其实它只是省去了主动轮询的代码,底层的刷新线程仍然在跑。

3.5 连接不上时的快速定位命令

遇到连接失败,我先跑这三个命令,能省半小时:

ping -n 2 192.168.1.20 telnet 192.168.1.20 135 dcomcnfg

第一条看网络通不通,第二条看 RPC 映射端口是否可达,第三条直接打开组件服务检查权限。注意 telnet 通了不代表 DCOM 一定通,还需要验证动态端口范围是否放行。如果日志里出现0x800706ba,说明 DCOM 通信失败,优先查防火墙;如果出现0x80070005,说明权限拒绝,优先查 DCOM 权限配置。

4. 连接 OPC Server 的经典坑:从 DCOM 权限到位数不匹配

4.1 环境类坑:拒绝访问、找不到服务器、JNI 加载失败

先说最频繁的“连接失败,错误码 0x80070005(拒绝访问)”。表象是 JeasyOpc 在connect()阶段直接抛异常,但 OPC 服务器在 Windows 本机用图形客户端能连上。原因通常是运行 Java 的账号没有 OPC Server 的 DCOM 启动权限。解决方法是打开组件服务,定位到对应的 OPC 服务器组件,在“启动权限”和“访问权限”里加入当前账号,或者干脆把这台机器上的“分布式 COM 用户”组加进去。注意改完后要重启 OPC Server,否则旧的权限缓存还在。还有个细节:如果你改了权限,但 Java 程序是 IDE 里启动的,需要完全关闭 IDE 再重开,因为 IDE 进程可能还持着旧的 token。

再说“OPC Server 在局域网里永远搜不到”。表象是程序列出服务器列表时是空的,本机能看到,远程看不到。原因多半是防火墙只放行了 135 端口,而 DCOM 实际数据走的是动态端口。解决办法就是前面提到过的固定 RPC 端口范围,并在防火墙里把该范围和 135 一起放行。这个坑最阴的地方在于程序不会报“连接超时”,而是直接报“找不到服务器”,容易让人误判成网络不可达。排查的时候可以用 Wireshark 抓包,如果看到服务器返回了端口映射信息但后续 TCP 连接被 RST,基本就是动态端口被挡。

再说“JNI 报 UnsatisfiedLinkError 或加载 DLL 失败”。表象是代码完全正确,但一执行到依赖 JOpc 的类就炸。原因是 JDK 位数、OPC 服务器组件位数、JOpc 本地库位数三者不一致。这种情况用 Windows 记事本能分清:如果 OPC Server 安装的是 32 位,Java 也要用 32 位 JDK,不能因为机器是 64 位系统就全部用 64 位。解决方法是确认位数后把 JDK 换掉,重开 cmd 让 PATH 生效。有时候还会遇到本机装了多个 JDK,java -version显示 64 位,但 IDE 里配的是 32 位,这种不一致很难一眼看出来。我在项目里是强制用maven-enforcer-plugin检测 JDK 版本和架构,提前报错。

4.2 代码与数据类坑:空值、并发挂死和路径写错

第四个坑是“能连能建组,但读到的一个 item 永远为空或 0”。表象:不报异常,数据就是不对。原因通常是 OPC Server 返回的 VARIANT 类型和 JeasyOpc 预设的映射不匹配,比如服务器返回 VT_I1,而 Java 侧映射到 Integer 时被截断。另一个原因是你读的 item 路径写错了,OPC 客户端通常会自动填充完整路径,手写经常少一级。解决方法是先用服务器自带图形客户端确认 item 路径,再看日志输出,把 value 按提示转成正确的目标类型。实在不行就用 Object 类型接收,再在 Java 侧解析。

第五个坑是“并发读多了以后,连接挂死”。表象:程序运行几小时偶发卡死,日志停在某次 read 或监听回调。原因多数是 OpcGroup 回调线程被阻塞,或没有设置读取超时,底层的 DCOM 调用积压。解决方法是给 JeasyOpc 的 API 调用包一层线程池,设超时时间;同时在监听回调里不要做阻塞操作。还有一个容易被忽略的点:同一个 OpcServer 实例不要多线程同时调 connect/disconnect,加锁或应用启动时初始化好了就不动它。

我踩过最惨的一次就是监听回调里直接写 Oracle 数据库,连接池被打满,然后 OPC 回调线程全卡在等待上,最终整个 Java 进程假死。从那以后,所有 OPC 回调都只做一件事:把数据放进阻塞队列。队列消费线程慢可以报警,但绝不反向阻塞 OPC 回调。

除了以上五个,还有一个很玄学的坑:连接成功后,过一段时间系统休眠或网络切换,DCOM 会话自动断开,Java 端还拿着旧的引用。表象是程序不报错,但值不再更新。解决方法是监听 DCOM 断线事件,或者用一个心跳线程定期读一个固定 item,读失败就重建连接。这个在移动办公现场调试时特别常见,固定 IP 也有,只是概率低一些。

5. 封装成 Spring Boot 服务:把 OPC 读数变成 REST 接口

5.1 生命周期管理

如果要在 Spring Boot 项目里长期运行,最简单的做法是把 OpcServer 当单例 Bean 管理:

@Configuration public class OpcAutoConfiguration { @Bean(destroyMethod = "disconnect") public OpcServer opcServer(OpcProperties props) { OpcServer server = new OpcServer(props.getServerName(), props.getHost()); server.connect(props.getUser(), props.getPassword()); return server; } }

这里的逻辑是让 Spring 容器控制连接生命周期,应用关闭时自动调用disconnect()。参数说明:OpcProperties可以从application.yml读取 serverName、host、user、password,避免硬编码。如果disconnect()方法不存在,destroyMethod可以用""代替,然后单独注册@PreDestroy回调。

5.2 验证技巧

封装好之后,启动时先打印 OPC 服务器里有哪些 item,再把某个已知 item 的值通过 REST 接口暴露出来。验证步骤:

  1. 用图形客户端手动改值。
  2. 调 REST 接口确认值是否一致。
  3. 记录从改值到接口返回变化的延迟。

如果延迟远大于设定的刷新周期,先查监听队列是否积压,再查消费线程池大小。如果读值是正常的但订阅不更新,基本可以确定是回调线程被阻塞或者 DCOM 会话断了。

从那以后,我每次接 OPC 项目都强制走一遍固定流程:先配 DCOM 权限和端口,再开模拟器试连接,最后才写业务代码。这一套流程跑通了,后面换多少 OPC Server 都只是改参数。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进