Apache PLC4X 1.0.0 构建并运行示例应用指南

Apache PLC4X 1.0.0 构建并运行示例应用指南:从零创建 Maven 工程、完整 pom.xml 与 HelloPlc4x 源码、编译运行命令与真实回显(读 RANDOM 标签、写读 STATE 标签、STDOUT 写入日志、200ms 周期订阅与取消订阅),含 JDK 21 要求与堆内存验证。

Apache PLC4X2026/10/12

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 = true1.0.0 中 getConnection 内部自动调用 connect(),拿到的连接已处于连接态
[2] protocol = unknown / unknownsimulated 驱动未覆写协议名取值接口,属库默认行为,不影响功能
[3] ping response = OK连接探活正常(1.0.0 把 ping 提升为 PlcConnection 一级能力)
[4] round 1 与 round 2 数值不同RANDOM 标签每次读取都由设备生成新随机值
[5]、[6] 写入 OK 并回读 4242、hello plc4xSTATE 标签值保存在客户端进程内存,写读一致
TEST PLC STDOUT [log]: written by HelloPlc4xSTDOUT 标签写入被驱动以 INFO 日志回显
[8] 三个事件间隔约 200ms周期订阅按 Duration 生效;事件值取自写入的 4242
[9] 事件值变为 1024写入后下一周期推送新值
[10] unsubscribed按 subscriptionHandle 完成退订

八、注意事项

⚠️ 注意:

  1. 周期订阅(addCyclicTagAddress)对 RANDOM 标签不产生事件:RANDOM 标签不落状态表,周期推送取值为空被跳过(实测现象,与源码逻辑一致)。周期订阅请使用 STATE 标签,CHANGE_OF_STATE 订阅同理只对 STATE 标签有意义。
  2. simulated 每次连接都是一台全新设备:STATE 值不跨连接共享,连接关闭即丢失;验证写入生效必须在同一连接内完成。
  3. 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/