企业应用架构15:Channel、Endpoint与Translator的真实消息边界
租赁合同引用设备 E1,维修系统识别的资产却叫 ASSET-E1。直接把合同对象序列化后发送,既暴露内部字段,也没有解决两套标识之间的关系。消息能够送达,只是集成的一部分;接收方还需要知道它属于哪种报文、该交给哪个端点,以及不能处理时保留在哪里。
本篇将设备维修申请放进真实 ActiveMQ 通道,经 TCP 交给独立消费进程,再将翻译结果写入 H2。消费者离线、broker 停启、未知版本、错误通道和毒消息分别运行,业务拒绝与基础设施重试保留不同证据。
通道与端点承担不同职责
EIP 的 Message Channel 讨论应用之间传输消息的通路;Message Endpoint 是应用与消息系统连接的边界;Message Translator 则处理报文格式差异。它们可以组合,但不能仅根据一条队列名称就认定转换规则已经存在。Message Channel;Message Translator
实验固定 maintenance.request 为维修申请通道。租赁方发送外部合同编号与设备引用,维修端点将 E1 翻译成 ASSET-E1,并生成维修申请文本。这个前缀规则仅服务于合成样本,真实资产匹配通常还需要受控映射表,不能从字符串拼接推导两个实物一定相同。
第14篇 使用独立 H2 spool 观察业务提交、发布与消费确认之间的窗口。本篇沿用业务消息编号的观念,但新建设备报文,不把第14篇的金额消息重新解释成设备事件。原实验没有承诺 broker 行为,因此新增真实运行时来验证交付层问题。flowchart LR
S[租赁方独立JVM] -->|TCP 持久消息| Q[maintenance.request]
Q --> E[维修端点独立JVM]
E --> T[Translator E1转为ASSET-E1]
T --> D[H2维修申请]
E -->|版本或字段无效| I[maintenance.quarantine]
W[wrong.channel] --> A[显式检查后隔离]
端点只消费自己的通道。向 wrong.channel 发出的 M4 不会出现在维修端点中;实验确认主通道耗尽之后,单独读取错误通道并转入隔离区。这个动作代表受控诊断路径,不是任意扫描所有队列并猜测该如何处理。
固定运行时和报文契约
运行时采用 ActiveMQ Classic 5.18.6,broker 位于单独 JVM,使用 KahaDB 持久化,通过本机随机 TCP 端口接收连接。生产者与消费者由后续独立 Java 命令启动,没有把进程内集合伪装成消息队列。ActiveMQ嵌入Broker文档
外层使用 JMS TextMessage,message_id 是业务消息编号,schema_version 是格式版本,JMSCorrelationID 保存合同关联号。broker 还分配 JMSMessageID,日志同时保留两种编号。业务去重键应当由业务语义定义,不应默认每次发送产生的新传输编号都表示一次新业务。
正文样本用竖线分隔合同编号和设备编号,例如 R1 与 E1。这个格式明确不支持字段内再出现分隔符,不是通用序列化方案。版本一只允许两个非空字段;字段演进应提高版本并提供兼容规则,不能悄悄把新增字段解释成旧字段的一部分。
原始报文与转换后报文一起写入 maintenance 表,主键是业务消息编号。读回断言核对只有一条合法维修申请,资产引用为 ASSET-E1。该表只保存接收结果,没有引入维修工单完整生命周期,也没有宣称设备已经完成检修。
消费者离线与broker重启
发送阶段没有消费者运行,消息使用持久交付模式,并在 JMS 事务提交后结束发送进程。此时维修数据库仍为零条记录,证明发送成功不会直接产生接收方结果。随后停止 broker,再用同一 KahaDB 目录启动新的 broker 进程。
重启后的端点实际收到三条维修通道消息:一条合法记录、一条缺设备编号记录和一条未知版本记录。日志保存通道、业务编号、版本、关联编号以及原始正文。对每条消息的观察来自真实 receive 调用,而不是先检查输入数组再打印“已投递”。
此次 broker 停启采用正常终止信号,客户端进程则是独立启动。它证明已提交持久消息经过一次正常停止与重启后仍可消费,没有模拟磁盘损坏、断电或多节点故障转移。持久化目录仍在临时工作区,运行报告记录其绝对路径,便于复查。
端点事务还包含不同系统的边界。写 H2 成功之后再确认 JMS 消息,两者没有组成分布式原子事务。当前场景没有注入这个窗口的重复投递,不能从一次维修结果推导全局恰好一次。第14篇的 Inbox 与载荷冲突策略,才是继续填补业务去重问题的明确方向。
业务隔离与死信分开观察
缺少设备编号和不认识的版本属于确定的业务拒绝。端点将原始报文转入 maintenance.quarantine,再提交消息事务。重试同一个未知版本不会让消费者突然理解它,因此这条路径不依赖反复抛异常来取得进展。
毒消息则进入另一条 poison 通道。消费者连续三次接收并回滚事务,日志中的 JMSXDeliveryCount 依次为一、二、三。配置允许两次重投,加上首次交付共三次;超过限制后,消息出现在 ActiveMQ.DLQ 中。ActiveMQ重投与DLQ文档
flowchart TD
P[poison持久消息] --> R1[首次交付 count=1]
R1 -->|rollback| R2[重投 count=2]
R2 -->|rollback| R3[重投 count=3]
R3 -->|rollback 超过两次重投| D[ActiveMQ.DLQ]
V[未知schema或缺字段] --> Q[应用隔离队列]
实验主动制造处理失败,以观察重投策略,没有模拟一台真的损坏设备。死信只说明当前处理策略已经停止自动尝试,不能直接等价于业务取消。重新投入主通道之前,仍需判断是否修复了原因、载荷是否兼容,以及原副作用是否已经发生。
隔离消息保留原业务编号和正文,诊断工具可以据此联系原合同。为了保持样本短小,实验没有加入访问控制、个人信息脱敏或隔离区保留策略;这些不是队列默认替应用完成的功能,进入真实系统时需要明确的数据治理要求。
消费者生命周期也是运行语义
事务会话中的消费者按通道复用,直到整个连接结束才关闭。JMS 接收并不只是一个无状态函数:预取、尚未确认的消息和事务边界都依赖消费者状态。每次取一条便新建再关闭消费者,会使后续接收行为难以按预期解释。
相同会话里的一次提交确认此前已消费的消息,也提交该会话产生的隔离消息。回滚则使当前未提交处理重新参与交付。本文把每个业务消息的提交位置写在端点流程中,避免只凭最终进程退出码推断所有消息都已经确认。
通道名字也不是授权边界。样本没有配置 broker 身份认证,只有本机地址和随机端口,因此“正确端点”表示路由语义得到验证,不表示恶意客户端不能向其他队列发送。网络隔离、凭据和资源配额需要独立的部署实验,不能由本机测试外推。
选择消息边界的代价
把同步调用改成消息以后,调用方得到的第一份确认通常来自消息系统,维修申请是否受理需要另一个可查询结果。如果页面仍把发送返回展示为“维修已受理”,界面就越过了尚未发生的业务步骤。实验中发送阶段数据库为零,正好提供了这个区别的可观察证据。状态字段至少应区分待接收、受理和拒绝,具体名称由业务合同决定。
Translator 也应留在集成边界内。领域中的设备对象不需要导入 JMS 类型,维修系统的资产字段变化则由转换代码吸收。样本通过独立字符串报文和接收结果表展示这一点,但没有执行模块依赖检查;若项目要求编译期隔离,还需要对领域模块的依赖边界另设检查。接口转换存在,并不自动意味着依赖方向已经得到强制约束。
保留原始载荷能够帮助定位转换错误,却会增加存储和访问管理成本。原始记录的保留期限应与重投窗口、审计要求和数据敏感程度一起设计。若只保存转换后的资产编号,后续很难判断错误来自发送方引用还是接收端映射;若永远保存所有载荷,又会让一次集成便利变成长期数据负担。
这个样本适合说明外部系统离线仍需接受请求的场景。若双方必须立即给出同一事务内的业务答案,引入队列会增加待定状态和查询路径。是否接受这些状态,应在接入之前决定,而不能等消息积压时再临时把超时全部解释成失败。
依赖与读者复跑
下载 实验包,保持 examples 目录结构,使用 Java 21 运行独立入口。依赖来自 Maven Central 固定坐标,清单包含文件摘要;运行时先检查缓存,缺失时下载并校验,源码包不捆绑第三方 jar。
1 | |
入口严格编译后,自动启动和停止本机 broker,执行离线发送、重启、接收、重投和数据库读回。正常结束返回零。末尾追加 negative,会通过相同翻译函数拒绝版本九十九并返回一;负例日志保存在独立目录,避免覆盖正常结果。
ENTERPRISE_INTEGRATION_CACHE 可以指定可写依赖缓存,ENTERPRISE_EVIDENCE_DIR 可以指定输出位置。日志、退出码、数据库 SQL 快照及运行时目录写入证据目录;主线业务契约的累计验收仍由系列入口单独执行,本篇通过只说明这一组集成场景成立。






