《Java 消息中间件·事件驱动架构》11.1 事件平台自助化

事件平台自助化的本质不是取消审批,而是把命名违规、权限越级等拒绝条件精准前置到成本最低的节点。本文用纯 JDK 完整模拟工单审批状态机,展示技术初审与安全审计的职责分工、拒绝条件列表与每一步产出物,为共建自助化事件平台提供可落地的流程设计与治理体系。

自助化的边界

几十个团队共用一个事件平台,最忌讳的做法是让每个人都写邮件申请、每个人都找架构师审批。审批是有成本的——架构师的时间成本、开发者的等待成本、平台运营的状态同步成本。自助化的目标不是「不要审批」,而是把审批放到成本最低的节点。

自助平台的边界是:开发者能自己做的自己做(建主题命名、声明消费者),机器能拦的机器拦(命名违规、权限越级),只有确实需要人判断的才交给人(安全审计、跨域数据共享)。

工单的四阶段流转

一个事件主题的创建申请,最少可以只走两个阶段(申请→技术初审),但完整流程有四阶段:

阶段名称谁执行检查什么拒绝条件
1INIT(申请)开发者填写主题名、域归属、初始消费者命名规范不通过
2TECH_REVIEW技术审核员消费者是否已声明、投影消费者是否幂等消费者未声明/未幂等
3SECURITY安全审计是否跨域消费、是否包含敏感字段跨域审批未通过
4CREATED平台创建主题、配置配额、注册到事件目录—

关键设计:拒绝条件必须在前置阶段拦掉。如果 INIT 阶段就发现命名违规,不要让工单走到 TECH_REVIEW 再拒绝——那样浪费了审核员的时间。

纯 Java 工作流模拟

下面的 PlatformSelfService 模拟工单在四个阶段的流转,包括自动拒绝、等待人工审核和强制拦截。

// PlatformSelfService.java — 事件平台自助化工单状态机

import java.util.*;

public class PlatformSelfService {

    enum Stage { INIT, TECH_REVIEW, SECURITY, CREATED, REJECTED }
    enum Role { DEVELOPER, TECH_REVIEWER, SECURITY_AUDITOR }

    record Ticket(String id, String topicName, String domain,
                  String consumerName, boolean declaredIdempotent,
                  boolean crossDomain, boolean hasSensitiveField) {

        boolean namingValid() {
            // domain.event.action.vN 格式
            return topicName.matches("^[a-z]+\\.[a-z]+\\.[a-z]+\\.v[0-9]+$");
        }
        boolean consumersDeclared() {
            return consumerName != null && !consumerName.isEmpty();
        }
    }

    static final Map<String, Set<String>> DOMAIN_PERMISSIONS = Map.of(
        "library", Set.of("library", "member"),
        "member",  Set.of("member"),
        "order",   Set.of("order", "payment", "inventory"),
        "payment", Set.of("payment")
    );

    static Stage next(Stage current, Ticket ticket, Role actor) {
        switch (current) {
            case INIT -> {
                // INIT 阶段:命名规范必须过,否则直接拒绝(成本最低)
                if (!ticket.namingValid()) {
                    System.out.println("  [INIT] 拒绝:命名不规范 " + ticket.topicName());
                    return Stage.REJECTED;
                }
                // INIT 阶段:权限越级也直接拒绝
                if (!DOMAIN_PERMISSIONS.getOrDefault(ticket.domain(), Set.of())
                        .contains(ticket.domain())) {
                    System.out.println("  [INIT] 拒绝:域 " + ticket.domain() + " 无权限");
                    return Stage.REJECTED;
                }
                return Stage.TECH_REVIEW;
            }
            case TECH_REVIEW -> {
                if (actor != Role.TECH_REVIEWER) {
                    System.out.println("  [TECH_REVIEW] 等待人工审核员处理");
                    return Stage.TECH_REVIEW; // 卡住,等待正确角色
                }
                if (!ticket.consumersDeclared()) {
                    System.out.println("  [TECH_REVIEW] 拒绝:消费者未声明");
                    return Stage.REJECTED;
                }
                if (!ticket.declaredIdempotent() && ticket.consumerName().contains("projection")) {
                    System.out.println("  [TECH_REVIEW] 拒绝:投影消费者未声明幂等");
                    return Stage.REJECTED;
                }
                return Stage.SECURITY;
            }
            case SECURITY -> {
                if (actor != Role.SECURITY_AUDITOR) {
                    System.out.println("  [SECURITY] 等待安全审计员处理");
                    return Stage.SECURITY;
                }
                if (ticket.crossDomain() && !ticket.hasSensitiveField()) {
                    // 跨域但不涉及敏感字段,可自动通过
                    System.out.println("  [SECURITY] 自动通过:跨域但无敏感字段");
                    return Stage.CREATED;
                }
                if (ticket.hasSensitiveField()) {
                    System.out.println("  [SECURITY] 拒绝:包含敏感字段须人工评审");
                    return Stage.REJECTED;
                }
                return Stage.CREATED;
            }
            case CREATED -> {
                System.out.println("  [CREATED] 主题 " + ticket.topicName() + " 已创建");
                return Stage.CREATED;
            }
            default -> { return Stage.REJECTED; }
        }
    }

