跳到内容

Hook Starter

受众:开发者 摘要:方法级 Hook 基础组件。通过 @Hook.ExecuteHook.execute(...) 在业务方法外层执行一组 HookHandler,适合日志、审计、指标、鉴权、幂等、风控和平台扩展点。

何时使用

场景建议
方法入口需要统一日志、审计、指标或 Trace用 Hook
关键写操作需要同步鉴权、幂等或风控校验用 Hook
希望业务方法外层可插拔扩展,但不想把扩展散落在 Service用 Hook
不确定该用 Hook、Pipeline 还是 Chain先看 钩子 / 管道 / 责任链 专题

Hook 的定位是:方法级同步技术关注点和扩展口。它包住业务方法,但不接管业务状态机、事务边界、最终一致性或跨系统恢复。钩子 / 管道 / 责任链的选型、注解糖和组合使用见 钩子 / 管道 / 责任链 专题

Starter 入口与源码骨架

Hook starter 采用 Facade / Annotation -> Executor -> Registry -> Handler 的骨架。

职责
FacadeHook手动调用入口:Hook.execute(point, param, target)
Annotation@Hook.Execute / HookAspect注解式入口,把目标方法交给 Hook executor 执行
ExecutorDefaultHookExecutor校验 point / context、处理 point 启停、执行 before / target / after / onError、生成执行快照并通知 listener
RegistryDefaultHookHandlerRegistryHookHandler.getPoint() 绑定 Handler,应用 point / handler 启停配置并排序
HandlerHookHandler<T, R>单个外层扩展动作,支持 before / after / onError
ContextHookContext<T, R>继承共享 ExecutionContext,暴露 point、traceId、入参、方法、结果、异常、ExecutionKey<T> 和中止状态
ListenerHookExecutionListener接收最终执行快照,用于审计、指标、日志和运维观测

关键机制顺序:

  1. @Hook.Execute("mall.order.create")Hook.execute("mall.order.create", cmd, target) 触发 Hook。
  2. DefaultHookExecutor 判断 point 是否启用。
  3. DefaultHookHandlerRegistry 查找同 point 的 Handler,按配置 order、HookHandler.order()Ordered@Order 排序。
  4. Handler 的 supports(context) 返回 false 时跳过并记录 trace。
  5. before 按正序执行;失败会调用已执行 Handler 的 onError,并阻断目标方法。
  6. 目标方法执行成功后,after 按逆序执行;after 可替换返回值。
  7. after / onError / listener 失败不会反向破坏已经成功的目标方法结果,只记录失败快照。

快速开始

yaml
spring:
  open:
    hook:
      enabled: true
      trace-enabled: true

注解方式

业务方法上加 @Hook.Execute

java
public class OrderService {

    @Hook.Execute("mall.order.create")
    public Order createOrder(CreateOrderCommand command) {
        // 业务主流程仍写在这里:校验库存、创建订单、扣库存、创建支付单等。
        return doCreate(command);
    }
}

@Hook.Execute 有三点注意:

  • 自调用不生效:注解走 Spring AOP 代理,必须从其他 Spring Bean 调用该方法;同一个类内部 this.createOrder(...) 自调用不会触发注解(与 @Pipeline.Execute 一致)。需要类内调用时改用 Hook.execute(...) 手动入口。
  • parameter 默认取第一个参数:handler 的 getParameter() 默认是方法第一个参数。若业务对象不在第一个位置(如 create(Long tenantId, OrderCmd cmd)),用 @Hook.Execute(value = "...", paramArg = 1) 指定,避免 handler 收到错误类型。paramArg 会在启动期校验,必须指向真实参数;负数、越界,或无参方法显式配置非零下标,都会直接失败。
  • 启动期校验是常规业务 Bean 的 fail-fast 护栏@Hook.Execute(paramArg) 会在 Spring 容器启动时扫描常规业务 Bean 并提前发现配置错误。极早创建的基础设施 Bean 可能早于 Hook 校验器实例化,此时启动期校验会退化为运行时校验;真正执行方法时仍会由 Hook 切面兜底抛出 HookException

添加一个 Handler:

java
@Component
public class OrderCreateAuditHookHandler implements HookHandler<CreateOrderCommand, Order> {

    @Override
    public String getPoint() {
        return "mall.order.create";
    }

