初看axum 时,对于它的示例代码惊为天人:一个静态强类型语言竟然可以做出比肩动态语言的效果,可以写出如此灵活,且做了严格类型检查的代码
不同于actix-web,rocket这些rust下的web框架,axum的路由处理设计并没有使用到过程宏。路由使用方式如下:
1 |
|
get
方法竟然可以接受root
/get_user
两个具有不同签名的函数,乍看之下属实使人匪夷所思
继续阅读route
方法的签名
1 | pub fn route(self, path: &str, method_router: MethodRouter<S>) -> Self { |
它接受path
& method_router
两个参数,那么get(root)
& get(get_user)
的类型一定是 MethodRouter<S>
进一步找到get
方法的定义
1 | pub fn get<H, T, S>(handler: H) -> MethodRouter<S, Infallible> |
所以get(root)
会被展开为 on(MethodFilter::GET, root)
,所以root
/ get_user
必然都实现了 Handler<T, S>
,否则不能通过编译
Handler
trait中最为重要的方法是call
1 | fn call(self, req: Request, state: S) -> Self::Future; |
call方法要求handler实现 request -> response的处理流程
对于无参的root方法,是通过以下代码实现,这在rust中称为blanket implementation:
1 | impl<F, Fut, Res, S> Handler<((),), S> for F |
对于所有实现了 FnOnce() -> Fut + Clone + Send + 'static
的类型,均实现了Handler
,root
函数实现了FnOnce
trait,因而也实现了Handler
对于1-16个参数的函数,axum通过过程宏提供了一个统一的逻辑:
1 | macro_rules! impl_handler { |
其中
1 | $( |
是对每个 $ty
(T1, T2, …)依次调用 from_request_parts()
,得到 handler 需要的变量
还可以发现,impl_handler
,对于最后一个参数$last
进行了区分处理,之前的参数要求实现**FromRequestParts**,最后一个参数要求实现 FromRequest。这两个trait的区别在于实现FromRequestParts
的extractor不会消耗request的body,在handler中可以按任意顺序排列,而FromRequest
会消耗request body,在handler中只能执行一次。所以像Json
blanket implementation这个特性是axum能够实现魔法参数的核心。其实像给函数实现接口这种设计,在其他语言也颇为常见,比如golang的HandlerFunc,但由于其没有blanket implementation,HandlerFunc
只能接受固定的ResponseWriter和Request
两个参数,无法像axum这样自由灵活的对handler参数进行组合
分析完request,可以举一反三的理解response。和response相关的trait是 IntoResponse
1 | pub trait IntoResponse { |
在 axum 中,很多类型都已经实现了 IntoResponse
trait,包括 &’static str
和 Json<T>
。get_user
返回的 (StatusCode, Json<User>)
同样也实现了它:
1 | impl<R> IntoResponse for (StatusCode, R) |
通过trait + macros + blanket implement,可以在保持静态检查及安全性的同时,大大提升用户代码的灵活性、易用性以及可组合性。不使用过程宏来实现路由处理,也方便了用户在测试时只需关注一个个独立的handler function。但凡事皆有两面,基于泛型的抽象设计带来极大灵活性的同时,也增加了错误消息的复杂度。如果输入的参数不符合 FromRequestParts / FromRequest trait,那么,axum 编译期产生的错误会非常晦涩难懂,为此axum还专门提供了debug_handler macro帮助排查此类问题