    static void simulate(Ticket ticket, List<Role> actions) {
        Stage s = Stage.INIT;
        System.out.println("\n工单 " + ticket.id() + " 主题=" + ticket.topicName());
        for (Role action : actions) {
            Stage prev = s;
            s = next(s, ticket, action);
            System.out.println("  角色=" + action + " : " + prev + " -> " + s);
            if (s == Stage.REJECTED || s == Stage.CREATED) break;
        }
        System.out.println("  最终状态: " + s);
    }

    public static void main(String[] args) {
        // 场景1:正常通过
        Ticket t1 = new Ticket("T-001", "library.loan.created.v1",
            "library", "loan-projection", true, false, false);
        simulate(t1, List.of(Role.DEVELOPER, Role.TECH_REVIEWER, Role.SECURITY_AUDITOR));

        // 场景2:命名违规,INIT 直接拒绝
        Ticket t2 = new Ticket("T-002", "loan-created",
            "library", "consumer-a", true, false, false);
        simulate(t2, List.of(Role.DEVELOPER));

        // 场景3:投影消费者未声明幂等
        Ticket t3 = new Ticket("T-003", "library.stock.adjusted.v1",
            "library", "stock-projection", false, false, false);
        simulate(t3, List.of(Role.DEVELOPER, Role.TECH_REVIEWER));

        // 场景4:跨域消费 + 敏感字段 → 安全审计拒绝
        Ticket t4 = new Ticket("T-004", "payment.order.completed.v1",
            "payment", "order-sync", true, true, true);
        simulate(t4, List.of(Role.DEVELOPER, Role.TECH_REVIEWER, Role.SECURITY_AUDITOR));

        // 场景5:跨域但无敏感字段 → 安全自动通过
        Ticket t5 = new Ticket("T-005", "order.inventory.reserved.v1",
            "order", "inventory-consumer", true, true, false);
        simulate(t5, List.of(Role.DEVELOPER, Role.TECH_REVIEWER, Role.SECURITY_AUDITOR));
    }
}

本机实测(Temurin 25.0.5+8 / 2026-10-29):
javac --release 21 PlatformSelfService.java && java PlatformSelfService


工单 T-001 主题=library.loan.created.v1
  角色=DEVELOPER : INIT -> TECH_REVIEW
  角色=TECH_REVIEWER : TECH_REVIEW -> SECURITY
  角色=SECURITY_AUDITOR : SECURITY -> CREATED
  最终状态: CREATED

工单 T-002 主题=loan-created
  [INIT] 拒绝:命名不规范 loan-created
  角色=DEVELOPER : INIT -> REJECTED
  最终状态: REJECTED

工单 T-003 主题=library.stock.adjusted.v1
  角色=DEVELOPER : INIT -> TECH_REVIEW
  [TECH_REVIEW] 拒绝:投影消费者未声明幂等
  角色=TECH_REVIEWER : TECH_REVIEW -> REJECTED
  最终状态: REJECTED

工单 T-004 主题=payment.order.completed.v1
  角色=DEVELOPER : INIT -> TECH_REVIEW
  角色=TECH_REVIEWER : TECH_REVIEW -> SECURITY
  [SECURITY] 拒绝:包含敏感字段须人工评审
  角色=SECURITY_AUDITOR : SECURITY -> REJECTED
  最终状态: REJECTED

工单 T-005 主题=order.inventory.reserved.v1
  角色=DEVELOPER : INIT -> TECH_REVIEW
  角色=TECH_REVIEWER : TECH_REVIEW -> SECURITY
  [SECURITY] 自动通过:跨域但无敏感字段
  角色=SECURITY_AUDITOR : SECURITY -> CREATED
  最终状态: CREATED

审批点的最优位置

三个被拒绝的工单展示了不同的拦截时机:

工单拒绝时机拒绝理由为什么在这里拦
T-002INIT命名不规范开发者自己就能判断,不需要任何人的时间投入
T-003TECH_REVIEW投影未声明幂等需要技术审核员对消费模式有判断力,开发者未必清楚
T-004SECURITY敏感字段跨域必须由安全审计员判断合规性,前两阶段无权决策

