Apache PLC4X 1.0.0 构建并运行示例应用指南
Apache PLC4X 1.0.0 构建并运行示例应用指南:从零创建 Maven 工程、完整 pom.xml 与 HelloPlc4x 源码、编译运行命令与真实回显(读 RANDOM 标签、写读 STATE 标签、STDOUT 写入日志、200ms 周期订阅与取消订阅),含 JDK 21 要求与堆内存验证。
Apache PLC4X 1.0.0 构建并运行示例应用指南
目标:从零构建并运行一个基于 Apache PLC4X 1.0.0 的最小示例应用 HelloPlc4x,使用内置模拟 PLC 驱动(无需真实硬件)完成驱动发现、连接、读取 RANDOM 标签、写入并回读 STATE 标签、STDOUT 写入日志、周期订阅与取消订阅全流程。所有命令与回显均在 Ubuntu 22.04.5 + JDK 21 环境实测通过。
一、环境信息
- 操作系统:Ubuntu 22.04.5 LTS
- Java:OpenJDK 21.0.12.1(plc4j 驱动构件 class 版本 65.0,必须 JDK 21)
- 构建工具:Apache Maven 3.6.3
- PLC4X 版本:1.0.0(依赖 plc4j-api 与 plc4j-driver-simulated)
- 日志实现:slf4j-simple 2.0.18(runtime)
- 示例工程目录:
/root/plc4x-demo/ - 演示账号:root(本示例使用模拟驱动,不涉及设备侧登录认证)
二、前置条件
-
已按《Ubuntu 22.04 安装 Apache PLC4X 1.0.0 指南》完成 JDK 21 与 Maven 安装,并确认版本:
java -version实测回显:
openjdk version "21.0.12.1" 2026-08-18 OpenJDK Runtime Environment (build 21.0.12.1+1-1-22.04.4-Ubuntu) -
已配置
JAVA_HOME与 Maven 镜像(如需加速):export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 export PATH=$JAVA_HOME/bin:$PATH
三、工程结构
标准 Maven 布局,一个 pom 与四个类(HelloPlc4x 为主示例,其余为行为探针,可按需保留):
plc4x-demo/
├── pom.xml
└── src/main/java/demo/
├── HelloPlc4x.java # 主示例:读/写/订阅全流程
├── CacheDemo.java # 连接缓存工具示例(见《常用工具与 API 详解》篇)
├── OpmDemo.java # OPM 实体映射示例(见《常用工具与 API 详解》篇)
├── ErrorProbe.java # 错误行为探针
└── AuthProbe.java # 认证行为探针
创建目录:
mkdir -p /root/plc4x-demo/src/main/java/demo
四、完整 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>demo</groupId>
<artifactId>plc4x-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4j-api</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4j-driver-simulated</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4j-tools-connection-cache</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4j-tools-opm</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4j-driver-simulated</artifactId>
</dependency>
<dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4j-tools-connection-cache</artifactId>
</dependency>
<dependency>
<groupId>org.apache.plc4x</groupId>
<artifactId>plc4j-tools-opm</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.18</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
💡 说明:本文示例只用
plc4j-api与plc4j-driver-simulated即可运行;connection-cache 与 opm 两个构件供《常用工具与 API 详解》篇示例复用,一起锁定在依赖管理中。
五、示例源码
/root/plc4x-demo/src/main/java/demo/HelloPlc4x.java:
package demo;
import org.apache.plc4x.java.DefaultPlcDriverManager;
import org.apache.plc4x.java.api.PlcConnection;
import org.apache.plc4x.java.api.messages.PlcReadRequest;
import org.apache.plc4x.java.api.messages.PlcReadResponse;
import org.apache.plc4x.java.api.messages.PlcSubscriptionEvent;
import org.apache.plc4x.java.api.messages.PlcSubscriptionResponse;
import org.apache.plc4x.java.api.messages.PlcWriteResponse;
import java.time.Duration;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class HelloPlc4x {
public static void main(String[] args) throws Exception {
// 1. 创建驱动管理器:通过 ServiceLoader 自动发现 classpath 上的驱动
DefaultPlcDriverManager driverManager = new DefaultPlcDriverManager();
System.out.println("[1] Registered protocol codes: " + driverManager.getProtocolCodes());
// 2. 连接 simulated 设备(getConnection 内部自动完成 connect())
try (PlcConnection connection = driverManager.getConnection("simulated:device1")) {
System.out.println("[2] isConnected = " + connection.isConnected());
System.out.println("[2] protocol = " + connection.getProtocolCode() + " / " + connection.getProtocolName());
// 3. 连接探活
System.out.println("[3] ping response = " + connection.ping().get(5, TimeUnit.SECONDS).getResponseCode());
// 4. 读取 RANDOM 类型标签:每次读取返回新的随机值
for (int round = 1; round <= 2; round++) {
PlcReadRequest readRequest = connection.readRequestBuilder()
.addTagAddress("foo", "RANDOM/foo:INT")
.addTagAddress("bar", "RANDOM/bar:REAL")
.build();
PlcReadResponse readResponse = readRequest.execute().get(5, TimeUnit.SECONDS);
System.out.println("[4] round " + round + " tagNames=" + readResponse.getTagNames()
+ " foo(INT)=" + readResponse.getInteger("foo")
+ " bar(REAL)=" + readResponse.getFloat("bar"));
}
// 5. 写入 STATE 类型标签:值保存在客户端进程内存中
PlcWriteResponse writeResponse = connection.writeRequestBuilder()
.addTagAddress("counter", "STATE/counter:DINT", 4242)
.addTagAddress("text", "STATE/text:STRING", "hello plc4x")
.build().execute().get(5, TimeUnit.SECONDS);
System.out.println("[5] write counter code = " + writeResponse.getResponseCode("counter"));
System.out.println("[5] write text code = " + writeResponse.getResponseCode("text"));
// 6. 回读 STATE 类型标签,验证写入生效
PlcReadResponse backResponse = connection.readRequestBuilder()
.addTagAddress("counter", "STATE/counter:DINT")
.addTagAddress("text", "STATE/text:STRING")
.build().execute().get(5, TimeUnit.SECONDS);
System.out.println("[6] read back counter(DINT) = " + backResponse.getLong("counter"));
System.out.println("[6] read back text(STRING) = " + backResponse.getString("text"));
// 7. 写入 STDOUT 类型标签:设备把写入值打印到日志
connection.writeRequestBuilder()
.addTagAddress("log", "STDOUT/log:STRING", "written by HelloPlc4x")
.build().execute().get(5, TimeUnit.SECONDS);
// 8. 周期订阅 STATE 类型标签:每 200ms 推送一次当前值
LinkedBlockingQueue<PlcSubscriptionEvent> events = new LinkedBlockingQueue<>();
PlcSubscriptionResponse subResponse = connection.subscriptionRequestBuilder()
.addCyclicTagAddress("tick", "STATE/counter:DINT", Duration.ofMillis(200),
events::offer)
.build().execute().get(5, TimeUnit.SECONDS);
System.out.println("[8] subscription code = " + subResponse.getResponseCode("tick"));
for (int i = 0; i < 3; i++) {
PlcSubscriptionEvent event = events.poll(3, TimeUnit.SECONDS);
System.out.println("[8] event " + (i + 1) + " timestamp=" + event.getTimestamp()
+ " counter(DINT)=" + event.getLong("tick"));
}
// 9. 修改订阅标签的值:下一个周期事件携带新值
connection.writeRequestBuilder()
.addTagAddress("counter", "STATE/counter:DINT", 1024)
.build().execute().get(5, TimeUnit.SECONDS);
PlcSubscriptionEvent changed = events.poll(3, TimeUnit.SECONDS);
System.out.println("[9] event after write counter(DINT)=" + changed.getLong("tick"));
// 10. 取消订阅
connection.unsubscriptionRequestBuilder()
.addHandles(subResponse.getSubscriptionHandle("tick"))
.build().execute().get(5, TimeUnit.SECONDS);
System.out.println("[10] unsubscribed");
}
System.out.println("[done] connection closed");
}
}
六、编译与运行
6.1 编译
export MAVEN_OPTS="-Xms256m -Xmx512m"
cd /root/plc4x-demo
mvn clean compile
实测回显(关键行):
[INFO] BUILD SUCCESS
6.2 导出运行 classpath
mvn dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt
实测结果:命令成功,/tmp/cp.txt 保存了完整依赖 classpath(含 26 个 jar 路径)。
6.3 运行(限制 JVM 堆 256M 至 512M)
java -Xms256m -Xmx512m -cp "target/classes:$(cat /tmp/cp.txt)" demo.HelloPlc4x 2>&1 | grep -vE "available"
实测完整回显(剔除两条 available 注册明细日志行,其余逐行照录):
[main] INFO org.apache.plc4x.java.DefaultPlcDriverManager - Instantiating new PLC Driver Manager with class loader jdk.internal.loader.ClassLoaders$AppClassLoader@639fee48
[main] INFO org.apache.plc4x.java.spi.transports.api.DefaultTransportManager - Instantiating new Transport Manager with class loader jdk.internal.loader.ClassLoaders$AppClassLoader@639fee48
[main] INFO org.apache.plc4x.java.DefaultPlcDriverManager - Registering driver for Protocol simulated (Simulated PLC4X Datasource)
[1] Registered protocol codes: [simulated]
[2] isConnected = true
[2] protocol = unknown / unknown
[3] ping response = OK
[4] round 1 tagNames=[foo, bar] foo(INT)=2863 bar(REAL)=6.473093E10
[4] round 2 tagNames=[foo, bar] foo(INT)=-9864 bar(REAL)=-6.5111705E-12
[5] write counter code = OK
[5] write text code = OK
[6] read back counter(DINT) = 4242
[6] read back text(STRING) = hello plc4x
[main] INFO org.apache.plc4x.java.simulated.connection.SimulatedDevice - TEST PLC STDOUT [log]: written by HelloPlc4x
[8] subscription code = OK
[8] event 1 timestamp=2026-10-11T17:02:28.700746422Z counter(DINT)=4242
[8] event 2 timestamp=2026-10-11T17:02:28.895257487Z counter(DINT)=4242
[8] event 3 timestamp=2026-10-11T17:02:29.094221375Z counter(DINT)=4242
[9] event after write counter(DINT)=1024
[10] unsubscribed
[done] connection closed
退出码 0(RANDOM 标签的数值每次运行不同,属预期行为;被过滤的两行 INFO 为驱动管理器与传输管理器的 available 注册明细,行尾带省略号,完整文本见证据文件)。
6.4 确认堆参数生效
运行期间用 jps -v 复核:
6321 demo.HelloPlc4x -Xms256m -Xmx512m
或用 PrintFlagsFinal 验证参数被 JVM 接受:
java -Xms256m -Xmx512m -XX:+PrintFlagsFinal -version 2>&1 | grep -E "size_t InitialHeapSize|size_t MaxHeapSize"
实测回显:
size_t InitialHeapSize = 268435456 {product} {command line}
size_t MaxHeapSize = 536870912 {product} {command line}
七、运行结果解读
| 输出 | 解读 |
|---|---|
[1] Registered protocol codes: [simulated] | ServiceLoader 只发现 classpath 上的 simulated 驱动;换真实设备需替换/追加对应协议驱动 jar |
[2] isConnected = true | 1.0.0 中 getConnection 内部自动调用 connect(),拿到的连接已处于连接态 |
[2] protocol = unknown / unknown | simulated 驱动未覆写协议名取值接口,属库默认行为,不影响功能 |
[3] ping response = OK | 连接探活正常(1.0.0 把 ping 提升为 PlcConnection 一级能力) |
[4] round 1 与 round 2 数值不同 | RANDOM 标签每次读取都由设备生成新随机值 |
[5]、[6] 写入 OK 并回读 4242、hello plc4x | STATE 标签值保存在客户端进程内存,写读一致 |
TEST PLC STDOUT [log]: written by HelloPlc4x | STDOUT 标签写入被驱动以 INFO 日志回显 |
[8] 三个事件间隔约 200ms | 周期订阅按 Duration 生效;事件值取自写入的 4242 |
[9] 事件值变为 1024 | 写入后下一周期推送新值 |
[10] unsubscribed | 按 subscriptionHandle 完成退订 |
八、注意事项
⚠️ 注意:
- 周期订阅(addCyclicTagAddress)对 RANDOM 标签不产生事件:RANDOM 标签不落状态表,周期推送取值为空被跳过(实测现象,与源码逻辑一致)。周期订阅请使用 STATE 标签,CHANGE_OF_STATE 订阅同理只对 STATE 标签有意义。
- simulated 每次连接都是一台全新设备:STATE 值不跨连接共享,连接关闭即丢失;验证写入生效必须在同一连接内完成。
- JVM 堆按 256M 至 512M 起步(本文用
-Xms256m -Xmx512m),连接数与标签规模增大后再按观测调大。
九、常见问题
Q1:启动即抛 UnsupportedClassVersionError,提示 class file version 65.0?
JVM 为 JDK 17,驱动构件需要 JDK 21。安装并切换 JDK 21(见《Ubuntu 22.04 安装 Apache PLC4X 1.0.0 指南》)。
Q2:标签地址写错会怎样?
build() 阶段只打 WARN 日志不抛异常(实测:WARN DefaultPlcReadRequest - Invalid address 'FOO/foo:INT' for tag 'x': No enum constant org.apache.plc4x.java.simulated.types.SimulatedTagType.FOO),请求仍会构建;到 execute() 返回的响应码为 INVALID_ADDRESS。排查思路:先看 WARN 日志中的地址回显,再核对「类型段/名称段/值类型段」三段语法。
Q3:getConnection("s7:192.168.0.1") 报 Unable to find driver for protocol 's7'?
classpath 上没有 s7 驱动 jar。本示例仅引了 simulated 驱动,接真实设备需按协议追加对应 plc4j-driver-* 依赖。
Q4:想静默 plc4j 的 INFO 日志?
加 JVM 参数 -Dorg.slf4j.simpleLogger.defaultLogLevel=off(实测 INFO 全部消失、程序自身输出保留),或换成 logback 等实现用配置文件精细控制。
十、参考链接
- Apache PLC4X 官网:
https://plc4x.apache.org/ - plc4j javadoc(1.0.0):
https://plc4x.apache.org/plc4x/latest/users/plc4j/ - Maven Central 构件目录:
https://repo1.maven.org/maven2/org/apache/plc4x/