告别RXTX和DLL!用JSSC+Modbus4j实现跨平台Java串口通信(附完整代码)

张开发
2026/4/16 1:49:20 15 分钟阅读

分享文章

告别RXTX和DLL!用JSSC+Modbus4j实现跨平台Java串口通信(附完整代码)
跨平台Java串口通信实战JSSCModbus4j替代RXTX方案如果你曾经在Java项目中尝试过串口通信大概率遇到过RXTX这个老朋友。它确实能解决问题但随之而来的DLL依赖、跨平台兼容性差、配置复杂等问题往往让开发者头疼不已。今天我要分享的JSSCModbus4j组合方案就像找到了串口通信的瑞士军刀——纯Java实现、零本地依赖、跨平台开箱即用。1. 为什么需要放弃RXTX传统RXTX方案最让人崩溃的瞬间在Windows开发机上调试好好的程序部署到Linux服务器上突然报错发现是缺少对应的.so文件或者明明代码一样同事的Mac就是跑不起来。这些问题根源在于RXTX的本地库依赖Windows需要rxtxSerial.dllLinux需要librxtxSerial.soMacOS需要librxtxSerial.jnilib更麻烦的是这些本地库还需要放置到JRE的特定目录如jre/bin或通过-Djava.library.path指定路径。对比之下JSSC的优势立现特性RXTXJSSC实现方式JNI调用本地库纯Java实现跨平台支持需要不同平台的本地库自动适配无需额外配置依赖管理手动配置复杂Maven直接引入资源占用需要加载本地库无额外开销维护状态已停止更新持续维护2. 环境搭建与依赖配置首先在pom.xml中添加必要的依赖。注意Modbus4j需要配置专门的仓库repositories repository idias-snapshots/id urlhttps://maven.mangoautomation.net/repository/ias-snapshot//url snapshots enabledtrue/enabled /snapshots /repository repository idias-releases/id urlhttps://maven.mangoautomation.net/repository/ias-release//url releases enabledtrue/enabled /releases /repository /repositories dependencies !-- JSSC串口通信库 -- dependency groupIdio.github.java-native/groupId artifactIdjssc/artifactId version2.9.4/version /dependency !-- Modbus协议实现 -- dependency groupIdcom.infiniteautomation/groupId artifactIdmodbus4j/artifactId version3.0.4/version /dependency !-- 可选用于简化日志 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version1.18.24/version scopeprovided/scope /dependency /dependencies提示如果遇到仓库访问问题可以考虑将依赖包下载到本地仓库或者搭建内部镜像仓库。3. 核心实现串口通信适配层Modbus4j需要通过SerialPortWrapper接口与串口交互我们需要用JSSC实现这个适配层。以下是关键代码实现3.1 自定义串口包装器import com.serotonin.modbus4j.serial.SerialPortWrapper; import jssc.SerialPort; import jssc.SerialPortException; import lombok.extern.slf4j.Slf4j; import java.io.InputStream; import java.io.OutputStream; Slf4j public class JsscSerialPortWrapper implements SerialPortWrapper { private final SerialPort port; private final int baudRate; private final int dataBits; private final int stopBits; private final int parity; private final int flowControlIn; private final int flowControlOut; public JsscSerialPortWrapper(String portName, int baudRate, int dataBits, int stopBits, int parity, int flowControl) { this.port new SerialPort(portName); this.baudRate baudRate; this.dataBits dataBits; this.stopBits stopBits; this.parity parity; this.flowControlIn flowControl; this.flowControlOut flowControl; } Override public void open() throws Exception { if (!port.openPort()) { throw new SerialPortException(port.getPortName(), openPort, Failed to open port); } port.setParams(baudRate, dataBits, stopBits, parity); port.setFlowControlMode(flowControlIn | flowControlOut); log.info(Port {} opened successfully, port.getPortName()); } Override public void close() throws Exception { if (port.isOpened()) { if (!port.closePort()) { log.warn(Port {} close failed, port.getPortName()); } } } Override public InputStream getInputStream() { return new JsscInputStream(port); } Override public OutputStream getOutputStream() { return new JsscOutputStream(port); } // 其他getter方法... }3.2 输入输出流实现// 输入流实现 public class JsscInputStream extends InputStream { private final SerialPort port; private int timeout 1000; // 默认超时1秒 public JsscInputStream(SerialPort port) { this.port port; } public void setTimeout(int timeout) { this.timeout timeout; } Override public int read() throws IOException { try { byte[] bytes port.readBytes(1, timeout); return bytes[0] 0xFF; } catch (SerialPortException e) { throw new IOException(Serial port read error, e); } } Override public int available() throws IOException { try { return port.getInputBufferBytesCount(); } catch (SerialPortException e) { throw new IOException(Failed to check available bytes, e); } } } // 输出流实现 public class JsscOutputStream extends OutputStream { private final SerialPort port; public JsscOutputStream(SerialPort port) { this.port port; } Override public void write(int b) throws IOException { try { port.writeByte((byte) b); } catch (SerialPortException e) { throw new IOException(Serial port write error, e); } } Override public void write(byte[] b, int off, int len) throws IOException { try { if (off 0 len b.length) { port.writeBytes(b); } else { byte[] segment new byte[len]; System.arraycopy(b, off, segment, 0, len); port.writeBytes(segment); } } catch (SerialPortException e) { throw new IOException(Serial port write error, e); } } }4. Modbus RTU通信实战有了上面的基础组件现在可以构建完整的Modbus RTU通信示例import com.serotonin.modbus4j.ModbusFactory; import com.serotonin.modbus4j.ModbusMaster; import com.serotonin.modbus4j.code.DataType; import com.serotonin.modbus4j.exception.ModbusInitException; import com.serotonin.modbus4j.locator.BaseLocator; public class ModbusRtuDemo { public static void main(String[] args) { // 1. 创建串口包装器 JsscSerialPortWrapper wrapper new JsscSerialPortWrapper( /dev/ttyUSB0, // Linux串口设备 9600, // 波特率 8, // 数据位 1, // 停止位 0, // 无校验 0 // 无流控 ); // 2. 创建Modbus Master ModbusFactory factory new ModbusFactory(); ModbusMaster master factory.createRtuMaster(wrapper); try { // 3. 初始化连接 master.init(); // 4. 读取保持寄存器示例 int slaveId 1; int register 0; BaseLocatorNumber locator BaseLocator.holdingRegister( slaveId, register, DataType.TWO_BYTE_INT_SIGNED); Number value master.getValue(locator); System.out.println(Register value: value); // 5. 写入线圈示例 int coilAddress 10; boolean coilValue true; master.setValue(slaveId, coilAddress, coilValue); } catch (ModbusInitException e) { System.err.println(Modbus初始化失败: e.getMessage()); } finally { master.destroy(); } } }注意实际使用时需要根据设备调整串口参数和Modbus从站地址。在Windows上串口名称通常是COM3这样的格式Linux下则是/dev/ttyS0或/dev/ttyUSB0。5. 高级技巧与故障排查5.1 多平台串口设备发现JSSC提供了跨平台的串口列表获取方法import jssc.SerialPortList; public class PortLister { public static void main(String[] args) { String[] portNames SerialPortList.getPortNames(); System.out.println(Available serial ports:); for (String name : portNames) { System.out.println(- name); } } }5.2 常见问题解决方案端口占用问题确保没有其他程序正在使用该串口权限问题Linux/Mac可能需要将用户加入dialout组sudo usermod -a -G dialout $USER超时设置根据网络质量调整超时时间master.setTimeout(2000); // 设置2秒超时 master.setRetries(1); // 设置重试次数5.3 性能优化建议对于高频数据采集考虑使用事件监听模式而非轮询合理设置串口缓冲区大小对关键操作添加重试机制这套方案在实际工业项目中已经稳定运行超过两年从Windows工控机到Linux边缘计算网关再到Mac开发环境真正实现了一次编写到处运行。最让我惊喜的是部署时再也不用带着一堆DLL/so文件到处跑了——这感觉就像卸下了沉重的包袱。

更多文章