Apache Commons SCXML 0.9 快速入门与集成示例
通过 10 分钟最小可用工程、五个高频集成坑位、条件转移与上下文变量、延时事件与监听器、superStep 宏步控制等实战示例,给出把 Apache Commons SCXML 0.9 集成进 Java 工程的完整实操路径,全部代码与回显实测通过。
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 >= 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-scxml | 0.9 | pom 主依赖 |
| commons-logging | 1.1.1 | 传递依赖 |
| commons-digester | 1.8 | 传递依赖 |
| commons-beanutils | 1.7.0 | 传递依赖 |
| commons-jexl | 1.1 | 显式声明(optional) |
| xalan | 2.7.2 | 显式声明(optional) |
七、选型建议
- 新项目需要状态机且模型要与代码分离、需要跨团队共享模型文件:commons-scxml 0.9 是标准 SCXML 格式的现成解释器,模型可被其他语言实现复用。
- Spring 技术栈、需要持久化与监控扩展:优先评估 Spring Statemachine(本专栏第 1 篇有四引擎对比表)。
- 已有 0.9 使用经验但需要 JDK 17+:实测可行(本文即 JDK 17 环境),务必遵守 3.2、3.3 两节的表达式与上下文配套要求。
- 0.9 已休眠不再更新:把它当作「模型解释器」而非「演进中的框架」,锁定版本、锁定期望即可。
八、参考链接
- Apache Commons SCXML 官方主页与用户指南:https://commons.apache.org/proper/commons-scxml/
- W3C State Chart XML (SCXML) 规范:https://www.w3.org/TR/scxml/
- Maven 中央仓库坐标页:https://repo1.maven.org/maven2/commons-scxml/commons-scxml/
- Commons JEXL 1.1 文档:https://commons.apache.org/proper/commons-jexl/reference/syntax.html
- 本专栏姊妹篇:《Ubuntu 22.04 安装 Apache Commons SCXML 指南》《CentOS 8 安装 Apache Commons SCXML 指南》《Apache Commons SCXML 0.9 常用 API 与状态机模型详解》《Apache Commons SCXML 0.9 状态机设计示例详解》