Stream、Function、Task、Contract 公开实验(U36)
课程正文在框架抽象与集成边界。本工程用合成订单载荷,观察框架公开 API、Kafka 记录和消费组位点、TaskExplorer 执行记录、生成的消费者契约测试。没有业务数据库扣款、真实任务调度平台或生产 broker。
固定组合与环境
| 层次 | 固定版本与作用 |
|---|---|
| JDK / Maven | JDK 21;Maven 3.9.x;本次发行商、补丁与 OS/架构见 evidence 环境字段 |
| Boot / Framework | Boot 4.0.9 BOM → Framework 7.0.10;不是把旧源码版本替换成此次运行 |
| Cloud | 2025.1.4 BOM → Stream 5.0.4、Function 5.0.5、Task 5.0.2 |
| Kafka | spring-kafka/test 4.0.8,Kafka broker/client 4.1.2;嵌入式单节点 KRaft、随机端口 |
| Task 存储 | H2 2.4.240 临时文件,Spring Integration JDBC 7.0.7;不代表 MySQL 锁语义 |
| Contract | 独立子工程显式导入 Contract 5.0.3 BOM,Maven plugin 同为 5.0.3,仍复用 Boot 4.0.9 |
2026-10-28 查询的官方项目 API 已将 Contract 标为 END_OF_LIFE,仓库已归档;Cloud 自 2025.1.3 起将 Contract 移出 BOM。因此本子工程固定历史版本 Contract 5.0.3 用于契约机制教学,不作为维护中的新项目选型建议,也不能省略版本后声称它随 Cloud 2025.1.4 train 一起验证。项目 API、归档库和 BOM 移除已确认;本次直接访问官方迁移博客返回403,未将其写成重新阅读全文。查询和固定提交证据见 docs/audits/java-depth-u36-source-review.md(仓库内审计文档,不进站点构建)。组合依赖依据为固定的上游 POM:Cloud 2025.1.4、Contract 5.0.3。完整源码 SHA、维护查询与版本边界由正文及范围审计登记。这里的版本锁定只表示本组合已解析并执行指定路径,不证明任意组件组合兼容。
启动与退出
在仓库根目录运行:
python3 labs/cloud-functions/run.py要求 JDK 21、Maven 3.9.x,以及首次运行可下载 Maven 依赖的网络。可用 JAVA_HOME、MAVEN_BIN 指定已安装工具;本入口不接受参数。它创建独有临时资源,不启动 Docker,不连接用户的 MySQL、Redis、MongoDB 或 Elasticsearch,也不修改它们的配置。
入口按顺序执行:
- 核对两份 POM、工具版本与实验文件 SHA。
mvn ... -f labs/cloud-functions/pom.xml clean test:8 条 Function/Stream/Task 测试。mvn ... -f labs/cloud-functions/contract/pom.xml clean test -Dtest=ContractVerifierTest -Du36.contract.break=BROKEN:提供者返回错误状态,要求 Maven exit=1,且生成的 verifier 确因 BROKEN 不等于 READY 失败;缺少测试、下载失败或其他异常不能充当此负控通过。- 将提供者恢复为 READY,重新
clean test,要求生成 verifier 与 StubRunner 两条测试均通过。 - 从 dependency:list 校验实际解析版本;核对报告类名/方法/数量、无跳过和预期观察行;检查运行期间实验文件没有变化。
必须 clean test:Contract plugin 的增量生成可能跳过生成源码目录注册;仅见 StubRunner 通过而遗漏生成 verifier 不算成功。最终 10 条测试通过 + 1 条确认会失败的契约负控,总体 exit=0;负控不混入“10 条通过”计数。
真实运行的输入、命令、退出码、环境、9 条观察行、5 份 suite 报告(共 10 条 testcase)、负控报告、依赖和文件 SHA 保存在 evidence/u36-2026-10-28.json。原始 Maven 日志和复制的 Surefire XML 保存在仓库外 ~/.cache/software-engineering-study/java-depth-impl/u36/run-*/,实际路径和 SHA 由 JSON 指向。每次开始先把旧成功状态改为 running,失败写 failed,不沿用旧成功。JSON 和控制台状态通过不代表生产环境通过。
逐条输入、观察与解释
| 测试/观察标记 | 输入、断言和解释 |
|---|---|
functionCatalog | uppercase|exclaim 处理 hello → HELLO!;Message header 路由到 uppercase → Message payload ROUTE;不存在的函数名被拒绝。传入 Message 的路由结果仍保留 Message 包装,不能强转为 String。没有 broker。 |
functionReactive | Flux[a,b,c] 在订阅前不执行,在一次订阅后得到 A/B/C;上游终止错误向下游传播。一次流调用不是每条记录重新构造一个函数调用,未测试响应式 binder 的 ack 语义。 |
streamCommit | 向独有主题发送 block:order-1;首次调用抛错,第二次阻塞。阻塞中业务成功数0且消费组未越过此记录;释放后成功数1,committed offset=1,尝试数2。ack-mode=RECORD、非事务消费,无数据库副作用。 |
streamDlq | poison:order-2 每次抛错;max-attempts=3、启用 DLQ。原载荷可从真实 DLQ 读出,尝试3次、业务成功0、原消费组位点1。DLQ 成功不等于业务成功,须另设恢复责任。 |
streamNoDlq | 相同非事务设置但关闭 DLQ;毒消息尝试3次、业务成功0,消费组仍推进;随后正常消息成功并推进到2。此分支会记录恢复/丢弃日志,不能声称“没配 DLQ 就一定无限重投”。 |
streamBridge | StreamBridge.send(dynamicDestination, Message[text/plain]) 返回true;再以独立 KafkaConsumer 读取并断言原载荷 dynamic-order。send=true 只是一层结果,额外读取才是本实验 broker 记录证据。 |
taskRepository | 文件 H2 上启动 success/fail/success,关闭 Context 后经公开 TaskExplorer 查询:3个执行ID、退出码0/1/0、完成时间、输入参数、失败栈和 @AfterTask 写入的退出消息。再次运行产生新ID,不会自动从业务中间位置续跑。 |
taskSingleInstance | 两个线程各启动独立 Context,使用同一文件库/任务名;首个 runner 阻塞,第二次启动在进入业务前被单实例锁拒绝。释放首个后再次启动成功,最终无 running execution。此用例避免同线程锁重入假象。 |
生成 validate_order_ready | Groovy DSL 描述 GET /orders/42 的 HTTP200、JSON字段 orderId=42/status=READY;插件生成 MockMvc verifier,针对真实 provider 控制器执行。负控把 provider status 改为 BROKEN,失败落在生成的 JSON 断言;恢复后通过。没有网络。 |
contractStubRunner | StubRunner 加载插件从同一 Groovy DSL 生成的 mapping,启动真实 loopback HTTP stub;GET /orders/42 为200且status=READY,/orders/99为404。证明消费者与已声明契约匹配,不证明真实 provider 的网络、授权或副作用。 |
Stream 消费属性均在测试启动参数中显式设置:labConsumer-in-0 绑定的 destination、group、content-type=text/plain、max-attempts=3、退避10ms/上限10ms/倍率1;Kafka binder 配置 brokers、replication-factor=1,扩展消费者 start-offset=earliest、ack-mode=RECORD、enable-dlq 和独有 dlq-name。重试上限包含第一次调用。本实验的确定失败在调用内部注入,没有通过杀死用户服务制造故障。
Task 的 @EnableTask 才导入生命周期记录;仅有普通 ApplicationRunner 不会自动成为有执行历史的 Cloud Task。Function/Stream 上下文通过 spring.cloud.task.autoconfiguration.enabled=false 关闭无关 Task 基础设施。任务通过 @AfterTask 在记录完成前写入退出消息。
单实例锁显式配置 spring.cloud.task.single-instance-lock-ttl=300、check-interval=50。Task 5.0.2 的属性注释写 milliseconds,但该实现传给 JdbcLockRegistry 时使用 Duration.ofSeconds(...);TTL 的实际单位为秒,这里300秒只防止短测试内自然过期。没有实测 TTL 到期、锁续期或进程崩溃后的恢复,不能从并发拒绝推出 exactly-once。
故障定位与清理
- 依赖解析失败:看 JSON
results指向的日志;不把下载失败当契约负控。检查网络和明确版本,再运行同一入口。 - broker 绑定失败:检查日志中的 brokers/主题/consumer group 与启动参数。共享本机 broker 或复用旧group会改变起点,不能与本实验等价比较。
- DLQ 没消息:核对 binding 名、Kafka 扩展前缀、group 和 enable-dlq;确认日志不是反序列化前失败;当前实验只注入函数处理异常。
- Task 无记录:确认
@EnableTask、共享文件数据库URL和任务名;普通 runner 完成不代表 Task 生命周期已经安装。重复业务执行仍需业务幂等,元数据无法替代。 - Contract verifier 缺失:重新 clean,并看
target/generated-test-sources/contracts;入口严格拒绝只跑了手写 StubRunner 测试。
Spring Context、Kafka producer/consumer/admin、嵌入式 broker、StubRunner 与 HTTP client 都有定向关闭;JUnit TempDir 清理独有 H2 文件,broker 自己清理日志目录。runner 对每个 Maven 命令使用独立进程组,并在成功/失败/中断后有界回收该组的剩余进程。target 是本实验可删除的构建产物;长期保留的仓库外日志便于审计,不是运行中的依赖服务。没有打开 Chrome 实例。
未运行与证明边界
Rabbit binder、同时使用两个 binder、事务/EOS 与数据库组合、Task launcher 经 Stream、Contract messaging、Function Web 适配器和云平台函数没有实跑;正文配置与固定源码推演另列。没有用 test binder 替代真实 Kafka 位点/DLQ 验证。
Kafka 是一个测试 JVM 里的单节点 KRaft;Task 竞争为一个 JVM 中两个线程/独立 Context,持久记录来自 H2 文件;MockMvc provider 和 WireMock consumer stub 的证明范围各不相同。没有独立消费者 OS 进程、跨机分区、生产ACL/TLS、RabbitMQ、平台任务拉起或真实提供者部署验证。本实验不证明跨资源原子性、生产容量或所有版本行为。实际组件的幂等/Outbox与贯通恢复分别归 U61/U80。