审批点的最优位置不是每个环节都审,而是把拒绝条件前置到成本最低的节点。 INIT 阶段的命名校验是零成本的——开发者提交前系统就跑完了正则。TECH_REVIEW 需要审核员人工判断(成本中等),SECURITY 需要安全专家(成本最高)。T-002 如果在 SECURITY 阶段再拒绝,等于前面两个审核阶段的时间全部被浪费。

现实中的自助化平台应该把 INIT 阶段的校验做成"实时反馈"——开发者在填写主题名时,输入框旁边立即显示命名是否合规,不等提交就拦掉,把成本降到接近零。

最小审批原则的应用

最小审批原则可以总结为三条规则:

  1. 机器能拦的先拦:命名规范、配额检查、权限校验,全部自动化,零人工。
  2. 业务团队能审的先审:技术初审交给业务线资深开发,不上升到平台架构师。
  3. 安全只审安全:安全审计只看数据合规,不看技术实现,也不看业务逻辑。

表皮是自助,内核是分权。每个角色的审批范围被精确缩小,审批效率才能提升。

事件目录的自注册

工单到达 CREATED 状态时,平台应该自动把主题注册到事件目录(2.3 节定义的结构):

# 自动生成的目录条目示例
name: library.loan.created
version: "1.0.0"
topic: library.loan.created.v1
domain: library
producers:
  - service: loan-service
    contact: team-library@example.com
consumers:
  - service: loan-projection
    declaredIdempotent: true
    since: "2026-10-29"
createdBy: T-001

注册后,事件血缘(10.3 节)和配额管理(11.3 节)才能自动关联到这个主题。没有注册的主题不能出现在血缘图和配额报表里。

为什么技术初审必须由平台组负责

把审批权下放给各自业务线看似高效,但实际上会导致命名规范不统一、安全配置差异化和孤立主题泛滥。平台组负责初审不是为了官僚,而是维护全局一致性。

理想的审批流程是:平台组做技术初审(命名是否合规、权限是否合理、分区数是否匹配吞吐量),安全组做隐私初审(事件是否包含 PII、是否需要加密传输、保留期是否合规),业务线内部做业务初审(事件语义是否正确、消费者列表是否完整)。

三层审批各审一层,互不争抢。如果让一个人同时审三层,要么审不透,要么审不完。分层的本质是把拒绝条件拆到对应的专业组,把单点审批变成流水线。

本节小结

  • 自助化的边界:开发者能做的自己做,机器能拦的机器拦,只有确实需要人判断的才交给人。

  • 工单四阶段:INIT(命名/权限自动检查)→ TECH_REVIEW(消费者声明/幂等检查)→ SECURITY(跨域/敏感数据)→ CREATED(自动注册目录)。

  • 拒绝条件前置:命名违规在 INIT 拦截(零成本),技术缺陷在 TECH_REVIEW 拦截(中等成本),安全问题在 SECURITY 拦截(最高成本)。

  • 审批点的最优位置不是"每个环节都审",而是"把拒绝条件前置到成本最低的节点"。

  • 实时命名校验可以把 INIT 阶段的拒绝成本降到接近零。

  • 工单在 CREATED 后必须自动注册到事件目录,否则血缘和配额无法追踪。

  • 最小审批原则三条规则:机器先拦、业务线先审、安全只审安全。

  • 自助化的本质是分权,不是放权——每个角色的审批范围必须被精确缩小。

  • 自助化平台必须提供异常工单通道,特殊场景不走标准流程,但需要更高层级的审批。

  • 审批平均耗时和使用频率应作为平台治理的核心 KPI——流程本身也需要被治理。

  • 工单系统自身的事件(如 “工单创建”、“审批通过”)也应该发到 Kafka,这样才能监控流程的健康度。

  • 自助化平台上线三个月后应做一次回访,统计最常见的拒绝原因并优化自动拦截规则。

  • 工单的历史数据是平台治理的宝贵输入:哪些团队的命名习惯最容易违规、哪些消费者的幂等声明总是不全,这些数据指导平台和规范迭代。

  • 工单系统的 UI 设计应该让开发者看到当前工单在哪个阶段、被谁卡住、预计等待时间,而不是只显示"处理中"。

阅读导航:上一节 10.3 事件血缘与影响分析 | 下一节 11.2 Topic 命名与生命周期

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《Java 消息中间件·事件驱动架构》附录 B 架构决策记录模板
  2. 《Java 消息中间件·事件驱动架构》附录 A 术语表
  3. 《Java 消息中间件·事件驱动架构》15.3 全书验收