Apache Commons SCXML 0.9 常用 API 与状态机模型详解
系统梳理 Apache Commons SCXML 0.9 的 API 体系,覆盖 SCXMLExecutor 生命周期与 Status 查询、模型解析与序列化、Context 层次与 JEXL 表达式引擎、事件模型与延时派发、SCXMLListener 监听器与 In() 谓词,全部示例与回显实测通过。
Apache Commons SCXML 0.9 常用 API 与状态机模型详解
目标:按「运行时 → 模型 → 上下文与表达式 → 事件 → 监听」的分层顺序,把 commons-scxml 0.9 常用 API 的职责、关键签名、配套关系与实测行为讲透;每个 API 均配可运行示例与真实回显,本文代码均已在 Ubuntu 22.04 + JDK 17 + Maven 3.9 实测通过。
commons-scxml 0.9 的 API 面可以概括为「一个核心运行时、一套模型对象、两个配套组件、三类扩展点」:SCXMLExecutor 是唯一的运行时核心;SCXML、State、Transition 等构成从 XML 反解出来的模型对象树;Context/Evaluator 是表达式求值的配套组件;EventDispatcher、SCXMLListener、ErrorReporter 是三类可注入的扩展点。本文逐一展开,安装与环境基线见第 1、2 篇,集成实战与坑位见第 3 篇,模型设计方法论见第 5 篇。
一、环境信息
- 操作系统:Ubuntu 22.04 LTS
- 架构:x86_64
- JDK:Temurin 17.0.20.1(OpenJDK 1.8.0_312 同样实测通过)
- 构建工具:Apache Maven 3.9.16
- 组件版本:commons-scxml 0.9(2008-11-22 发布)
- Maven 坐标:
commons-scxml:commons-scxml:0.9+ 显式补充commons-jexl:1.1、xalan:2.7.2 - 工程目录:
/opt/scxml-demo/scxml-demo(模型位于models/,示例源码位于src/main/java/demo/)
java -version | head -1
openjdk version "17.0.20.1" 2026-08-18
💡 技术拓展:0.9 的表达式引擎有两个实现可选,行为差异直接影响集成方案——
引擎 类 表达式风格 环境要求 本文选用 Commons JEXL 1.1 env.jexl.JexlEvaluatoramount >= 100、可调用 Java 对象方法仅需 commons-jexl 1.1 ✅ JSP EL env.jsp.ELEvaluator${amount >= 100}需 JSP/Servlet API(commons-el 等) ❌ JEXL 路线依赖最少、可在现代 JVM 上免额外装配运行,且表达式能直接调用上下文里的 Java 对象方法(5.4 节实测),本文全部示例采用 JEXL。
二、API 总览
按包分层的 API 地图(jar tf 实测类清单整理):
| 层 | 核心类 | 职责 | 对应章节 |
|---|---|---|---|
| 运行时 | org.apache.commons.scxml.SCXMLExecutor | 状态机引擎:解析产物的驱动器 | 三 |
| 运行时 | org.apache.commons.scxml.Status | 当前状态集合与终态判定 | 3.2 |
| 运行时 | org.apache.commons.scxml.SCInstance | 执行器内部装配体(上下文、监听注册表) | 5.1 |
| 模型 IO | org.apache.commons.scxml.io.SCXMLParser | XML → 模型对象(SAX/Digester 实现) | 4.1 |
| 模型 IO | org.apache.commons.scxml.io.SCXMLSerializer | 模型对象 → XML(round-trip) | 4.2 |
| 模型 IO | org.apache.commons.scxml.io.SCXMLDigester | 同解析职责的旧式 Digester 入口 | 4.1 |
| 模型 | org.apache.commons.scxml.model.SCXML/State/Transition 等 | 模型对象树 | 4.3 |
| 模型动作 | model.Assign/Var/Send/Log/If/ForEach 等 | onentry/transition 内可执行动作 | 4.3 |
| 上下文 | org.apache.commons.scxml.Context/env.SimpleContext/env.jexl.JexlContext | 变量存储(根/局部两层) | 5.1 |
| 表达式 | org.apache.commons.scxml.Evaluator/env.jexl.JexlEvaluator | 表达式求值、条件求值 | 5.2 |
| 事件 | org.apache.commons.scxml.TriggerEvent | 内外部事件对象(五种类型) | 6.1 |
| 事件 | org.apache.commons.scxml.EventDispatcher/env.SimpleDispatcher/env.SimpleScheduler | 外发事件派发与延时回投递 | 6.3 |
| 监听 | org.apache.commons.scxml.SCXMLListener/env.Tracer/env.SimpleSCXMLListener | 进入/退出/转移回调 | 七 |
| 扩展 | org.apache.commons.scxml.Builtin | In() 谓词等模型内置函数 | 5.5 |
| 便捷 | org.apache.commons.scxml.env.AbstractStateMachine | 事件即方法的薄封装基类 | 八 |
三、运行时核心:SCXMLExecutor
3.1 生命周期五步
SCXMLExecutor 的完整生命周期固定为「解析 → 组装 → 挂载 → 启动 → 派发」。关键方法签名(javap 实测):
javap -cp $HOME/.m2/repository/commons-scxml/commons-scxml/0.9/commons-scxml-0.9.jar org.apache.commons.scxml.SCXMLExecutor | grep -E 'public|void go'
public org.apache.commons.scxml.Status getCurrentStatus()
public void go() throws org.apache.commons.scxml.model.ModelException
public void reset() throws org.apache.commons.scxml.model.ModelException
public void setStateMachine(org.apache.commons.scxml.model.SCXML)
public void triggerEvent(org.apache.commons.scxml.TriggerEvent)
public void triggerEvents(org.apache.commons.scxml.TriggerEvent[])
public org.apache.commons.scxml.Context getRootContext()
public void setRootContext(org.apache.commons.scxml.Context)
public void addListener(org.apache.commons.scxml.model.SCXML, org.apache.commons.scxml.SCXMLListener)
public void addListener(org.apache.commons.scxml.model.TransitionTarget, org.apache.commons.scxml.SCXMLListener)
public void addListener(org.apache.commons.scxml.model.Transition, org.apache.commons.scxml.SCXMLListener)
public void setEventdispatcher(org.apache.commons.scxml.EventDispatcher)
public void setSuperStep(boolean)
public void setRootContext(org.apache.commons.scxml.Context)
构造器三参数就是扩展点装配位:new SCXMLExecutor(evaluator, eventDispatcher, errorReporter)。最小驱动模板(源码 /opt/scxml-demo/scxml-demo/src/main/java/demo/TrafficLightDemo.java):
SCXML scxml = SCXMLParser.parse(model, new SimpleErrorHandler());
SCXMLExecutor exec = new SCXMLExecutor(new JexlEvaluator(), null, new SimpleErrorReporter());
exec.setStateMachine(scxml);
exec.go();
exec.triggerEvent(new TriggerEvent("timer", TriggerEvent.SIGNAL_EVENT));
三个装配要点:
eventDispatcher传null时仍可同步派发事件,但不能处理<send delay>;需要延时事件就装配SimpleScheduler(6.3 节)。go()抛出的ModelException只在模型本身不合法时出现,解析阶段的 XML 错误则在SCXMLParser.parse处由ErrorHandler回调。triggerEvent同步执行:方法返回时宏步(含级联内部事件)已跑完,getCurrentStatus()立即反映最终状态。
3.2 Status:当前状态查询
Status 是每步派发后的状态快照(javap 实测签名):
public boolean isFinal()
public java.util.Set getStates()
public java.util.Collection getEvents()
getStates()返回当前活跃状态集合(TransitionTarget)。普通顺序模型只有一个元素;并行(<parallel>)或历史组合下可能有多个。isFinal()表示引擎是否已到达顶层<final>。一个实测细节:superStep 微步模式逐级推进时该标志不即时翻转(见 3.3 节回显),判终建议同时检查getStates()。
打印当前状态的标准写法(复用全文示例):
Set<TransitionTarget> states = exec.getCurrentStatus().getStates();
for (TransitionTarget t : states) {
System.out.println("当前状态: " + t.getId());
}
3.3 reset 复位与 superStep 宏步
reset() 把引擎拉回初始状态并复位数据模型(订单示例实测回显,见第 3 篇 4 节):
======== 场景三: reset() 复位 ========
reset 后回到: [created], payCount=0
setSuperStep(false) 关闭宏步:每次 triggerEvents 只处理一级事件与一级内部事件,用于单步调试(模型 /opt/scxml-demo/scxml-demo/models/chain.scxml,链式内部事件,源码 demo/SuperStepDemo.java,实测回显):
---- superStep=true (默认, 宏步跑到底) ----
触发 go 后: [d], isFinal=true
---- superStep=false (每次只推进一步) ----
触发 go 后: [b], isFinal=false
空事件推进一步: [c], isFinal=false
再进一步: [d], isFinal=false
微步推进用空事件驱动:exec.triggerEvents(new TriggerEvent[] {new TriggerEvent(null, TriggerEvent.SIGNAL_EVENT)});。
四、模型解析与序列化
4.1 SCXMLParser:XML 到模型对象
public static SCXML parse(final URL scxmlURL, final ErrorHandler errHandler)
- 入参是模型 URL 与 SAX
ErrorHandler;env.SimpleErrorHandler把解析告警打到日志后继续,errHandler传null则抛出即失败。 - 解析器按 W3C 命名空间
http://www.w3.org/2005/07/scxml识别元素;Commons 扩展动作(如<var>)在http://commons.apache.org/scxml命名空间下(第 3 篇 3.4 节实测细节)。 - 解析成功返回
SCXML根对象,scxml.getChildren()即顶层状态列表:
SCXML scxml = SCXMLParser.parse(model, new SimpleErrorHandler());
System.out.println("模型解析成功, 根状态个数=" + scxml.getChildren().size());
模型解析成功, 根状态个数=3
SCXMLDigester 是同一职责的旧式入口(直接持有 Digester 实例),新代码统一用 SCXMLParser。
4.2 SCXMLSerializer:模型对象到 XML
序列化器把内存中的模型对象还原为 XML,用于模型审计、对比与落库(源码 /opt/scxml-demo/scxml-demo/src/main/java/demo/SerializerDemo.java):
SCXML scxml = SCXMLParser.parse(model, new SimpleErrorHandler());
String xml = SCXMLSerializer.serialize(scxml);
System.out.println(xml);
运行回显(实测,红绿灯模型 round-trip 结果):
解析后重新序列化的模型:
<?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">
</transition>
</state>
<state id="green">
<transition event="timer" target="yellow">
</transition>
</state>
<state id="yellow">
<transition event="timer" target="red">
</transition>
</state>
</scxml>
「解析 → 序列化」与原文件语义等价(仅缩进与自闭合风格不同),这为「外部系统改模型 → 引擎热加载」提供了往返能力。
4.3 模型对象树与内嵌动作
SCXML 树上的常用节点与内嵌动作类(全部位于 org.apache.commons.scxml.model 包):
| 类 | 对应 XML | 读取要点 |
|---|---|---|
SCXML | <scxml> | getInitialTarget()、getChildren()、getNamespaces() |
State/Parallel/Final | <state>/<parallel>/<final> | 均实现 TransitionTarget(有 getId()) |
Transition | <transition> | getEvent()、getCond()、getTargets()、getActions() |
Datamodel/Data | <datamodel>/<data> | SCXML.getDatamodel(),初始变量在此 |
Assign | <assign> | getName()、getExpr();要求变量已存在(先 <data> 声明) |
Var | <var>(Commons 命名空间) | setLocal 语义,写入状态局部上下文 |
Send | <send> | getEvent()/getDelay() 均为表达式,见 6.3 节 |
Log/If/ElseIf/Else/ForEach | 对应动作元素 | execute() 在语义步进中被回调 |
onentry/onexit 通过 State.getOnEntry()/getOnExit() 访问,动作按文档顺序执行——订单示例里 paid 状态进入即执行 <assign name="payCount" expr="payCount + 1"/>,Java 侧随即读到 payCount=1(第 3 篇 4 节实测)。
五、上下文与表达式引擎
5.1 Context:根上下文与状态局部上下文
Context 接口(javap 实测签名):
public void set(java.lang.String, java.lang.Object)
public void setLocal(java.lang.String, java.lang.Object)
public java.lang.Object get(java.lang.String)
public boolean has(java.lang.String)
public java.util.Map getVars()
public void reset()
两层语义:
- 根上下文:
exec.getRootContext(),模型<datamodel>声明的变量、Java 侧预置的对象都住在这里;set()按「就近已有定义」写入。 - 状态局部上下文:每个状态进入时创建,
setLocal()只写当前层(<var>动作即此语义),变量解析沿父链向上查找。
Java 侧读写示例(源码 demo/OrderDemo.java,实测回显):
Context ctx = exec.getRootContext();
System.out.println("根上下文初始变量: amount=" + ctx.get("amount") + ", payCount=" + ctx.get("payCount"));
ctx.set("amount", Integer.valueOf(50));
根上下文初始变量: amount=100, payCount=0
Java 侧改写变量: amount=50
Java 侧改写 amount 后,模型转移的 cond="amount >= 100" 立即按新值求值并走兜底转移——这就是模型与宿主代码共享状态的通道。
⚠️ 注意:自定义根上下文必须与表达式引擎配套,JEXL 路线用
new JexlContext();误用SimpleContext运行时报SimpleContext cannot be cast to class org.apache.commons.scxml.env.jexl.JexlContext(完整报错见第 3 篇 3.2 节)。
5.2 Evaluator:表达式求值入口
Evaluator 接口四个核心方法:eval(Context, String) 求表达式值、evalCond(Context, String) 求布尔条件、evalAssign(Context, String, Object) 求赋值右侧值、newContext(Context) 创建配套子上下文。JexlEvaluator 实现委托给 Commons JEXL 1.1 的解释器,因此模型表达式可以直接使用 JEXL 语法(>、>=、==、&&、!,字符串用单引号)。
JexlEvaluator 对未定义变量的求值结果是 null 而非异常——这让 <send event="wake">(漏掉引号)这类笔误静默失效(第 3 篇 3.3 节),排查时优先怀疑表达式被当成变量名。
5.3 事件 payload 与 _eventdata
TriggerEvent 构造器的第三个参数是 payload(可序列化对象)。当前事件派发时,语义实现把 payload 暴露为上下文变量 _eventdata(多事件并存时则是 _eventdatamap,键为事件名)。模型里直接参与条件判断(模型 models/payload.scxml):
<state id="wait">
<transition event="msg" cond="_eventdata == 'vip'" target="vipLane"/>
<transition event="msg" target="normalLane"/>
</state>
exec.triggerEvent(new TriggerEvent("msg", TriggerEvent.SIGNAL_EVENT, "vip"));
实测回显:
---- 携带 payload='vip' 触发 msg 事件 ----
当前状态: vipLane
---- 携带 payload='basic' 触发 msg 事件 ----
当前状态: normalLane
5.4 在模型表达式中调用 Java 对象
JEXL 表达式能调用上下文中 Java 对象的公有方法——这是 0.9 与宿主代码深度协作的关键通道。把秒表 POJO 预置进根上下文(源码 demo/CalcDemo.java):
Context rootCtx = new JexlContext();
rootCtx.set("calc", new CalcDemo.Calc());
SCXMLExecutor exec = new SCXMLExecutor(evaluator, null, new SimpleErrorReporter());
exec.setRootContext(rootCtx);
exec.setStateMachine(scxml);
exec.go();
System.out.println("模型 onentry 调用 Java 对象后: sum=" + rootCtx.get("sum"));
模型侧在 onentry 里调用并落库到 <datamodel>(模型 models/calc.scxml):
<datamodel>
<data name="sum" expr="0"/>
<data name="grade" expr="'-'"/>
</datamodel>
<state id="start">
<onentry>
<assign name="sum" expr="calc.add(7, 35)"/>
<assign name="grade" expr="calc.grade(sum)"/>
</onentry>
<transition event="go" target="end"/>
</state>
实测回显:
模型 onentry 调用 Java 对象后: sum=42
模型调用 Java 方法返回: grade=不及格
isFinal()=true
验证点:calc.add(7, 35) 在模型里执行并写入 sum=42;后续表达式 calc.grade(sum) 又引用了刚写入的变量,说明同一 onentry 内动作按序执行且上下文即时可见。
5.5 Builtin:模型内置谓词 In()
org.apache.commons.scxml.Builtin 提供 In(stateId) 谓词,在 cond 中判断「当前是否处于某状态」,配合层级/并行模型做跨状态守卫(模型 models/hierarchy.scxml:outer 内含 inner 与 sibling,父级转移带 In('inner') 条件):
<state id="outer" initial="inner">
<state id="inner">
<transition event="skip" target="sibling"/>
</state>
<state id="sibling"/>
<transition event="check" cond="In('inner')" target="done"/>
</state>
源码 demo/BuiltinDemo.java 实测回显:
---- inner 激活时发 check, In('inner') 为真 ----
当前状态: done
---- 先 skip 到 sibling 再发 check, In('inner') 为假 ----
当前状态: sibling
当前状态: sibling
六、事件模型与派发
6.1 TriggerEvent:五种事件类型
public static final int SIGNAL_EVENT = 0;
public static final int CALL_EVENT = 1;
public static final int CHANGE_EVENT = 2;
public static final int TIME_EVENT = 3;
public static final int ERROR_EVENT = 4;
public TriggerEvent(final String name, final int type)
public TriggerEvent(final String name, final int type, final Object payload)
- 应用层派发统一用
SIGNAL_EVENT;TIME_EVENT由延时派发器产生;ERROR_EVENT用于引擎错误通报。 - 事件名允许点号风格(如
watch.start),只是普通字符串,与包名无关。
6.2 同步派发与事件队列
triggerEvent 是应用入口:外部事件入队后,引擎在当前调用线程内执行完整个宏步(含内部事件级联)才返回。内部事件由无延时 <send> 产生,会在同一宏步内被消费(链式模型 models/chain.scxml 一次触发直达终态,见 3.3 节回显)。批量派发用 triggerEvents(TriggerEvent[]),也是微步模式的推进入口。
6.3 EventDispatcher:外发与延时事件
EventDispatcher 接口负责 <send> 的落地,两个实现分工明确:
| 实现 | 职责 | 延时支持 | 适用 |
|---|---|---|---|
env.SimpleDispatcher | 把 <send> 内容打印到日志(无外部传输通道) | ❌(仅登记) | 验证模型 |
env.SimpleScheduler | 无延时事件立即回投引擎;延时事件经内部定时器到期回投 | ✅ | 生产内嵌 |
延时事件完整链路实测(模型 models/alarm.scxml,源码 demo/SendDelayDemo.java):
<state id="idle">
<onentry>
<send event="'wake'" delay="'1s'"/>
</onentry>
<transition event="wake" target="awake"/>
</state>
exec.setEventdispatcher(new SimpleScheduler(exec));
exec.setStateMachine(scxml);
exec.go();
实测回显(毫秒数随机器浮动):
[主线程] go() 完成, 初始状态=[idle] (onentry 已登记 delay=1s 的 send)
[监听] 进入 awake, 距 go() 已 1030 ms
[主线程] 等待 1800 ms 后状态=[awake]
<send> 的 event 与 delay 都是表达式,字符串字面量必须加引号:event="'wake'"、delay="'1s'"(错误写法与报错见第 3 篇 3.3 节)。SimpleScheduler 还提供 cancel(String) 取消已登记的延时事件,对应模型 <cancel sendid="..."/> 动作。
七、监听器与调试
7.1 SCXMLListener:三个回调
public void onEntry(TransitionTarget state)
public void onExit(TransitionTarget state)
public void onTransition(TransitionTarget from, TransitionTarget to, Transition transition)
注册点有粒度差异(javap 实测重载):
addListener(SCXML, listener):挂根模型,覆盖全部状态——观测用首选;addListener(TransitionTarget, listener):只收单个状态的通知——精准埋点用;addListener(Transition, listener):只收单条转移的通知。
自定义监听器实测(源码 demo/ListenerDemo.java,完整回显):
---- 调用 go(), 观察初始进入 ----
[onEntry] 进入 red
---- 触发两次 timer, 观察回调序列 ----
[onExit] 退出 red
[onTransition] red --(timer)--> green
[onEntry] 进入 green
[onExit] 退出 green
[onTransition] green --(timer)--> yellow
[onEntry] 进入 yellow
7.2 Tracer 与官方 REPL
env.Tracer 是官方提供的监听器实现(同时充当 ErrorReporter),通过 JUL 打印 onExit/onTransition/onEntry。直接用官方 REPL 体验(实测回显):
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
排障建议组合拳:模型语法问题看 SCXMLParser 的 IgnoredElementRule 日志(第 3 篇 3.4 节);流转行为问题用 Tracer 或自定义监听器;表达式问题把 SimpleErrorReporter 换成 Tracer 让 SCXMLExpressionException 打到控制台。
八、便捷基类 AbstractStateMachine 与 env 包速查
env.AbstractStateMachine 是给「一个模型对应一个 Java 门面」场景的薄封装:构造时加载模型并 go(),提供 fire(String) 派发事件、getStatus()/getStatus().getStates() 查询、resetMachine() 复位。适合把状态机封装成领域对象的小型应用;需要精细控制装配(延时事件、自定义监听)时仍直接使用 SCXMLExecutor。
org.apache.commons.scxml.env 包速查表(jar tf 实测类清单节选):
| 类 | 用途 |
|---|---|
SimpleErrorHandler | 解析阶段容错回调 |
SimpleErrorReporter | 引擎错误收集(onError 打日志) |
SimpleContext | 通用上下文(EL 路线或独立使用) |
SimpleDispatcher | <send> 日志派发器 |
SimpleScheduler | 延时事件调度器 |
SimpleSCXMLListener | 监听器空实现(按需覆盖) |
Tracer | 监听器 + 错误上报一体 |
AbstractSCXMLListener | 监听器适配基类 |
AbstractStateMachine | 便捷门面基类 |
jexl.JexlEvaluator/jexl.JexlContext | JEXL 表达式路线(本文推荐) |
jsp.ELEvaluator | JSP EL 路线(需 JSP/Servlet API) |
LogUtils | commons-logging 辅助 |
九、参考链接
- Commons SCXML 0.9 Javadoc(离线包随官方 bin 包附赠,亦可在线):https://commons.apache.org/proper/commons-scxml/apidocs/index.html
- Commons SCXML 用户指南:https://commons.apache.org/proper/commons-scxml/userguide.html
- W3C State Chart XML (SCXML) 规范:https://www.w3.org/TR/scxml/
- Commons JEXL 1.1 语法参考:https://commons.apache.org/proper/commons-jexl/reference/syntax.html
- 本专栏姊妹篇:《Apache Commons SCXML 0.9 快速入门与集成示例》《Apache Commons SCXML 0.9 状态机设计示例详解》