0%

Javalin 贡献记

1,579 字 约 4 分钟

从一个 Issue 开始:我第一次参与 Javalin 开源社区讨论

最近我在 Javalin 社区参与了第一次开源讨论,提了个 API设计上的问题,然后跟着维护者的反馈一起慢慢梳理,最后把想法整理成了一个相对清晰的 proposal。

这次经历主要围绕两个 issue 展开:

我遇到的问题

我最初遇到的问题是:在 Javalin 中,如何优雅地给一组路由挂载统一的 middleware 或 handler chain?

在一些 Web 框架里,比如 Gin 或 Hertz,可以创建一个 route group,然后给这个 group 挂 middleware:

v1 := h.Group("/v1")
v1.Use(middleware1, middleware2)
v1.GET("/users", handler)

这样 /v1 下面的所有路由都会继承这些 middleware。执行顺序也很直观:

group middleware
  route middleware
    endpoint handler

如果 middleware 支持 next(),还可以形成类似 onion-style 的结构:

requestLog before
  requireAuth before
    endpoint
  requireAuth after
requestLog after

以上这些可以在我的这一片博客中找到:Spring Boot VS Hertz:框架托管复杂性 vs 开发者显式组织复杂性

我希望在 Javalin 中也能表达类似的模型:

path("/admin") {
    use(requireAuth, audit)

    get("/me") { ctx ->
        // ...
    }

    get("/users", middlewares = listOf(requireRole("admin"))) { ctx ->
        // ...
    }
}

理想中的执行顺序是:

requireAuth
  audit
    requireRole("admin")
      endpoint

我认为这类需求在实际项目里很常见,比如后台管理、用户中心、API 版本分组、鉴权、审计日志、限流等。它们通常不是单个 endpoint 的问题,而是一组路由的共同策略。天然适合这一类带有 scope 的模型。

提出第一个 issue:不是提需求,而是提问题

于是我在 javalin/javalin 中创建了 #2601,标题是:

Route-group-level middleware / handler chain API?

我并不是直接说“请加一个 middleware API”,而是先整理了现有 Javalin 已经提供的相关机制:

  • ApiBuilder.path(...) 可以组织路径;
  • RouteRolerouteRoles()beforeMatched(...) 可以做访问控制;
  • beforeMatched(...) / afterMatched(...) 可以处理匹配后的请求 hook;
  • config.router.handlerWrapper(...) 可以在 router 层包装 endpoint handler。

这些能力已经能覆盖很多场景,但它们并没有直接表达“某个 route group 自带一组 middleware,并且被内部 route 继承”的模型。

所以我在 issue 中提出的问题其实是:

  • Javalin 是否会考虑支持 route-group-level middleware / handler chain?
  • 这种 API 是否符合 Javalin 的设计哲学?
  • 如果不适合进入框架本身,是否推荐用户继续使用现有机制,并在应用层自己组合?

我也补充了一个 application-level workaround:用 RouteMiddlewarechain(...) 在应用层手动组合 handler。这样做能解决问题,但 route definition 和 middleware model 并没有真正集成在一起。

维护者的反馈:方向可以讨论,但不属于 Javalin 7

维护者 tipsy 回复说,这是一个有意思的想法,他也思考过这种模型是否会更好。但这个方向更适合在 Javalin 8 中考虑,不会作为 Javalin 7 的另一套方案加入:

“it's an interesting thought, I've wondered before if this would be a better model. It's something we can consider for Javalin 8, but we will not be adding this as a an alternative approach in Javalin 7. Let's keep the issue open though.”

所以我回复说,这个判断是合理的。我会继续在应用层使用 wrapper,如果未来进入 Javalin 8 规划,我也愿意提供更具体的例子或 PoC。

开源项目中的很多决定,不是“能不能实现”,而是“应该在什么时候、以什么方式实现”。

转向 javalin-routing-extensions

之后,另一位维护者 dzikoysk 提出了一个新方向:也许可以在 javalin-routing-extensions 的 In-Place DSL 中探索这个能力:

"I guess we could extend In-Place DSL routing module with something like that: https://javalin.github.io/javalin-routing-extensions/dsl/in-place.html It'd be a cool exercise and something unique to it, because as of right now it doesn't offer that much."

他认为,In-Place DSL 当前没有提供太多额外能力,如果在这里尝试 route-group middleware 或类似结构,可能会是一个有价值的扩展。

随后,dzikoysk 创建了 #64

Support multi-layered path operations in In-Place DSL

这个 issue 最初关注的是 In-Place DSL 当前只支持 flat routes,能否支持类似 Javalin API Builder 的 nested path operations。

也就是说,问题从最初的 route-group middleware,转移到了 nested path operations。

从 nested path 到 nested route scope

#64 中,我一开始提出一个最小起点:先支持路径组合。

config.routes(Dsl) {
    path("/api") {
        path("/admin") {
            get("/users") {
                result("users")
            }

            post("/users") {
                result("user created")
            }
        }
    }
}

它应该注册出:

GET  /api/admin/users
POST /api/admin/users

但我同时指出,一旦有了嵌套 path 结构,它也可以成为 route-group-level behavior 的基础,比如 scoped interceptors 或 middleware-like handler composition。

dzikoysk 很快提醒我:如果 scoped middleware 会影响 DSL 的内部结构,最好现在就把完整目标形态描述清楚。即使后续实现可以拆成多个 PR,设计讨论中也应该先把最终用户视角写出来。

于是我把问题重新表述为:

这里的核心概念可能不只是嵌套 path(...),而是嵌套的 route scopes。

一个 route scope 不只是 path prefix。它还可以承载:

  • 路径前缀;
  • scoped middleware / handler composition;
  • route metadata,比如 roles。

例如:

enum class Role : RouteRole {
    AUTHENTICATED,
    ADMIN
}

config.routes(Dsl) {
    path("/api") {
        use(requestLog)

        path("/admin") {
            use(requireAuth)

            path("/users", roles = setOf(Role.ADMIN)) {
                use(requireAudit("users"))

                get { result("users") }
                post { result("user created") }
            }
        }
    }
}

它不仅注册出:

GET  /api/admin/users
POST /api/admin/users

更重要的是表达出这样的结构:

/api
  requestLog
    /admin
      requireAuth
        /users
          roles = [ADMIN]
          requireAudit("users")
            endpoint

这一步是整个讨论中最重要的变化:原始需求是 route-group middleware,维护者创建的 issue 是 nested path operations,而后续讨论把它进一步抽象成 nested route scopes。

技术讨论的核心

这两个 issue 背后的技术问题,其实不是某个具体函数名,而是一组设计问题。

首先,path(...) 到底只是 prefix,还是 scope?

如果它只是路径拼接,实现可以很简单:维护一个 path prefix stack,注册 route 时把路径拼起来。但如果未来要支持 middleware、roles、metadata,它就更像一个 route scope,需要携带一组可继承的上下文。

其次,use(...) 的继承和执行顺序要明确。

例如:

path("/api") {
    use(requestLog)

    path("/admin") {
        use(requireAuth)

        get("/users") { ... }
    }
}

最终顺序应该是:

requestLog
  requireAuth
    endpoint

也就是说,外层 middleware 包裹内层 middleware,内层 middleware 再包裹 endpoint。

第三,它和 Javalin 现有生命周期机制的关系要讲清楚。

Javalin 已经有 before(...)beforeMatched(...)after(...)afterMatched(...)routeRoles()handlerWrapper(...) 等能力。新的 DSL 不应该完全另起炉灶,而应该尽量建立在这些机制之上。

换句话说,它不是替代现有 API,而是给用户一个更结构化的方式来组合这些能力。

后话

对我来说,这是一次很好的开始。它让我从“使用者”稍微向“参与者”迈了一步。后续如果有跟进,我会继续更新。