    @Override
    public String getCode() {
        return "audit";
    }

    @Override
    public int order() {
        return 100;
    }

    @Override
    public void before(HookContext<CreateOrderCommand, Order> context) {
        auditService.recordStart(context.getParameter().userId());
    }

    @Override
    public Order after(HookContext<CreateOrderCommand, Order> context, Order result) {
        auditService.recordSuccess(result.id());
        return result;
    }

    @Override
    public void onError(HookContext<CreateOrderCommand, Order> context, Throwable error) {
        auditService.recordFailure(context.getTraceId(), error.getMessage());
    }
}

手动方式

不方便使用注解时,直接用 Hook.execute(...)

java
Order order = Hook.execute(
        "mall.order.create",
        command,
        () -> doCreate(command));

Hook.execute 适合在已有 Service 中手动包住一段动作;@Hook.Execute 适合固定方法入口。

Handler 语义

java
public interface HookHandler<T, R> {
    String getPoint();
    default String getCode();
    default int order();
    default boolean supports(HookContext<T, R> context);
    default void before(HookContext<T, R> context);
    default R after(HookContext<T, R> context, R result);
    default void onError(HookContext<T, R> context, Throwable error);
}
方法说明
getPoint()精确 Hook point,例如 mall.order.create
getCode()稳定 handler code,用于配置、trace 和运维展示
order()默认排序,未设置时等同 Spring Ordered.LOWEST_PRECEDENCE
supports(context)运行期条件判断,返回 false 时跳过
before(context)目标方法前执行;抛异常会阻断目标方法
after(context, result)目标方法成功后逆序执行;返回值会成为新的结果
onError(context, error)before、target 或 after 失败时执行;自身异常会被隔离

Context 可用数据

HookContext<T, R> 会把一次调用中的关键信息传给 Handler:

字段 / 方法说明
getPoint()当前 Hook point
getTraceId()本次 Hook 执行 trace id
getParameter()手动调用时传入的参数;注解方式默认取第一个方法参数,可用 @Hook.Execute(paramArg = n) 指定;无参方法默认 paramArg = 0 时为 null
getArgs()注解方式下的完整方法参数数组
getTarget()注解方式下的目标 Bean
getMethod()注解方式下的目标方法
getResult()目标方法或 after 处理后的当前结果
getError()当前失败异常
set(key, value) / get(key)Handler 间共享的轻量属性
set(ExecutionKey<T>, value) / get(ExecutionKey<T>)与 Pipeline / Chain 共用的类型安全属性
terminate(reason) / isTerminated()与 Pipeline / Chain 共用的同步中止语义

排序与启停

排序来源按优先级生效:

来源示例优先级
配置覆盖points.mall.order.create.handlers.audit.order: 10最高
Handler 默认排序HookHandler.order()
Spring 排序Ordered / @Order
默认值Ordered.LOWEST_PRECEDENCE最低

配置示例:

yaml
spring:
  open:
    hook:
      points:
        mall.order.create:
          enabled: true
          handlers:
            risk-check:
              enabled: true
              order: 10
            audit:
              enabled: true
              order: 100

如果多个扩展之间存在强业务依赖,不要靠复杂 Hook 顺序硬拼;应改用 Pipeline 表达主流程步骤。

失败策略

失败位置默认行为
supports / before阻断目标方法,调用已执行 Handler 的 onError,最终向调用方抛出异常
目标方法调用已执行 Handler 的 onError,向调用方抛出原始异常
after记录失败快照并调用 onError,但返回目标方法已有结果
onError记录 warning,不覆盖原始失败
HookExecutionListener记录 warning,不影响调用方

手动 Hook.execute(...) 遇到目标方法 checked exception 时会包装为 HookException;注解方式由 AOP 入口保留目标方法抛出的异常语义。

Listener 与 Trace

实现 HookExecutionListener 可以接收最终执行快照:

java
@Component
public class HookMetricsListener implements HookExecutionListener {
    @Override
    public void onExecution(HookExecutionResult<?, ?> result) {
        meterRegistry.counter("spring_open_hook_total",
                "point", result.getPoint(),
                "status", result.getStatus().name()).increment();
    }
}

HookExecutionResult 包含 point、traceId、status、result、exception、elapsedMillis 和 HookHandlerTrace 列表。关闭 trace-enabled 后,快照仍会生成,但 handler trace 为空。

