Before / After / Catch 过程链
SmartWeb 的 Controller 过程模型来自 Rain controller 模块。它保留了 Play Framework 早期 Controller 中 @Before、@After、@Catch 的直观写法,又加入了作用域、权重、过滤器和业务注解扩展。
这套机制的重点不是“在方法前后调用另一个方法”,而是把认证、授权、上下文装配、审计、异常翻译等横切业务,在应用启动时组合成每个 Action 独立的执行链。
最小示例
@WebController
@Path("orders")
class OrderController {
@Before(weight = -100)
fun authenticate(user: WebUser?) {
requireNotNull(user) { "请先登录" }
}
@Before(weight = 0)
fun loadTenant(tenantId: Long): TenantContext =
TenantContext(tenantId)
@GetAction("{id}")
fun detail(id: Long, tenantContext: TenantContext): Order =
orderService.find(tenantContext, id)
@After(weight = 100)
fun audit() {
auditService.record("order.detail")
}
@Catch(error = IllegalArgumentException::class)
fun badRequest(error: IllegalArgumentException): ApiError =
ApiError("BAD_REQUEST", error.message)
}过程方法与 Action 使用同一套 WebMethodInvoker 参数装配能力,因此可以读取请求参数、路径变量、用户和 Context 中的值。
一次请求如何执行
最终的 WebActionInvoker 执行顺序是:
匹配 HTTP 路由
↓
按 weight 升序执行 Before
↓(没有提前终止)
执行 Action
↓(没有提前终止)
按 weight 升序执行 After
↓(任一步骤抛出异常)
按 weight 升序尝试 Catch
↓
检查结果并交给响应渲染对应源码非常直接:SimpleActionInvoker.invoke 依次遍历 beforeProcesses、调用 action、遍历 afterProcesses,外层使用 runCatching 将异常交给 catchProcesses。
这体现了 Rain 的实现哲学:不用庞大的 Handler Mapping/Adapter/Interceptor 体系描述一个请求,而是把核心语义压缩为三个 ProcessInvoker[] 和一个 Action 调用器。
Before
@Before 适合在 Action 前完成前置条件或准备上下文:
- 登录状态验证。
- 权限和租户边界验证。
- 从 Header、Session 或 Token 中建立业务上下文。
- 加载后续 Action 需要的对象。
- 限流、幂等检查。
@Before(weight = -100)
fun requireLogin(user: WebUser?) {
requireNotNull(user)
}多个 Before 按 weight 从小到大执行。相同权重下不要依赖稳定顺序;存在依赖关系时应显式拉开权重。
过程方法返回普通对象时,Rain 会按类型简单名首字母小写写入 ActionContext。例如返回 TenantContext,会保存为 tenantContext,后续过程或 Action 可以按上下文参数读取。这使 Before 不只是“检查器”,也可以是请求级数据生产者。
After
@After 只在 Before 与 Action 正常走完、且没有产生提前终止标记时运行,适合:
- 业务审计。
- 补充响应上下文。
- 记录成功指标。
- 对 Action 结果进行后续业务处理。
@After(weight = 100)
fun recordSuccess(currentUser: WebUser?) {
metrics.success(currentUser?.id)
}它不是 Java finally:如果 Before 或 Action 抛出异常,控制流会直接进入 Catch,After 不执行。必须无论成功失败都执行的资源释放,不应只依赖 After,应放在底层服务的 try/finally 或生命周期组件中。
Catch
@Catch 只处理指定异常类型:
@Catch(error = OrderNotFoundException::class, weight = 0)
fun notFound(error: OrderNotFoundException): ApiError =
ApiError("ORDER_NOT_FOUND", error.message)底层 SimpleCatchMethodInvoker / WebCatchMethodInvoker 会先检查 throwableType.isInstance(context.runtimeError),不匹配时返回 null,让后续 Catch 继续尝试。
Catch 可以把领域异常转换为稳定的 API 错误对象。若 Catch 自己抛出 ActionResult,Rain 会清空运行时异常并使用其中结果;若 Catch 抛出普通异常,原始异常继续向外传播。
当前行为
Catch 方法返回普通对象时,会像其他过程结果一样写入 Context,而不是自动成为 context.result。需要明确中止并返回结果时,应使用框架的 ActionResult 流程控制,或采用项目统一的异常 Render/响应策略。请针对当前 SmartWeb 版本做端到端测试。
作用域:Controller 与根路由
默认情况下,Controller 内声明的 Before、After、Catch 只加入当前 Controller 的过程集合。给过程方法加 @Global 后,它会加入当前根路由:
@Global
@Before(weight = -1000)
fun traceRequest(): RequestTrace = RequestTrace.new()最终为 Action 建链时,Rain 按以下来源收集:
- 当前根路由的过程。
- 当前 Controller 的过程。
- 按
weight对合并结果统一排序。
因此全局过程和局部过程不是两段固定执行;权重决定它们在同一 Before/After/Catch 队列中的位置。
SmartWeb 使用 Controller Bean 的 @Named 名称选择根路由。不同名称形成不同 rootRouterMap 项,也会创建不同命名 Web Server。@Global 的“全局”只在当前根路由/服务器内全局,不会天然跨越所有命名服务器。
父 Controller 与子 Controller
Rain 的动静分离 Controller Loader 会沿 Controller 的父类向上收集 @Path:
@Path("api")
open class ApiController
@WebController
@Path("orders")
class OrderController : ApiController() {
@GetAction("{id}")
fun detail(id: Long) = TODO()
}最终路径为 /api/orders/{id}。如果某一层 @Path 以 / 开头,路径收集会在该层停止,它相当于新的绝对路径边界。
Controller Loader 扫描 allMethod,因此继承方法也可能成为子 Controller 的过程或 Action。推荐将父类用于稳定、通用的过程模板,而把具体 Action 留在子类:
open class SecuredController {
@Before(weight = -100)
open fun authenticate(user: WebUser?) {
requireNotNull(user)
}
}
@WebController
@Path("admin/users")
class AdminUserController : SecuredController() {
@GetAction
fun listUsers() = userService.list()
}这种方式比把权限检查复制到每个 Action 更适合业务分区;但继承层级过深会隐藏行为。跨多个无继承关系 Controller 的能力,优先使用业务注解与 ProcessProvider。
only 与 except
三个过程注解都支持 only 和 except:
@Before(only = ["create", "update@java.lang.Long"])
fun requireWritePermission() = Unit
@After(except = ["health"])
fun audit() = Unit匹配规则来自源码:
methodName:匹配同名方法的所有重载。methodName@:匹配无参数方法。methodName@java.lang.Long:使用参数完整类名匹配。methodName@.Long:参数名前加.,使用简单类名匹配。- 多参数使用逗号分隔。
only 非空时必须命中其中一项;except 命中任一项就排除。重载较多时应使用带参数形式,避免误匹配。
用业务注解声明过程
把权限需求表达成业务注解,比直接在 Controller 中写重复 Before 更清晰:
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@ProcessBy(PermissionProcessProvider::class)
annotation class RequirePermission(val value: String)
@Before(weight = -50)
class PermissionProcessProvider(
private val permissionService: PermissionService,
) : ProcessProvider<WebActionContext> {
override fun <T> invoke(
provideAnnotation: Annotation,
functionAnnotation: Annotation,
controllerClass: Class<T>,
controllerInstance: T,
action: Method?,
): ProcessInvoker<WebActionContext> {
val permission = provideAnnotation as RequirePermission
return ProcessInvoker { context ->
permissionService.require(context.user, permission.value)
}
}
}然后在类或 Action 上声明:
@WebController
@Path("admin")
@RequirePermission("admin:enter")
class AdminController {
@DeleteAction("users/{id}")
@RequirePermission("user:delete")
fun deleteUser(id: Long) = userService.delete(id)
}加载时,Rain 读取业务注解上的 @ProcessBy,从 DI 容器获取 Provider,并根据 Provider 类上的 @Before / @After / @Catch 决定过程类型和权重。Provider 在启动期为每个声明位置创建 ProcessInvoker,运行时只调用已经组装好的 invoker。
关于示例语法
ProcessInvoker 是 Kotlin 函数式接口/函数类型时可直接使用 lambda;请以当前源码中的实际类型签名调整构造方式。核心设计是 Provider 返回一个接收 WebActionContext 的调用器。
ProcessFilterBy
当注解是否适用于某个 Action 不能由 only / except 静态表达时,可以在注解声明位置增加 @ProcessFilterBy,由 DI Bean 实现 ProcessFilter:
class PublicEndpointFilter : ProcessFilter {
override fun <T> invoke(
provideAnnotation: Annotation,
functionAnnotation: Annotation,
controllerClass: Class<T>,
controllerInstance: T,
action: Method?,
): Boolean = action?.isAnnotationPresent(PublicEndpoint::class.java) != true
}过滤发生在 Action 执行链构建阶段,不是每次请求重新反射判断。它适合根据 Controller/Action 元数据决定是否挂载过程,不适合根据当前用户或请求数据做动态判断;请求级判断应放在 ProcessInvoker 内。
流程控制值
SimpleActionInvoker 识别两个特殊结果:
DoNone:停止当前链,并保留特殊结果。SkipMe:停止当前 Action,最终让路由返回false,可继续尝试其他匹配逻辑。
抛出 ActionResult(value) 会中断当前调用并直接设置 Action 结果。普通过程返回值则写入 Context,供后续过程和 Action 使用。
面向业务的推荐分层
| 需求 | 推荐位置 |
|---|---|
| 全站 Trace、访问日志 | 根路由 @Global @Before/@After |
| 某业务域统一登录校验 | 父 Controller 或类级业务注解 |
| 单 Action 权限 | Action 上的业务注解 |
| 参数依赖的动态权限 | Provider 生成的 Before invoker |
| 领域异常转 API 错误 | Controller 或根路由 Catch |
| 必须执行的资源释放 | 服务内部 try/finally,不要只依赖 After |
与 Play Framework 的关系
Play 1.x 的 @Before、@After、@Catch 强调 Controller 生命周期回调,使用直观但容易把横切逻辑集中到继承体系。Rain/SmartWeb 保留这种易读体验,同时增加:
weight统一排序。only/except精确作用到 Action 或重载。@Global提升到根路由。@ProcessBy把业务注解转为可复用过程。@ProcessFilterBy在启动期按元数据筛选 Action。- 过程返回对象写入 Context,形成显式的请求数据生产链。
因此它更适合把“进入业务 Action 前必须完成什么”表达成可组合的业务协议,而不仅是传统 MVC 拦截器。