Java 集成 modbus4j 3.0.3 实现 PLC 数据采集与连接管理
如果你正在为 Java 项目集成 PLC 数据采集而头疼,特别是面对西门子、三菱、欧姆龙这些主流品牌设备时,不知道如何稳定可靠地建立通信连接,那么这篇文章就是为你准备的。工业现场的环境远比实验室复杂,网络抖动、设备响应慢、数据类型错位都是家常便饭,很多教程只告诉你如何连接,却没告诉你连接断了怎么办、数据错了怎么查。今天我们不谈理论,直接从实战出发,分享一套经过生产环境验证的 modbus4j 3.0.3 集成方案,重点解决那些让新手栽跟头的超时、重试、连接池和数据解析问题。
1. 项目准备与依赖配置:避开第一个大坑
开始写代码之前,环境配置是第一个拦路虎。modbus4j 的官方仓库不在 Maven 中央库,如果你用了阿里云镜像,大概率会下载失败。很多开发者在这里卡住,以为是网络问题,其实只是仓库地址不对。
首先,在 pom.xml 中正确配置仓库和依赖。关键点在于要禁用默认的阿里云镜像对这两个仓库的代理,让 Maven 直接去官方地址拉取。
<repositories>
<repository>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
<id>ias-snapshots</id>
<name>Infinite Automation Snapshot Repository</name>
<url>https://maven.mangoautomation.net/repository/ias-snapshot/</url>
</repository>
<repository>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>ias-releases</id>
<name>Infinite Automation Release Repository</name>
<url>https://maven.mangoautomation.net/repository/ias-release/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.infiniteautomation</groupId>
<artifactId>modbus4j</artifactId>
<version>3.0.3</version>
</dependency>
<!-- 建议引入,用于日志和工具类 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
</dependencies>
注意:如果你在公司内网,可能需要让运维人员将这两个仓库地址加入内部 Nexus 或 Artifactory 的代理列表,否则构建过程会因为无法访问外网而失败。
版本选择上,我坚持用 3.0.3 而不是追新。这个版本经过了大量生产验证,API 稳定,社区遇到的问题和解决方案也最丰富。新版本可能引入了不兼容的改动,在工业场景下,稳定压倒一切。
2. 构建稳健的 TCP 连接管理器:告别单次连接
绝大多数入门示例的 ModbusMaster 都是用完即弃,这在频繁读取的工业场景中是致命的性能瓶颈。更糟糕的是,它们没有重连机制,一旦网络闪断,整个采集链路就瘫痪了。我们需要一个能够管理连接生命周期、支持自动重连的连接管理器。
下面这个 ModbusTcpManager 类封装了连接池的核心逻辑。它使用 ConcurrentHashMap 缓存不同设备的连接,确保每个设备 IP 和端口对应一个独立的 ModbusMaster 实例,避免多线程下的连接混乱。
import com.serotonin.modbus4j.ModbusFactory;
import com.serotonin.modbus4j.ModbusMaster;
import com.serotonin.modbus4j.exception.ModbusInitException;
import com.serotonin.modbus4j.ip.IpParameters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
public class ModbusTcpManager {
private static final Logger log = LoggerFactory.getLogger(ModbusTcpManager.class);
private static final ModbusFactory modbusFactory = new ModbusFactory();
// 连接池:key 为 "ip:port"
private final Map<String, ModbusMaster> connectionPool = new ConcurrentHashMap<>();
private final Map<String, ReentrantLock> connectionLocks = new ConcurrentHashMap<>();
// 默认超时和重试配置
private int defaultTimeout = 3000; // 3 秒
private int defaultRetries = 2; // 失败后重试 2 次
/**
* 获取或创建指定设备的 ModbusMaster 连接
* @param ip 设备 IP
* @param port 端口,默认 502
* @param slaveId 从站 ID,通常为 1
* @return 可用的 ModbusMaster 实例
*/
public ModbusMaster getMaster(String ip, int port, int slaveId) throws ModbusInitException {
String key = ip + ":" + port;
ModbusMaster master = connectionPool.get(key);
if (master == null || !isConnectionValid(master)) {
ReentrantLock lock = connectionLocks.computeIfAbsent(key, k -> new ReentrantLock());
lock.lock();
try {
// 双重检查,防止重复创建
master = connectionPool.get(key);
if (master == null || !isConnectionValid(master)) {
if (master != null) {
try {
master.destroy();
} catch (Exception e) {
log.warn("关闭无效连接失败", e);
}
}
master = createNewMaster(ip, port);
connectionPool.put(key, master);
log.info("创建新的 ModbusTCP 连接:{}:{}", ip, port);
}
} finally {
lock.unlock();
}
}
return master;
}
private ModbusMaster createNewMaster(String ip, int port) throws ModbusInitException {
IpParameters params = new IpParameters();
params.setHost(ip);
params.setPort(port);
// 关键参数:encapsulated=false 表示标准 Modbus TCP,不是 RTU over TCP
params.setEncapsulated(false);
ModbusMaster master = modbusFactory.createTcpMaster(params, false);
master.setTimeout(defaultTimeout);
master.setRetries(defaultRetries);
master.init();
// 验证连接是否真正建立
try {
// 发送一个无害的测试请求,比如读取 0 地址的 1 个线圈(如果设备支持)
// 这里简化处理,实际可根据设备情况调整
log.debug("连接初始化成功:{}:{}", ip, port);
} catch (Exception e) {
master.destroy();
throw new ModbusInitException("连接测试失败:" + e.getMessage(), e);
}
return master;
}
private boolean isConnectionValid(ModbusMaster master) {
try {
// modbus4j 没有直接的 isConnected 方法,通过尝试执行一个轻量级操作判断
} catch (Exception e) {
return false;
}
return true;
}
}
