初看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帮助排查此类问题