生产接入点

框架内置业务会在稳定方法入口外层接入 Hook。业务 Service 仍负责事务、落库、状态流转、事件、通知和回执,Hook 只承接同步扩展关注点。

模块Hook point上下文边界
Conversationconversation.message.sendConversationMessageSendExecutionContext 暴露会话 ID / 类型、发送人、客户端消息 ID、内容类型、附件数量和原始发送命令可做审计、埋点、轻量同步或可解释阻断;不替代内容安全、消息落库、WebSocket 推送、站内通知或 ACK 回执
Mallmall.order.createMallOrderCreateExecutionContext 暴露用户、收货地址、幂等键、优惠券码、购物车商品数量、购物车项 ID、地址快照和原始下单命令可做下单审计、指标、营销埋点、内部提醒和轻量风控;阻断发生在库存锁定和订单落库前,不替代扣库存、锁券、支付单、虚拟交付或状态流转
Mallmall.order.paidMallOrderPaidExecutionContext 暴露订单 ID / 编号 / 用户、支付前后状态、原因、traceId、支付 Provider / 网关 / 交易号、支付金额、币种、支付时间、订单和支付结果可做支付成功后的审计、指标和轻量运营动作;在支付成功主链路副作用完成后执行,扩展失败只记录 warning,不回滚已确认支付结果,不替代 Payment Provider、账务一致性、优惠券核销、秒杀确认、虚拟交付或订单状态流转
IAMiam.user.registerUserRegisterExecutionContext 暴露注册用户 ID / 用户名 / 昵称 / 邮箱 / 手机号 / 头像、注册来源 / 设备、用户实体和原始注册命令可做注册成功后的欢迎任务、审计增强、埋点、CRM 同步和默认画像补充;在用户落库和注册审计成功后执行,扩展失败只记录 warning,不回滚已注册用户,不替代用户名 / 身份唯一性、密码策略或用户落库主流程
Blogblog.article.saveBlog 文章保存命令可做站点二开策略、租户限制、发布窗口检查和审计;不接管文章字段规范化、内容安全、状态处理或落库
Forumforum.topic.publish / forum.reply.publishForumPublishExecutionContext 暴露作者、板块、主题、父回复和原始命令可做发布方法外层策略;内部内容安全、计数和事件仍由 Forum Service 控制
Forumforum.topic.report / forum.reply.report / forum.report.status.updateForumReportExecutionContext 暴露举报对象、举报人、处理人和状态流转可做举报治理审计或策略;不接管处理记录、Outbox 或状态事件
CMScms.content.review.submit / cms.content.review.finishCmsReviewExecutionContext 暴露内容、客户端、操作人、审核动作和备注可做审核方法外层策略;不替代审批工作流、审核状态字段、操作日志或通知事件
Open Platformopen-platform.app-key.createOpenPlatformAppKeyCreateExecutionContext 暴露应用、创建命令、密钥 Key、脱敏密钥、hint、过期时间、轮换来源、状态原因、操作人和创建结果可做 App Key 创建审计、运营埋点、开发者欢迎流或轻量同步;不暴露一次性明文 secret,创建后扩展失败只记录 warning,不回滚已创建密钥
Runtime Content Safetycontent-safety.reviewContentSafetyReviewExecutionContext 暴露检测日志、scene、业务对象、用户、风险等级、复核前后状态、复核人、动作、备注和权限检查标识可做复核动作审计、指标、轻量二开策略或同步扩展;阻断发生在复核状态落库前,跨模块事后通知仍优先 Event / Notification 规则

不建议用 Hook 的场景

场景原因
高频循环中的细粒度方法Handler 链和 trace 会带来额外成本
复杂主流程编排应使用 Pipeline,使步骤、跳过、中断和补偿显式可见
条件决策责任链应使用 Chain,使每条规则通过 proceed 明确是否继续传递
异步通知、跨模块广播应使用 Event
分布式事务 / 跨进程恢复Hook 不落库,不承担恢复语义
供应商多实现选择应使用 Provider 扩展模型

验证命令

bash
./mvnw -pl spring-open-starters/spring-open-starter-hook -am test -DskipITs -DskipFrontend
git diff --check

Released under the Apache License 2.0.