Apache Commons SCXML 0.9 快速入门与集成示例

通过 10 分钟最小可用工程、五个高频集成坑位、条件转移与上下文变量、延时事件与监听器、superStep 宏步控制等实战示例,给出把 Apache Commons SCXML 0.9 集成进 Java 工程的完整实操路径,全部代码与回显实测通过。

Apache Commons SCXMLApache Commons SCXML2026/10/15

Apache Commons SCXML 0.9 快速入门与集成示例

目标:以最小代价把 commons-scxml 0.9 集成进一个 Java 工程:10 分钟跑通「模型解析 → 状态机驱动」的最小可用程序,掌握 0.9 特有的五个集成坑位,再通过条件转移、上下文变量、延时事件、监听器、宏步控制五个实战示例建立完整认知;本文全部代码与命令回显均已在 Ubuntu 22.04 + JDK 17 + Maven 3.9 实测通过。

Apache Commons SCXML 是 Apache Commons 的状态机引擎,实现了 W3C SCXML 标准:状态机写成 .scxml 标准模型文件,Java 侧用 SCXMLExecutor 加载驱动。模型与代码分离带来的直接收益是——状态流转逻辑改动不需要重新设计类结构,甚至可以用官方 REPL 工具(6.2 节)在不动一行 Java 代码的情况下调试模型。本文是本专栏的主线教程:安装与环境细节见第 1、2 篇,API 全景见第 4 篇,状态机设计方法论见第 5 篇。

一、环境信息

  • 操作系统:Ubuntu 22.04 LTS
  • 架构:x86_64
  • JDK:Temurin 17.0.20.1(本文示例在 OpenJDK 1.8.0_312 上同样实测通过,0.9 字节码为 Java 1.4 版本,两代 JVM 均兼容)
  • 构建工具:Apache Maven 3.9.16
  • 组件版本:commons-scxml 0.9(2008-11-22 发布,0.x 系列最终版本)
  • Maven 坐标:commons-scxml:commons-scxml:0.9(org.apache.commons:commons-scxml 在中央仓库不存在)
  • 传递依赖:commons-logging 1.1.1、commons-digester 1.8、commons-beanutils 1.7.0(自动);commons-jexl 1.1、xalan 2.7.2(必须显式声明,见 3.1 节)
  • 工程目录:/opt/scxml-demo/scxml-demo

环境版本确认(实时回显):

java -version
openjdk version "17.0.20.1" 2026-08-18
OpenJDK Runtime Environment Temurin-17.0.20.1+1 (build 17.0.20.1+1)
OpenJDK 64-Bit Server VM Temurin-17.0.20.1+1 (build 17.0.20.1+1, mixed mode, sharing)
mvn -version | head -3
Apache Maven 3.9.16 (2bdd9fddda4b155ebf8000e807eb73fd829a51d5)
Maven home: /usr/share/maven
Java version: 17.0.20.1, vendor: Eclipse Adoptium, runtime: /opt/java/openjdk

💡 技术拓展:为什么 2008 年的库能跑在 17 上?0.9 的字节码版本是 major 48(Java 1.4),JVM 向下兼容旧字节码;它在 JDK 17 上真正的风险点不在自身,而在依赖行为——实测依赖链上的 digester 1.8、beanutils 1.7.0 运行均正常,唯一的硬性要求是表达式引擎必须选 JEXL 路线并使用配套的 JexlContext(见 3.2 节)。

二、10 分钟最小可用

2.1 引入依赖

工程结构:

mkdir -p /opt/scxml-demo/scxml-demo/src/main/java/demo /opt/scxml-demo/scxml-demo/models

pom.xml 依赖部分(完整工程可直接使用):

<dependencies>
  <dependency>
    <groupId>commons-scxml</groupId>
    <artifactId>commons-scxml</artifactId>
    <version>0.9</version>
  </dependency>
  <dependency>
    <groupId>commons-jexl</groupId>
    <artifactId>commons-jexl</artifactId>
    <version>1.1</version>
  </dependency>
  <dependency>
    <groupId>xalan</groupId>
    <artifactId>xalan</artifactId>
    <version>2.7.2</version>
  </dependency>
</dependencies>

commons-jexl 与 xalan 是 0.9 optional 依赖,缺一不可,原因与真实报错见 3.1 节。

2.2 第一份状态机模型

