告别RXTX和DLL!用JSSC+Modbus4j实现跨平台Java串口通信(附完整代码)
跨平台Java串口通信实战:JSSC+Modbus4j替代RXTX方案
如果你曾经在Java项目中尝试过串口通信,大概率遇到过RXTX这个"老朋友"。它确实能解决问题,但随之而来的DLL依赖、跨平台兼容性差、配置复杂等问题,往往让开发者头疼不已。今天我要分享的JSSC+Modbus4j组合方案,就像找到了串口通信的"瑞士军刀"——纯Java实现、零本地依赖、跨平台开箱即用。
1. 为什么需要放弃RXTX?
传统RXTX方案最让人崩溃的瞬间:在Windows开发机上调试好好的程序,部署到Linux服务器上突然报错,发现是缺少对应的.so文件;或者明明代码一样,同事的Mac就是跑不起来。这些问题根源在于RXTX的本地库依赖:
- Windows:需要
rxtxSerial.dll - Linux:需要
librxtxSerial.so - MacOS:需要
librxtxSerial.jnilib
更麻烦的是,这些本地库还需要放置到JRE的特定目录(如jre/bin)或通过-Djava.library.path指定路径。对比之下,JSSC的优势立现:
| 特性 | RXTX | JSSC |
|---|---|---|
| 实现方式 | JNI调用本地库 | 纯Java实现 |
| 跨平台支持 | 需要不同平台的本地库 | 自动适配,无需额外配置 |
| 依赖管理 | 手动配置复杂 | Maven直接引入 |
| 资源占用 | 需要加载本地库 | 无额外开销 |
| 维护状态 | 已停止更新 | 持续维护 |
2. 环境搭建与依赖配置
首先在pom.xml中添加必要的依赖。注意Modbus4j需要配置专门的仓库:
<repositories> <repository> <id>ias-snapshots</id> <url>https://maven.mangoautomation.net/repository/ias-snapshot/</url> <snapshots> <enabled>true</enabled> </snapshots> </repository> <repository> <id>ias-releases</id> <url>https://maven.mangoautomation.net/repository/ias-release/</url> <releases> <enabled>true</enabled> </releases> </repository> </repositories> <dependencies> <!-- JSSC串口通信库 --> <dependency> <groupId>io.github.java-native</groupId> <artifactId>jssc</artifactId> <version>2.9.4</version> </dependency> <!-- Modbus协议实现 --> <dependency> <groupId>com.infiniteautomation</groupId> <artifactId>modbus4j</artifactId> <version>3.0.4</version> </dependency> <!-- 可选,用于简化日志 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.24</version> <scope>provided</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; BaseLocator<Number> 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文件到处跑了——这感觉,就像卸下了沉重的包袱。