红绿灯模型(文件路径 /opt/scxml-demo/scxml-demo/models/traffic-light.scxml):三个状态靠 timer 事件循环。

<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="red">

  <state id="red">
    <transition event="timer" target="green"/>
  </state>

  <state id="green">
    <transition event="timer" target="yellow"/>
  </state>

  <state id="yellow">
    <transition event="timer" target="red"/>
  </state>

</scxml>

2.3 加载与驱动

最小驱动代码 TrafficLightDemo.java(文件路径 /opt/scxml-demo/scxml-demo/src/main/java/demo/TrafficLightDemo.java):

package demo;

import java.net.URL;
import java.util.Set;
import org.apache.commons.scxml.SCXMLExecutor;
import org.apache.commons.scxml.TriggerEvent;
import org.apache.commons.scxml.env.SimpleErrorHandler;
import org.apache.commons.scxml.env.SimpleErrorReporter;
import org.apache.commons.scxml.env.jexl.JexlEvaluator;
import org.apache.commons.scxml.io.SCXMLParser;
import org.apache.commons.scxml.model.SCXML;
import org.apache.commons.scxml.model.TransitionTarget;

public class TrafficLightDemo {
    public static void main(String[] args) throws Exception {
        URL model = new java.io.File(args[0]).toURI().toURL();
        SCXML scxml = SCXMLParser.parse(model, new SimpleErrorHandler());
        System.out.println("模型解析成功, 根状态个数=" + scxml.getChildren().size());

        SCXMLExecutor exec = new SCXMLExecutor(new JexlEvaluator(), null, new SimpleErrorReporter());
        exec.setStateMachine(scxml);
        exec.go();
        printStates("初始配置", exec);

        exec.triggerEvent(new TriggerEvent("timer", TriggerEvent.SIGNAL_EVENT));
        printStates("发出第 1 个 timer 后", exec);

        exec.triggerEvent(new TriggerEvent("timer", TriggerEvent.SIGNAL_EVENT));
        printStates("发出第 2 个 timer 后", exec);

        exec.triggerEvent(new TriggerEvent("timer", TriggerEvent.SIGNAL_EVENT));
        printStates("发出第 3 个 timer 后", exec);
    }

    static void printStates(String tag, SCXMLExecutor exec) {
        Set<TransitionTarget> states = exec.getCurrentStatus().getStates();
        for (TransitionTarget t : states) {
            System.out.println(tag + " -> " + t.getId());
        }
    }
}

驱动模型只有固定五步:SCXMLParser.parse() 解析模型 → new SCXMLExecutor(evaluator, dispatcher, reporter) 组装执行器 → setStateMachine() 挂载 → go() 进入初始态 → triggerEvent() 派发事件。

2.4 编译运行

cd /opt/scxml-demo/scxml-demo && mvn -B clean compile 2>&1 | tail -3
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
mvn -q -B compile exec:java -Dexec.mainClass=demo.TrafficLightDemo -Dexec.args='/opt/scxml-demo/scxml-demo/models/traffic-light.scxml'
模型解析成功, 根状态个数=3
初始配置 -> red
发出第 1 个 timer 后 -> green
发出第 2 个 timer 后 -> yellow
发出第 3 个 timer 后 -> red

验证点:初始配置与模型 initial="red" 一致;三次 timer 事件后状态沿 red → green → yellow → red 循环一周。达成即集成完成,2.3 节的五步驱动模板可以复用到任何模型。

三、五个必须知道的集成要点

0.9 是 2008 年的交付形态,五个坑位全部踩实过,逐个给出真实报错与解法。

3.1 optional 依赖:xalan 与 commons-jexl 必须显式声明

0.9 的 POM 把解析器需要的 xalan 和表达式引擎需要的 commons-jexl 都声明为 optional,Maven 不传递 optional 依赖。缺 xalan 时解析任何模型都会失败,真实报错(实测):

[ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:3.6.4:java (default-cli) on project scxml-demo: An exception occurred while executing the Java class. org/apache/xml/utils/PrefixResolver: org.apache.xml.utils.PrefixResolver -> [Help 1]

缺 commons-jexl 时模型能解析,但 JexlEvaluator 初始化即抛类缺失。解法:两个依赖都写进 pom(2.1 节)。依赖树确认(输出节选,实测):

[INFO] +- commons-scxml:commons-scxml:jar:0.9:compile
[INFO] |  +- commons-logging:commons-logging:jar:1.1.1:compile
[INFO] |  +- commons-digester:commons-digester:1.8:compile
[INFO] |  \- commons-beanutils:commons-beanutils:jar:1.7.0:compile
[INFO] +- commons-jexl:commons-jexl:jar:1.1:compile
[INFO] \- xalan:xalan:jar:2.7.2:compile

3.2 表达式引擎与根上下文必须配套

选了 JexlEvaluator,Java 侧若要自定义根上下文,必须用配套的 org.apache.commons.scxml.env.jexl.JexlContext。实测用 SimpleContext 的报错(运行期):

class org.apache.commons.scxml.env.SimpleContext cannot be cast to class org.apache.commons.scxml.env.jexl.JexlContext (org.apache.commons.scxml.env.SimpleContext is in unnamed module of loader 'app'; org.apache.commons.scxml.env.jexl.JexlContext is in unnamed module of loader 'app')

解法二选一:不需要预置变量就不设根上下文(执行器会经 evaluator.newContext(null) 创建配套上下文);需要预置变量就 Context rootCtx = new JexlContext(); rootCtx.set("key", value); exec.setRootContext(rootCtx);。

3.3 <send> 的事件名与延时值都是表达式

0.9 的语义实现把 <send> 的 event 与 delay 属性按表达式求值。按字面直觉写 event="wake" 会被 JEXL 当成未定义变量求值为 null(事件静默丢失),写 delay="1s" 则直接表达式解析失败,实测报错:

[ERROR] commons.scxml - org.apache.commons.scxml.SCXMLExpressionException: org.apache.commons.jexl.parser.ParseException: Lexical error at line 1, column 2.  Encountered: "s" (115), after : "1"
Was expecting one of: <EOF> ... <OR> ... <AND> ... <EQ> ... <NE> ... <LT> ... <LTE> ... <GT> ... <GTE> ...

正确写法是用字符串字面量表达式(实测通过):

<send event="'wake'" delay="'1s'"/>

无延时的内部事件 <send event="'evt_b'"/> 同理。

3.4 <var> 动作属于 Commons 自定义命名空间

0.9 支持几个规范外的扩展动作,<var> 在其中,但必须绑定 http://commons.apache.org/scxml 命名空间,否则解析时被当作未知元素静默忽略(实测只在日志里留下痕迹):

Oct 15, 2026 2:05:41 AM org.apache.commons.scxml.io.SCXMLParser$IgnoredElementRule begin
INFO: Ignoring <var> element, line: 8, column: 21

解法:根元素声明 xmlns:cs 前缀后,扩展动作以 <cs:var> 形式书写(完整模型见第 5 篇 5.1 节秒表示例):

<scxml xmlns="http://www.w3.org/2005/07/scxml"
       xmlns:cs="http://commons.apache.org/scxml"
       version="1.0" initial="start">
  <state id="start">
    <onentry>
      <cs:var name="noop" expr="watch.reset()"/>
    </onentry>
  </state>
</scxml>

注意 <var> 写入的是状态局部上下文(setLocal 语义),Java 侧要跨状态共享的变量应改用根级 <datamodel> + <assign>(见 4.1 节)。

3.5 编码与 locale

最小化 Linux 环境(LANG=POSIX)下中文输出会变成问号(实测现象):

?????,??????=3

解法:所有 java 直跑命令带 -Dfile.encoding=UTF-8,或在会话 export LANG=C.UTF-8。经 mvn exec:java 运行时该参数须经 MAVEN_OPTS 传入,所以本专栏统一使用 java -cp 直跑方式出结果。

四、上下文与条件转移实战

状态机的实用价值在于「带数据流转」。本节示例 OrderDemo(订单状态机)演示三件事:模型级 <datamodel> 变量、Java 侧读写上下文、cond 条件转移(源码 /opt/scxml-demo/scxml-demo/src/main/java/demo/OrderDemo.java,模型 /opt/scxml-demo/scxml-demo/models/order.scxml)。

模型要点:created 状态收到 pay 事件时,先判断 amount >= 100 决定进 paid 还是兜底 failed;paid 状态的 onentry 动作把 payCount 加一。

<datamodel>
  <data name="payCount" expr="0"/>
  <data name="amount" expr="100"/>
</datamodel>

<state id="created">
  <transition event="pay" cond="amount &gt;= 100" target="paid"/>
  <transition event="pay" target="failed"/>
</state>

<state id="paid">
  <onentry>
    <assign name="payCount" expr="payCount + 1"/>
  </onentry>
  <transition event="ship" target="shipped"/>
</state>

Java 侧三个场景:正常履约走到底、Java 改写变量后条件不满足走兜底、reset() 复位(运行回显,实测):

======== 场景一: 正常支付履约 (amount=100) ========
根上下文初始变量: amount=100, payCount=0
当前状态: [paid]
onentry 执行 assign 后: payCount=1
当前状态: [shipped]
当前状态: [done]
isFinal()=true
======== 场景二: 条件不满足走兜底转移 (amount=50) ========
Java 侧改写变量: amount=50
当前状态: [failed]
isFinal()=false
======== 场景三: reset() 复位 ========
reset 后回到: [created], payCount=0

验证点:cond="amount >= 100" 在 amount=100 与 Java 侧改写为 50 后分别命中目标转移与兜底转移;onentry 的 <assign> 真实改写了根上下文变量(payCount=1);reset() 同时复位状态与数据模型(payCount 回到 0)。

事件携带 payload(TriggerEvent 第三个参数)时,模型转移里可以用 _eventdata 变量访问,配合 cond 做路由(模型 /opt/scxml-demo/scxml-demo/models/payload.scxml):

<state id="wait">
  <transition event="msg" cond="_eventdata == 'vip'" target="vipLane"/>
  <transition event="msg" target="normalLane"/>
</state>

Java 侧触发(源码 /opt/scxml-demo/scxml-demo/src/main/java/demo/PayloadDemo.java,运行回显,实测):

---- 携带 payload='vip' 触发 msg 事件 ----
当前状态: vipLane
---- 携带 payload='basic' 触发 msg 事件 ----
当前状态: normalLane

五、延时事件与监听器实战

5.1 延时事件:SimpleScheduler

<send> 支持 delay,但引擎默认构造时事件派发器传 null,延时事件无法回投递,必须装配 SimpleScheduler(模型 /opt/scxml-demo/scxml-demo/models/alarm.scxml,源码 /opt/scxml-demo/scxml-demo/src/main/java/demo/SendDelayDemo.java):

<state id="idle">
  <onentry>
    <send event="'wake'" delay="'1s'"/>
  </onentry>
  <transition event="wake" target="awake"/>
</state>

Java 侧关键三行:监听器记录 awake 进入时刻、装配调度器、驱动引擎:

exec.setEventdispatcher(new SimpleScheduler(exec));
exec.setStateMachine(scxml);
exec.go();

go() 后主线程等 1800 ms,监听器输出进入 awake 的耗时(运行回显,实测;毫秒数随机器略有浮动):

[主线程] go() 完成, 初始状态=[idle] (onentry 已登记 delay=1s 的 send)
[监听] 进入 awake, 距 go() 已 1030 ms
[主线程] 等待 1800 ms 后状态=[awake]

验证点:状态从 idle 到 awake 没有外部事件参与,由 onentry 登记的 1 秒延时事件驱动,实测延迟 1030 ms(登记与调度开销约 30 ms)。

5.2 监听器:SCXMLListener 三个回调

实现 SCXMLListener 接口并注册到执行器,即可观测进入、退出、转移三类事件(源码 /opt/scxml-demo/scxml-demo/src/main/java/demo/ListenerDemo.java,自定义监听器核心):

static class TraceListener implements SCXMLListener {
    public void onEntry(TransitionTarget state) {
        System.out.println("[onEntry]      进入 " + state.getId());
    }

    public void onExit(TransitionTarget state) {
        System.out.println("[onExit]       退出 " + state.getId());
    }

    public void onTransition(TransitionTarget from, TransitionTarget to, Transition transition) {
        System.out.println("[onTransition] " + from.getId() + " --("
                + transition.getEvent() + ")--> " + to.getId());
    }
}

注册在根模型上即可覆盖全部状态:exec.addListener(scxml, new TraceListener());。两次 timer 事件的完整回调序列(运行回显,实测):

---- 调用 go(), 观察初始进入 ----
[onEntry]      进入 red
---- 触发两次 timer, 观察回调序列 ----
[onExit]       退出 red
[onTransition] red --(timer)--> green
[onEntry]      进入 green
[onExit]       退出 green
[onTransition] green --(timer)--> yellow
[onEntry]      进入 yellow

验证点:回调顺序为「退出旧状态 → 转移 → 进入新状态」,onTransition 能拿到事件名与两端状态,适合做审计日志或流程可视化埋点。

六、集成到既有工程

6.1 superStep:宏步与微步控制

SCXML 语义是「运行到完成」:事件驱动状态迁移后,迁移中产生的内部事件(无延时 <send>)默认被连续处理完,这就是宏步(superStep 默认为 true)。设为 false 后每次 triggerEvents 只推进一步,便于单步调试与逐级审计(模型 /opt/scxml-demo/scxml-demo/models/chain.scxml:链式内部事件 go → b --evt_b--> c --evt_c--> d,源码 /opt/scxml-demo/scxml-demo/src/main/java/demo/SuperStepDemo.java,运行回显,实测):

---- superStep=true (默认, 宏步跑到底) ----
触发 go 后: [d], isFinal=true
---- superStep=false (每次只推进一步) ----
触发 go 后: [b], isFinal=false
空事件推进一步: [c], isFinal=false
再进一步: [d], isFinal=false

验证点:默认模式一次 triggerEvent("go") 直达终点 d;微步模式下同一次事件只到 b,后续用空事件(TriggerEvent 名为 null)逐级推进。注意微步模式下判终请以 getStates() 是否含 final 状态为准。

6.2 官方 REPL:不改代码调试模型

主 jar 内置 org.apache.commons.scxml.test.StandaloneJexlExpressions:加载模型后从标准输入读事件,实时打印序列化模型与状态回调。事件流经管道喂入即可(实时回显,实测):

cd /opt/scxml-demo
CP=commons-scxml-0.9/commons-scxml-0.9.jar:lib/commons-logging-1.1.1.jar:lib/commons-digester-1.8.jar:lib/commons-beanutils-1.7.0.jar:lib/commons-jexl-1.1.jar:lib/xalan-2.7.2.jar
printf 'timer\ntimer\nquit\n' | java -cp $CP org.apache.commons.scxml.test.StandaloneJexlExpressions scxml-demo/models/traffic-light.scxml 2>&1 | tail -6
Oct 15, 2026 2:12:31 AM org.apache.commons.scxml.env.SimpleSCXMLListener onExit
INFO: /green
Oct 15, 2026 2:12:31 AM org.apache.commons.scxml.env.SimpleSCXMLListener onTransition
INFO: transition (event = timer, cond = null, from = /green, to = /yellow)
Oct 15, 2026 2:12:31 AM org.apache.commons.scxml.env.SimpleSCXMLListener onEntry
INFO: /yellow

6.3 依赖清单汇总

最终 classpath 共六个 jar(版本实测可运行):

jar版本来源
commons-scxml0.9pom 主依赖
commons-logging1.1.1传递依赖
commons-digester1.8传递依赖
commons-beanutils1.7.0传递依赖
commons-jexl1.1显式声明(optional)
xalan2.7.2显式声明(optional)

七、选型建议

  • 新项目需要状态机且模型要与代码分离、需要跨团队共享模型文件:commons-scxml 0.9 是标准 SCXML 格式的现成解释器,模型可被其他语言实现复用。
  • Spring 技术栈、需要持久化与监控扩展:优先评估 Spring Statemachine(本专栏第 1 篇有四引擎对比表)。
  • 已有 0.9 使用经验但需要 JDK 17+:实测可行(本文即 JDK 17 环境),务必遵守 3.2、3.3 两节的表达式与上下文配套要求。
  • 0.9 已休眠不再更新:把它当作「模型解释器」而非「演进中的框架」,锁定版本、锁定期望即可。

八、参考链接

  1. Apache Commons SCXML 官方主页与用户指南:https://commons.apache.org/proper/commons-scxml/
  2. W3C State Chart XML (SCXML) 规范:https://www.w3.org/TR/scxml/
  3. Maven 中央仓库坐标页:https://repo1.maven.org/maven2/commons-scxml/commons-scxml/
  4. Commons JEXL 1.1 文档:https://commons.apache.org/proper/commons-jexl/reference/syntax.html
  5. 本专栏姊妹篇:《Ubuntu 22.04 安装 Apache Commons SCXML 指南》《CentOS 8 安装 Apache Commons SCXML 指南》《Apache Commons SCXML 0.9 常用 API 与状态机模型详解》《Apache Commons SCXML 0.9 状态机设计示例详解》